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.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user