L1: shared LICE drawing kit — theme/palette + component geometry + draw kit; retire GDI text in bank_panel

This commit is contained in:
2026-07-26 20:04:04 -04:00
parent ec05af35ef
commit beca6487c7
10 changed files with 1394 additions and 42 deletions
+129
View File
@@ -0,0 +1,129 @@
#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 / action_buttons / 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/action_buttons, which own their
// OWN placement within their strips — 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);
} // namespace reasampler