instrument: a deck group's reserved cell width goes to the cells present

A Trigger face dropping Sustain and Release now gets wider cells instead of
144 px of dead slots. Group widths, row packing and Gate are untouched.
This commit is contained in:
2026-07-31 22:43:13 -04:00
parent 1c774226d3
commit d8ffd860d1
8 changed files with 213 additions and 29 deletions
+61 -8
View File
@@ -2,15 +2,17 @@
// assert loop as the sibling pure tests. Assert the r11 deck layout HARD:
//
// * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths.
// * layout — caption toggle right-anchored IN the caption row; cells fixed 48x58 left-to-right
// * layout — caption toggle right-anchored IN the caption row; cells abutting left-to-right
// inside the box; knob square centered; label band beneath; row toggle after the cells.
// * reserves — a -1 id holds the group's width and hands its pixels to the cells present.
// * wrap — deterministic whole-group wrap at a narrowing width; the first group of a row
// always places; deckHeight consistency with deckRowCount.
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, blank (-1) cells
// and fence padding miss, outside-deck miss.
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, fence padding
// misses, outside-deck misses.
#include "../src/core/instrument/ui/knob_deck.h"
#include <algorithm>
#include <cstdio>
#include <vector>
@@ -146,14 +148,19 @@ static void testHitTest() {
h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1);
CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);
// A blank cell (id -1) misses even though its rect exists.
// A reserve (id -1) yields no cell of its own, so every point of the knob row lands on a
// real control: no dead rect survives for a grab to fall into.
std::vector<DeckGroupDesc> trig;
trig.push_back({0, 78, {}, {100, 44}, {}, {20, 21, 22, -1, -1}, {}});
const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
const DeckCellLayout& blank = tl.groups[0].cells[4];
CHECK(blank.id == -1);
h = hitTestDeck(tl, blank.cell.x + 5, blank.cell.y + 5);
CHECK(h.kind == DeckHitKind::None);
const DeckGroupLayout& tg = tl.groups[0];
CHECK(tg.cells.size() == 3);
for (const DeckCellLayout& c : tg.cells) CHECK(c.id >= 0);
const DeckCellLayout& last = tg.cells.back();
for (int px = tg.cells[0].cell.x; px < last.cell.right(); ++px) {
const DeckHit rowHit = hitTestDeck(tl, px, last.cell.y + 5);
CHECK(rowHit.kind == DeckHitKind::Knob && rowHit.id >= 0);
}
// The fence padding inside the box misses; outside the deck misses.
h = hitTestDeck(dl, amp.box.x + 1, amp.box.bottom() - 1);
@@ -162,6 +169,51 @@ static void testHitTest() {
CHECK(h.kind == DeckHitKind::None);
}
// A reserve holds the group's WIDTH and hands its pixels to the cells that are present. The
// three properties together are what stops a narrower face reading as a hole: the group is
// exactly as wide as the full-face one, the cells are uniform and abutting, and what they do
// not cover is smaller than one pixel per cell.
static void testReservedCellWidthGoesToTheCellsPresent() {
const DeckGroupDesc full{0, 78, {}, {100, 44}, {}, {20, 21, 22, 23, 24}, {}};
// Three, four, and a lone cell against the same five-slot reserve — 240/3, 240/4, 240/1.
const std::vector<std::vector<int>> faces = {
{20, 21, 22, -1, -1}, {20, 21, 22, 23, -1}, {20, -1, -1, -1, -1}};
for (const std::vector<int>& ids : faces) {
DeckGroupDesc narrow = full;
narrow.cellIds = ids;
CHECK(deckGroupWidth(narrow) == deckGroupWidth(full));
std::vector<DeckGroupDesc> g{narrow};
const DeckLayout dl = layoutDeck(g, 0, 0, 824);
const DeckGroupLayout& lay = dl.groups[0];
const int present = static_cast<int>(lay.cells.size());
CHECK(present == 5 - static_cast<int>(std::count(ids.begin(), ids.end(), -1)));
const int run = 5 * kDeckCellW;
for (int i = 0; i < present; ++i) {
const DeckCellLayout& c = lay.cells[static_cast<std::size_t>(i)];
CHECK(c.cell.width == lay.cells[0].cell.width); // uniform
CHECK(c.knob.width == kDeckKnobSize); // the dial itself is fixed
CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
if (i > 0) CHECK(c.cell.x == lay.cells[static_cast<std::size_t>(i - 1)].cell.right());
}
// Uncovered run is the indivisible residue only, split evenly at the two ends.
const int covered = lay.cells.back().cell.right() - lay.cells[0].cell.x;
CHECK(run - covered < present);
const int leadPad = lay.cells[0].cell.x - (lay.box.x + kDeckGroupPadX);
CHECK(leadPad == (run - covered) / 2);
}
// A reserve does not move the row toggle: it anchors past the whole run, so the FILTER
// group's law switch cannot drift when a neighbouring face changes shape.
DeckGroupDesc withToggle{1, 40, {}, {}, {}, {20, 21, 22, 23, 24}, {104, 44}};
std::vector<DeckGroupDesc> a{withToggle};
withToggle.cellIds = {20, 21, -1, -1, -1};
std::vector<DeckGroupDesc> b{withToggle};
CHECK(layoutDeck(a, 0, 0, 824).groups[0].rowToggle.seg0 ==
layoutDeck(b, 0, 0, 824).groups[0].rowToggle.seg0);
}
// The corner radio widens the caption row, takes the far corner, and pushes the caption
// toggle left of itself — the three properties the overlay-select switch relies on.
static void testCaptionRadioGeometryAndHit() {
@@ -254,6 +306,7 @@ int main() {
testFirstGroupAlwaysPlaces();
testGroupInnerGeometry();
testHitTest();
testReservedCellWidthGoesToTheCellsPresent();
testCaptionRadioGeometryAndHit();
testInnerDialHit();
testCaptionToggle2();