// 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 #include #include 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; }