// Standalone tests for reasampler::action_buttons — no REAPER, no test framework. // Same fast loop as the sibling pure tests (mode_switch / tab_strip et al.): assert the // action-button strip layout + hit-testing and the label-format logic directly. // // Covers (M11 brief §test cases): // * Layout: N buttons in a strip — all fit (exact equal tiling), overflow on a narrow // panel (only the fitting count laid out, rest hidden, never clipped), zero-width edge. // * Hit-test: inside each button, outside the band, half-open boundary, the narrow-panel // overflow dead-zone, hit/layout agreement. // * Label format: bound ("name binding"), unbound marker, empty / whitespace-only SDK // return -> unbound, over-long binding truncation. #include "../src/action_buttons.h" #include #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) // --- Layout: all fit ---------------------------------------------------------- // Strip 300 wide at origin (0, 40), height 24, 3 buttons at min width 80: all fit // (300/80 = 3), tiled equally -> boundaries floor(i*300/3) = 0,100,200,300, widths 100. static void testAllButtonsFitEqualTiling() { ButtonStripRect s{0, 40, 300, 24}; ButtonFit fit = computeButtonFit(s, 3, 80); CHECK(fit.visibleCount == 3); CHECK(fit.hiddenCount == 0); auto rects = computeButtonRects(s, 3, 80); CHECK(rects.size() == 3); CHECK((rects[0] == ActionButtonRect{0, 0, 40, 100, 24})); CHECK((rects[1] == ActionButtonRect{1, 100, 40, 100, 24})); CHECK((rects[2] == ActionButtonRect{2, 200, 40, 100, 24})); // Abut exactly; last reaches strip right edge. CHECK(rects[1].x == rects[0].x + rects[0].width); CHECK(rects[2].x + rects[2].width == s.x + s.width); } // Uneven width absorbed at boundaries: 100 wide / 3 -> edges 0,33,66,100 -> 33,33,34. static void testUnevenWidthTilesExactly() { ButtonStripRect s{7, 3, 100, 16}; auto rects = computeButtonRects(s, 3, 30); // 100/30 = 3 fit CHECK(rects.size() == 3); CHECK(rects[0].width == 33); CHECK(rects[1].width == 33); CHECK(rects[2].width == 34); CHECK(rects.front().x == s.x); CHECK(rects.back().x + rects.back().width == s.x + s.width); } // --- Layout: overflow on a narrow panel --------------------------------------- // 5 buttons at min width 80 into a 200-wide strip: only 2 fit (200/80 = 2). The two // visible buttons share the FULL strip (100 each — never clipped, never below min), and // 3 are hidden (the overflow the shell degrades, not clipped garbage). static void testOverflowHidesExcessNotClipped() { ButtonStripRect s{0, 0, 200, 24}; ButtonFit fit = computeButtonFit(s, 5, 80); CHECK(fit.visibleCount == 2); CHECK(fit.hiddenCount == 3); auto rects = computeButtonRects(s, 5, 80); CHECK(rects.size() == 2); CHECK(rects[0].width == 100); // >= min width 80, shares full strip CHECK(rects[1].width == 100); CHECK(rects[1].x + rects[1].width == s.x + s.width); } // A strip too narrow for even one min-width button lays out nothing (all hidden). The // shell draws an empty strip rather than a sub-minimum clipped button. static void testStripTooNarrowForAny() { ButtonStripRect s{0, 0, 50, 24}; ButtonFit fit = computeButtonFit(s, 3, 80); CHECK(fit.visibleCount == 0); CHECK(fit.hiddenCount == 3); CHECK(computeButtonRects(s, 3, 80).empty()); } // --- Layout: degenerate -------------------------------------------------------- static void testLayoutDegenerate() { CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 0, 80).empty()); CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, -2, 80).empty()); CHECK(computeButtonRects(ButtonStripRect{0, 0, 0, 24}, 3, 80).empty()); CHECK(computeButtonRects(ButtonStripRect{0, 0, -5, 24}, 3, 80).empty()); CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, 0).empty()); CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, -10).empty()); ButtonFit fit = computeButtonFit(ButtonStripRect{0, 0, 0, 24}, 3, 80); CHECK(fit.visibleCount == 0 && fit.hiddenCount == 0); } // A single button fills the whole strip. static void testSingleButtonFillsStrip() { ButtonStripRect s{5, 5, 120, 24}; auto rects = computeButtonRects(s, 1, 80); CHECK(rects.size() == 1); CHECK((rects[0] == ActionButtonRect{0, 5, 5, 120, 24})); } // --- Hit-test: hits ----------------------------------------------------------- static void testHitTestHitsEachButton() { ButtonStripRect s{0, 40, 300, 24}; // 3 buttons, 100 wide each CHECK(hitTestButton(0, 40, s, 3, 80) == 0); // top-left of button 0 CHECK(hitTestButton(50, 51, s, 3, 80) == 0); // middle of button 0 CHECK(hitTestButton(99, 63, s, 3, 80) == 0); // last pixel of button 0 CHECK(hitTestButton(100, 50, s, 3, 80) == 1); // first pixel of button 1 CHECK(hitTestButton(299, 40, s, 3, 80) == 2); // last column of button 2 } // The boundary pixel belongs to exactly ONE button (half-open): px==100 starts button 1. static void testHitTestBoundaryHalfOpen() { ButtonStripRect s{0, 0, 300, 24}; CHECK(hitTestButton(99, 10, s, 3, 80) == 0); CHECK(hitTestButton(100, 10, s, 3, 80) == 1); CHECK(hitTestButton(199, 10, s, 3, 80) == 1); CHECK(hitTestButton(200, 10, s, 3, 80) == 2); } // --- Hit-test: misses --------------------------------------------------------- static void testHitTestMissesOutsideBand() { ButtonStripRect s{10, 40, 200, 24}; CHECK(hitTestButton(9, 50, s, 3, 60) == -1); // left of strip CHECK(hitTestButton(210, 50, s, 3, 60) == -1); // right edge (== x+width, excluded) CHECK(hitTestButton(50, 39, s, 3, 60) == -1); // above the band CHECK(hitTestButton(50, 64, s, 3, 60) == -1); // below the band } // On a narrow panel the visible buttons fill the whole strip, so there is no in-band // dead-zone; but when buttonCount is 0 or the strip too narrow, every in-band point // misses (the shell draws nothing and ignores the click). static void testHitTestOverflowDeadZone() { ButtonStripRect s{0, 0, 50, 24}; // too narrow for any 80-min button CHECK(hitTestButton(25, 10, s, 3, 80) == -1); CHECK(hitTestButton(0, 0, s, 0, 80) == -1); // no buttons } static void testHitTestDegenerate() { ButtonStripRect s{0, 0, 300, 24}; CHECK(hitTestButton(50, 10, s, 0, 80) == -1); CHECK(hitTestButton(50, 10, s, -2, 80) == -1); CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 0, 24}, 3, 80) == -1); CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 300, 0}, 3, 80) == -1); } // Every point in the strip hit-tests to the button that DREW it (hit-test and layout // agree — the load-bearing consistency invariant), across an awkward width and count. static void testHitTestMatchesLayout() { ButtonStripRect s{4, 2, 173, 22}; const int count = 4, minW = 40; // 173/40 = 4 fit auto rects = computeButtonRects(s, count, minW); for (int px = s.x; px < s.x + s.width; ++px) { const int b = hitTestButton(px, s.y + 1, s, count, minW); CHECK(b >= 0); const ActionButtonRect& r = rects[static_cast(b)]; CHECK(px >= r.x && px < r.x + r.width); } } // --- Label format ------------------------------------------------------------- static void testLabelBound() { CHECK(formatButtonLabel("Capture Item", "Ctrl+Shift+C") == "Capture Item Ctrl+Shift+C"); CHECK(formatButtonLabel("Insert", "F3") == "Insert F3"); } static void testLabelUnboundEmpty() { CHECK(formatButtonLabel("Capture Item", "") == "Capture Item (unbound)"); } // Whitespace-only SDK returns (spaces, tabs) collapse to the unbound marker — the // explicit blank-return handling the brief requires. static void testLabelUnboundBlank() { CHECK(formatButtonLabel("Capture Track", " ") == "Capture Track (unbound)"); CHECK(formatButtonLabel("Insert", "\t") == "Insert (unbound)"); CHECK(formatButtonLabel("Insert", " \t ") == "Insert (unbound)"); } // Leading/trailing whitespace on a real binding is trimmed before formatting. static void testLabelTrimsSurroundingBlanks() { CHECK(formatButtonLabel("Insert", " F3 ") == "Insert F3"); } // An over-long binding is truncated to kMaxBindingChars-1 chars + "~" so the label stays // bounded (a pathological multi-chord custom binding never blows the button budget). static void testLabelTruncatesLongBinding() { const std::string longB(40, 'X'); // 40 > kMaxBindingChars (24) const std::string label = formatButtonLabel("Cancel", longB); // "Cancel " (8) + 23 'X' + "~" (kMaxBindingChars total in the binding portion). const std::string expectedBinding = std::string(kMaxBindingChars - 1, 'X') + "~"; CHECK(label == "Cancel " + expectedBinding); CHECK(static_cast(expectedBinding.size()) == kMaxBindingChars); } // A binding exactly at the limit is NOT truncated (boundary: <= kMaxBindingChars kept). static void testLabelAtLimitNotTruncated() { const std::string atLimit(kMaxBindingChars, 'Y'); CHECK(formatButtonLabel("X", atLimit) == "X " + atLimit); } int main() { testAllButtonsFitEqualTiling(); testUnevenWidthTilesExactly(); testOverflowHidesExcessNotClipped(); testStripTooNarrowForAny(); testLayoutDegenerate(); testSingleButtonFillsStrip(); testHitTestHitsEachButton(); testHitTestBoundaryHalfOpen(); testHitTestMissesOutsideBand(); testHitTestOverflowDeadZone(); testHitTestDegenerate(); testHitTestMatchesLayout(); testLabelBound(); testLabelUnboundEmpty(); testLabelUnboundBlank(); testLabelTrimsSurroundingBlanks(); testLabelTruncatesLongBinding(); testLabelAtLimitNotTruncated(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }