Re-derive the floor-size test fixtures from the constants and move the ceiling to sample_bands.h
This commit is contained in:
@@ -304,7 +304,7 @@ anything for a trigger shape.
|
||||
### `ui/`
|
||||
|
||||
- `editor_geometry` (`core/instrument/ui`) — the shared geometry VOCABULARY every instrument UI module speaks: the `core::ui::Rect` alias, `contains()`, and `OverlayArea` (a one-field `Rect` wrapper, no implicit conversion from `Rect`). Header-only (an INTERFACE CMake target), so it carries no layout of its own.
|
||||
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory — including `kEditorMinWidth`/`kEditorMinHeight`, the editor's client-area floor, which IS its default size (the shell's `checkSizeConstraint` and opening `ViewRect` both read it; the face grows, never shrinks below what the stack is laid out for). Three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
|
||||
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory — including `kEditorMinWidth`/`kEditorMinHeight`, the editor's client-area floor, which IS its default size (the shell's `checkSizeConstraint` and opening `ViewRect` both read it; the face grows, never shrinks below what the stack is laid out for), and `kEditorCeilingWidth`, the floor's sibling window fact (the hard cap the floor may not exceed) — moved here from `knob_deck.h` since it is a window fact, not a deck one; the derivation identity against the deck's width budget stays in `test_deck_groups.cpp`, the one place that already includes both headers. Three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
|
||||
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
|
||||
- `bake_hold` — the Hold knob's value domain and nothing else: the knob's normalized [0,1] mapped onto the note-length ladder and back, ordered by LENGTH rather than by the ladder's presentation order. Split from `sample_chrome` on the same axis `deck_values` was split from `knob_deck` — that says where the cell is, this says what its position means.
|
||||
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
|
||||
@@ -314,7 +314,7 @@ anything for a trigger shape.
|
||||
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
|
||||
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
|
||||
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
|
||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. Carries a SECOND hit-test, `hitTestKnobFace`, resolved against the drawn CIRCLES rather than the cell: a double-click reset is aimed at a dial, so the label band and the cell margins must miss where a drag grab deliberately does not, and only a radial resolve can tell the inner curve dial from the outer ring it sits inside. The deck's width budget at the editor's floor — the row block, the spanning deck's reserve, the ceiling, and what drives the floor — is declared and reasoned at the constants themselves (`knob_deck.h`); every group's categorical row is `deck_groups`' `deckRowFor`. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, where a `rowToggle` widens the GROUP and is charged against that budget — which is why the env decks' mode toggles ride the caption row. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. Carries a SECOND hit-test, `hitTestKnobFace`, resolved against the drawn CIRCLES rather than the cell: a double-click reset is aimed at a dial, so the label band and the cell margins must miss where a drag grab deliberately does not, and only a radial resolve can tell the inner curve dial from the outer ring it sits inside. The deck's width budget at the editor's floor — the row block, the spanning deck's reserve, and what drives the floor — is declared and reasoned at the constants themselves (`knob_deck.h`; the ceiling itself now lives in `sample_bands.h` as a window fact); every group's categorical row is `deck_groups`' `deckRowFor`. A group carries TWO caption-toggle slots, laid right-to-left: the second exists because a group whose knob row is wider than its caption row has caption slack a toggle can occupy for free, where a `rowToggle` widens the GROUP and is charged against that budget — which is why the env decks' mode toggles ride the caption row. **A group's cell run is a RESERVED WIDTH, not a fixed cell size**: a `-1` id reserves one cell's width without a cell, and the cells present divide the whole run between them at one uniform integer width (residue in symmetric end margins). That is what lets a mode flip drop controls from a face — Trigger's AMP and FILTER ENV lose their Sustain/Release stages — without either reflowing the deck or leaving dead slots in the box; a face with fewer controls simply gets roomier cells. Do not reintroduce fixed-width cells with blank slots.
|
||||
- `deck_values` — the deck's control-id ↔ parameter-set BINDING and its display units, split
|
||||
from the editor shell on the same axis `deck_groups` was split from `knob_deck`: `deck_groups`
|
||||
says which controls exist, this says what each one's value MEANS. Holds `deckParamNorm` /
|
||||
|
||||
@@ -55,10 +55,10 @@ inline constexpr int kDeckGroupH =
|
||||
// include edge to this header.
|
||||
inline constexpr int kDeckRowBlockW = 1020; // the block both categorical rows justify inside
|
||||
inline constexpr int kDeckSpanningW = 142; // the right-anchored spanning deck, outside the block
|
||||
// The hard ceiling the FLOOR may not exceed; the window itself still grows freely above it.
|
||||
// The gap between it and kEditorMinWidth is the whole width budget for the life of this
|
||||
// layout — see instrument-control-surface.md §1.6 before spending any of it.
|
||||
inline constexpr int kEditorCeilingWidth = 1280;
|
||||
// The hard ceiling the floor may not exceed lives beside the floor itself, in sample_bands.h's
|
||||
// kEditorCeilingWidth — a window fact, not a deck one. Today's gap between the two is 90px,
|
||||
// the whole width budget for the life of this layout (asserted in test_deck_groups.cpp) — see
|
||||
// instrument-control-surface.md §1.6 before spending any of it.
|
||||
|
||||
// A two-segment compact toggle (always 2 segments — the Mono/Stereo grammar). id -1 = absent.
|
||||
struct DeckToggleDesc {
|
||||
|
||||
@@ -24,6 +24,13 @@ inline constexpr int kPad = 8;
|
||||
inline constexpr int kEditorMinWidth = 1190;
|
||||
inline constexpr int kEditorMinHeight = 680;
|
||||
|
||||
// The hard ceiling the floor above may not exceed; the window itself still grows freely above
|
||||
// it. A window fact, sibling of kEditorMinWidth/kEditorMinHeight, not a deck one — moved here
|
||||
// from knob_deck.h for that reason. The gap to the floor (today: 90px) is the deck's whole
|
||||
// width budget, spent once; the identity is asserted in test_deck_groups.cpp, the one place
|
||||
// that already includes both this header and knob_deck.h.
|
||||
inline constexpr int kEditorCeilingWidth = 1280;
|
||||
|
||||
// Chrome band: the toolbar row (title + nav) stacked over the control row (piano strip,
|
||||
// preview, velocity knob, channel toggle). sample_chrome partitions it.
|
||||
inline constexpr int kTitleHeight = 26;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
// octave boundaries and the 0..127 extremes; keyRect tiling and black-over-white overlap;
|
||||
// keyAtPoint resolving black-over-white by zone and missing off-band; the root affordance's
|
||||
// hit-to-marker round trip; resolveDragNote clamping a wandering pointer; noteName under the
|
||||
// C4 (MIDI 60) DAW convention; and the gutter pinned at the shipped default window size.
|
||||
// C4 (MIDI 60) DAW convention; the gutter at the sawtooth's maximum residue; and the gutter
|
||||
// pinned at the shipped default window size (derived from kEditorMinWidth/kEditorMinHeight).
|
||||
//
|
||||
// Client-pixel-only guarantee: every width swept below is a CLIENT-pixel width. Nothing in
|
||||
// the instrument implements IPlugViewContentScaleSupport, so if a host scales the plugin
|
||||
@@ -280,28 +281,18 @@ static void testNoteNamesFollowTheC4Convention() {
|
||||
CHECK(noteName(500) == "G9");
|
||||
}
|
||||
|
||||
// --- the gutter at the shipped default window size -----------------------------
|
||||
// --- the gutter at the sawtooth's maximum residue -------------------------------
|
||||
|
||||
// The rule-based sweep above pins `margins == w % 75` and `leftMargin == margins/2` at every
|
||||
// width, but pins no concrete number — Daniel is making a visual call on the specific gutter
|
||||
// at the shipped default, and neither kPad nor the 840 default is covered by another test
|
||||
// firing if either ever changes. The strip is a sawtooth with period kStripWhiteKeyCount (75)
|
||||
// px of window width, and the shipped 840 default lands on residue 74 — the cycle's maximum:
|
||||
// one pixel of resize (840->841) collapses both gutters to zero and grows every white key
|
||||
// from 10px to 11px.
|
||||
static void testGutterAtTheShippedDefaultWindowSize() {
|
||||
constexpr int kShippedDefaultWindowW = 840; // editor_session.cpp's ViewRect default
|
||||
constexpr int kShippedDefaultWindowH = 620; // editor_session.cpp's ViewRect default
|
||||
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
||||
// chrome layout, rather than re-deriving the inset formula — so a change to either one
|
||||
// fails this test instead of silently moving the shipped gutter out from under it.
|
||||
const SampleBands bands =
|
||||
computeSampleBands(kShippedDefaultWindowW, kShippedDefaultWindowH, 0);
|
||||
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
||||
const int stripW = chrome.rootStrip.width;
|
||||
CHECK(stripW == 824);
|
||||
|
||||
const StripLayout L = layoutStrip(stripW, 30);
|
||||
// width, but pins no concrete number. The strip is a sawtooth with period kStripWhiteKeyCount
|
||||
// (75) px of window width; 840 is an arbitrary but independently useful sample because it
|
||||
// lands on residue 74 — the cycle's maximum: one pixel of resize (840->841) collapses both
|
||||
// gutters to zero and grows every white key from 10px to 11px. Kept as a synthetic worst-case
|
||||
// sample; NOT tied to any shipped window size (see testGutterAtTheShippedDefaultWindowSize
|
||||
// below for that).
|
||||
static void testGutterAtTheSawtoothMaximumResidueWidth() {
|
||||
constexpr int kSawtoothMaxResidueStripW = 824; // an arbitrary width landing on residue 74
|
||||
const StripLayout L = layoutStrip(kSawtoothMaxResidueStripW, 30);
|
||||
CHECK(L.whiteWidth == 10);
|
||||
const int margins = L.band.width - L.keys.width;
|
||||
CHECK(margins == 74);
|
||||
@@ -309,6 +300,31 @@ static void testGutterAtTheShippedDefaultWindowSize() {
|
||||
CHECK(leftMargin == 37);
|
||||
}
|
||||
|
||||
// --- the gutter at the shipped default window size -----------------------------
|
||||
|
||||
// Daniel is making a visual call on the specific gutter at the shipped default; neither kPad
|
||||
// nor the floor is covered by another test firing if either ever changes. Derived from
|
||||
// kEditorMinWidth/kEditorMinHeight (editor_session.cpp's ViewRect default IS the floor) rather
|
||||
// than a hardcoded window size, so a floor change fails HERE instead of silently moving the
|
||||
// shipped gutter out from under it. The numbers below are today's floor (1190x680); re-derive
|
||||
// them by hand if the floor ever moves.
|
||||
static void testGutterAtTheShippedDefaultWindowSize() {
|
||||
// Derive rootStrip's width the same way the shell does, through the real allocator +
|
||||
// chrome layout, rather than re-deriving the inset formula.
|
||||
const SampleBands bands = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 0);
|
||||
const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24);
|
||||
const int stripW = chrome.rootStrip.width;
|
||||
CHECK(stripW == kEditorMinWidth - 2 * kPad);
|
||||
CHECK(stripW == 1174);
|
||||
|
||||
const StripLayout L = layoutStrip(stripW, 30);
|
||||
CHECK(L.whiteWidth == 15);
|
||||
const int margins = L.band.width - L.keys.width;
|
||||
CHECK(margins == 49);
|
||||
const int leftMargin = L.keys.x - L.band.x;
|
||||
CHECK(leftMargin == 24);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testLayoutFillsTheBandAndCentresTheKeys();
|
||||
testDegenerateSizesYieldNoKeys();
|
||||
@@ -325,6 +341,7 @@ int main() {
|
||||
testHitTestingAKeyMarksThatSameKey();
|
||||
testDragTracksThePointerAndClampsWhenItWanders();
|
||||
testNoteNamesFollowTheC4Convention();
|
||||
testGutterAtTheSawtoothMaximumResidueWidth();
|
||||
testGutterAtTheShippedDefaultWindowSize();
|
||||
|
||||
if (g_fail == 0) {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
// three-band vertical inventory (chrome over waveform over decks, no overlap, no
|
||||
// inversion) asserted as pure geometry with no paint call; the waveform band's two-lane
|
||||
// floor and the bands-clip-rather-than-squeeze rule on a short window; the deck band's
|
||||
// bottom anchor and its exact requested height; and the lane split (mono = one full-band
|
||||
// lane, stereo = two lanes with the seam gap between them).
|
||||
// bottom anchor and its exact requested height; the same stack/anchor/degrade properties
|
||||
// re-anchored AT the allocator's own kEditorMinWidth/kEditorMinHeight floor rather than only
|
||||
// below it; and the lane split (mono = one full-band lane, stereo = two lanes with the seam
|
||||
// gap between them).
|
||||
|
||||
#include "../src/core/instrument/ui/sample_bands.h"
|
||||
|
||||
@@ -104,6 +106,56 @@ static void testDegenerateWindowYieldsNoInvertedRects() {
|
||||
CHECK(tiny.decks.right() >= tiny.decks.x);
|
||||
}
|
||||
|
||||
// --- the allocator at its own floor (kEditorMinWidth x kEditorMinHeight) -------
|
||||
|
||||
// The allocator's own client-area floor is a fact IT owns (kEditorMinWidth/kEditorMinHeight
|
||||
// above); the fixtures above validate the general shape entirely below that floor (840x620,
|
||||
// 840x160). These anchor the same properties AT the floor itself, so a floor move that broke
|
||||
// the stack there would have nothing else in this file to catch it.
|
||||
|
||||
static void testBandsStackWithoutOverlapAtTheEditorFloor() {
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 120);
|
||||
CHECK(b.chrome.y == 0);
|
||||
CHECK(b.chrome.width == kEditorMinWidth);
|
||||
CHECK(b.chrome.height == kTitleHeight + kChromeRowHeight);
|
||||
CHECK(b.waveform.y >= b.chrome.bottom());
|
||||
CHECK(b.decks.y >= b.waveform.bottom());
|
||||
CHECK(b.waveform.x == kPad && b.waveform.right() == kEditorMinWidth - kPad);
|
||||
CHECK(b.decks.x == kPad && b.decks.right() == kEditorMinWidth - kPad);
|
||||
}
|
||||
|
||||
static void testDeckBandIsBottomAnchoredAtTheEditorFloor() {
|
||||
constexpr int deckH = 120;
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, deckH);
|
||||
CHECK(b.decks.height == deckH);
|
||||
CHECK(b.decks.bottom() == kEditorMinHeight - kPad);
|
||||
}
|
||||
|
||||
// At a representative two-row deck height (216px — the ceiling test_deck_groups.cpp bounds
|
||||
// the wrapped deck to), the waveform gets exactly what the floor's own height leaves it: an
|
||||
// equality, not a bound, so a floor-height change that quietly ate into the waveform's slack
|
||||
// would fail here rather than only widen/narrow a `>=`.
|
||||
static void testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck() {
|
||||
constexpr int twoRowDeckH = 216;
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, twoRowDeckH);
|
||||
const int expected = kEditorMinHeight - kPad - twoRowDeckH - 2 * kBandGap -
|
||||
(kTitleHeight + kChromeRowHeight);
|
||||
CHECK(b.waveform.height == expected);
|
||||
CHECK(b.waveform.height == 358);
|
||||
CHECK(b.waveform.height >= kWaveformMinHeight);
|
||||
}
|
||||
|
||||
// The floor is not immune to the degrade path: a deck grown too tall for the floor's OWN
|
||||
// height still hits the floor-wins rule instead of squeezing the waveform below its usable
|
||||
// minimum — the sub-floor fixtures above cover the general rule; this is the same rule
|
||||
// exercised at the allocator's own minimum width.
|
||||
static void testAnOversizedDeckAtTheFloorStillDegradesGracefully() {
|
||||
const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 500);
|
||||
CHECK(b.waveform.height == kWaveformMinHeight);
|
||||
CHECK(b.decks.y >= b.waveform.bottom());
|
||||
CHECK(b.decks.bottom() > kEditorMinHeight); // clipped below the floor's own window
|
||||
}
|
||||
|
||||
// --- the waveform band's lanes ------------------------------------------------
|
||||
|
||||
static void testMonoUsesOneFullBandLane() {
|
||||
@@ -149,6 +201,10 @@ int main() {
|
||||
testWaveformNeverShrinksBelowTheTwoLaneFloor();
|
||||
testTwoLaneFloorHoldsTwoUsableLanes();
|
||||
testDegenerateWindowYieldsNoInvertedRects();
|
||||
testBandsStackWithoutOverlapAtTheEditorFloor();
|
||||
testDeckBandIsBottomAnchoredAtTheEditorFloor();
|
||||
testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck();
|
||||
testAnOversizedDeckAtTheFloorStillDegradesGracefully();
|
||||
testMonoUsesOneFullBandLane();
|
||||
testStereoSplitsIntoTwoLanesWithTheSeamGap();
|
||||
testStereoOddRemainderGoesToTheUpperLane();
|
||||
|
||||
Reference in New Issue
Block a user