Bake window derives itself: %-knob fold, declick pad, Gate held to exhaustion, preview velocity; Hold is the one knob a loop needs

This commit is contained in:
2026-08-01 20:26:04 -04:00
parent d3894dae6d
commit 19aeb92775
36 changed files with 1040 additions and 274 deletions
+36 -12
View File
@@ -2,9 +2,10 @@
// test framework.
//
// Covers: the chrome band's two rows (toolbar over strip row, tiling the band exactly); the
// toolbar's fixed right-anchored run in order (preview, velocity cell, Mono|Stereo,
// Browse) with the title taking the remainder; the velocity knob centred in its
// cell above its label; the piano strip owning its whole row at every width; no rect on the
// toolbar's fixed right-anchored run in order (Hold, bake, preview, velocity cell,
// Mono|Stereo, Browse) with the title taking the remainder; the velocity and Hold knobs
// centred in their cells above their labels; the piano strip owning its whole row at every
// width; no rect on the
// toolbar overlapping any other; degenerate bands yielding no inverted rects; and the preview
// button's play-triangle glyph, which sits inside the button without changing its rect.
@@ -63,12 +64,13 @@ static void testToolbarRunIsOrderedRightToLeftWithoutOverlap() {
CHECK(r.bake.width == kBakeButtonWidth);
CHECK(r.bake.y == r.preview.y); // shares the run's button baseline
CHECK(r.bake.height == r.preview.height);
CHECK(r.title.right() <= r.bake.x); // the title yields to the bake, not the preview
CHECK(r.holdCell.right() <= r.bake.x);
CHECK(r.title.right() <= r.holdCell.x); // the title yields to the whole run
CHECK(r.title.x == band.x + kPad);
CHECK(r.title.width > 0);
// Every toolbar rect sits inside the toolbar row.
const Rect items[] = {r.title, r.bake, r.preview, r.velCell, r.chanMono,
const Rect items[] = {r.title, r.holdCell, r.bake, r.preview, r.velCell, r.chanMono,
r.chanStereo, r.navBrowse};
for (const Rect& it : items) {
CHECK(it.y >= r.toolbar.y && it.bottom() <= r.toolbar.bottom());
@@ -82,16 +84,17 @@ static void testChromePartsNeverOverlapAtAnyWidth() {
// stay inside its own row, clear of every control.
CHECK(!overlaps(r.toolbar, r.rootStrip));
CHECK(r.rootStrip.y >= r.controls.y && r.rootStrip.bottom() <= r.controls.bottom());
const Rect items[] = {r.bake, r.preview, r.velCell, r.chanMono, r.chanStereo,
r.navBrowse};
const Rect items[] = {r.holdCell, r.bake, r.preview, r.velCell, r.chanMono,
r.chanStereo, r.navBrowse};
for (const Rect& it : items) {
CHECK(!overlaps(it, r.rootStrip));
CHECK(!overlaps(it, r.title));
}
// The run's own members are pairwise disjoint (velKnob/velLabel are inside velCell,
// so they are checked against the cell's neighbours, not the cell).
for (int i = 0; i < 5; ++i) {
for (int j = i + 1; j < 5; ++j) CHECK(!overlaps(items[i], items[j]));
// The run's own members are pairwise disjoint (the knob/label pairs are inside their
// cells, so they are checked against the cell's neighbours, not the cell).
constexpr int kRunCount = static_cast<int>(sizeof(items) / sizeof(items[0]));
for (int i = 0; i < kRunCount; ++i) {
for (int j = i + 1; j < kRunCount; ++j) CHECK(!overlaps(items[i], items[j]));
}
}
}
@@ -125,6 +128,25 @@ static void testVelocityKnobIsCentredInItsCellAboveTheLabel() {
CHECK(r.velLabel.x == r.velCell.x && r.velLabel.right() == r.velCell.right());
}
// The Hold cell reserves its width whether or not the control is drawn — a run laid out
// conditionally would slide Bake and Preview out from under the pointer whenever a loop is
// dialled in or out. Its interior follows the velocity cell's grammar exactly.
static void testHoldCellIsReservedAndFollowsTheVelocityCellGrammar() {
const ChromeRects r = chromeRects(chromeBand(), kKnob);
CHECK(r.holdCell.width == r.velCell.width);
CHECK(r.holdCell.y == r.velCell.y && r.holdCell.height == r.velCell.height);
CHECK(r.holdKnob.width == kKnob && r.holdKnob.height == kKnob);
CHECK(r.holdKnob.y == r.holdCell.y);
CHECK(r.holdKnob.x - r.holdCell.x == r.holdCell.right() - r.holdKnob.right());
CHECK(r.holdLabel.y == r.holdKnob.bottom());
CHECK(r.holdLabel.bottom() == r.holdCell.bottom());
CHECK(r.holdLabel.x == r.holdCell.x && r.holdLabel.right() == r.holdCell.right());
// Widening the window leaves the reservation alone; the title takes the extra pixels.
const ChromeRects wide = chromeRects(chromeBand(1000, 620), kKnob);
CHECK(wide.holdCell.width == r.holdCell.width);
}
static void testDegenerateBandYieldsNoInvertedRects() {
const ChromeRects empty = chromeRects(Rect{}, kKnob);
CHECK(empty.toolbar.empty() && empty.controls.empty());
@@ -133,7 +155,8 @@ static void testDegenerateBandYieldsNoInvertedRects() {
// A band far too narrow for the fixed run: everything collapses left, nothing inverts.
const ChromeRects tiny = chromeRects(Rect::ltrb(0, 0, 40, kTitleHeight + kChromeRowHeight),
kKnob);
const Rect items[] = {tiny.title, tiny.preview, tiny.velCell, tiny.velKnob, tiny.velLabel,
const Rect items[] = {tiny.title, tiny.holdCell, tiny.holdKnob, tiny.holdLabel,
tiny.bake, tiny.preview, tiny.velCell, tiny.velKnob, tiny.velLabel,
tiny.chanMono, tiny.chanStereo, tiny.navBrowse,
tiny.rootStrip};
for (const Rect& it : items) CHECK(it.right() >= it.x && it.bottom() >= it.y);
@@ -182,6 +205,7 @@ int main() {
testChromePartsNeverOverlapAtAnyWidth();
testStripOwnsItsWholeRowAndGrowsWithTheWindow();
testVelocityKnobIsCentredInItsCellAboveTheLabel();
testHoldCellIsReservedAndFollowsTheVelocityCellGrammar();
testDegenerateBandYieldsNoInvertedRects();
testPreviewGlyphSitsInsideTheButtonAndPointsRight();
testPreviewGlyphDegradesRatherThanOverflowing();