Add D5 Design-View mode switch to bank panel

New pure mode_switch module (header-rect -> N equal segment rects + point hit-test, unit-tested) plus a segmented switch in the bank_panel header: one lit segment per registered mode, click activates via view::applyMode. Grid offset below the header.
This commit is contained in:
2026-07-23 12:35:46 -04:00
parent 9ff123e5e1
commit 314c3331da
5 changed files with 438 additions and 7 deletions
+172
View File
@@ -0,0 +1,172 @@
// Standalone tests for reasampler::mode_switch — no REAPER, no test framework.
// Same fast loop as the sibling pure tests (bank_grid et al.): assert the segmented
// mode-switch layout math and hit-testing directly.
//
// Covers (D5 brief §unit-test): N=2 and N=3 segment layout; exact tiling (adjacent
// segments abut with no gap/overlap, half-open boundaries — no pixel claimed twice,
// last segment reaches the header's right edge even when width doesn't divide
// evenly); hit-test hits per segment and misses (outside the band above/below/
// left/right, boundary pixels); degenerate widths and counts.
#include "../src/mode_switch.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)
// --- Layout: N=2 -------------------------------------------------------------
// A header 100 wide at origin (10, 4), height 24, split in two: segments
// [10,60) and [60,110), each 50 wide, full header y/height.
static void testTwoSegmentsEvenSplit() {
HeaderRect h{10, 4, 100, 24};
auto rects = computeSegmentRects(h, 2);
CHECK(rects.size() == 2);
CHECK((rects[0] == SegmentRect{10, 4, 50, 24}));
CHECK((rects[1] == SegmentRect{60, 4, 50, 24}));
}
// --- Layout: N=3, uneven width absorbed at boundaries ------------------------
// 100 wide / 3 doesn't divide evenly: boundaries at floor(i*100/3) =
// 0, 33, 66, 100 -> widths 33, 33, 34. The rounding lands in the LAST segment;
// segments still abut exactly and the last reaches header.x + width (0+100).
static void testThreeSegmentsUnevenTilesExactly() {
HeaderRect h{0, 0, 100, 20};
auto rects = computeSegmentRects(h, 3);
CHECK(rects.size() == 3);
CHECK((rects[0] == SegmentRect{0, 0, 33, 20}));
CHECK((rects[1] == SegmentRect{33, 0, 33, 20}));
CHECK((rects[2] == SegmentRect{66, 0, 34, 20}));
// Adjacent segments abut (no gap, no overlap): each left edge == prior right.
CHECK(rects[1].x == rects[0].x + rects[0].width);
CHECK(rects[2].x == rects[1].x + rects[1].width);
// Last segment's right edge reaches the header's right edge exactly.
CHECK(rects[2].x + rects[2].width == h.x + h.width);
}
// --- Layout: full coverage invariant for a range of widths -------------------
// For any width and count, the segments must tile [x, x+width) with no gap/overlap:
// first left == x, last right == x+width, every mid boundary shared.
static void testTilingCoversHeaderForVariousWidths() {
for (int width = 1; width <= 200; ++width) {
for (int count = 1; count <= 5; ++count) {
HeaderRect h{7, 3, width, 16};
auto rects = computeSegmentRects(h, count);
CHECK(static_cast<int>(rects.size()) == count);
CHECK(rects.front().x == h.x);
CHECK(rects.back().x + rects.back().width == h.x + h.width);
for (std::size_t i = 1; i < rects.size(); ++i)
CHECK(rects[i].x == rects[i - 1].x + rects[i - 1].width);
}
}
}
// --- Layout: degenerate --------------------------------------------------------
// segmentCount <= 0 or non-positive width yields no rects (the panel draws nothing
// / falls back to full-client grid).
static void testLayoutDegenerate() {
CHECK(computeSegmentRects(HeaderRect{0, 0, 100, 20}, 0).empty());
CHECK(computeSegmentRects(HeaderRect{0, 0, 100, 20}, -3).empty());
CHECK(computeSegmentRects(HeaderRect{0, 0, 0, 20}, 2).empty());
CHECK(computeSegmentRects(HeaderRect{0, 0, -5, 20}, 2).empty());
}
// A single segment fills the whole header.
static void testSingleSegmentFillsHeader() {
HeaderRect h{5, 5, 80, 24};
auto rects = computeSegmentRects(h, 1);
CHECK(rects.size() == 1);
CHECK((rects[0] == SegmentRect{5, 5, 80, 24}));
}
// --- Hit-test: hits -----------------------------------------------------------
// A point inside each segment maps to that segment's index. Header 100 wide at
// x=10, two segments [10,60) and [60,110).
static void testHitTestHitsEachSegment() {
HeaderRect h{10, 4, 100, 24};
CHECK(hitTestSegment(10, 4, h, 2) == 0); // top-left corner of segment 0
CHECK(hitTestSegment(35, 15, h, 2) == 0); // middle of segment 0
CHECK(hitTestSegment(59, 27, h, 2) == 0); // last pixel of segment 0 (band)
CHECK(hitTestSegment(60, 15, h, 2) == 1); // first pixel of segment 1
CHECK(hitTestSegment(109, 4, h, 2) == 1); // last pixel column of segment 1
}
// The boundary pixel belongs to exactly ONE segment (half-open): px==60 is the
// start of segment 1, never the end of segment 0 — so no pixel is double-claimed.
static void testHitTestBoundaryHalfOpen() {
HeaderRect h{10, 4, 100, 24};
CHECK(hitTestSegment(59, 10, h, 2) == 0);
CHECK(hitTestSegment(60, 10, h, 2) == 1);
// Three-way boundaries at floor(i*100/2)+x are consistent for N=3 too.
HeaderRect h3{0, 0, 99, 20}; // 99/3 = 33 exactly -> edges 0,33,66,99
CHECK(hitTestSegment(32, 5, h3, 3) == 0);
CHECK(hitTestSegment(33, 5, h3, 3) == 1);
CHECK(hitTestSegment(65, 5, h3, 3) == 1);
CHECK(hitTestSegment(66, 5, h3, 3) == 2);
CHECK(hitTestSegment(98, 5, h3, 3) == 2);
}
// --- Hit-test: misses ---------------------------------------------------------
// Points outside the header band on any side miss (-1). Below the band is where
// the grid lives; the panel treats -1 as "fall through to grid handling".
static void testHitTestMissesOutsideBand() {
HeaderRect h{10, 4, 100, 24};
CHECK(hitTestSegment(9, 10, h, 2) == -1); // left of header
CHECK(hitTestSegment(110, 10, h, 2) == -1); // right edge (px == x+width, excluded)
CHECK(hitTestSegment(50, 3, h, 2) == -1); // above the band
CHECK(hitTestSegment(50, 28, h, 2) == -1); // below the band (into the grid)
CHECK(hitTestSegment(200, 200, h, 2) == -1); // far away
}
// Degenerate hit-test inputs miss safely.
static void testHitTestDegenerate() {
HeaderRect h{10, 4, 100, 24};
CHECK(hitTestSegment(50, 15, h, 0) == -1); // no segments
CHECK(hitTestSegment(50, 15, h, -2) == -1);
CHECK(hitTestSegment(50, 15, HeaderRect{10, 4, 0, 24}, 2) == -1); // zero width
CHECK(hitTestSegment(50, 15, HeaderRect{10, 4, 100, 0}, 2) == -1); // zero height
}
// --- Cross-check: every point in the header hit-tests to the segment that DREW
// it (hit-test and layout agree, the load-bearing consistency invariant). ------
static void testHitTestMatchesLayout() {
HeaderRect h{4, 2, 87, 18}; // deliberately awkward width/origin
const int count = 3;
auto rects = computeSegmentRects(h, count);
for (int px = h.x; px < h.x + h.width; ++px) {
const int seg = hitTestSegment(px, h.y + 1, h, count);
CHECK(seg >= 0);
// The returned segment's rect must contain px (half-open).
const SegmentRect& r = rects[static_cast<std::size_t>(seg)];
CHECK(px >= r.x && px < r.x + r.width);
}
}
int main() {
testTwoSegmentsEvenSplit();
testThreeSegmentsUnevenTilesExactly();
testTilingCoversHeaderForVariousWidths();
testLayoutDegenerate();
testSingleSegmentFillsHeader();
testHitTestHitsEachSegment();
testHitTestBoundaryHalfOpen();
testHitTestMissesOutsideBand();
testHitTestDegenerate();
testHitTestMatchesLayout();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}