// Standalone tests for reasampler::instrument::ui::knob_deck — no VST3, no REAPER, no framework. Same fast // 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 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, fence padding // misses, outside-deck misses. #include "../src/core/instrument/ui/knob_deck.h" #include #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) // A representative deck shaped like the shell's: AMP (5 cells + caption toggle), PITCH // (1 cell + caption toggle), PITCH ENV (3 cells + caption toggle), VOICE (1 cell + caption // toggle + row toggle), MASTER (1 cell, no toggle). static std::vector shellLikeDeck() { std::vector g; g.push_back({0, 78, {}, {100, 44}, {}, {1, 2, 3, 4, 5}, {}}); g.push_back({1, 38, {}, {101, 48}, {}, {6}, {}}); g.push_back({2, 58, {}, {102, 32}, {}, {7, 8, 9}, {}}); g.push_back({3, 38, {}, {103, 40}, {}, {10}, {104, 44}}); g.push_back({4, 46, {}, {}, {}, {11}, {}}); return g; } static void testGroupWidth() { // Knob row dominates: 5 cells (240) > caption row (78 + 4 + 88 = 170) -> 240 + 2*6. DeckGroupDesc amp{0, 78, {}, {100, 44}, {}, {1, 2, 3, 4, 5}, {}}; CHECK(deckGroupWidth(amp) == 5 * kDeckCellW + 2 * kDeckGroupPadX); // Caption row dominates: 38 + 4 + 96 = 138 > 48 -> 138 + 12. DeckGroupDesc pitch{1, 38, {}, {101, 48}, {}, {6}, {}}; CHECK(deckGroupWidth(pitch) == 38 + kDeckToggleGap + 2 * 48 + 2 * kDeckGroupPadX); // Row toggle counts into the knob row: 48 + 4 + 88 = 140 > caption 38+4+80=122. DeckGroupDesc voice{3, 38, {}, {103, 40}, {}, {10}, {104, 44}}; CHECK(deckGroupWidth(voice) == kDeckCellW + kDeckToggleGap + 2 * 44 + 2 * kDeckGroupPadX); // No toggles: max(caption, cells) + padding. DeckGroupDesc master{4, 46, {}, {}, {}, {11}, {}}; CHECK(deckGroupWidth(master) == kDeckCellW + 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; } } 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); } static void testGroupInnerGeometry() { const auto deck = shellLikeDeck(); const DeckLayout dl = layoutDeck(deck, 8, 50, 824); const DeckGroupLayout& amp = dl.groups[0]; // Caption row at the top padding; caption toggle right-anchored inside the box. CHECK(amp.caption.y == amp.box.y + kDeckGroupPadY); CHECK(amp.captionToggle.id == 100); CHECK(amp.captionToggle.seg1.right() == amp.box.right() - kDeckGroupPadX); CHECK(amp.captionToggle.seg0.right() == amp.captionToggle.seg1.x); CHECK(amp.captionToggle.seg0.width == 44 && amp.captionToggle.seg1.width == 44); CHECK(amp.captionToggle.seg0.height == kDeckToggleH); // The caption text rect stops before the toggle. CHECK(amp.caption.right() <= amp.captionToggle.seg0.x); // Cells: five, fixed size, abutting, inside the box, below the caption row. CHECK(static_cast(amp.cells.size()) == 5); for (std::size_t i = 0; i < amp.cells.size(); ++i) { const DeckCellLayout& c = amp.cells[i]; CHECK(c.cell.width == kDeckCellW && c.cell.height == kDeckCellH); CHECK(c.cell.y == amp.box.y + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap); if (i > 0) CHECK(c.cell.x == amp.cells[i - 1].cell.right()); // Knob square centered horizontally, label band beneath it, both inside the cell. CHECK(c.knob.width == kDeckKnobSize && c.knob.height == kDeckKnobSize); CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right()); CHECK(c.label.y >= c.knob.bottom()); CHECK(c.label.bottom() <= c.cell.bottom()); } // VOICE group's row toggle sits after its cell, vertically centered in the cell row. const DeckGroupLayout& voice = dl.groups[3]; CHECK(voice.rowToggle.id == 104); CHECK(voice.rowToggle.seg0.x == voice.cells[0].cell.right() + kDeckToggleGap); CHECK(voice.rowToggle.seg0.height == kDeckToggleH); CHECK(voice.rowToggle.seg0.y > voice.cells[0].cell.y); // MASTER has no toggles. CHECK(dl.groups[4].captionToggle.id == -1); CHECK(dl.groups[4].rowToggle.id == -1); } static void testHitTest() { const auto deck = shellLikeDeck(); const DeckLayout dl = layoutDeck(deck, 8, 50, 824); const DeckGroupLayout& amp = dl.groups[0]; // Knob hit: anywhere in the cell (including the label band) resolves to the cell id. const DeckCellLayout& c0 = amp.cells[0]; DeckHit h = hitTestDeck(dl, c0.cell.x + 1, c0.cell.y + 1); CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1); h = hitTestDeck(dl, c0.label.x + 2, c0.label.y + 2); CHECK(h.kind == DeckHitKind::Knob && h.id == 1); // Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1. h = hitTestDeck(dl, amp.captionToggle.seg0.right() - 1, amp.captionToggle.seg0.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0); h = hitTestDeck(dl, amp.captionToggle.seg1.x, amp.captionToggle.seg1.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1); // Row toggle. const DeckGroupLayout& voice = dl.groups[3]; 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 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 trig; trig.push_back({0, 78, {}, {100, 44}, {}, {20, 21, 22, -1, -1}, {}}); const DeckLayout tl = layoutDeck(trig, 0, 0, 824); 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); CHECK(h.kind == DeckHitKind::None); h = hitTestDeck(dl, -50, -50); 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> faces = { {20, 21, 22, -1, -1}, {20, 21, 22, 23, -1}, {20, -1, -1, -1, -1}}; for (const std::vector& ids : faces) { DeckGroupDesc narrow = full; narrow.cellIds = ids; CHECK(deckGroupWidth(narrow) == deckGroupWidth(full)); std::vector g{narrow}; const DeckLayout dl = layoutDeck(g, 0, 0, 824); const DeckGroupLayout& lay = dl.groups[0]; const int present = static_cast(lay.cells.size()); CHECK(present == 5 - static_cast(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(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(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 a{withToggle}; withToggle.cellIds = {20, 21, -1, -1, -1}; std::vector 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() { const DeckGroupDesc bare{7, 78, {}, {200, 44}, {}, {1, 2}, {}}; const DeckGroupDesc withRadio{7, 78, {201}, {200, 44}, {}, {1, 2}, {}}; // Caption row grows by exactly gap + radio; the knob row is unchanged, so a group whose // caption row already dominated grows by that much. CHECK(deckGroupWidth(withRadio) - deckGroupWidth(bare) == kDeckToggleGap + kDeckRadioSize); std::vector g{withRadio}; const DeckLayout dl = layoutDeck(g, 0, 0, 800); const DeckGroupLayout& lay = dl.groups[0]; CHECK(lay.captionRadio.id == 201); CHECK(lay.captionRadio.box.width == kDeckRadioSize); // Far corner: flush with the group's inner right edge. CHECK(lay.captionRadio.box.right() == lay.box.right() - kDeckGroupPadX); // The toggle sits entirely left of the radio, and the caption text left of the toggle. CHECK(lay.captionToggle.seg1.right() <= lay.captionRadio.box.x); CHECK(lay.caption.right() <= lay.captionToggle.seg0.x); const DeckHit h = hitTestDeck(dl, lay.captionRadio.box.x + 2, lay.captionRadio.box.y + 2); CHECK(h.kind == DeckHitKind::CaptionRadio && h.id == 201); } // The inner dial is a concentric sub-region of the knob: a grab there still names the cell, // with `inner` set, so a cell with no inner value simply ignores the flag. static void testInnerDialHit() { const std::vector g = shellLikeDeck(); const DeckLayout dl = layoutDeck(g, 0, 0, 900); const DeckCellLayout& c = dl.groups[0].cells[0]; CHECK(c.inner.width == kDeckInnerDialSize && c.inner.height == kDeckInnerDialSize); // Concentric with the knob square. CHECK(c.inner.x + c.inner.width / 2 == c.knob.x + c.knob.width / 2); CHECK(c.inner.y + c.inner.height / 2 == c.knob.y + c.knob.height / 2); DeckHit h = hitTestDeck(dl, c.inner.x + c.inner.width / 2, c.inner.y + c.inner.height / 2); CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && h.inner); // A grab on the outer ring is the same cell WITHOUT the inner flag. h = hitTestDeck(dl, c.knob.x + 1, c.knob.y + 1); CHECK(h.kind == DeckHitKind::Knob && h.id == c.id && !h.inner); } // captionToggle2 sits immediately left of captionToggle when both are present (no overlap, and // the caption text stops before the LEFTMOST one), and takes captionToggle's own slot when // captionToggle is absent — the shipped FILTER ENV group's exact shape (deck_groups.cpp). static void testCaptionToggle2() { const DeckGroupDesc both{9, 40, {}, {300, 30}, {301, 20}, {1, 2, 3}, {}}; std::vector g{both}; const DeckLayout dl = layoutDeck(g, 0, 0, 800); const DeckGroupLayout& lay = dl.groups[0]; CHECK(lay.captionToggle.id == 300); CHECK(lay.captionToggle2.id == 301); CHECK(lay.captionToggle2.seg0.width == 20 && lay.captionToggle2.seg1.width == 20); // Left of the first, with exactly one gap between — no overlap by construction. CHECK(lay.captionToggle2.seg1.right() == lay.captionToggle.seg0.x - kDeckToggleGap); // Caption text stops before the LEFTMOST toggle (toggle2), not just the first-placed one. CHECK(lay.caption.right() <= lay.captionToggle2.seg0.x); DeckHit h = hitTestDeck(dl, lay.captionToggle2.seg0.right() - 1, lay.captionToggle2.seg0.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 301 && h.segment == 0); h = hitTestDeck(dl, lay.captionToggle2.seg1.x, lay.captionToggle2.seg1.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 301 && h.segment == 1); // FILTER ENV's real shape: captionToggle absent, captionToggle2 present with a radio — it // takes the first (rightmost) slot rather than leaving a gap where captionToggle would sit. const DeckGroupDesc filterEnvLike{10, 66, {200}, {}, {302, 23}, {1, 2, 3, 4, 5}, {}}; std::vector g2{filterEnvLike}; const DeckLayout dl2 = layoutDeck(g2, 0, 0, 800); const DeckGroupLayout& fe = dl2.groups[0]; CHECK(fe.captionToggle.id == -1); CHECK(fe.captionToggle2.id == 302); CHECK(fe.captionToggle2.seg1.right() == fe.captionRadio.box.x - kDeckToggleGap); h = hitTestDeck(dl2, fe.captionToggle2.seg1.x, fe.captionToggle2.seg1.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 302 && h.segment == 1); } static void testEmptyDeck() { const std::vector none; CHECK(deckRowCount(none, 800) == 0); CHECK(deckHeight(none, 800) == 0); const DeckLayout dl = layoutDeck(none, 0, 0, 800); CHECK(dl.groups.empty() && dl.rowCount == 0 && dl.height == 0); } int main() { testGroupWidth(); testWrapAtNarrowWidthIsDeterministic(); testFirstGroupAlwaysPlaces(); testGroupInnerGeometry(); testHitTest(); testReservedCellWidthGoesToTheCellsPresent(); testCaptionRadioGeometryAndHit(); testInnerDialHit(); testCaptionToggle2(); testEmptyDeck(); if (g_fail) { std::printf("%d FAILURE(S)\n", g_fail); return 1; } std::printf("knob_deck tests passed\n"); return 0; }