// editor_paint_deck.cpp — the DECKS band's painter: the fenced control groups, their // captions, the compact caption and row toggles, and the radial knobs with the label<->value // swap on hover/drag. Windows-only; the deck's cell geometry is the pure knob_deck layout and // its group composition the pure deck_groups list. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include #include #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" // resolveLaneSplit (THE lane-split fold) #include "shell/instrument/editor_internal.h" // kit adapters + knob face #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; // kit vocabulary using namespace reasampler::instrument::ui; // deck geometry // The knob's own name/value band. One size up from the group captions and the toggles, which // 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 the RESOLVED 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); // A column narrower than the interior needs yields all-empty rects, which under rect.h's // contract means suppressed — not a zero-height field to fill, tick twelve times and cap. if (m.field.empty()) return; 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(instrument::engine::kMeterTopDb); db >= static_cast(instrument::engine::kMeterFloorDb); db -= static_cast(kMeterTickStepDb)) { const int y = meterDbToY(m.field, db); const bool zero = (db == 0); LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1, zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0); if (meterTickNumeralled(db)) { kitText(bmp, meterNumeralRect(m.labels, y), tickLabel(db).c_str(), Font::Micro, Role::TextDim, Align::Right); } } // The bars. A single-lane surface shows ONE bar folding both channels per field // (meterSingleLaneState) — 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) { drawBar(m.barA, meterSingleLaneState(state)); } 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; const DeckLayout dl = layoutDeck(fl.deckDescs, deckArea.x, deckArea.y, deckArea.width); 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 — // resolveLaneSplit is the one home of it. Asked directly rather than read back off // waveformSurface, whose laneCount additionally folds in the waveform BAND's pixel height, // which decides nothing about how many channels the bus is carrying. const LaneSplit meterSplit = resolveLaneSplit(channelMode_ == ChannelMode::Stereo, channelPcmFor(selectedId_).channelCount); // 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. // `seg0Disabled` disables ONE segment: Gate is unselectable while an EG is drawn, but // Trigger — the mode it is stuck in — must still read as the live choice. const auto drawToggle = [&](const DeckToggleLayout& t, const char* s0, const char* s1, bool seg1Active, bool disabled, bool seg0Disabled = false) { const bool hov = !disabled && isHovered(HoverKind::kControl, t.id); const bool d0 = disabled || seg0Disabled; const InteractionState st0 = d0 ? InteractionState::Disabled : (!seg1Active ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest)); const InteractionState st1 = disabled ? InteractionState::Disabled : (seg1Active ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest)); fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st0); fillSurface(bmp, toKitBox(t.seg1), Role::BgCell, st1); kitTextCentered(bmp, t.seg0, s0, Font::Micro, d0 ? Role::TextDim : (!seg1Active ? Role::BgBase : Role::TextPrimary)); kitTextCentered(bmp, t.seg1, s1, Font::Micro, 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. const auto drawButtonToggle = [&](const DeckToggleLayout& t, const char* label, bool active, bool disabled) { const bool hov = !disabled && isHovered(HoverKind::kControl, t.id); const InteractionState st = disabled ? InteractionState::Disabled : (active ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest)); fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st); kitTextCentered(bmp, t.seg0, label, Font::Micro, active && !disabled ? Role::BgBase : (hov ? Role::TextPrimary : Role::TextDim)); }; const auto drawModeToggle = [&](const DeckToggleLayout& t, EnvMode mode) { drawButtonToggle(t, mode == EnvMode::Spline ? "Spline" : "Stage", /*active=*/true, /*disabled=*/false); }; const bool anySpline = splineActive(play); // The knob's short name label (swapped for the live value during hover/drag — no third // line, no permanent value clutter). const auto knobName = [](ParamControl c) -> const char* { switch (c) { case ParamControl::kAttack: return "Attack"; case ParamControl::kHold: return "Hold"; case ParamControl::kDecay: return "Decay"; case ParamControl::kSustain: return "Sustain"; case ParamControl::kRelease: return "Release"; case ParamControl::kTrigLength: return "Len %"; case ParamControl::kTrigAttack: return "Attack"; case ParamControl::kTrigHold: return "Hold"; case ParamControl::kTrigDecay: return "Decay"; case ParamControl::kKeyTrack: return "Key Trk"; case ParamControl::kRate: return "Rate"; case ParamControl::kPitch: return "Pitch"; case ParamControl::kPitchEnvAttack: return "P.Att"; case ParamControl::kPitchEnvHold: return "P.Hold"; case ParamControl::kPitchEnvDecay: return "P.Dec"; case ParamControl::kPitchEnvDepth: return "P.Depth"; case ParamControl::kVoiceCount: return "Voices"; case ParamControl::kMasterGain: return "Gain"; case ParamControl::kFilterMorph: return "Mode"; case ParamControl::kFilterCutoff: return "Cutoff"; case ParamControl::kFilterQ: return "Res"; case ParamControl::kFilterDrive: return "Drive"; case ParamControl::kFilterModAmt: return "Mod"; case ParamControl::kFilterVel: return "Vel"; case ParamControl::kFilterKeyTrack: return "Key Trk"; case ParamControl::kAmpVelCurve: return "Amp"; case ParamControl::kPitchVelCurve: return "Pitch"; case ParamControl::kFilterVelCurve: return "Filter"; case ParamControl::kFilterEnvAttack: return "F.Att"; case ParamControl::kFilterEnvHold: return "F.Hold"; case ParamControl::kFilterEnvDecay: return "F.Dec"; case ParamControl::kFilterEnvSustain: return "F.Sus"; case ParamControl::kFilterEnvRelease: return "F.Rel"; case ParamControl::kFilterTrigAttack: return "F.Att"; case ParamControl::kFilterTrigHold: return "F.Hold"; case ParamControl::kFilterTrigDecay: return "F.Dec"; default: return ""; } }; for (const DeckGroupLayout& g : dl.groups) { // 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, focused ? toLice(roleColor(Role::AccentPrimary)) : hairline, 1.0f, 0); const char* caption = ""; switch (g.id) { case kGroupAmpEnv: caption = "AMP ENVELOPE"; break; case kGroupPitch: caption = "PITCH/RATE"; break; case kGroupPitchEnv: caption = "PITCH ENV"; break; case kGroupFilter: caption = "FILTER"; break; case kGroupFilterEnv: caption = "FILTER ENV"; break; case kGroupVelocity: caption = "VELOCITY"; break; case kGroupVoice: caption = "VOICE"; break; case kGroupMaster: caption = "MASTER"; break; default: break; } 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 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; const DeckToggleLayout& t = *tp; switch (static_cast(t.id)) { case ParamControl::kPlayMode: drawToggle(t, "Gate", "Trigger", play.playMode == PlayMode::Trigger, false, /*seg0Disabled=*/anySpline); break; case ParamControl::kPitchEngine: drawToggle(t, "Varisp", "Presrv", play.pitchEngine == PitchEngine::Preserve, false); break; case ParamControl::kPitchEnvEnable: drawButtonToggle(t, "Envelope", play.pitchEnv.enabled, false); break; case ParamControl::kVoiceMode: drawToggle(t, "Poly", "Mono", isMono, false); break; case ParamControl::kFilterEnable: drawButtonToggle(t, "Filter", 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: 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: drawModeToggle(t, play.ampSpline.mode); break; case ParamControl::kPitchEnvMode: drawModeToggle(t, play.pitchSpline.mode); break; case ParamControl::kFilterEnvMode: drawModeToggle(t, play.filterSpline.mode); break; default: break; } } // The one row toggle left: VOICE's Retrig|Legato, live only in Mono. if (g.rowToggle.id >= 0) { 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) { const bool disabled = deckKnobDisabled(c.id); // A VELOCITY cell is a popup opener, not a dial: it shows its curve in miniature // where a knob face would be, and its whole cell is the click target. const CurveTarget curveCell = curveTargetFor(c.id); if (curveCell != CurveTarget::kNone) { paintCurveButton(bmp, c.knob, curveCell, disabled, !disabled && isHovered(HoverKind::kControl, c.id)); kitTextCentered(bmp, c.label, knobName(static_cast(c.id)), kCellLabelFont, Role::TextDim); continue; } const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id); const bool hov = !disabled && isHovered(HoverKind::kControl, c.id); const InteractionState st = disabled ? InteractionState::Disabled : (dragging ? InteractionState::Dragging : (hov ? InteractionState::Hover : InteractionState::Rest)); drawKnobFace(bmp, c.knob, deckControlNorm(c.id), st); // The inner dial rides only the knobs whose stage is sloped — deck_groups owns // that rule, so a Hold or Sustain cell simply has no curve id and draws none. const ParamControl curve = curveParamFor(static_cast(c.id)); const bool innerDragging = (drag_ == DragKind::kDeckKnob && dragInnerCellId_ == c.id); const bool innerHov = !disabled && isHovered(HoverKind::kInnerDial, c.id); if (curve != ParamControl::kCount) { const InteractionState ist = disabled ? InteractionState::Disabled : (innerDragging ? InteractionState::Dragging : (innerHov ? InteractionState::Hover : InteractionState::Rest)); drawInnerDial(bmp, c.inner, deckControlNorm(static_cast(curve)), ist); } // One label band, so the inner dial's readout takes it while the inner dial is the // one being touched. std::string label; if (innerDragging || innerHov) label = deckValueLabel(static_cast(curve)); else if (dragging || hov) label = deckValueLabel(c.id); else label = std::string(knobName(static_cast(c.id))); kitTextCentered(bmp, c.label, label.c_str(), kCellLabelFont, Role::TextDim); } } } } // namespace reasampler::vst #endif // _WIN32