Reflow the deck into two categorical rows plus a double-height MASTER bus deck
Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
This commit is contained in:
+182
-37
@@ -5,8 +5,8 @@
|
||||
// * 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.
|
||||
// * rows — membership comes from the group's own DeckRow, never from a wrap outcome;
|
||||
// space-between justification inside the row block; the right-anchored spanning deck.
|
||||
// * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, fence padding
|
||||
// misses, outside-deck misses.
|
||||
// * knob-FACE hit-test — the reset resolve against the drawn circles: inner disc, outer ring,
|
||||
@@ -52,42 +52,183 @@ static void testGroupWidth() {
|
||||
// No toggles: max(caption, cells) + padding.
|
||||
DeckGroupDesc master{4, 46, {}, {}, {}, {11}, {}};
|
||||
CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
|
||||
// A spanning group's cells STACK, so extra slots cost it no width — only its readout
|
||||
// column does. Two slots measure the same as one.
|
||||
DeckGroupDesc bus{5, 46, {}, {}, {}, {11}, {}, DeckRow::Spanning, {300, 62}};
|
||||
CHECK(deckGroupWidth(bus) == kDeckCellW + kDeckColumnGap + 62 + 2 * kDeckGroupPadX);
|
||||
bus.cellIds = {11, -1, -1};
|
||||
CHECK(deckGroupWidth(bus) == kDeckCellW + kDeckColumnGap + 62 + 2 * kDeckGroupPadX);
|
||||
}
|
||||
|
||||
static void testWrapAtNarrowWidthIsDeterministic() {
|
||||
// A width that forces this synthetic deck to wrap: TWO rows, whole trailing groups only.
|
||||
// Deliberately narrower than the shipped editor floor — this pins the wrap MECHANISM, not
|
||||
// the shipped deck's row count (that is deck_groups' own test).
|
||||
const auto deck = shellLikeDeck();
|
||||
CHECK(deckRowCount(deck, 544) == 2);
|
||||
CHECK(deckHeight(deck, 544) == 2 * kDeckGroupH + kDeckRowGap);
|
||||
const DeckLayout dl = layoutDeck(deck, 8, 100, 544);
|
||||
CHECK(dl.rowCount == 2);
|
||||
CHECK(dl.height == deckHeight(deck, 544));
|
||||
CHECK(dl.groups.size() == 5);
|
||||
// Row membership: groups on row 1 share the first top; the wrapped groups sit one row
|
||||
// pitch lower and restart at the left margin.
|
||||
const int row0Top = dl.groups[0].box.y;
|
||||
const int row1Top = row0Top + kDeckGroupH + kDeckRowGap;
|
||||
CHECK(dl.groups[0].box.y == row0Top);
|
||||
CHECK(dl.groups[1].box.y == row0Top);
|
||||
bool sawWrap = false;
|
||||
for (std::size_t i = 1; i < dl.groups.size(); ++i) {
|
||||
if (dl.groups[i].box.y == row1Top && dl.groups[i - 1].box.y == row0Top) {
|
||||
CHECK(dl.groups[i].box.x == 8); // wrapped row restarts at the left edge
|
||||
sawWrap = true;
|
||||
}
|
||||
// A two-row deck with a spanning bus deck, shaped like the shipped one but with synthetic
|
||||
// widths: two Sound groups, two Contour groups, one Spanning group carrying a column.
|
||||
static std::vector<DeckGroupDesc> tworowDeck() {
|
||||
std::vector<DeckGroupDesc> g;
|
||||
g.push_back({0, 78, {}, {100, 44}, {}, {1, 2, 3, 4, 5}, {}, DeckRow::Sound, {}});
|
||||
g.push_back({1, 38, {}, {101, 48}, {}, {6, 7}, {}, DeckRow::Sound, {}});
|
||||
g.push_back({2, 58, {}, {102, 32}, {}, {8, 9, 10}, {}, DeckRow::Contour, {}});
|
||||
g.push_back({3, 38, {}, {103, 40}, {}, {11}, {}, DeckRow::Contour, {}});
|
||||
g.push_back({4, 46, {200, true}, {104, 32}, {}, {12, -1}, {},
|
||||
DeckRow::Spanning, {300, 62}});
|
||||
return g;
|
||||
}
|
||||
|
||||
// Row membership is the GROUP's, and nothing about the width can change it: the same list at
|
||||
// three very different widths lays out as the same two rows plus the same spanning deck.
|
||||
static void testRowMembershipComesFromTheGroupNotTheWidth() {
|
||||
const auto deck = tworowDeck();
|
||||
CHECK(deckRowCount(deck) == 2);
|
||||
CHECK(deckHeight(deck) == 2 * kDeckGroupH + kDeckRowGap);
|
||||
CHECK(deckHeight(deck) == kDeckSpanningH);
|
||||
|
||||
for (int avail : {600, 1174, 2000}) {
|
||||
const DeckLayout dl = layoutDeck(deck, 8, 100, avail);
|
||||
CHECK(dl.rowCount == 2);
|
||||
CHECK(dl.height == deckHeight(deck));
|
||||
CHECK(dl.groups.size() == 5);
|
||||
// The output is in DECK order, not row order — a layout pairs with the descriptor at
|
||||
// the same index whichever row it landed in.
|
||||
CHECK(dl.groups[0].id == 0 && dl.groups[1].id == 1);
|
||||
CHECK(dl.groups[2].id == 2 && dl.groups[3].id == 3);
|
||||
CHECK(dl.groups[4].id == 4);
|
||||
CHECK(dl.groups[0].box.y == 100 && dl.groups[1].box.y == 100);
|
||||
const int row1Top = 100 + kDeckGroupH + kDeckRowGap;
|
||||
CHECK(dl.groups[2].box.y == row1Top && dl.groups[3].box.y == row1Top);
|
||||
// Both rows start flush left.
|
||||
CHECK(dl.groups[0].box.x == 8 && dl.groups[2].box.x == 8);
|
||||
// The spanning deck stands across both rows and is right-anchored.
|
||||
CHECK(dl.groups[4].box.y == 100);
|
||||
CHECK(dl.groups[4].box.height == kDeckSpanningH);
|
||||
CHECK(dl.groups[4].box.right() == 8 + avail);
|
||||
}
|
||||
CHECK(sawWrap);
|
||||
// Every box stays within the available width (no group straddles the right edge).
|
||||
for (const auto& g : dl.groups) CHECK(g.box.right() <= 8 + 544);
|
||||
}
|
||||
|
||||
static void testFirstGroupAlwaysPlaces() {
|
||||
// A group wider than the row still places (degenerate width) — exactly one row per group.
|
||||
const auto deck = shellLikeDeck();
|
||||
CHECK(deckRowCount(deck, 100) == 5);
|
||||
CHECK(deckHeight(deck, 100) == 5 * kDeckGroupH + 4 * kDeckRowGap);
|
||||
// Space-between: slack becomes gutters, divided equally with the integer residue on the
|
||||
// LEFTMOST ones, and the row ends flush against the block. Decks are never stretched.
|
||||
static void testJustificationSpreadsSlackIntoEqualGutters() {
|
||||
const auto deck = tworowDeck();
|
||||
const int soundW = deckGroupWidth(deck[0]) + deckGroupWidth(deck[1]);
|
||||
const int contourW = deckGroupWidth(deck[2]) + deckGroupWidth(deck[3]);
|
||||
const int spanW = deckGroupWidth(deck[4]);
|
||||
|
||||
const int avail = 900;
|
||||
const int block = avail - spanW - kDeckGroupGap;
|
||||
const DeckLayout dl = layoutDeck(deck, 8, 0, avail);
|
||||
// Natural widths, unstretched.
|
||||
CHECK(dl.groups[0].box.width == deckGroupWidth(deck[0]));
|
||||
CHECK(dl.groups[1].box.width == deckGroupWidth(deck[1]));
|
||||
// One gutter per row here, so it takes the whole slack and both rows end on the block.
|
||||
CHECK(dl.groups[1].box.x - dl.groups[0].box.right() == block - soundW);
|
||||
CHECK(dl.groups[3].box.x - dl.groups[2].box.right() == block - contourW);
|
||||
CHECK(dl.groups[1].box.right() == 8 + block);
|
||||
CHECK(dl.groups[3].box.right() == 8 + block);
|
||||
|
||||
// Three gutters over an indivisible slack: base everywhere, +1 on the leftmost ones.
|
||||
std::vector<DeckGroupDesc> four = {deck[0], deck[1], deck[1], deck[1]};
|
||||
int total = 0;
|
||||
for (const auto& g : four) total += deckGroupWidth(g);
|
||||
const int block4 = 940;
|
||||
const DeckLayout d4 = layoutDeck(four, 0, 0, block4);
|
||||
const int slack = block4 - total;
|
||||
CHECK(slack % 3 != 0); // the case the residue rule exists for
|
||||
const int base = slack / 3;
|
||||
const int residue = slack % 3;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int gut = d4.groups[static_cast<std::size_t>(i + 1)].box.x -
|
||||
d4.groups[static_cast<std::size_t>(i)].box.right();
|
||||
CHECK(gut == base + (i < residue ? 1 : 0));
|
||||
CHECK(gut >= kDeckGroupGap);
|
||||
}
|
||||
CHECK(d4.groups.back().box.right() == block4); // flush right
|
||||
}
|
||||
|
||||
// Below the width the block needs, gutters floor at kDeckGroupGap and the row overruns to the
|
||||
// right. It never wraps — the editor clamps its window above this, so the degrade only has to
|
||||
// be defined, not pretty.
|
||||
static void testTooNarrowFloorsTheGuttersRatherThanWrapping() {
|
||||
const auto deck = tworowDeck();
|
||||
CHECK(deckRowCount(deck) == 2); // unchanged: a row count is not a width outcome
|
||||
const DeckLayout dl = layoutDeck(deck, 0, 0, 200);
|
||||
CHECK(dl.rowCount == 2);
|
||||
CHECK(dl.height == 2 * kDeckGroupH + kDeckRowGap);
|
||||
CHECK(dl.groups[1].box.x - dl.groups[0].box.right() == kDeckGroupGap);
|
||||
CHECK(dl.groups[3].box.x - dl.groups[2].box.right() == kDeckGroupGap);
|
||||
CHECK(dl.groups[1].box.right() > 200); // overruns rather than wrapping
|
||||
}
|
||||
|
||||
// The spanning deck's left column uses FIXED slots at the row baselines. Applying the
|
||||
// horizontal run-division law vertically would stretch its one knob over the whole box — this
|
||||
// is the regression guard against exactly that.
|
||||
static void testSpanningColumnStacksFixedSlotsAndCarriesItsReadout() {
|
||||
const auto deck = tworowDeck();
|
||||
const DeckLayout dl = layoutDeck(deck, 8, 100, 900);
|
||||
const DeckGroupLayout& bus = dl.groups[4];
|
||||
CHECK(bus.cells.size() == 1); // the -1 slot reserves height without drawing a cell
|
||||
|
||||
const DeckCellLayout& gain = bus.cells[0];
|
||||
CHECK(gain.cell.width == kDeckCellW); // fixed, NOT the box's inner width
|
||||
CHECK(gain.cell.height == kDeckCellH); // fixed, NOT half the double-height box
|
||||
CHECK(gain.cell.x == bus.box.x + kDeckGroupPadX);
|
||||
// Slot 0 shares row 0's knob baseline; the reserve below it shares row 1's.
|
||||
CHECK(gain.cell.y == dl.groups[0].cells[0].cell.y);
|
||||
const int reserveTop = gain.cell.y + kDeckGroupH + kDeckRowGap;
|
||||
CHECK(reserveTop == dl.groups[2].cells[0].cell.y);
|
||||
|
||||
// ONE readout rect spanning both slots, right of the cell column, flush to the padding.
|
||||
CHECK(bus.column.id == 300);
|
||||
CHECK(bus.column.box.width == 62);
|
||||
CHECK(bus.column.box.x == gain.cell.right() + kDeckColumnGap);
|
||||
CHECK(bus.column.box.right() == bus.box.right() - kDeckGroupPadX);
|
||||
CHECK(bus.column.box.y == gain.cell.y);
|
||||
CHECK(bus.column.box.bottom() == bus.box.bottom() - kDeckGroupPadY);
|
||||
CHECK(bus.column.box.height == kDeckSpanningH - kDeckGroupPadY - kDeckCaptionH -
|
||||
kDeckCaptionGap - kDeckGroupPadY);
|
||||
|
||||
// The group is exactly as wide as its two columns plus padding.
|
||||
CHECK(deckGroupWidth(deck[4]) ==
|
||||
2 * kDeckGroupPadX + kDeckCellW + kDeckColumnGap + 62);
|
||||
|
||||
// The column answers its own hit kind; the cell above it still answers as a knob.
|
||||
const DeckHit col = hitTestDeck(dl, bus.column.box.x + 4, bus.column.box.y + 40);
|
||||
CHECK(col.kind == DeckHitKind::Column && col.id == 300);
|
||||
const DeckHit knob = hitTestDeck(dl, gain.cell.x + 4, gain.cell.y + 4);
|
||||
CHECK(knob.kind == DeckHitKind::Knob && knob.id == 12);
|
||||
// The reserved slot draws nothing and answers nothing — it is height, not a control.
|
||||
CHECK(hitTestDeck(dl, gain.cell.x + 4, reserveTop + 4).kind == DeckHitKind::None);
|
||||
}
|
||||
|
||||
// A passive corner radio keeps its rect (the shell draws a lamp there) but is unreachable by
|
||||
// the hit-test, so no gesture can grow on it by accident.
|
||||
static void testPassiveRadioIsLaidOutButNeverHit() {
|
||||
const auto deck = tworowDeck();
|
||||
const DeckLayout dl = layoutDeck(deck, 8, 100, 900);
|
||||
const DeckGroupLayout& bus = dl.groups[4];
|
||||
CHECK(bus.captionRadio.id == 200);
|
||||
CHECK(bus.captionRadio.passive);
|
||||
CHECK(bus.captionRadio.box.width == kDeckRadioSize);
|
||||
CHECK(bus.captionRadio.box.right() == bus.box.right() - kDeckGroupPadX);
|
||||
const DeckHit h = hitTestDeck(dl, bus.captionRadio.box.x + 2, bus.captionRadio.box.y + 2);
|
||||
CHECK(h.kind == DeckHitKind::None);
|
||||
|
||||
// An INTERACTIVE radio in the same slot still answers — the flag is what changed, not the
|
||||
// geometry.
|
||||
std::vector<DeckGroupDesc> active{deck[4]};
|
||||
active[0].captionRadio.passive = false;
|
||||
const DeckLayout dl2 = layoutDeck(active, 0, 0, 400);
|
||||
const DeckHit h2 = hitTestDeck(dl2, dl2.groups[0].captionRadio.box.x + 2,
|
||||
dl2.groups[0].captionRadio.box.y + 2);
|
||||
CHECK(h2.kind == DeckHitKind::CaptionRadio && h2.id == 200);
|
||||
}
|
||||
|
||||
// A deck with only a spanning group is as tall as that group, not as tall as zero rows.
|
||||
static void testSpanningOnlyDeckKeepsItsHeight() {
|
||||
std::vector<DeckGroupDesc> only{tworowDeck()[4]};
|
||||
CHECK(deckRowCount(only) == 0);
|
||||
CHECK(deckHeight(only) == kDeckSpanningH);
|
||||
const DeckLayout dl = layoutDeck(only, 0, 0, 400);
|
||||
CHECK(dl.rowCount == 0);
|
||||
CHECK(dl.height == kDeckSpanningH);
|
||||
CHECK(dl.groups.size() == 1);
|
||||
}
|
||||
|
||||
static void testGroupInnerGeometry() {
|
||||
@@ -401,16 +542,20 @@ static void testInKnobFaceUsesTheSmallerDimensionOnANonSquareRect() {
|
||||
|
||||
static void testEmptyDeck() {
|
||||
const std::vector<DeckGroupDesc> none;
|
||||
CHECK(deckRowCount(none, 800) == 0);
|
||||
CHECK(deckHeight(none, 800) == 0);
|
||||
CHECK(deckRowCount(none) == 0);
|
||||
CHECK(deckHeight(none) == 0);
|
||||
const DeckLayout dl = layoutDeck(none, 0, 0, 800);
|
||||
CHECK(dl.groups.empty() && dl.rowCount == 0 && dl.height == 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testGroupWidth();
|
||||
testWrapAtNarrowWidthIsDeterministic();
|
||||
testFirstGroupAlwaysPlaces();
|
||||
testRowMembershipComesFromTheGroupNotTheWidth();
|
||||
testJustificationSpreadsSlackIntoEqualGutters();
|
||||
testTooNarrowFloorsTheGuttersRatherThanWrapping();
|
||||
testSpanningColumnStacksFixedSlotsAndCarriesItsReadout();
|
||||
testPassiveRadioIsLaidOutButNeverHit();
|
||||
testSpanningOnlyDeckKeepsItsHeight();
|
||||
testGroupInnerGeometry();
|
||||
testHitTest();
|
||||
testReservedCellWidthGoesToTheCellsPresent();
|
||||
|
||||
Reference in New Issue
Block a user