// knob_deck.h — PURE knob-deck layout + hit-test for the r11 Sample-face recomposition // (Wave B, FB1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary, and — like // param_slider — NO engine types: cells and toggles carry opaque shell-owned control ids. // The mirror of action_bar / param_slider: the fiddly group-box / caption-row / cell-grid // arithmetic lives here, unit-tested outside the DAW, while the editor shell draws each // group (fence, caption, compact toggles, knobs) through the L1 kit and routes clicks/drags // via the hit-test. The KNOB PRIMITIVE itself (value<->needle-angle, vertical drag) is // param_slider's (FA4); a knob cell here is just a rect — the shell composes the two. // // THE DECK (CONTEXT.md §S-VIEW r11). A horizontal run of FENCED GROUPS, left -> right, each // a hairline-bordered bg/panel box with a CAPTION ROW (micro-caps caption left; the group's // compact mode toggle right-anchored IN the caption row — this is where the not-full-width // toggles live) over a KNOB ROW of fixed 48x58 cells (28px knob centered, 12px label band // beneath). A group may additionally place one 18px-tall two-segment toggle IN the knob row // after its cells (the VOICE group's Retrig|Legato — same Mono/Stereo segment grammar, // vertically centered). Groups that must keep stable geometry across a mode flip reserve // blank cells (id -1): the AMP ENVELOPE group always spans 5 cells so Gate<->Trigger never // reflows its neighbours. // // WRAP (deterministic): groups place left-to-right with kDeckGroupGap between; a group that // does not fit the remaining width starts a new deck row (whole groups only, never split). // The first group of a row always places even if wider than the row (degenerate width). // deckHeight() exposes the resulting height so the shell can bottom-anchor the deck band and // give the ELASTIC HERO the rest (r11 band order). #pragma once #include #include "core/instrument/ui/editor_geometry.h" // Rect, contains — the shared geometry idiom namespace reasampler::instrument::ui { // Fixed deck metrics (spec r11), exposed so the shell and tests agree. inline constexpr int kDeckCellW = 48; // one knob cell inline constexpr int kDeckCellH = 58; inline constexpr int kDeckKnobSize = 28; // knob diameter inside the cell inline constexpr int kDeckCellLabelH = 12; // the Micro label band under the knob inline constexpr int kDeckCaptionH = 20; // the group caption row inline constexpr int kDeckToggleH = 18; // compact toggle segment height inline constexpr int kDeckGroupPadX = 6; // group box horizontal inner padding inline constexpr int kDeckGroupPadY = 4; // group box vertical inner padding inline constexpr int kDeckCaptionGap = 2; // caption row -> knob row gap inline constexpr int kDeckToggleGap = 4; // caption text -> toggle / cells -> row toggle gap inline constexpr int kDeckGroupGap = 12; // gap between groups on a row inline constexpr int kDeckRowGap = 8; // gap between wrapped deck rows // One group box: padding + caption + gap + cell row + padding. inline constexpr int kDeckGroupH = kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap + kDeckCellH + kDeckGroupPadY; // A two-segment compact toggle (always 2 segments — the Mono/Stereo grammar). id -1 = absent. struct DeckToggleDesc { int id = -1; // shell control id returned by the hit-test; -1 = no toggle int segWidth = 44; // px per segment }; // One fenced group, in deck order. `cellIds` are the knob cells left-to-right; an id of -1 // is a RESERVED BLANK cell (geometry held, never hit — the AMP ENVELOPE Trigger face). // `captionWidth` is the px the shell reserves for the caption text (this module does not // measure text — the house constant-metrics pattern). struct DeckGroupDesc { int id = 0; // shell group id (opaque here) int captionWidth = 60; DeckToggleDesc captionToggle; // right-anchored in the caption row; id -1 = none std::vector cellIds; // knob cells; -1 = blank reserve DeckToggleDesc rowToggle; // in the knob row after the cells; id -1 = none }; // --- Laid-out geometry --------------------------------------------------------------- struct DeckToggleLayout { int id = -1; Rect seg0; // left segment Rect seg1; // right segment }; struct DeckCellLayout { int id = -1; Rect cell; // the full 48x58 cell Rect knob; // the centered kDeckKnobSize square (the knob circle inscribes it) Rect label; // the 12px label band beneath the knob }; struct DeckGroupLayout { int id = 0; Rect box; // the fenced group box Rect caption; // caption text rect (left part of the caption row) DeckToggleLayout captionToggle; // id -1 when absent (rects empty) std::vector cells; DeckToggleLayout rowToggle; // id -1 when absent }; struct DeckLayout { std::vector groups; int rowCount = 0; int height = 0; // rowCount * kDeckGroupH + (rowCount-1) * kDeckRowGap; 0 for no groups }; // The width of one group box: the wider of its caption row (caption + gap + toggle) and its // knob row (cells + gap + row toggle), plus the horizontal padding. Pure. int deckGroupWidth(const DeckGroupDesc& g); // The number of deck rows the groups occupy at `availWidth` under the greedy whole-group // wrap (a group that does not fit the remaining row width starts a new row; the first group // of a row always places). 0 for an empty group list. Pure — the wrap is deterministic. int deckRowCount(const std::vector& groups, int availWidth); // The total deck height at `availWidth` (rows * kDeckGroupH + inter-row gaps). 0 for an // empty list. The shell bottom-anchors a band of exactly this height. Pure. int deckHeight(const std::vector& groups, int availWidth); // Lay the groups out from (left, top) within `availWidth`, wrapping per deckRowCount's rule. // Every rect is absolute. Pure — same inputs, same layout. DeckLayout layoutDeck(const std::vector& groups, int left, int top, int availWidth); // --- Hit-test -------------------------------------------------------------------------- enum class DeckHitKind { None, Knob, CaptionToggle, RowToggle }; struct DeckHit { DeckHitKind kind = DeckHitKind::None; int id = -1; // the control id of the hit element (cell id / toggle id) int segment = -1; // 0/1 for a toggle hit; -1 otherwise }; // The deck element a point lands on: a knob CELL (the whole 48x58 cell — friendlier than the // bare knob circle; the shell anchors the vertical drag wherever the grab lands), a caption- // toggle segment, or a row-toggle segment. Blank cells (id -1) and everything else miss. // Pure — the shell's routing entry point. DeckHit hitTestDeck(const DeckLayout& layout, int x, int y); } // namespace reasampler::instrument::ui