a67a2f9479
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.
203 lines
8.8 KiB
C++
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;
|
|
}
|