// knob_deck.cpp — see knob_deck.h. Pure arithmetic; no LICE/VST3/REAPER includes. #include "knob_deck.h" #include namespace reasampler::vst { namespace { // The knob-row width of a group: cells side by side (no inter-cell gap — the 48px cell // already carries its own breathing room around the 28px knob), plus the optional row // toggle after a kDeckToggleGap. int knobRowWidth(const DeckGroupDesc& g) { int w = static_cast(g.cellIds.size()) * kDeckCellW; if (g.rowToggle.id >= 0) { if (w > 0) w += kDeckToggleGap; w += 2 * g.rowToggle.segWidth; } return w; } // The caption-row width: the caption reserve plus the optional caption toggle. int captionRowWidth(const DeckGroupDesc& g) { int w = g.captionWidth; if (g.captionToggle.id >= 0) w += kDeckToggleGap + 2 * g.captionToggle.segWidth; return w; } // Place one group's inner geometry given its box. DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) { DeckGroupLayout out; out.id = g.id; out.box = box; const int captionTop = box.top + kDeckGroupPadY; const int innerLeft = box.left + kDeckGroupPadX; const int innerRight = box.right - kDeckGroupPadX; // Caption row: text left, compact toggle right-anchored (r11 — the not-full-width home). out.caption = Rect{innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH}; if (g.captionToggle.id >= 0) { const int segW = g.captionToggle.segWidth; const int togTop = captionTop + (kDeckCaptionH - kDeckToggleH) / 2; const Rect seg1{innerRight - segW, togTop, innerRight, togTop + kDeckToggleH}; const Rect seg0{seg1.left - segW, togTop, seg1.left, togTop + kDeckToggleH}; out.captionToggle = DeckToggleLayout{g.captionToggle.id, seg0, seg1}; out.caption.right = seg0.left - kDeckToggleGap; // caption text stops at the toggle } // Knob row: fixed cells left-to-right, then the optional row toggle. const int cellTop = captionTop + kDeckCaptionH + kDeckCaptionGap; int x = innerLeft; for (int id : g.cellIds) { DeckCellLayout c; c.id = id; c.cell = Rect{x, cellTop, x + kDeckCellW, cellTop + kDeckCellH}; const int knobLeft = x + (kDeckCellW - kDeckKnobSize) / 2; const int knobTop = cellTop + 4; c.knob = Rect{knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize}; const int labelTop = knobTop + kDeckKnobSize + 4; c.label = Rect{c.cell.left, labelTop, c.cell.right, labelTop + kDeckCellLabelH}; out.cells.push_back(c); x += kDeckCellW; } if (g.rowToggle.id >= 0) { if (!g.cellIds.empty()) x += kDeckToggleGap; const int segW = g.rowToggle.segWidth; const int togTop = cellTop + (kDeckCellH - kDeckToggleH) / 2; const Rect seg0{x, togTop, x + segW, togTop + kDeckToggleH}; const Rect seg1{seg0.right, togTop, seg0.right + segW, togTop + kDeckToggleH}; out.rowToggle = DeckToggleLayout{g.rowToggle.id, seg0, seg1}; } return out; } } // namespace int deckGroupWidth(const DeckGroupDesc& g) { return (std::max)(captionRowWidth(g), knobRowWidth(g)) + 2 * kDeckGroupPadX; } int deckRowCount(const std::vector& groups, int availWidth) { if (groups.empty()) return 0; int rows = 1; int x = 0; for (const DeckGroupDesc& g : groups) { const int w = deckGroupWidth(g); if (x > 0 && x + kDeckGroupGap + w > availWidth) { ++rows; x = w; } else { x += (x > 0 ? kDeckGroupGap : 0) + w; } } return rows; } int deckHeight(const std::vector& groups, int availWidth) { const int rows = deckRowCount(groups, availWidth); if (rows == 0) return 0; return rows * kDeckGroupH + (rows - 1) * kDeckRowGap; } DeckLayout layoutDeck(const std::vector& groups, int left, int top, int availWidth) { DeckLayout out; if (groups.empty()) return out; int x = left; int y = top; bool rowHasGroup = false; out.rowCount = 1; for (const DeckGroupDesc& g : groups) { const int w = deckGroupWidth(g); if (rowHasGroup && (x + kDeckGroupGap + w) > (left + availWidth)) { // Wrap: whole trailing group onto the next row (mirror of deckRowCount). ++out.rowCount; x = left; y += kDeckGroupH + kDeckRowGap; rowHasGroup = false; } if (rowHasGroup) x += kDeckGroupGap; const Rect box{x, y, x + w, y + kDeckGroupH}; out.groups.push_back(layoutGroup(g, box)); x = box.right; rowHasGroup = true; } out.height = out.rowCount * kDeckGroupH + (out.rowCount - 1) * kDeckRowGap; return out; } DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) { for (const DeckGroupLayout& g : layout.groups) { if (!contains(g.box, x, y)) continue; if (g.captionToggle.id >= 0) { if (contains(g.captionToggle.seg0, x, y)) return {DeckHitKind::CaptionToggle, g.captionToggle.id, 0}; if (contains(g.captionToggle.seg1, x, y)) return {DeckHitKind::CaptionToggle, g.captionToggle.id, 1}; } if (g.rowToggle.id >= 0) { if (contains(g.rowToggle.seg0, x, y)) return {DeckHitKind::RowToggle, g.rowToggle.id, 0}; if (contains(g.rowToggle.seg1, x, y)) return {DeckHitKind::RowToggle, g.rowToggle.id, 1}; } for (const DeckCellLayout& c : g.cells) { if (c.id >= 0 && contains(c.cell, x, y)) return {DeckHitKind::Knob, c.id, -1}; } return {}; // inside the box but on fence/padding/blank — a miss (groups never overlap) } return {}; } } // namespace reasampler::vst