Loop: an explicit enable, four named marks with grabbable caps, and the crossfade painted where it is actually heard

hasLoop becomes user-owned with the gestures as shortcuts onto it; no format change. START uses overlay/trace, not accent/primary, which is the waveform's own fill.
This commit is contained in:
2026-08-02 05:18:53 -04:00
parent ef59265e7a
commit a7c3c7a828
26 changed files with 1190 additions and 191 deletions
+27 -8
View File
@@ -3,7 +3,8 @@
//
// Covers: the chrome band's two rows (toolbar over strip row, tiling the band exactly); 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
// Loop Off|On, Mono|Stereo, Browse) with the title taking the remainder and still holding its
// text at the editor's floor; 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
@@ -58,7 +59,10 @@ static void testToolbarRunIsOrderedRightToLeftWithoutOverlap() {
CHECK(r.navBrowse.width == kNavButtonWidth);
CHECK(r.chanStereo.right() <= r.navBrowse.x);
CHECK(r.chanMono.right() == r.chanStereo.x);
CHECK(r.velCell.right() <= r.chanMono.x);
CHECK(r.loopOn.right() <= r.chanMono.x); // the enable is immediately left of Mono|Stereo
CHECK(r.loopOff.right() == r.loopOn.x); // its two segments abut, like the channel pair
CHECK(r.loopOff.y == r.chanMono.y && r.loopOff.height == r.chanMono.height);
CHECK(r.velCell.right() <= r.loopOff.x);
CHECK(r.preview.right() <= r.velCell.x);
CHECK(r.bake.right() <= r.preview.x);
CHECK(r.bake.width == kBakeButtonWidth);
@@ -70,8 +74,8 @@ static void testToolbarRunIsOrderedRightToLeftWithoutOverlap() {
CHECK(r.title.width > 0);
// Every toolbar rect sits inside the toolbar row.
const Rect items[] = {r.title, r.holdCell, r.bake, r.preview, r.velCell, r.chanMono,
r.chanStereo, r.navBrowse};
const Rect items[] = {r.title, r.holdCell, r.bake, r.preview, r.velCell, r.loopOff,
r.loopOn, r.chanMono, r.chanStereo, r.navBrowse};
for (const Rect& it : items) {
CHECK(it.y >= r.toolbar.y && it.bottom() <= r.toolbar.bottom());
}
@@ -84,8 +88,8 @@ 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.holdCell, r.bake, r.preview, r.velCell, r.chanMono,
r.chanStereo, r.navBrowse};
const Rect items[] = {r.holdCell, r.bake, r.preview, r.velCell, r.loopOff, r.loopOn,
r.chanMono, r.chanStereo, r.navBrowse};
for (const Rect& it : items) {
CHECK(!overlaps(it, r.rootStrip));
CHECK(!overlaps(it, r.title));
@@ -147,6 +151,20 @@ static void testHoldCellIsReservedAndFollowsTheVelocityCellGrammar() {
CHECK(wide.holdCell.width == r.holdCell.width);
}
// The enable joins a RIGHT-ANCHORED run, so it is charged to the title slot and not to the
// window. kEditorMinWidth itself is guarded by test_deck_groups' derived-floor assertion — this
// is the other half of that contract: the title must still hold its text AT that floor, because
// the agreed remedy if it cannot is to narrow the enable's segments, never to move the floor.
static void testTheControlRunLeavesTheTitleReadableAtTheEditorFloor() {
const ChromeRects r = chromeRects(chromeBand(), kKnob);
// "ReaSampler 9000" (15 chars) plus a bracketed 20-char capture name, at the toolbar font's
// generous ~7 px/char estimate: 38 * 7.
constexpr int kTitleTextFloorPx = 266;
CHECK(r.title.width >= kTitleTextFloorPx);
// Nothing in the run reaches into the title's slot.
CHECK(r.title.right() <= r.holdCell.x);
}
static void testDegenerateBandYieldsNoInvertedRects() {
const ChromeRects empty = chromeRects(Rect{}, kKnob);
CHECK(empty.toolbar.empty() && empty.controls.empty());
@@ -157,8 +175,8 @@ static void testDegenerateBandYieldsNoInvertedRects() {
kKnob);
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};
tiny.loopOff, tiny.loopOn, tiny.chanMono, tiny.chanStereo,
tiny.navBrowse, tiny.rootStrip};
for (const Rect& it : items) CHECK(it.right() >= it.x && it.bottom() >= it.y);
}
@@ -206,6 +224,7 @@ int main() {
testStripOwnsItsWholeRowAndGrowsWithTheWindow();
testVelocityKnobIsCentredInItsCellAboveTheLabel();
testHoldCellIsReservedAndFollowsTheVelocityCellGrammar();
testTheControlRunLeavesTheTitleReadableAtTheEditorFloor();
testDegenerateBandYieldsNoInvertedRects();
testPreviewGlyphSitsInsideTheButtonAndPointsRight();
testPreviewGlyphDegradesRatherThanOverflowing();