From d380f8643b0c3f988fc8541873d7d02e5fc02751 Mon Sep 17 00:00:00 2001 From: Reactorcoremeltdown Date: Tue, 14 Jul 2026 07:37:48 +0200 Subject: [PATCH] UI Improvements: Piano widget, rigid sizes, proportions, etc --- .../sizzletracker/ui/components/Piano.kt | 107 ++++++++++++++++++ .../sizzletracker/ui/components/Widgets.kt | 27 ++++- .../sizzletracker/ui/mixer/MixerScreen.kt | 26 ++++- .../sizzletracker/ui/toolbox/ToolboxScreen.kt | 64 +++++------ .../ui/tracker/CellEditorPopup.kt | 62 ++++------ .../sizzletracker/ui/tracker/TrackerScreen.kt | 18 +-- 6 files changed, 210 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Piano.kt diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Piano.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Piano.kt new file mode 100644 index 0000000..36c260a --- /dev/null +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Piano.kt @@ -0,0 +1,107 @@ +package com.reactorcoremeltdown.sizzletracker.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.reactorcoremeltdown.sizzletracker.ui.theme.LocalRetro + +/** + * Lays out one octave as a real piano keyboard: seven white keys across the bottom + * with the five black keys overlaid on top, each centred on the gap between the two + * white keys it sits between (C# D# _ F# G# A#). + * + * Layout-only: the [key] slot renders each key and callers attach their own gesture + * to [keyModifier] (tap-to-enter, hold-to-audition, …). Black keys are emitted last + * so they draw — and receive touches — on top of the white keys they overlap, just + * like a physical keyboard. + */ +@Composable +fun PianoOctave( + modifier: Modifier = Modifier, + blackWidthFraction: Float = 0.62f, + blackHeightFraction: Float = 0.62f, + key: @Composable (pitchClass: Int, isBlack: Boolean, keyModifier: Modifier) -> Unit, +) { + BoxWithConstraints(modifier) { + val whiteW = maxWidth / 7 + val blackW = whiteW * blackWidthFraction + val blackH = maxHeight * blackHeightFraction + // White keys: C D E F G A B, filling the width evenly. + Row(Modifier.fillMaxSize()) { + for (pc in WHITE_PCS) key(pc, false, Modifier.weight(1f).fillMaxHeight()) + } + // Black keys, each centred on the boundary after a given white-key index. + for ((pc, afterWhite) in BLACK_PCS) { + key( + pc, true, + Modifier + .offset(x = whiteW * (afterWhite + 1) - blackW / 2) + .width(blackW) + .height(blackH), + ) + } + } +} + +/** + * A single piano key face: light for white keys, dark for black, dimmed when + * [enabled] is false and accent-filled when [selected]. The gesture (if any) is + * supplied by the caller through [modifier]. + */ +@Composable +fun PianoKey( + label: String, + isBlack: Boolean, + enabled: Boolean, + selected: Boolean, + modifier: Modifier, +) { + val c = LocalRetro.current + val bg = when { + selected -> c.accent + isBlack -> if (enabled) BLACK_KEY else BLACK_KEY_OFF + else -> if (enabled) WHITE_KEY else WHITE_KEY_OFF + } + val fg = when { + selected -> c.background + isBlack -> if (enabled) Color(0xFFDDDDDD) else Color(0xFF555555) + else -> if (enabled) Color(0xFF141414) else Color(0xFF6A6A6A) + } + Box( + modifier + .background(bg) + .border(1.dp, if (selected) c.accent else KEY_BORDER, RectangleShape), + contentAlignment = Alignment.BottomCenter, + ) { + Text( + label, color = fg, fontFamily = FontFamily.Monospace, fontSize = 8.sp, + modifier = Modifier.padding(bottom = 3.dp), + ) + } +} + +private val WHITE_PCS = intArrayOf(0, 2, 4, 5, 7, 9, 11) // C D E F G A B +// pitch class -> index of the white key it sits immediately after (0=C .. 6=B). +private val BLACK_PCS = listOf(1 to 0, 3 to 1, 6 to 3, 8 to 4, 10 to 5) +private val WHITE_KEY = Color(0xFFE8E8E8) +private val WHITE_KEY_OFF = Color(0xFF5A5A5A) +private val BLACK_KEY = Color(0xFF181820) +private val BLACK_KEY_OFF = Color(0xFF0C0C10) +private val KEY_BORDER = Color(0xFF000000) diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Widgets.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Widgets.kt index a779487..86bc9e4 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Widgets.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/components/Widgets.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.ui.graphics.RectangleShape import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -22,6 +23,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.reactorcoremeltdown.sizzletracker.ui.theme.LocalRetro @@ -33,17 +36,24 @@ import com.reactorcoremeltdown.sizzletracker.ui.theme.LocalRetro * Reusing these keeps every screen visually consistent. */ -/** A flat, square-cornered button. [active] paints it in the accent colour. */ +/** + * A flat, square-cornered button. [active] paints it in the accent colour. + * Pass [width] to pin a fixed width (e.g. a button whose label toggles, like + * PLAY/PAUSE) so it never resizes; the single-line label clips with an ellipsis + * if it can't fit. + */ @Composable fun RetroButton( label: String, modifier: Modifier = Modifier, active: Boolean = false, + width: Dp? = null, onClick: () -> Unit, ) { val c = LocalRetro.current Box( modifier + .then(if (width != null) Modifier.width(width) else Modifier) .border(1.dp, if (active) c.accent else c.grid, RectangleShape) .background(if (active) c.accent.copy(alpha = 0.18f) else c.surface) .clickable(onClick = onClick) @@ -55,6 +65,9 @@ fun RetroButton( color = if (active) c.accent else c.text, fontFamily = FontFamily.Monospace, fontSize = 12.sp, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, ) } } @@ -71,6 +84,9 @@ fun RetroDropdown( ) { val c = LocalRetro.current var open by remember { mutableStateOf(false) } + // Reserve the width of the longest option so the field never resizes as the + // selection changes — a common source of toolbar "jitter". + val longest = options.maxByOrNull { optionLabel(it).length }?.let(optionLabel).orEmpty() Box(modifier) { Box( Modifier @@ -79,9 +95,18 @@ fun RetroDropdown( .clickable { open = true } .padding(horizontal = 8.dp, vertical = 6.dp), ) { + // Invisible sizer (longest option) fixes the width; the visible value + // draws on top and clips with an ellipsis if it ever overflows. + Text( + "$label:$longest ▾", + color = Color.Transparent, + fontFamily = FontFamily.Monospace, fontSize = 12.sp, + maxLines = 1, softWrap = false, + ) Text( "$label:${optionLabel(selected)} ▾", color = c.text, fontFamily = FontFamily.Monospace, fontSize = 12.sp, + maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis, ) } DropdownMenu(expanded = open, onDismissRequest = { open = false }) { diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/mixer/MixerScreen.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/mixer/MixerScreen.kt index eb12035..c45b8fa 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/mixer/MixerScreen.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/mixer/MixerScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -21,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.reactorcoremeltdown.sizzletracker.model.Mixer @@ -74,6 +76,7 @@ private fun ChannelStrip(vm: AppViewModel, channel: MixerChannel, modifier: Modi selected = channel.instrumentSlot, optionLabel = { idx -> if (idx < 0) "—" else "${idx + 1}:${toolbox[idx].name}" }, onSelect = { channel.instrumentSlot = it; vm.bumpForToolbar() }, + modifier = Modifier.fillMaxWidth(), ) SectionLabel("FX 1-4") @@ -91,8 +94,12 @@ private fun ChannelStrip(vm: AppViewModel, channel: MixerChannel, modifier: Modi SectionLabel("MIDI Ch") Row(verticalAlignment = Alignment.CenterVertically) { RetroButton("-") { channel.midiChannel = (channel.midiChannel - 1).coerceAtLeast(1); vm.bumpForToolbar() } - Text(" ${channel.midiChannel} ", color = c.text, - fontFamily = FontFamily.Monospace, fontSize = 12.sp) + Text( + "${channel.midiChannel}", color = c.text, + fontFamily = FontFamily.Monospace, fontSize = 12.sp, + textAlign = TextAlign.Center, maxLines = 1, + modifier = Modifier.width(28.dp), + ) RetroButton("+") { channel.midiChannel = (channel.midiChannel + 1).coerceAtMost(16); vm.bumpForToolbar() } } @@ -115,7 +122,8 @@ private fun ChannelStrip(vm: AppViewModel, channel: MixerChannel, modifier: Modi } } -/** A vertical volume fader: fill grows from the bottom; tap or drag to set. */ +/** A vertical volume fader: fill grows from the bottom; tap or drag to set. + * Snaps to exactly zero (silence) near the bottom so full-off is easy to hit. */ @Composable private fun VerticalFader(value: Float, onValueChange: (Float) -> Unit, modifier: Modifier) { val c = LocalRetro.current @@ -124,12 +132,12 @@ private fun VerticalFader(value: Float, onValueChange: (Float) -> Unit, modifier .border(1.dp, c.grid, RectangleShape) .background(c.background) .pointerInput(Unit) { - detectTapGestures { off -> onValueChange((1f - off.y / size.height).coerceIn(0f, 1f)) } + detectTapGestures { off -> onValueChange(snapZero(1f - off.y / size.height)) } } .pointerInput(Unit) { detectDragGestures { change, _ -> change.consume() - onValueChange((1f - change.position.y / size.height).coerceIn(0f, 1f)) + onValueChange(snapZero(1f - change.position.y / size.height)) } }, contentAlignment = Alignment.BottomCenter, @@ -145,3 +153,11 @@ private fun VerticalFader(value: Float, onValueChange: (Float) -> Unit, modifier } } } + +/** Snap the fader to exactly 0 (silence) within a small band of the bottom. */ +private fun snapZero(raw: Float): Float { + val v = raw.coerceIn(0f, 1f) + return if (v < ZERO_SNAP_BAND) 0f else v +} + +private const val ZERO_SNAP_BAND = 0.03f diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/ToolboxScreen.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/ToolboxScreen.kt index f42bfab..aa3bcf5 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/ToolboxScreen.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/ToolboxScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -41,6 +40,8 @@ import com.reactorcoremeltdown.sizzletracker.model.ToolboxKind import com.reactorcoremeltdown.sizzletracker.model.ToolboxSlot import com.reactorcoremeltdown.sizzletracker.model.ToolboxType import com.reactorcoremeltdown.sizzletracker.ui.AppViewModel +import com.reactorcoremeltdown.sizzletracker.ui.components.PianoKey +import com.reactorcoremeltdown.sizzletracker.ui.components.PianoOctave import com.reactorcoremeltdown.sizzletracker.ui.theme.LocalRetro /** @@ -67,7 +68,9 @@ fun ToolboxScreen(vm: AppViewModel) { Column(Modifier.fillMaxSize().background(c.background)) { LazyVerticalGrid( columns = GridCells.Fixed(4), - modifier = Modifier.weight(1f).fillMaxWidth().padding(6.dp), + // Same top:bottom split as the tracker/arranger (1.7 : 1): tiles above, + // the piano audition keyboard below. + modifier = Modifier.weight(1.7f).fillMaxWidth().padding(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { @@ -147,11 +150,10 @@ private fun SlotTile( // ---------------------------------------------------------- test keyboard -private val ACCIDENTALS = setOf(1, 3, 6, 8, 10) // pitch-classes of the "black keys" - /** - * Two stacked octaves (one per row) that audition the selected instrument tile. - * Disabled (dimmed) when nothing is selected or the selection is an effect. + * Two stacked octaves (one per row), each drawn as a real piano keyboard, that + * audition the selected instrument tile. Disabled (dimmed) when nothing is selected + * or the selection is an effect. */ @Composable private fun AuditionKeyboard(vm: AppViewModel, slot: ToolboxSlot?, modifier: Modifier) { @@ -172,36 +174,28 @@ private fun AuditionKeyboard(vm: AppViewModel, slot: ToolboxSlot?, modifier: Mod ) val slotIndex = slot?.index ?: -1 val base = 48 // C3 - KeyRow(vm, slotIndex, isInstrument, base + 12, Modifier.weight(1f)) // upper octave (C4..B4) on top - KeyRow(vm, slotIndex, isInstrument, base, Modifier.weight(1f)) // lower octave (C3..B3) + PianoRow(vm, slotIndex, isInstrument, base + 12, Modifier.weight(1f)) // upper octave (C4..B4) on top + PianoRow(vm, slotIndex, isInstrument, base, Modifier.weight(1f)) // lower octave (C3..B3) } } +/** One octave of the audition keyboard: every key is playable (hold to sound it) + * when an instrument is selected, since this is for testing, not scale-locked. */ @Composable -private fun KeyRow(vm: AppViewModel, slotIndex: Int, enabled: Boolean, startMidi: Int, modifier: Modifier) { - Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(2.dp)) { - for (i in 0 until 12) { - Key(vm, slotIndex, enabled, startMidi + i, Modifier.weight(1f).fillMaxHeight()) - } - } -} - -@Composable -private fun Key(vm: AppViewModel, slotIndex: Int, enabled: Boolean, midi: Int, modifier: Modifier) { - val c = LocalRetro.current - val accidental = (midi % 12) in ACCIDENTALS - val keyColor = (if (accidental) c.grid else c.surface).let { if (enabled) it else it.copy(alpha = 0.4f) } - // Keep the gesture handler stable across selection changes. Keying pointerInput - // on `midi` alone (constant per key) means selecting a different instrument does - // NOT tear down and relaunch all 24 keys' gesture coroutines every tap — the - // live slot/enabled are read through rememberUpdatedState instead. - val liveSlot = rememberUpdatedState(slotIndex) - val liveEnabled = rememberUpdatedState(enabled) - Box( - modifier - .background(keyColor) - .border(1.dp, c.grid, RectangleShape) - .pointerInput(midi) { +private fun PianoRow(vm: AppViewModel, slotIndex: Int, enabled: Boolean, startMidi: Int, modifier: Modifier) { + PianoOctave(modifier.fillMaxWidth()) { pc, isBlack, keyMod -> + val midi = startMidi + pc + // Keep each key's gesture stable across selection changes: keyed on `midi` + // only (constant), reading the live slot/enabled via rememberUpdatedState so + // switching instruments doesn't relaunch every key's coroutine. + val liveSlot = rememberUpdatedState(slotIndex) + val liveEnabled = rememberUpdatedState(enabled) + PianoKey( + label = Pitch.name(midi).take(2), + isBlack = isBlack, + enabled = enabled, + selected = false, + modifier = keyMod.pointerInput(midi) { // Press → note on; release/cancel → note off (proper key behaviour). awaitEachGesture { awaitFirstDown() @@ -215,12 +209,6 @@ private fun Key(vm: AppViewModel, slotIndex: Int, enabled: Boolean, midi: Int, m } } }, - contentAlignment = Alignment.Center, - ) { - Text( - Pitch.name(midi), - color = if (accidental) c.textDim else c.text, - fontFamily = FontFamily.Monospace, fontSize = 9.sp, ) } } diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/CellEditorPopup.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/CellEditorPopup.kt index 1f876b9..e4bdf4e 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/CellEditorPopup.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/CellEditorPopup.kt @@ -29,6 +29,8 @@ import com.reactorcoremeltdown.sizzletracker.model.Cell import com.reactorcoremeltdown.sizzletracker.model.CellColumn import com.reactorcoremeltdown.sizzletracker.model.Pitch import com.reactorcoremeltdown.sizzletracker.ui.AppViewModel +import com.reactorcoremeltdown.sizzletracker.ui.components.PianoKey +import com.reactorcoremeltdown.sizzletracker.ui.components.PianoOctave import com.reactorcoremeltdown.sizzletracker.ui.components.RetroButton import com.reactorcoremeltdown.sizzletracker.ui.theme.LocalRetro @@ -82,7 +84,8 @@ fun CellEditorPopup(vm: AppViewModel, column: CellColumn, onDismiss: () -> Unit) } } -/** One octave of piano keys + an octave switcher, writing the note on tap. */ +/** A real one-octave piano keyboard + an octave switcher, writing the note on tap. + * Only in-scale keys are enabled (all keys when the scale is Chromatic). */ @Composable private fun NoteEditor(vm: AppViewModel, cell: Cell) { val c = LocalRetro.current @@ -90,6 +93,11 @@ private fun NoteEditor(vm: AppViewModel, cell: Cell) { // Start on the octave of the cell's note, or of the last note entered if empty. val seedMidi = if (cell.isPlayable) cell.note else vm.lastNote var octave by remember { mutableIntStateOf((seedMidi / 12 - 1).coerceIn(0, 9)) } + // Pitch-classes the current scale/root permits (Chromatic yields all twelve). + val root = vm.project.rootNote + val inKey = remember(vm.project.scale, root) { + vm.project.scale.intervals.map { (it + root) % 12 }.toSet() + } Column( Modifier.fillMaxWidth(), @@ -104,16 +112,16 @@ private fun NoteEditor(vm: AppViewModel, cell: Cell) { Text("OCTAVE $octave", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 14.sp) RetroButton("+") { octave = (octave + 1).coerceAtMost(9) } } - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(2.dp)) { - for (pc in 0 until 12) { - val midi = ((octave + 1) * 12 + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST) - PianoKey( - label = Pitch.name(midi).take(2), - accidental = pc in ACCIDENTAL_PCS, - selected = cell.isPlayable && cell.note == midi, - modifier = Modifier.weight(1f), - ) { vm.setFocusedNote(midi) } - } + PianoOctave(Modifier.fillMaxWidth().height(104.dp)) { pc, isBlack, keyMod -> + val midi = ((octave + 1) * 12 + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST) + val enabled = pc in inKey + PianoKey( + label = Pitch.name(midi).take(2), + isBlack = isBlack, + enabled = enabled, + selected = cell.isPlayable && cell.note == midi, + modifier = if (enabled) keyMod.clickable { vm.setFocusedNote(midi) } else keyMod, + ) } Text( "NOTE: " + when { @@ -126,37 +134,6 @@ private fun NoteEditor(vm: AppViewModel, cell: Cell) { } } -@Composable -private fun PianoKey( - label: String, - accidental: Boolean, - selected: Boolean, - modifier: Modifier, - onClick: () -> Unit, -) { - val c = LocalRetro.current - val bg = when { - selected -> c.accent.copy(alpha = 0.45f) - accidental -> c.grid - else -> c.background - } - Box( - modifier - .height(72.dp) - .border(1.dp, if (selected) c.accent else c.grid, RectangleShape) - .background(bg) - .clickable(onClick = onClick), - contentAlignment = Alignment.BottomCenter, - ) { - Text( - label, - color = if (accidental) c.textDim else c.text, - fontFamily = FontFamily.Monospace, fontSize = 9.sp, - modifier = Modifier.padding(bottom = 4.dp), - ) - } -} - /** A titled value readout with - / + steppers; [onStep] receives -1 or +1. */ @Composable private fun Stepper(title: String, value: String, onStep: (Int) -> Unit) { @@ -177,7 +154,6 @@ private fun Stepper(title: String, value: String, onStep: (Int) -> Unit) { } } -private val ACCIDENTAL_PCS = setOf(1, 3, 6, 8, 10) private fun velLabel(cell: Cell) = cell.velocity.toString(16).uppercase().padStart(2, '0') private fun chanLabel(cell: Cell) = cell.channel.toString().padStart(2, '0') private fun lineLabel(line: Int) = line.toString(16).uppercase().padStart(2, '0') diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/TrackerScreen.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/TrackerScreen.kt index 3332707..b4ad240 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/TrackerScreen.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/tracker/TrackerScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,6 +18,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.reactorcoremeltdown.sizzletracker.model.TimeSignature @@ -74,15 +76,17 @@ private fun TransportToolbar(vm: AppViewModel) { horizontalArrangement = Arrangement.spacedBy(6.dp), ) { RetroButton("⧉ STOP", onClick = vm::stop) + // Fixed width so the button doesn't resize as the label toggles. RetroButton(if (transport.isPlaying) "❚❚ PAUSE" else "▶ PLAY", - active = transport.isPlaying, onClick = vm::playPause) - // Tempo nudger: tap -/+ around a readout. - Row { + active = transport.isPlaying, width = 112.dp, onClick = vm::playPause) + // Tempo nudger: tap -/+ around a fixed-width readout. + Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { RetroButton("-", onClick = { vm.setTempo(project.tempoBpm - 1) }) Text( - " ${project.tempoBpm.toInt()} BPM ", + "${project.tempoBpm.toInt()} BPM", color = c.text, fontFamily = FontFamily.Monospace, fontSize = 12.sp, - modifier = Modifier.padding(vertical = 6.dp), + textAlign = TextAlign.Center, maxLines = 1, + modifier = Modifier.width(72.dp).padding(vertical = 6.dp), ) RetroButton("+", onClick = { vm.setTempo(project.tempoBpm + 1) }) } @@ -93,8 +97,6 @@ private fun TransportToolbar(vm: AppViewModel) { optionLabel = { it.label }, onSelect = vm::setTimeSignature, ) - // Insert a note-off ("===") at the cursor. - RetroButton("OFF ===", onClick = vm::noteOffFocused) } // Row 2: block + length + scale + root. Row( @@ -141,6 +143,8 @@ private fun TransportToolbar(vm: AppViewModel) { RetroButton("COPY", onClick = vm::copySelection) RetroButton("PASTE", active = vm.canPaste, onClick = vm::pasteClipboard) RetroButton("DEL", onClick = vm::deleteSelection) + // Insert a note-off ("===") at the cursor. + RetroButton("OFF ===", onClick = vm::noteOffFocused) } } }