Files
reasampler/tests/test_tab_strip.cpp
T
daniel a67a2f9479 feat(bank_panel): B4 vertical-split UI — LICE tab strip, id-keyed bank ops, move/copy drag
Pool grid on top, LICE-drawn named-banks tab strip below with overflow-scroll
(new pure tab_strip seam, unit-tested). Full-height toggles, unmistakable
active-bank readout distinct from the shown tab, tab context menu
(activate/rename/delete/evacuate/create) with rich confirm-on-non-empty-delete,
and move/copy via menu + drag with drop-highlighting. Fold-in: deserialize
auto-disambiguates duplicate folded bank names instead of rejecting the book.
2026-07-25 14:37:57 -04:00

203 lines
8.8 KiB
C++

// Standalone tests for reasampler::tab_strip — no REAPER, no test framework. Same
// fast loop as the sibling pure tests (mode_switch / bank_grid et al.): assert the
// named-banks tab-strip layout, overflow/scroll math, and hit-testing directly.
//
// Covers (B4 brief §unit-test the pure seam): no-overflow tiling (tabs fit, no
// chevrons, track == strip); overflow (chevrons reserved, track shrinks, maxScroll
// = run - track); scroll clamping to [0, maxScroll]; clipped visible rects (a
// partially-scrolled tab is clipped to the track, a fully-scrolled-out tab is
// omitted); scrolling to the end surfaces the last tab; hit-testing (tab hit,
// left/right chevron precedence at the ends, dead space between visible tabs,
// outside the band above/below/left/right, half-open boundary pixels).
#include "../src/tab_strip.h"
#include <cstddef>
#include <cstdio>
#include <vector>
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- No overflow: tabs fit ---------------------------------------------------
// 3 tabs at 96px = 288 fit a 300-wide strip: no overflow, no chevrons, the whole
// strip is the track, maxScroll 0.
static void testFitsNoOverflow() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
TabStripLayout layout = computeTabStripLayout(strip, 3, spec, 0);
CHECK(!layout.overflow);
CHECK(!layout.leftChevron && !layout.rightChevron);
CHECK(layout.trackX == 0);
CHECK(layout.trackWidth == 300);
CHECK(layout.maxScroll == 0);
auto rects = computeTabRects(strip, 3, spec, 0);
CHECK(rects.size() == 3);
CHECK((rects[0] == TabRect{0, 0, 0, 96, 24}));
CHECK((rects[1] == TabRect{1, 96, 0, 96, 24}));
CHECK((rects[2] == TabRect{2, 192, 0, 96, 24}));
}
// A fitting strip ignores a stray non-zero scroll offset (maxScroll 0 clamps it).
static void testFitStripIgnoresScroll() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
auto rects = computeTabRects(strip, 3, spec, /*scrollOffset=*/500);
CHECK(rects.size() == 3);
CHECK(rects[0].x == 0); // offset was clamped to 0
}
// --- Overflow: chevrons reserved, track shrinks ------------------------------
// 10 tabs at 96 = 960 overflow a 300-wide strip. Chevrons (20 each) are reserved,
// so the track is [20, 280) = 260 wide; maxScroll = 960 - 260 = 700.
static void testOverflowReservesChevrons() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
CHECK(layout.overflow);
CHECK(layout.leftChevron && layout.rightChevron);
CHECK(layout.trackX == 20);
CHECK(layout.trackWidth == 260);
CHECK(layout.maxScroll == 700);
}
// At scroll 0 the first tabs are visible from the track's left edge; a tab that
// straddles the right chevron is clipped to the track's right edge, and tabs fully
// past it are omitted.
static void testOverflowScrollZeroClipsRight() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
auto rects = computeTabRects(strip, 10, spec, 0);
// Track is [20, 280). Tab 0 at [20,116), tab 1 [116,212), tab 2 [212,308) clipped
// to [212,280). Tabs 3.. start past 280 -> omitted.
CHECK(rects.size() == 3);
CHECK((rects[0] == TabRect{0, 20, 0, 96, 24}));
CHECK((rects[1] == TabRect{1, 116, 0, 96, 24}));
CHECK((rects[2] == TabRect{2, 212, 0, 68, 24})); // clipped at the track's right
}
// Scrolling to maxScroll surfaces the LAST tab flush against the track's right edge
// and drops the earliest tabs off the left. This is the property that makes overflow
// usable: every tab is reachable by scrolling.
static void testScrollToEndSurfacesLastTab() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
auto rects = computeTabRects(strip, 10, spec, layout.maxScroll);
CHECK(!rects.empty());
const TabRect& last = rects.back();
CHECK(last.index == 9); // the last tab is visible
CHECK(last.x + last.width == layout.trackX + layout.trackWidth); // flush right (280)
}
// --- Scroll clamping ---------------------------------------------------------
static void testClampScroll() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
CHECK(clampTabScroll(-50, layout) == 0);
CHECK(clampTabScroll(0, layout) == 0);
CHECK(clampTabScroll(300, layout) == 300);
CHECK(clampTabScroll(layout.maxScroll, layout) == layout.maxScroll);
CHECK(clampTabScroll(layout.maxScroll + 999, layout) == layout.maxScroll);
TabStripLayout fits = computeTabStripLayout(strip, 2, spec, 0);
CHECK(clampTabScroll(123, fits) == 0); // no overflow -> everything clamps to 0
}
// --- Hit-testing -------------------------------------------------------------
// No overflow: a point in a tab returns that tab; the gap-free tiling means every
// x in the strip band lands on some tab.
static void testHitFitStrip() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
CHECK((hitTestTabStrip(10, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 0}));
CHECK((hitTestTabStrip(100, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 1}));
CHECK((hitTestTabStrip(250, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 2}));
// Outside the band: above, below, left, right all miss.
CHECK((hitTestTabStrip(10, -1, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
CHECK((hitTestTabStrip(10, 24, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
CHECK((hitTestTabStrip(-1, 12, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
CHECK((hitTestTabStrip(300, 12, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
}
// Overflow: the reserved chevron bands hit-test to the scroll affordances and take
// precedence over any tab that would otherwise sit there.
static void testHitChevrons() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
// Left chevron band [0,20).
CHECK((hitTestTabStrip(5, 12, strip, 10, spec, 0) ==
TabHit{TabHitKind::ScrollLeft, -1}));
CHECK((hitTestTabStrip(19, 12, strip, 10, spec, 0) ==
TabHit{TabHitKind::ScrollLeft, -1}));
// Right chevron band [280,300).
CHECK((hitTestTabStrip(280, 12, strip, 10, spec, 0) ==
TabHit{TabHitKind::ScrollRight, -1}));
CHECK((hitTestTabStrip(299, 12, strip, 10, spec, 0) ==
TabHit{TabHitKind::ScrollRight, -1}));
// Just inside the track (x=20) is the first tab, not the left chevron.
CHECK((hitTestTabStrip(20, 12, strip, 10, spec, 0) == TabHit{TabHitKind::Tab, 0}));
}
// A hit in the track matches the drawn (clipped) rects; a point in track dead space
// (no visible tab under it) is None. With overflow at scroll 0 the visible tabs are
// 0,1,2 (2 clipped to [212,280)); everything in [20,280) is covered here, so we test
// dead space by scrolling so a tab boundary leaves no gap — instead assert the hit
// agrees with computeTabRects for a mid-scroll offset.
static void testHitMatchesRectsMidScroll() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
const int offset = 150;
auto rects = computeTabRects(strip, 10, spec, offset);
CHECK(!rects.empty());
for (const TabRect& r : rects) {
// A point at the rect's left edge and one just inside its right edge both
// resolve to this tab (half-open bounds).
CHECK((hitTestTabStrip(r.x, 12, strip, 10, spec, offset) ==
TabHit{TabHitKind::Tab, r.index}));
CHECK((hitTestTabStrip(r.x + r.width - 1, 12, strip, 10, spec, offset) ==
TabHit{TabHitKind::Tab, r.index}));
}
}
// --- Degenerate inputs -------------------------------------------------------
static void testDegenerate() {
TabStripRect strip{0, 0, 300, 24};
TabStripSpec spec{96, 20};
CHECK(computeTabRects(strip, 0, spec, 0).empty());
CHECK((hitTestTabStrip(10, 12, strip, 0, spec, 0) == TabHit{TabHitKind::None, -1}));
TabStripRect empty{0, 0, 0, 24};
CHECK(computeTabRects(empty, 3, spec, 0).empty());
TabStripLayout layout = computeTabStripLayout(empty, 3, spec, 0);
CHECK(!layout.overflow);
CHECK(layout.maxScroll == 0);
}
int main() {
testFitsNoOverflow();
testFitStripIgnoresScroll();
testOverflowReservesChevrons();
testOverflowScrollZeroClipsRight();
testScrollToEndSurfacesLastTab();
testClampScroll();
testHitFitStrip();
testHitChevrons();
testHitMatchesRectsMidScroll();
testDegenerate();
if (g_fail == 0) std::printf("tab_strip: all tests passed\n");
else std::printf("tab_strip: %d FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}