// 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 "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; 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)); // 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)); }; 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. 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); 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 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) { // 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; 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: drawToggle(t, "Off", "On", 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); break; case ParamControl::kAmpEnvMode: drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false); break; case ParamControl::kPitchEnvMode: drawToggle(t, "Stg", "Spl", play.pitchSpline.mode == EnvMode::Spline, false); break; case ParamControl::kFilterEnvMode: drawToggle(t, "Stg", "Spl", play.filterSpline.mode == EnvMode::Spline, false); break; default: break; } } // Row toggles: VOICE's Retrig|Legato (live only in Mono) and FILTER's morph law. if (g.rowToggle.id >= 0) { if (static_cast(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); } } // 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