Merge Ω-W1-T4: deck reflow, focus-by-click overlay selection, single-button toggles

This commit is contained in:
2026-08-03 14:30:59 -04:00
23 changed files with 990 additions and 524 deletions
+54 -30
View File
@@ -162,6 +162,43 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
disabled ? Role::TextDim
: (seg1Active ? Role::BgBase : Role::TextPrimary));
};
// The single-button ENABLE form. Reads Primary on / dim gray off; a control the ENABLE
// gates draws Disabled, a third state and not a synonym for off (off is live and
// clickable). No deck ENABLE currently drives `disabled` true through this path — the
// deck's one Disabled control (FILTER's Band|Notch law when the filter itself is off)
// stays segmented and draws through `drawToggle` above — but the parameter stays for
// parity with that segmented form and because a future ENABLE could plausibly gate on
// something else the way Loop (chrome) already does outside this deck.
const auto drawButtonToggle = [&](const DeckToggleLayout& t, const char* label, bool active,
bool disabled) {
const bool hov = !disabled && isHovered(HoverKind::kControl, t.id);
// Hover outranks Active here (the reverse of the old order) so an enabled button still
// gets a hover cue — same gap drawModeToggle's comment above explains, just on the
// ENABLE form's own base role: mixing toward accent/hot FROM accent/primary (rather
// than bg/cell) is what makes the already-lit button visibly brighten on hover.
const InteractionState st =
disabled ? InteractionState::Disabled
: (hov ? InteractionState::Hover
: (active ? InteractionState::Active : InteractionState::Rest));
fillSurface(bmp, toKitBox(t.seg0), active ? Role::AccentPrimary : Role::BgCell, st);
kitTextCentered(bmp, t.seg0, label, Font::Micro,
active && !disabled ? Role::BgBase
: (hov ? Role::TextPrimary : Role::TextDim));
};
// The single-button MODE form: the label reads the current mode, so there is no off state
// and no dim-gray rest — it is always "on". Filled by accent/primary rather than routed
// through drawButtonToggle (which would force InteractionState::Active regardless of hov,
// leaving MODE with no hover cue at all): Hover mixes toward accent/hot FROM whatever base
// roleColorState is handed, so passing accent/primary here is what lets the already-lit
// button visibly brighten on hover instead of a hover mix nobody would notice against
// bg/cell's dark base.
const auto drawModeToggle = [&](const DeckToggleLayout& t, EnvMode mode) {
const char* label = mode == EnvMode::Spline ? "Spline" : "Stage";
const bool hov = isHovered(HoverKind::kControl, t.id);
const InteractionState st = hov ? InteractionState::Hover : InteractionState::Active;
fillSurface(bmp, toKitBox(t.seg0), Role::AccentPrimary, st);
kitTextCentered(bmp, t.seg0, label, Font::Micro, Role::BgBase);
};
const bool anySpline = splineActive(play);
// The knob's short name label (swapped for the live value during hover/drag — no third
@@ -209,10 +246,14 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
};
for (const DeckGroupLayout& g : dl.groups) {
// The fence: a bg/panel box with a hairline border, caption micro-caps left.
// The fence: a bg/panel box with a hairline border, caption micro-caps left. An
// envelope deck whose overlay is the one on the waveform takes the primary accent
// instead — the deck itself is the selection affordance, so the whole box says so.
const OverlayEnv groupEnv = overlayEnvForGroup(g.id);
const bool focused = groupEnv != OverlayEnv::kNone && groupEnv == overlayEnv_;
fillSurface(bmp, toKitBox(g.box), Role::BgPanel, InteractionState::Rest);
LICE_DrawRect(bmp, g.box.x, g.box.y, g.box.width - 1, g.box.height - 1,
hairline, 1.0f, 0);
focused ? toLice(roleColor(Role::AccentPrimary)) : hairline, 1.0f, 0);
const char* caption = "";
switch (g.id) {
case kGroupAmpEnv: caption = "AMP ENVELOPE"; break;
@@ -227,8 +268,9 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
}
kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);
// The gain-reduction lamp. ROUND, where the overlay radios in this same slot are
// square, so it reads as a lamp rather than a control.
// The gain-reduction lamp — MASTER's own corner slot, the one the env decks' overlay
// radios used to share before focus-by-click replaced them. ROUND, not square, so it
// reads as a passive readout rather than a control.
if (g.captionRadio.id >= 0 && g.captionRadio.passive) {
const Rect& rb = g.captionRadio.box;
const float r = rb.width / 2.0f - 0.5f;
@@ -237,26 +279,6 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
: Role::LineHairline)),
1.0f, 0, true);
}
// The overlay-select radio: filled in the tertiary accent (the colour the overlay
// traces in) when this group's envelope is the one on the waveform, hollow otherwise.
if (g.captionRadio.id >= 0 && !g.captionRadio.passive) {
// overlayEnvForRadio returns kNone for BOTH "not a radio id" and "no selection" —
// a non-radio id must never read as lit just because nothing is selected, so the
// picked env has to be checked against kNone itself, not just matched by equality.
const OverlayEnv picked = overlayEnvForRadio(g.captionRadio.id);
const bool on = picked != OverlayEnv::kNone && overlayEnv_ == picked;
const bool hov = isHovered(HoverKind::kEnvRadio, g.captionRadio.id);
const Rect& rb = g.captionRadio.box;
LICE_DrawRect(bmp, rb.x, rb.y, rb.width - 1, rb.height - 1,
toLice(roleColor(on || hov ? Role::AccentTertiary
: Role::LineHairline)),
1.0f, 0);
if (on) {
LICE_FillRect(bmp, rb.x + 3, rb.y + 3, rb.width - 6, rb.height - 6,
toLice(roleColor(Role::AccentTertiary)), 1.0f, 0);
}
}
// The compact caption toggles (right-anchored in the caption row, never full-width).
for (const DeckToggleLayout* tp : {&g.captionToggle, &g.captionToggle2}) {
if (tp->id < 0) continue;
@@ -271,13 +293,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
play.pitchEngine == PitchEngine::Preserve, false);
break;
case ParamControl::kPitchEnvEnable:
drawToggle(t, "Off", "On", play.pitchEnv.enabled, false);
drawButtonToggle(t, "Envelope", play.pitchEnv.enabled, false);
break;
case ParamControl::kVoiceMode:
drawToggle(t, "Poly", "Mono", isMono, false);
break;
case ParamControl::kFilterEnable:
drawToggle(t, "Off", "On", play.filter.enabled, false);
drawButtonToggle(t, "Filter", play.filter.enabled, false);
break;
case ParamControl::kFilterLaw:
drawToggle(t, "Band", "Notch",
@@ -286,16 +308,18 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
!play.filter.enabled);
break;
case ParamControl::kLimiterEnable:
drawToggle(t, "Off", "On", params_.limiterEnabled, false);
drawButtonToggle(t, "Limiter", params_.limiterEnabled, false);
break;
// A mode SELECTOR: the label is the state, so it is drawn Active either way —
// there is nothing here for a dim-gray off to mean.
case ParamControl::kAmpEnvMode:
drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
drawModeToggle(t, play.ampSpline.mode);
break;
case ParamControl::kPitchEnvMode:
drawToggle(t, "Stg", "Spl", play.pitchSpline.mode == EnvMode::Spline, false);
drawModeToggle(t, play.pitchSpline.mode);
break;
case ParamControl::kFilterEnvMode:
drawToggle(t, "Stg", "Spl", play.filterSpline.mode == EnvMode::Spline, false);
drawModeToggle(t, play.filterSpline.mode);
break;
default: break;
}