// Layout-BUDGET tests for reasampler::instrument::ui::deck_groups, split from // test_deck_groups.cpp on the seam CMakeLists.txt already named: these fixtures need // sample_bands (the window-floor constants, computeSampleBands) and master_meter // (kMeterColumnW, MASTER's reserve), which test_deck_groups.cpp's WHICH-descriptors fixtures do // not. Pins the editor floor's derivation from the deck's width budget, the row/gutter // justification arithmetic at and above the floor, the pinned Gate group widths, and MASTER's // interior to the pixel. test_deck_groups.cpp pins WHICH descriptors the deck carries; this // file pins what the width budget MEASURES them at. #include "../src/core/instrument/ui/deck_groups.h" #include "../src/core/instrument/ui/master_meter.h" // kMeterColumnW: MASTER's reserve IS this #include "../src/core/instrument/ui/sample_bands.h" #include #include using namespace reasampler; using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // The editor's floor width, which is also its default (checkSizeConstraint clamps to it), less // the band allocator's kPad inset on each side. static constexpr int kAvailAtMinWidth = kEditorMinWidth - 2 * kPad; static int indexOfGroup(const std::vector& g, int id) { for (std::size_t i = 0; i < g.size(); ++i) { if (g[i].id == id) return static_cast(i); } return -1; } static int cell(DeckParam p) { return static_cast(p); } // The guard the raised floor exists to provide: at the smallest window the host can produce, // the deck band still lands inside the client area AND the waveform still gets its two-lane // floor. Growing the deck past what the floor height can hold fails HERE instead of silently pushing // FILTER ENV / AMP / VOICE / MASTER off-screen, where there is no scroll to reach them. static void testDeckFitsInsideTheEnforcedMinimumWindow() { for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { const std::vector g = sampleDeckGroups(mode); const int h = deckHeight(g); const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, h); CHECK(deckRowCount(g) == 2); // either face CHECK(b.decks.height == h); // The reflow's 112 px land in the waveform: at two rows the deck band is 216 and the // waveform 358, against 328/246 before. Pinned now that both are reached by // construction rather than by a pack outcome. CHECK(b.decks.height == 2 * kDeckGroupH + kDeckRowGap); CHECK(b.waveform.height == 358); // Bottom-anchored INSIDE the pad is the whole assertion: the degrade path pushes the // deck down until the waveform hits its floor, so any deck too tall to fit stops // landing on this exact line. A `<= kEditorMinHeight` bound would not catch it — the // degrade can still leave the deck ending at the window edge. CHECK(b.decks.bottom() == kEditorMinHeight - kPad); CHECK(b.waveform.height >= kWaveformMinHeight); } } // The floor is a DERIVED number, and this is the one place the derivation is written down — // sample_bands stays independent of knob_deck, so neither header can hold it. This fixture is // the only one that includes both. static void testTheEditorFloorIsDerivedFromTheDeckWidthBudget() { CHECK(kDeckRowBlockW + kDeckGroupGap + kDeckSpanningW + 2 * kPad == kEditorMinWidth); // The budget: what is left between the derived floor and the hard ceiling, and it is spent // once. A cell costs 60 of it. CHECK(kEditorCeilingWidth - kEditorMinWidth == 82); // 82 still buys one more deck cell (60), which is the only purchase the ledger promises — // the widen below spent 8 px of slack, not the layout's purchasing power. CHECK(kEditorCeilingWidth - kEditorMinWidth >= kDeckCellW); // The reflow's 112 px goes entirely to the waveform, so the height does not move. CHECK(kEditorMinHeight == 680); // 1190 + 8: the row block was widened 1020 -> 1028 to put the two rows' filter edges on // one pixel, which is the only reason the floor moved off its originally specified value. CHECK(kEditorMinWidth == 1198); CHECK(kEditorMinWidth <= kEditorCeilingWidth); CHECK(kEditorMinHeight <= 720); // And the row block really is what the two rows justify inside — derived from the floor // and the spanning reserve, not restated. CHECK(kEditorMinWidth - 2 * kPad - kDeckSpanningW - kDeckGroupGap == kDeckRowBlockW); } // Both rows fit their block, in BOTH play modes. The filter mod depth's move across the rows is // what these two numbers now carry: SOUND loses one cell (980 -> 920) and CONTOUR gains one // (876 -> 936). Row 2's 936 is mode-stable because FILTER ENV's and AMP's reserve slots hold // them at 372/312 in Trigger too — asserted here rather than assumed. static void testBothRowsAndTheSpanningDeckFitTheBudget() { for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { const std::vector g = sampleDeckGroups(mode); int width[3] = {0, 0, 0}; int count[3] = {0, 0, 0}; for (const DeckGroupDesc& d : g) { const int r = static_cast(deckRowFor(static_cast(d.id))); width[r] += deckGroupWidth(d); ++count[r]; } const int sound = static_cast(DeckRow::Sound); const int contour = static_cast(DeckRow::Contour); const int spanning = static_cast(DeckRow::Spanning); CHECK(count[sound] == 4); CHECK(width[sound] == 920); // 192 + 372 + 192 + 164 CHECK(count[contour] == 3); CHECK(width[contour] == 936); // 252 + 372 + 312 CHECK(count[spanning] == 1); CHECK(width[spanning] == kDeckSpanningW); // 142 exactly — the reserve is now spent for (int r : {sound, contour}) { CHECK(width[r] <= kDeckRowBlockW); // Slack enough that no gutter in the row falls under the minimum. CHECK(kDeckRowBlockW - width[r] >= (count[r] - 1) * kDeckGroupGap); } } } // The gutters the justification law produces at the floor — and the alignment it no longer // buys. THE FILTER TIE-LINE IS GONE, and it is recorded here as a LOSS rather than left to be // rediscovered: moving the mod depth from FILTER to FILTER ENV made the two filter groups // EQUAL in width (372 each), and under space-between two equal groups whose rows carry // different preceding widths can only share a right edge at one block width — which the // arithmetic below shows is far below the width either row needs. It is unreachable, not // merely missed, so kDeckRowBlockW and the editor floor are deliberately NOT moved to chase it. static void testGutterArithmeticAndTheLostFilterTieLineAtTheFloor() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); const auto box = [&](int id) { return dl.groups[static_cast(indexOfGroup(g, id))].box; }; // Row 1: flush left, flush right on the block, and three EQUAL gutters — 108 divides by 3 // with no residue, so no gutter carries a leftover pixel. CHECK(box(kGroupPitch).x == kPad); CHECK(box(kGroupFilter).x - box(kGroupPitch).right() == 36); CHECK(box(kGroupVelocity).x - box(kGroupFilter).right() == 36); CHECK(box(kGroupVoice).x - box(kGroupVelocity).right() == 36); CHECK(box(kGroupVoice).right() == kPad + kDeckRowBlockW); // Row 2: flush left, flush right, two gutters exactly equal — 92 over two. CHECK(box(kGroupPitchEnv).x == kPad); CHECK(box(kGroupFilterEnv).x - box(kGroupPitchEnv).right() == 46); CHECK(box(kGroupAmpEnv).x - box(kGroupFilterEnv).right() == 46); CHECK(box(kGroupAmpEnv).right() == kPad + kDeckRowBlockW); // The loss, block-relative and exact: row 1's filter edge lands 70 px LEFT of row 2's. CHECK(box(kGroupFilter).right() - kPad == 600); CHECK(box(kGroupFilterEnv).right() - kPad == 670); CHECK(box(kGroupFilter).right() != box(kGroupFilterEnv).right()); // And it is unreachable at any block width, which is the part that makes it a loss rather // than a tuning problem. Solving 192 + (W-920)/3 == 252 + (W-936)/2 over the reals gives // W = 608 — narrower than either row's own content (920 and 936), so no block that can // hold the deck at all can also tie the two edges. const double tieAt = 608.0; for (int W : {920, 936, kDeckRowBlockW}) CHECK(static_cast(W) > tieAt); CHECK(192.0 + (tieAt - 920.0) / 3.0 == 252.0 + (tieAt - 936.0) / 2.0); // MASTER is right-anchored outside the block, one kDeckGroupGap clear of it. CHECK(box(kGroupMaster).x - box(kGroupVoice).right() == kDeckGroupGap); CHECK(box(kGroupMaster).right() == kPad + kAvailAtMinWidth); } // No gutter is ever narrower than kDeckGroupGap at or above the floor, and both rows stay // flush at every width — the property the exact-at-the-floor numbers above are one point of. // The two filter edges SEPARATE monotonically with width, which is accepted and deliberate: // row 1 divides its slack over three gutters and row 2 over two, so row 2's filter edge pulls // right past row 1's and the gap only opens. Encoded as EXPECTED, not as a failure. // // Checked per ROW (tracking the last-seen box in each of the two categorical rows while // walking dl.groups in deck order), not just deck-order neighbours: two same-row groups can // sit apart in deck order with a different-row group between them, and a deck-order-only // check would silently skip that gutter. static void testGuttersHoldTheirMinimumAndTheTieLineDriftsAboveTheFloor() { for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { const std::vector g = sampleDeckGroups(mode); int lastDrift = 1 << 20; // sentinel above any real drift for (int avail = kAvailAtMinWidth; avail <= kAvailAtMinWidth + 600; avail += 37) { const DeckLayout dl = layoutDeck(g, kPad, 0, avail); const DeckGroupLayout* prevInRow[2] = {nullptr, nullptr}; for (const DeckGroupLayout& gl : dl.groups) { const DeckRow row = deckRowFor(static_cast(gl.id)); if (row == DeckRow::Spanning) continue; const int r = row == DeckRow::Contour ? 1 : 0; if (prevInRow[r]) { CHECK(gl.box.x - prevInRow[r]->box.right() >= kDeckGroupGap); } prevInRow[r] = ≷ } const auto right = [&](int id) { return dl.groups[static_cast(indexOfGroup(g, id))].box.right(); }; // Flush right on the block at every width, both rows. CHECK(right(kGroupVoice) == right(kGroupAmpEnv)); // Monotone in width rather than oscillating: row 2's two gutters absorb slack // faster than row 1's three, so the gap only ever opens. const int drift = right(kGroupFilter) - right(kGroupFilterEnv); CHECK(drift <= lastDrift); lastDrift = drift; } // It really does open up, from the −70 the floor already carries. CHECK(lastDrift < -70); } } // MASTER's interior, exact to the pixel (§1.4). The two left slots sit on the two rows' own // knob baselines — that is what "stitched to both rows" means — and the meter is ONE rect // across both, never a readout per row. static void testTheMasterDeckInteriorLandsOnBothRowBaselines() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); const DeckGroupLayout& m = dl.groups[static_cast(indexOfGroup(g, kGroupMaster))]; CHECK(m.box.width == kDeckSpanningW); CHECK(m.box.height == 216); // 6 + 60 + 8 + 62 + 6 — the decomposition, not just the total, and the 62 is the meter // module's own kMeterColumnW rather than a copy of it. That link is the whole point: the // column is banked to GROW (§1.2), and a reserve that did not track it would leave the // interior underfilling or overrunning with every test still green. CHECK(kDeckGroupPadX + kDeckCellW + kDeckColumnGap + kMeterColumnW + kDeckGroupPadX == kDeckSpanningW); CHECK(m.column.id == cell(DeckParam::kMasterMeter)); CHECK(m.column.box.width == kMeterColumnW); // One cell drawn (gain) and one slot RESERVED below it: the reserve is height at a fixed // position and draws nothing. CHECK(m.cells.size() == 1); CHECK(m.cells[0].id == cell(DeckParam::kMasterGain)); CHECK(m.cells[0].cell.y - m.box.y == 26); const int reserveTop = m.cells[0].cell.y + kDeckGroupH + kDeckRowGap; CHECK(reserveTop - m.box.y == 138); // The two baselines are row 1's and row 2's own. const DeckGroupLayout& filter = dl.groups[static_cast(indexOfGroup(g, kGroupFilter))]; const DeckGroupLayout& amp = dl.groups[static_cast(indexOfGroup(g, kGroupAmpEnv))]; CHECK(m.cells[0].cell.y == filter.cells[0].cell.y); CHECK(reserveTop == amp.cells[0].cell.y); // The meter: one rect spanning both baselines, 62 x 186. CHECK(m.column.id == cell(DeckParam::kMasterMeter)); CHECK(m.column.box.width == kMeterColumnW); CHECK(m.column.box.height == 186); CHECK(m.column.box.y == m.cells[0].cell.y); CHECK(m.column.box.bottom() - m.box.y == 212); } // The regression guard for the rule most likely to be "generalised" wrongly: MASTER's left // column is FIXED slots at the two baselines, NOT knob_deck's horizontal run-division law // applied vertically — which would stretch the one gain knob over the whole 186 px. static void testTheMasterColumnDoesNotDivideItsRunVertically() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); const DeckGroupLayout& m = dl.groups[static_cast(indexOfGroup(g, kGroupMaster))]; CHECK(m.cells[0].cell.height == kDeckCellH); CHECK(m.cells[0].cell.width == kDeckCellW); // Under the run-division law the lone present cell would take the whole two-slot run; // here it takes exactly one slot and leaves the rest empty. CHECK(m.cells[0].cell.height < m.column.box.height); CHECK(m.cells[0].cell.bottom() < m.column.box.bottom()); CHECK(m.cells[0].knob.width == kDeckKnobSize && m.cells[0].knob.height == kDeckKnobSize); // And dropping the reserve does not move the gain knob or the meter — the slot below it is // reserved height, so nothing above it depends on whether it is there. std::vector noReserve = g; for (DeckGroupDesc& d : noReserve) { if (d.id == kGroupMaster) d.cellIds = {cell(DeckParam::kMasterGain)}; } const DeckLayout dl2 = layoutDeck(noReserve, kPad, 0, kAvailAtMinWidth); const DeckGroupLayout& m2 = dl2.groups[static_cast(indexOfGroup(noReserve, kGroupMaster))]; CHECK(m2.cells[0].cell == m.cells[0].cell); CHECK(m2.column.box == m.column.box); } // MASTER is the group that BINDS the single-button enable width, and it has ZERO slack: its // knob row measures kDeckSpanningW − 2·pad, so the caption row (46 + gap + button + gap + the // GR lamp) may reach exactly that and no more. Past 64 the caption row takes over, the spanning // deck grows, and the growth comes straight out of the 82 px between the editor's floor and its // ceiling. Pinned at the boundary in both directions rather than as an inequality. static void testTheLimiterButtonIsAtMostSixtyFourPxBeforeMasterGrows() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckGroupDesc& m = g[static_cast(indexOfGroup(g, kGroupMaster))]; CHECK(m.captionToggle.id == cell(DeckParam::kLimiterEnable)); CHECK(m.captionToggle.style == DeckToggleStyle::kEnable); CHECK(deckGroupWidth(m) == kDeckSpanningW); // The knob row IS the measurement, and it is exactly the group's inner width. CHECK(kDeckCellW + kDeckColumnGap + kMeterColumnW == kDeckSpanningW - 2 * kDeckGroupPadX); DeckGroupDesc probe = m; probe.captionToggle.width = 64; CHECK(deckGroupWidth(probe) == kDeckSpanningW); // at the ceiling, still knob-row-driven probe.captionToggle.width = 65; CHECK(deckGroupWidth(probe) > kDeckSpanningW); // one past it, the spanning deck grows // And the shipped width is inside the ceiling, so the budget below stays unspent. CHECK(m.captionToggle.width <= 64); } // hitTestKnobFace resolves against the drawn CIRCLES and runs no toggle-precedence pass, so it // is only correct while no toggle rect reaches a dial. The single-button styles made every // button on the deck wider, so the claim is re-checked here over the SHIPPED descriptors in // both faces — test_knob_deck's peer proves the geometry over a synthetic group; this proves it // for the buttons that actually ship. Rect disjointness rather than a pixel sweep: inKnobFace // answers only inside the knob rect, so no overlapping pixel can exist without one. static void testNoShippedToggleReachesADrawnKnobFace() { const auto disjoint = [](const Rect& a, const Rect& b) { return a.empty() || b.empty() || a.right() <= b.x || b.right() <= a.x || a.bottom() <= b.y || b.bottom() <= a.y; }; for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { const std::vector g = sampleDeckGroups(mode); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); int swept = 0; for (const DeckGroupLayout& lay : dl.groups) { for (const DeckToggleLayout* t : {&lay.captionToggle, &lay.captionToggle2, &lay.rowToggle}) { if (t->id < 0) continue; ++swept; for (const Rect& seg : {t->seg0, t->seg1}) { // Against every group's cells, not just this one's: the row toggle anchors // past its own run and a neighbour is what it would reach first. for (const DeckGroupLayout& other : dl.groups) { for (const DeckCellLayout& c : other.cells) CHECK(disjoint(seg, c.knob)); } } } } CHECK(swept == 11); // every shipped toggle was actually reached by the sweep } } // The 82 px between the floor and the ceiling is untouched by this whole reflow — the mod // depth's move is a swap between the two rows, not a purchase. static void testTheEditorWidthBudgetIsStillUnspent() { CHECK(kEditorMinWidth == 1198); CHECK(kEditorCeilingWidth - kEditorMinWidth == 82); CHECK(kDeckRowBlockW == 1028); CHECK(kDeckSpanningW == 142); } // Widen the caption reserve alone (as a wider caption or a limiter-toggle change would) and the // column must still land flush against the group's own right padding, derived from innerRight // rather than measured past the cell slots — the bug a balanced caption row once hid. static void testMasterColumnStaysRightAnchoredWhenCaptionRowOutgrowsTheKnobRow() { const std::vector g = sampleDeckGroups(PlayMode::Gate); DeckGroupDesc probe = g[static_cast(indexOfGroup(g, kGroupMaster))]; probe.captionWidth += 40; // unbalances it: the caption row now measures past the knob row const std::vector one = {probe}; const DeckLayout dl = layoutDeck(one, kPad, 0, kAvailAtMinWidth); const DeckGroupLayout& m = dl.groups[0]; CHECK(m.box.width > kDeckSpanningW); // the widen is real, not absorbed elsewhere CHECK(m.column.box.right() == m.box.right() - kDeckGroupPadX); } // The three mode toggles ride each env group's caption slack, on the CONTOUR row: raising // their segment width past the caption headroom would widen that row and eat its gutters, // not add a row — row count is a property of the group inventory, not of width. static void testTheModeTogglesCostNoGroupWidth() { for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { for (const DeckGroupDesc& g : sampleDeckGroups(mode)) { if (g.captionToggle2.id < 0) continue; DeckGroupDesc without = g; without.captionToggle2 = DeckToggleDesc{}; CHECK(deckGroupWidth(g) == deckGroupWidth(without)); } } } // PITCH/RATE carries three cells and measures exactly 192 — the KNOB row (3 x kDeckCellW plus // padding) is what it measures from, and the caption row must stay under that. The ceiling is // asserted by construction rather than as a comment: at a caption reserve of 80 the group is // still 192, and at 81 it is not, which is the whole content of "hard ceiling 80". Widening the // group is not the remedy if the caption text ever outgrows it — narrowing the mode toggle is. static void testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckGroupDesc* pitch = nullptr; for (const DeckGroupDesc& d : g) if (d.id == kGroupPitch) pitch = &d; CHECK(pitch != nullptr); if (!pitch) return; CHECK(pitch->cellIds.size() == 3); CHECK(pitch->cellIds[0] == static_cast(DeckParam::kKeyTrack)); CHECK(pitch->cellIds[1] == static_cast(DeckParam::kRate)); CHECK(pitch->cellIds[2] == static_cast(DeckParam::kPitch)); CHECK(deckGroupWidth(*pitch) == 192); CHECK(3 * kDeckCellW + 2 * kDeckGroupPadX == 192); // the knob row IS the measurement DeckGroupDesc probe = *pitch; probe.captionWidth = 80; CHECK(deckGroupWidth(probe) == 192); // at the ceiling the caption row still fits under it probe.captionWidth = 81; CHECK(deckGroupWidth(probe) > 192); // one past it, the caption row takes over } // The kEnvModeW ceilings recorded in deck_groups.cpp's own comment (PITCH ENV binds at 122, // AMP at 126) pinned against the descriptors they derive from, the same way the Pitch/Rate // caption ceiling above is: a change to either group's caption width or its enable button // would otherwise invalidate the recorded numbers with nothing failing. static void testEnvModeCeilingsArePinnedForPitchEnvAndAmp() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckGroupDesc& penv = g[static_cast(indexOfGroup(g, kGroupPitchEnv))]; const DeckGroupDesc& amp = g[static_cast(indexOfGroup(g, kGroupAmpEnv))]; CHECK(deckGroupWidth(penv) == 252); CHECK(deckGroupWidth(amp) == 312); DeckGroupDesc penvProbe = penv; penvProbe.captionToggle2.width = 122; CHECK(deckGroupWidth(penvProbe) == 252); // at the ceiling, still knob-row-driven penvProbe.captionToggle2.width = 123; CHECK(deckGroupWidth(penvProbe) > 252); // one past it, the caption row takes over DeckGroupDesc ampProbe = amp; ampProbe.captionToggle2.width = 126; CHECK(deckGroupWidth(ampProbe) == 312); ampProbe.captionToggle2.width = 127; CHECK(deckGroupWidth(ampProbe) > 312); // The two ceilings above are what make PITCH ENV the binding group: 122 < 126, so the // shipped width has to clear PITCH ENV's, and it does. CHECK(penv.captionToggle2.width <= 122); } // Every group's width, in BOTH play modes, against the measured layout table // (instrument-control-surface.md §1.2). Mode-independence is the second half of the claim: the // reserve slots hold the two mode-dependent groups at 372 (FILTER ENV) and 312 (AMP ENVELOPE) // either way, which is what makes the contour row's 936 a constant rather than a Gate-only fact. static void testEveryGroupWidthMatchesTheMeasuredLayout() { const struct { int id; int width; } want[] = { {kGroupPitch, 192}, {kGroupPitchEnv, 252}, {kGroupFilter, 372}, {kGroupFilterEnv, 372}, {kGroupAmpEnv, 312}, {kGroupVelocity, 192}, {kGroupVoice, 164}, {kGroupMaster, 142}, }; for (PlayMode mode : {PlayMode::Gate, PlayMode::Trigger}) { const std::vector g = sampleDeckGroups(mode); CHECK(g.size() == sizeof(want) / sizeof(want[0])); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); for (const auto& w : want) { const int i = indexOfGroup(g, w.id); CHECK(i >= 0); if (i < 0) continue; CHECK(deckGroupWidth(g[static_cast(i)]) == w.width); const DeckGroupLayout& lay = dl.groups[static_cast(indexOfGroup(g, w.id))]; CHECK(lay.box.width == w.width); } // EVERY cell is kDeckCellW in EITHER mode — the spacing law. Trigger's two reduced // faces keep the same reserved run and spend it on end margins, not on wider knobs. for (const DeckGroupLayout& lay : dl.groups) { for (const DeckCellLayout& c : lay.cells) CHECK(c.cell.width == kDeckCellW); } } } // THE Gate-face regression pin. Every group box and every cell rect at the editor's floor, // block-relative, against the pre-reflow measurements. Three of the eight are the whole point: // FILTER 432 -> 372, one cell narrower — the mod depth left it. // FILTER ENV 312 -> 372, one cell wider — the mod depth arrived. // VELOCITY its box translates 20 px LEFT. Nothing about the group changed; row 1's freed // 60 px is divided over three gutters by the space-between law, and every group // between the narrowed one and the row's flush-right end shifts by the share it // did not absorb. That translation is the law working, not a second edit. // Everything else — PITCH/RATE, PITCH ENV, AMP ENV, VOICE, MASTER — is pinned UNCHANGED to the // pixel, boxes and cells alike, which is the criterion this reflow is measured against. static void testTheGateFaceIsPixelIdenticalApartFromTheTwoFilterGroups() { const std::vector g = sampleDeckGroups(PlayMode::Gate); const DeckLayout dl = layoutDeck(g, kPad, 0, kAvailAtMinWidth); const auto lay = [&](int id) -> const DeckGroupLayout& { return dl.groups[static_cast(indexOfGroup(g, id))]; }; // {group, block-relative box x, width, cell count} — the pre-reflow numbers for the five // untouched groups, and the derived ones for the three the move implicates. const struct { int id; int x; int w; std::size_t cells; } want[] = { {kGroupPitch, 0, 192, 3}, // unchanged {kGroupFilter, 228, 372, 6}, // was x=208 w=432 with 7 cells {kGroupVelocity, 636, 192, 3}, // unchanged group, box translated from x=656 {kGroupVoice, 864, 164, 1}, // unchanged {kGroupPitchEnv, 0, 252, 4}, // unchanged {kGroupFilterEnv, 298, 372, 6}, // was x=328 w=312 with 5 cells {kGroupAmpEnv, 716, 312, 5}, // unchanged {kGroupMaster, 1040, 142, 1}, // unchanged — the claim above actually pins it }; for (const auto& w : want) { const DeckGroupLayout& l = lay(w.id); CHECK(l.box.x - kPad == w.x); CHECK(l.box.width == w.w); CHECK(l.cells.size() == w.cells); // Cells: natural pitch, abutting, starting flush at the group's inner left (no Gate // group carries a reserve, so the centring offset is zero everywhere here). CHECK(l.cells.front().cell.x == l.box.x + kDeckGroupPadX); for (std::size_t k = 0; k < l.cells.size(); ++k) { CHECK(l.cells[k].cell.width == kDeckCellW); CHECK(l.cells[k].cell.x - l.box.x == kDeckGroupPadX + static_cast(k) * kDeckCellW); } } // The two filter groups moved by EXACTLY one cell, in opposite directions. CHECK(lay(kGroupFilter).box.width + kDeckCellW == 432); CHECK(lay(kGroupFilterEnv).box.width - kDeckCellW == 312); } int main() { testDeckFitsInsideTheEnforcedMinimumWindow(); testTheGateFaceIsPixelIdenticalApartFromTheTwoFilterGroups(); testTheEditorFloorIsDerivedFromTheDeckWidthBudget(); testBothRowsAndTheSpanningDeckFitTheBudget(); testGutterArithmeticAndTheLostFilterTieLineAtTheFloor(); testGuttersHoldTheirMinimumAndTheTieLineDriftsAboveTheFloor(); testTheMasterDeckInteriorLandsOnBothRowBaselines(); testTheMasterColumnDoesNotDivideItsRunVertically(); testTheLimiterButtonIsAtMostSixtyFourPxBeforeMasterGrows(); testNoShippedToggleReachesADrawnKnobFace(); testTheEditorWidthBudgetIsStillUnspent(); testMasterColumnStaysRightAnchoredWhenCaptionRowOutgrowsTheKnobRow(); testTheModeTogglesCostNoGroupWidth(); testThePitchRateGroupIsKnobRowDrivenAtExactlyOneNinetyTwo(); testEnvModeCeilingsArePinnedForPitchEnvAndAmp(); testEveryGroupWidthMatchesTheMeasuredLayout(); if (g_fail == 0) std::printf("deck_groups_measured: all tests passed\n"); return g_fail == 0 ? 0 : 1; }