fix(instrument-ui): distinguish Disabled from Off, add hover cue to mode toggles, close review minors
Adds a hairline outline for Disabled chrome buttons, resolves Hover on the four mode-selector single-buttons, re-measures the control-surface doc's 1.2 layout table post-reflow, and adds a structural test pinning every enable/mode toggle to a derived segment.
This commit is contained in:
@@ -18,8 +18,9 @@ double clamp(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi
|
||||
constexpr int kEnvModeW = 46;
|
||||
|
||||
// The single-button enables ("Envelope" / "Filter" / "Limiter"). One width because they are one
|
||||
// control grammar. MASTER is the group that BINDS it: its caption row and knob row both measure
|
||||
// 130, so anything past 64 widens kDeckSpanningW and spends the editor's width budget.
|
||||
// control grammar. MASTER is the group that BINDS it: its knob row measures 130 (the caption
|
||||
// row, at this width, measures 118), so anything past 64 widens kDeckSpanningW and spends the
|
||||
// editor's width budget.
|
||||
constexpr int kEnableBtnW = 52;
|
||||
|
||||
DeckToggleDesc enableButton(DeckParam p) {
|
||||
|
||||
@@ -18,7 +18,7 @@ constexpr int kStripBandHeight = 30;
|
||||
|
||||
constexpr int kRunGap = 6; // between adjacent items of the toolbar run
|
||||
constexpr int kChanBtnW = 60; // fits the longer of the two mode labels ("Stereo")
|
||||
constexpr int kChanSegH = 18;
|
||||
constexpr int kChanBtnH = 18; // shared height of the Loop and channel-mode single buttons
|
||||
// If the title slot ever fails to hold its text at the editor's floor, THESE narrow — the
|
||||
// floor does not move.
|
||||
constexpr int kLoopBtnW = 52;
|
||||
@@ -54,14 +54,14 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
|
||||
r.navBrowse = Rect::ltrb(leftOf(navRight, kNavButtonWidth), navTop, navRight,
|
||||
navTop + navH);
|
||||
|
||||
const int chanTop = topFor(kChanSegH);
|
||||
const int chanTop = topFor(kChanBtnH);
|
||||
const int chanRight = leftOf(r.navBrowse.x, kRunGap);
|
||||
r.channel = Rect::ltrb(leftOf(chanRight, kChanBtnW), chanTop, chanRight,
|
||||
chanTop + kChanSegH);
|
||||
chanTop + kChanBtnH);
|
||||
|
||||
const int loopRight = leftOf(r.channel.x, kRunGap);
|
||||
r.loop = Rect::ltrb(leftOf(loopRight, kLoopBtnW), chanTop, loopRight,
|
||||
chanTop + kChanSegH);
|
||||
chanTop + kChanBtnH);
|
||||
|
||||
const int cellH = std::min(row.height, knobSize + kVelLabelH);
|
||||
const int cellTop = topFor(cellH);
|
||||
|
||||
@@ -78,8 +78,9 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
|
||||
// still offer to reverse. One button, so the click's meaning is "the other state": read the
|
||||
// current enable off the markers, which is the same source the button's paint reads.
|
||||
if (loopControlsLive() && contains(cr.loop, x, y)) {
|
||||
// setLoopEnabled owns the frames <= 0 guard (empty capture) — no need to repeat it here.
|
||||
const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
|
||||
if (frames > 0) setLoopEnabled(!pickedMarkers(frames).hasLoop);
|
||||
setLoopEnabled(!pickedMarkers(frames).hasLoop);
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -226,15 +226,35 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
|
||||
: (active ? InteractionState::Active
|
||||
: (hov ? InteractionState::Hover : InteractionState::Rest));
|
||||
fillSurface(bmp, toKitBox(r), Role::BgCell, st);
|
||||
// Disabled's wash over bg/cell is barely a shade off Rest's — the third state needs
|
||||
// its own mark, not just a slightly-quieter fill, so it draws the one outline this
|
||||
// control ever gets. See PLAN.md's Off-vs-Disabled ruling: Off is live and
|
||||
// clickable, Disabled is not, and the two must not read as the same thing.
|
||||
if (disabled) {
|
||||
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||
}
|
||||
kitTextCentered(bmp, r, label, kToolbarFont,
|
||||
active && !disabled ? Role::BgBase
|
||||
: (hov ? Role::TextPrimary : Role::TextDim));
|
||||
};
|
||||
drawChromeToggle(cr.loop, "Loop", HoverKind::kLoop, marks.hasLoop,
|
||||
!loopControlsLive());
|
||||
|
||||
// The channel button is a MODE selector like the deck's Stage|Spline buttons — always
|
||||
// "on", so InteractionState::Active would otherwise swallow every hover. Filled by
|
||||
// accent/primary rather than bg/cell so Hover's mix-toward-accent/hot actually moves the
|
||||
// surface instead of nudging bg/cell by a few percent (roleColorState's Active case
|
||||
// always answers accent/primary regardless of the role passed in, so Rest is unreachable
|
||||
// here and this is purely which color Hover mixes FROM).
|
||||
const auto drawModeChromeToggle = [&](const Rect& r, const char* label, HoverKind hk) {
|
||||
const bool hov = isHovered(hk, -1);
|
||||
const InteractionState st = hov ? InteractionState::Hover : InteractionState::Active;
|
||||
fillSurface(bmp, toKitBox(r), Role::AccentPrimary, st);
|
||||
kitTextCentered(bmp, r, label, kToolbarFont, Role::BgBase);
|
||||
};
|
||||
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
|
||||
drawChromeToggle(cr.channel, isStereo ? "Stereo" : "Mono", HoverKind::kChannel,
|
||||
/*active=*/true, /*disabled=*/false);
|
||||
drawModeChromeToggle(cr.channel, isStereo ? "Stereo" : "Mono", HoverKind::kChannel);
|
||||
}
|
||||
|
||||
// The strip row: the full 128-key piano with the root lit. The loaded capture responds
|
||||
|
||||
@@ -135,10 +135,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
disabled ? Role::TextDim
|
||||
: (seg1Active ? Role::BgBase : Role::TextPrimary));
|
||||
};
|
||||
// The single-button forms. ENABLE names what it controls and reads Primary on / dim gray
|
||||
// off; MODE reads the current mode and has no off state. A control the MODE refuses draws
|
||||
// Disabled, which is a third state and not a synonym for off: off is live and clickable,
|
||||
// and the washed Disabled surface is what separates them.
|
||||
// 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);
|
||||
@@ -151,9 +154,19 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
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) {
|
||||
drawButtonToggle(t, mode == EnvMode::Spline ? "Spline" : "Stage", /*active=*/true,
|
||||
/*disabled=*/false);
|
||||
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);
|
||||
|
||||
@@ -224,8 +237,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;
|
||||
|
||||
Reference in New Issue
Block a user