#pragma once // component_geometry — the REAPER-free, LICE-free geometry + hit-test math for the shared // drawing kit's generic components (Phase L, L1): a button box, a slider's track/handle, // and a list row. These are the kit-level primitives that DON'T already have a pure owner: // bank_grid / mode_switch / tab_strip / prune_button stay the source of truth for the // surfaces THEY own; this module carries only the new, reusable component // shapes the kit's drawButton / drawSlider / drawListRow draw against. // // Why pure (CLAUDE.md §load-bearing split, DS-1 caution): even where the draw shell reuses // a WDL/vwnd drawing idiom, the hit-test geometry stays HERE, unit-tested outside the DAW — // vwnd's retained-mode controls own their hit-test internally, which this deliberately does // NOT import. The shell asks this module where a handle is and whether a point hit a row. // // NAME NOTE (brief §name-collision): the surrounding modules already own ButtonRect / // SegmentRect / CellRect / FooterRect etc. in this namespace, so this module's types are // named KitButtonBox / SliderGeometry / ListRowBox to avoid collision — checked with grep // before minting. They are distinct concepts (kit-generic component boxes vs. a specific // surface's hit rects), so the separate names are correct, not merely non-colliding. // // PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library // only. Builds and unit-tests without REAPER. Mirror of mode_switch / prune_button. namespace reasampler { // A generic pixel box, top-left origin (SWELL/LICE convention). Shared shape for the kit // component rects below. A zero-area box (empty()) means "nothing to draw / hit" — the // same graceful-suppression convention prune_button uses. struct KitBox { int x = 0; int y = 0; int width = 0; int height = 0; bool empty() const { return width <= 0 || height <= 0; } bool operator==(const KitBox& o) const { return x == o.x && y == o.y && width == o.width && height == o.height; } }; // True iff (px, py) falls inside `box`, half-open bounds [x, x+width) x [y, y+height) — // the same discipline as every sibling hit-test so draw and hit-test never double-claim a // pixel. An empty box claims no point (always false). bool hitTestBox(int px, int py, const KitBox& box); // --- Button ------------------------------------------------------------------ // // A button drawn inside a host cell, inset by a uniform padding so it reads as a raised // control rather than a full-bleed fill (the kit's drawButton draws the micro-gradient // surface inside this box). Distinct from prune_button, which owns its OWN placement // within its strip — this is the generic "given a cell, where's the // button" helper for new kit consumers. struct KitButtonBox { KitBox box; bool operator==(const KitButtonBox& o) const { return box == o.box; } }; // The button box inside `cell`, inset uniformly by `padding` on all four sides. Returns an // empty box (suppressed) when the cell is degenerate or the padding would collapse it to // zero-or-negative area — the caller then draws nothing (graceful, mirrors prune_button). // padding < 0 is treated as 0. KitButtonBox computeButtonBox(const KitBox& cell, int padding); // --- Slider (horizontal) ----------------------------------------------------- // // A horizontal slider: a track spanning the control width (inset at both ends by the // handle's half-width so the handle never clips past the track), and a square handle // centered on the track and positioned by the normalized value. drawSlider draws the // track, the filled portion up to the handle, and the handle. Hit-test is against the // handle (grab) and the track (jump); both are pure here. struct SliderGeometry { KitBox track; // the full track rect (the groove) KitBox filled; // the filled portion from the track's left up to the handle center KitBox handle; // the draggable handle rect bool operator==(const SliderGeometry& o) const { return track == o.track && filled == o.filled && handle == o.handle; } }; // Lays out a horizontal slider inside `control` for a normalized `value` in [0, 1] with a // square handle of side `handleSize`. The track is vertically centered at a fixed // `trackThickness`, inset horizontally by handleSize/2 at each end so the handle's travel // stays within `control`. value is clamped to [0, 1]; a value of 0 puts the handle flush // left, 1 flush right. Returns all-empty boxes when the control is degenerate or too // small to host the handle (control width < handleSize or height < handleSize) — the // caller draws nothing. handleSize <= 0 or trackThickness <= 0 also yields empty. SliderGeometry computeSlider(const KitBox& control, double value, int handleSize, int trackThickness); // The normalized value [0, 1] a click at px maps to, for a slider laid out in `control` // with `handleSize` (the inverse of computeSlider's handle placement — a track jump). // px left of / at the track start yields 0.0, at/right of the track end yields 1.0, // linear in between. Returns 0.0 for a degenerate/too-small control (no travel). py is // unused (a horizontal slider maps X only); the caller gates the whole slider region // with hitTestBox(control) before calling this. double sliderValueAt(int px, const KitBox& control, int handleSize); // --- List row ---------------------------------------------------------------- // // A single selectable row in a vertical list: full-width, fixed height, stacked from the // list's top by index (no scroll — the caller offsets the list origin for scroll). The // kit's drawListRow draws the row surface (rest/hover/selected/focus) and an optional // leading thumbnail; the panel's waveform cell is a specialization drawn the same way. struct ListRowBox { int index = 0; // the row's index in the caller's list (0-based, top-first) KitBox box; bool operator==(const ListRowBox& o) const { return index == o.index && box == o.box; } }; // The row box for `index` in a list laid out inside `list` at `rowHeight` per row. Rows // stack from list.y; row i spans [list.y + i*rowHeight, +rowHeight). Returns an empty box // when the list is degenerate, rowHeight <= 0, index < 0, or the row would fall entirely // below the list's bottom (fully clipped) — a partially-visible last row IS returned (the // caller clips the draw). This is layout only; the caller decides how many rows exist. ListRowBox computeListRow(const KitBox& list, int index, int rowHeight); // The index of the row a point (px, py) lands on, for a list laid out inside `list` at // `rowHeight`. Returns -1 for a miss: outside the list bounds, in the list band but below // the last row of `rowCount` rows (the empty tail), or a degenerate list/rowHeight/count. // rowCount bounds the hit so a click in blank space past the last row is a clean miss, not // a phantom row. Half-open bounds match computeListRow so the hit maps to the drawn row. int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCount); // --- Waveform column count --------------------------------------------------- // // The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side // insets), never negative. Callers pass this count directly as the `binCount` argument to // peaks::computeEnvelope — one bin per column is the correct resolution, and // peaks::columnMinMax's exact partition makes the render gap-free at any bins-to-pixels // ratio. Overbinning does NOT improve render quality (columnMinMax's frame union is // identical whether bins == columns or bins == k*columns) and wastes memory and CPU. int waveformColumnCount(const KitBox& box); } // namespace reasampler