UI Improvements: Piano widget, rigid sizes, proportions, etc
This commit is contained in:
@@ -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)
|
||||
@@ -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 <T> 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 <T> 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 }) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
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)
|
||||
Box(
|
||||
modifier
|
||||
.background(keyColor)
|
||||
.border(1.dp, c.grid, RectangleShape)
|
||||
.pointerInput(midi) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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),
|
||||
accidental = pc in ACCIDENTAL_PCS,
|
||||
isBlack = isBlack,
|
||||
enabled = enabled,
|
||||
selected = cell.isPlayable && cell.note == midi,
|
||||
modifier = Modifier.weight(1f),
|
||||
) { vm.setFocusedNote(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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user