Reflow the deck into two categorical rows plus a double-height MASTER bus deck

Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
This commit is contained in:
2026-08-02 07:41:10 -04:00
parent f60c05c042
commit 0627398bbb
19 changed files with 1239 additions and 236 deletions
+104 -11
View File
@@ -12,6 +12,8 @@
#include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's state)
#include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize
#include "core/instrument/ui/master_meter.h" // the bus meter's column interior + ballistics
#include "core/instrument/ui/waveform_view.h" // waveformSurface (THE lane-split fold)
#include "shell/instrument/editor_internal.h" // kit adapters + knob face
#include "shell/instrument/reasampler_processor.h"
@@ -24,6 +26,75 @@ using namespace reasampler::instrument::ui; // deck geometry
// are chrome you read once — this is the readout you read while turning something.
constexpr Font kCellLabelFont = Font::Label;
namespace {
// One tick's numeral, in whole dB ("0", "-12"). No unit suffix — the column is 22px wide and
// the scale's unit is stated once, by the caption.
std::string tickLabel(int db) {
char buf[8];
std::snprintf(buf, sizeof(buf), "%d", db);
return std::string(buf);
}
// The MASTER column: dB scale in the label gutter, one or two bars, the held peak tick, and
// the latched clip cap. `split` is waveformSurface's own lane decision — see master_meter.h.
void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi& state,
LaneSplit split) {
if (column.width <= 0 || column.height <= 0) return;
const MeterRects m = meterRects(column, split);
fillSurface(bmp, toKitBox(m.field), Role::BgCell, InteractionState::Rest);
// Scale: a rule every 6 dB, numeralled every 12 with 0 dB heavier — the reference the
// limiter-off case is read against.
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
for (int db = static_cast<int>(instrument::engine::kMeterTopDb);
db >= static_cast<int>(instrument::engine::kMeterFloorDb);
db -= static_cast<int>(kMeterTickStepDb)) {
const int y = meterDbToY(m.field, db);
const bool zero = (db == 0);
const bool numeralled = zero || (db % 12 == 0);
LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1,
zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
if (numeralled) {
kitText(bmp, Rect::ltrb(m.labels.x, y - 5, m.labels.right(), y + 5),
tickLabel(db).c_str(), Font::Micro, Role::TextDim, Align::Right);
}
}
// The bars. A single-lane surface shows ONE bar off the louder channel: the two are the
// same signal there (dual-mono), so two bars would be a duplicate rather than a reading.
const LICE_pixel barInk = toLice(roleColor(Role::AccentPrimary));
const LICE_pixel holdInk = toLice(roleColor(Role::TextPrimary));
const auto drawBar = [&](const Rect& bar, const instrument::engine::MeterState& ch) {
if (bar.empty()) return;
const int top = meterDbToY(bar, ch.levelDb);
if (top < bar.bottom()) {
LICE_FillRect(bmp, bar.x, top, bar.width, bar.bottom() - top, barInk, 1.0f, 0);
}
if (ch.holdDb > instrument::engine::kMeterFloorDb) {
// Clamped so the 2px tick cannot hang past the bar when the hold sits on the floor.
const int hold = (std::min)(meterDbToY(bar, ch.holdDb), bar.bottom() - 2);
LICE_FillRect(bmp, bar.x, hold, bar.width, 2, holdInk, 1.0f, 0);
}
};
if (split == LaneSplit::Single) {
const instrument::engine::MeterState& loudest =
state.left.levelDb >= state.right.levelDb ? state.left : state.right;
drawBar(m.barA, loudest);
} else {
drawBar(m.barA, state.left);
drawBar(m.barB, state.right);
}
// The clip cap: latched over the whole field, click to clear.
if (meterClipped(state)) {
LICE_FillRect(bmp, m.field.x, m.field.y, m.field.width, 3,
toLice(roleColor(Role::Warn)), 1.0f, 0);
}
}
} // namespace
void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
const Rect& deckArea = fl.bands.decks;
if (deckArea.width <= 0 || deckArea.height <= 0) return;
@@ -31,6 +102,14 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
const PlaySeconds& play = params_.play;
const bool isMono = (voiceMode_ == VoiceMode::Mono);
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
// The meter's bar count is the SAME resolved decision the waveform's lane split is — read
// off waveformSurface rather than re-derived, so it can never become a second rule.
const LaneSplit meterSplit =
waveformSurface(fl.bands.waveform, channelMode_ == ChannelMode::Stereo,
channelPcmFor(selectedId_).channelCount)
.laneCount == 2
? LaneSplit::Stereo
: LaneSplit::Single;
// One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
// segments draw inert so the dependency (Retrig|Legato needs Mono) reads at a glance.
@@ -122,9 +201,19 @@ 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.
if (g.captionRadio.id >= 0 && g.captionRadio.passive) {
const Rect& rb = g.captionRadio.box;
const float r = rb.width / 2.0f - 0.5f;
LICE_FillCircle(bmp, rb.x + rb.width / 2.0f, rb.y + rb.height / 2.0f, r,
toLice(roleColor(grLampLit(masterMeter_) ? Role::Warn
: 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) {
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.
@@ -164,6 +253,15 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
case ParamControl::kFilterEnable:
drawToggle(t, "Off", "On", play.filter.enabled, false);
break;
case ParamControl::kFilterLaw:
drawToggle(t, "Band", "Notch",
play.filter.settings.morphLaw ==
instrument::engine::filter::MorphLaw::HighNotchLow,
!play.filter.enabled);
break;
case ParamControl::kLimiterEnable:
drawToggle(t, "Off", "On", params_.limiterEnabled, false);
break;
case ParamControl::kAmpEnvMode:
drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
break;
@@ -176,19 +274,14 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
default: break;
}
}
// Row toggles: VOICE's Retrig|Legato (live only in Mono) and FILTER's morph law.
// The one row toggle left: VOICE's Retrig|Legato, live only in Mono.
if (g.rowToggle.id >= 0) {
if (static_cast<ParamControl>(g.rowToggle.id) == ParamControl::kFilterLaw) {
drawToggle(g.rowToggle, "Band", "Notch",
play.filter.settings.morphLaw ==
instrument::engine::filter::MorphLaw::HighNotchLow,
!play.filter.enabled);
} else {
drawToggle(g.rowToggle, "Retrig", "Legato",
monoTrigger_ == MonoTrigger::Legato, !isMono);
}
drawToggle(g.rowToggle, "Retrig", "Legato",
monoTrigger_ == MonoTrigger::Legato, !isMono);
}
if (g.column.id >= 0) paintMeterColumn(bmp, g.column.box, masterMeter_, meterSplit);
// The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
// The predicate is the input side's, so the drawn state and the inert grab agree.
for (const DeckCellLayout& c : g.cells) {