f2cdf676f3
Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
142 lines
12 KiB
Markdown
142 lines
12 KiB
Markdown
# src/core/ui — pure UI geometry, palette, and interaction-decision modules
|
||
|
||
## Scope
|
||
|
||
Pure, REAPER-free UI geometry, palette, and interaction-decision modules shared by
|
||
the extension's docked bank panel and the VST3 instrument's editor/embed surfaces:
|
||
layout math, hit-testing, hover/drag-gesture-precedence decisions, and the
|
||
role-based color palette. Does **not** include: the actual LICE drawing (`draw_kit`
|
||
lives in `shell/panel`; the editor's own paint code lives in `shell/instrument`),
|
||
REAPER/SWELL window or dialog mechanics, or the DAW-side Design View flag
|
||
application (`shell/view`).
|
||
|
||
## Invariants
|
||
|
||
Look-and-feel — visual design language (Phase L) (settled decisions, 2026-07-26;
|
||
L7 sub-pass, 2026-07-27):
|
||
|
||
- **DS-1 — toolkit discipline.** Draw with LICE + reused WDL/vwnd pieces directly;
|
||
external frameworks (iPlug2/JUCE/VSTGUI) are rejected. "**Caution, not a ban:**
|
||
keep hit-test **geometry** in pure CTest-covered modules — do not import vwnd's
|
||
retained-mode object model wholesale (its controls own their hit-test internally,
|
||
which would move geometry into untestable shell code and undercut the pure/shell
|
||
split)." This directory is that pure-geometry seam.
|
||
- **DS-2 — palette is role-based, not hardcoded hue**, in one constants block
|
||
(`theme`): `bg/base`, `bg/panel`, `bg/cell`, `line/hairline`, `text/primary`,
|
||
`text/dim`, `accent/primary`, `accent/secondary`, `accent/tertiary`,
|
||
`accent/hot`, `overlay/trace`, `warn`. Neutral ladder sits in REAPER's mid-grey
|
||
theme family (`bg/base` ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈
|
||
`#3a3a3a`, `line/hairline` ≈ `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈
|
||
`~#a0a0a0`+), elevation-ladder discipline (base < panel < cell by a few %,
|
||
micro-gradient + inner highlight/shadow carry elevation, not hard borders). The
|
||
accents carry categorical meaning: **primary (pastel lime) =
|
||
live/active/selected**, secondary (deep teal) + tertiary (pastel purple) =
|
||
supporting categorical distinctions (kinds, not intensity). `warn` (red/amber)
|
||
is reserved **only** for byte-deleting or clip states (prune, delete).
|
||
**The role count is not fixed at three** — a shade that works better in one spot
|
||
earns its own named role (Daniel, 2026-07-31). What does not relax: draw sites
|
||
ask by role. A literal color outside `theme`'s constants block is the violation,
|
||
not the role count.
|
||
- **WCAG-floor discipline (tight pairs to re-verify on any palette change):**
|
||
`text/dim` on `bg/panel`/`bg/cell` is the classic mid-grey-on-mid-grey floor
|
||
failure — must clear AA 4.5:1 body text. The accents as state indicators/active
|
||
fills on `bg/cell` have a shrunken contrast cushion (primary ~7.6:1, tertiary
|
||
~5.5:1, secondary only ~3.9:1 since it was darkened — all still clear 3:1, but
|
||
re-check every one on any hue nudge). Body text on an accent fill is a tight AA
|
||
4.5:1 pair (4.91:1 for `accent/secondary`). **The *binding* limiter on how dark
|
||
`accent/secondary` may go is neither of those: it is the velocity-curve trace on
|
||
a HOVER-lightened `bg/cell` at 3.03:1 against a 3:1 floor** — 1.2% headroom, the
|
||
tightest pair in the tree, and invisible to any rest-surface check. Take the most
|
||
pastel value that still clears the floor, approached from the soft side, never
|
||
re-saturated toward neon "to be safe."
|
||
- **A state surface is a DIFFERENT surface.** Hover lightens toward `accent/hot`,
|
||
pressed darkens, disabled desaturates — each changes the contrast of everything
|
||
drawn on it. Enumerate the state surfaces a role is drawn over, not just the rest
|
||
ones; the limiter above is exactly the pair that enumeration missed.
|
||
- **A mark drawn OVER an accent fill answers to two neighbours at once.** THE home
|
||
for this rule. `overlay/trace` (the envelope trace on the waveform) must clear 3:1
|
||
against both `accent/primary` and `bg/base`, which sit 9.41:1 apart — so
|
||
`sqrt(9.41)` ≈ 3.07:1 is the ceiling for *any* single value, and the role sits at
|
||
that optimum with no headroom either way. Three consequences, all load-bearing:
|
||
the role is confined to the waveform band (no value clears 3:1 on `bg/cell` and
|
||
against the lime at once); inside the 20%-alpha loop-span fill it is under floor
|
||
at 2.25:1, and against `line/hairline`'s zero line at 1.92:1 — both KNOWN,
|
||
ACCEPTED, and asserted as such, because no trace value fixes either; and any two
|
||
values that BOTH clear the ceiling sit within 1.05:1 of each other, so a state of
|
||
this mark (grabbed, hot, selected) **cannot** be carried by color and must be
|
||
carried by size, fill-vs-outline, or shape.
|
||
- **"Speed is the selling point" — a design constraint on this geometry, not
|
||
just the draw layer.** Sub-frame hover/press/drag feedback must repaint
|
||
immediately on the input message. **No decorative animation** — no
|
||
tweens/fades/pulses; the only permitted motion is a level/meter readout
|
||
following audio directly. Any glow/bloom state is a static drawn state, never
|
||
a pulse.
|
||
- **Precision/invariant implications (Phase L does not change these):** "the
|
||
pure/shell split holds" — all layout/hit-test stays in pure CTest-covered
|
||
geometry modules; the kit's *draw* half is shell, its *geometry* half is pure,
|
||
even where a WDL piece is reused. Look-and-feel work never touches capture,
|
||
placement, or bank data ownership.
|
||
- **L7 drag-gesture precedence is a pure decision helper.** The rule — leave
|
||
client rect → OS drag-out; else drop on a tab/other bank → move/copy; else
|
||
same-bank grid → reorder-to-slot (empty slot = place, occupied + no modifier =
|
||
insert-before-and-shift, occupied + Alt = replace) — is "encoded in a pure
|
||
decision helper (mirror `drag_out::decideGesture`)"; the shell only reads live
|
||
pointer/focus/client-rect/modifier state and calls it, then maps the resolved
|
||
gesture to a cursor via `SetCursor`. No cue or precedence logic belongs in the
|
||
shell.
|
||
|
||
## Modules
|
||
|
||
- `rect` (`core/ui`, header-only) — the ONE concrete pixel rectangle (Q-W1): XYWH storage + `right()`/`bottom()`/`ltrb()`/`contains()`, replacing 12+ byte-identical role structs (`ButtonRect`/`FooterRect`/`CellRect`/`KitBox`/…) and the VST side's separate LTRB `Rect`; every prior role name survives as a `using` alias at its old site (e.g. `editor_geometry::Rect`).
|
||
- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
|
||
- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
|
||
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
|
||
- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag.
|
||
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + the pastel accent system, the keyboard strip's spectral ramp, WCAG contrast-floor helpers + `compositeOver` (the effective color of a translucent fill, so alpha overlays are testable). Only the ramp's MID stop is its own constant; lo/hi are still aliases of `accent/primary`/`accent/tertiary`, so a categorical accent move CAN still reorder the ramp — `testSpectralRampLuminanceIsMonotonic` is the build-time catch, not the structure.
|
||
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
|
||
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
|
||
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate AND under whether the shell resolved a routable command id for it (both bools passed IN, so this stays REAPER-free).
|
||
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
|
||
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
|
||
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
|
||
- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace.
|
||
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped.
|
||
- `stroke_aa` — analytic antialiased thick-stroke COVERAGE (the shell blends it): `StrokeCanvas`, a reusable mask holding distance-to-polyline coverage MAX-accumulated across segments, plus `strokePolyline` / `strokeBounds` / `appendArc` / `rasterRowOffset` (the row-major offset
|
||
math for a possibly bottom-up raster, pulled out of the shell's LICE blend so its flipped
|
||
branch is pinned by a host-free test). An arc is just a flattened polyline, so ONE path serves the knob arcs, the inner dial, the envelope polyline and both spline traces. Coverage is `clamp(halfWidth + 0.5 - distance, 0, 1)`, which makes perpendicular weight exactly `2·halfWidth` at every angle. **The guaranteed-opaque-core threshold is width ≥ 2 px, not any width above 1 px**: opacity needs `distance <= halfWidth - 0.5`, and the worst-case distance from a pixel centre to the centreline is 0.5, so a 1 px stroke (`halfWidth = 0.5`) has zero slack — its peak alpha modulates with the stroke's exact alignment to the pixel grid instead of pinning to 255 (Daniel's ruling, 2026-08-01: every stroker-drawn width on the editor is now >= 2 px for this reason — `testSubOpaqueCoreAtOnePixelWidth` in `tests/test_stroke_aa.cpp` still pins the 1 px case as a property of the stroker, independent of whether any surface ships at that width). Long segments are subdivided before rasterizing — EXACT, not an approximation (min-distance to a partition of a segment is min-distance to the whole), purely to keep each piece's bounding box tight, since one long diagonal's box has area O(len²).
|
||
|
||
## Gotchas
|
||
|
||
- The WCAG contrast-floor pairs above are real math in `theme`'s tests, not a
|
||
visual eyeball check — any new hue or role needs its own contrast-floor
|
||
assertion. Enumerate the draw sites by grep, not from memory: the enumeration
|
||
must cover state surfaces (hover/pressed/active), translucent composites, and
|
||
anything else drawn into the same rect, not just the rest surfaces.
|
||
- **A text color's floor is set by its FONT, not by its role.** WCAG's large-scale
|
||
class needs ≥24px, or ≥18.66px BOLD (`theme.h`'s thresholds); anything smaller is
|
||
body class and answers to 4.5:1. A draw site relying on the 3:1 floor must pin its
|
||
font metrics against those constants — `draw_kit.cpp`'s `Font::RegionTitle` is the
|
||
worked example. A prior revision wrote the threshold ~25% low and let 15px
|
||
semibold clear a floor it was not entitled to.
|
||
- `card_drag`'s precedence order must stay a pure decision helper mirroring
|
||
`drag_out::decideGesture` — don't let a shell reimplement gesture precedence
|
||
ad hoc; the cursor-cue mapping in the shell must stay a thin lookup over the
|
||
pure result.
|
||
- `rect`'s prior role names survive only as `using` aliases at their old call
|
||
sites — changing `rect.h` itself ripples across every directory that aliases
|
||
it (e.g. `editor_geometry::Rect`); check all alias sites, not just this one.
|
||
- **`stroke_aa`'s mask is deliberately NOT cleared on `reset`.** Only
|
||
`[rowLo, rowHi)` of each row holds meaningful coverage; everything else is
|
||
whatever the reused buffer last held. That is what keeps a stroke's cost
|
||
proportional to its ink rather than to its bounding box — but it means any new
|
||
reader must respect the row extents, and any new writer must grow them through
|
||
`extendRow`, which zero-fills the newly-valid cells INCLUDING the gap when a
|
||
stroke revisits a row far from where it left it (a circle touches most rows on
|
||
both sides). Reading the raw buffer outside the extents returns garbage by
|
||
design, not zero.
|
||
- **Neither `LICE_Arc` nor `LICE_ThickFLine` can draw these strokes** — the first
|
||
never reaches an opaque core, the second's width is along the minor axis so its
|
||
perpendicular weight falls off as `cos θ`. The evidence and the measurements
|
||
live in `docs/product/visual-design-language.md` §8; do not "simplify" a stroke
|
||
site back onto either primitive.
|