// Standalone tests for reasampler::action_bar — no REAPER, no test framework. Same fast loop // as the sibling pure tests (mode_switch / prune_button): assert the // task-grouped action-bar layout, overflow-on-narrow, and hit-testing directly. // // Covers (L2 brief §test cases, updated for L6 single-row face change): // * Layout: correct rects for each action button across representative panel widths; buttons // pack at a fixed width with intra-cluster + inter-cluster gaps. // * Overflow/hiding when the bar is too narrow (whole trailing buttons dropped, never // clipped; earlier frequent clusters survive), and the More-button reserve the panel // subtracts before tiling (composed here with overflow_menu, as panel_layout does). // * Label sub-rect correct — spans full button height (L6: keybinding sub-row removed from // the face; binding is in the hover tooltip instead). // * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs // across clusters; inter-cluster gaps are wider than intra-cluster gaps. // * Hover hit-test: right element for in-bounds points, -1 outside bounds AND in the gaps; // degenerate/too-narrow bar handled without crash or overlap. // * Resize: no inventory item cut off or overlapping across a representative width range. #include "../src/core/ui/action_bar.h" #include "../src/core/ui/overflow_menu.h" #include #include #include using namespace reasampler; using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // The panel's real inventory shape: 2 capture, 1 maintenance (Re-capture), 3 placement // (Insert / Insert Conform / Insert as FX) = 6 buttons. Cluster order: // Capture -> Maintenance -> Placement (Re-capture sits between the two capture verbs and the // placement verbs). static std::vector inventory() { return { {ActionCluster::Capture, 2}, {ActionCluster::Maintenance, 1}, {ActionCluster::Placement, 3}, }; } // A spec with round numbers so expected pixels are hand-checkable. static ActionBarSpec roundSpec() { ActionBarSpec s; s.buttonWidth = 100; s.buttonGap = 4; s.clusterGap = 16; s.sidePad = 8; s.verticalInset = 3; return s; } // --- Layout: all fit, correct rects + gaps ------------------------------------ // A wide bar fits all 6 buttons. Verify the first few rects, the intra-cluster gap, and the // (wider) inter-cluster gap between button 1 (last capture) and button 2 (maintenance). // // Pixel walk with roundSpec and bar at (0,40): // btn0 (Capture): x=8, right=108 // btn1 (Capture): x=112, right=212 (intraGap = 4 after btn0's right) // btn2 (Maintenance): x=228, right=328 (clusterGap = 16 after btn1's right) // btn3 (Placement): x=344, right=444 (clusterGap = 16 after btn2's right) // btn4 (Placement): x=448, right=548 (intraGap = 4 after btn3's right) // btn5 (Placement): x=552, right=652 (intraGap = 4 after btn4's right) // Minimum bar width: 652 + sidePad(8) = 660. Give it 700. static void testAllFitRectsAndGaps() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 40, 700, 34}; BarFit fit = computeBarFit(bar, clusters, spec); CHECK(fit.visibleCount == 6); CHECK(fit.hiddenCount == 0); auto slots = computeBarSlots(bar, clusters, spec); CHECK(slots.size() == 6); // Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset. CHECK(slots[0].x == 8); CHECK(slots[0].y == 43); CHECK(slots[0].width == 100); CHECK(slots[0].height == 28); CHECK(slots[0].cluster == ActionCluster::Capture); CHECK(slots[0].index == 0); // Button 1: intra-cluster gap of 4 after button 0's right edge (8+100=108) -> 112. CHECK(slots[1].x == 112); CHECK(slots[1].cluster == ActionCluster::Capture); // Button 2 (maintenance / Re-capture): cluster gap of 16 after 212 -> 228. CHECK(slots[2].x == 228); CHECK(slots[2].cluster == ActionCluster::Maintenance); CHECK(slots[2].index == 2); // Button 3 (first placement): cluster gap of 16 after 328 -> 344. CHECK(slots[3].x == 344); CHECK(slots[3].cluster == ActionCluster::Placement); CHECK(slots[3].index == 3); // Button 4 (second placement): intra-cluster gap of 4 after 444 -> 448. CHECK(slots[4].x == 448); CHECK(slots[4].cluster == ActionCluster::Placement); CHECK(slots[4].index == 4); // Button 5 (Insert as FX — third placement): intra-cluster gap of 4 after 548 -> 552. // It joins the Placement cluster, so no cluster gap opens before it. CHECK(slots[5].x == 552); CHECK(slots[5].cluster == ActionCluster::Placement); CHECK(slots[5].index == 5); CHECK(slots[5].x - (slots[4].x + slots[4].width) == spec.buttonGap); // Inter-cluster gap (slot[2].x - slot[1].right = 228 - 212 = 16) is wider than the // intra-cluster gap (slot[1].x - slot[0].right = 112 - 108 = 4) — task grouping is // structurally visible in the geometry. const int interGap = slots[2].x - (slots[1].x + slots[1].width); const int intraGap = slots[1].x - (slots[0].x + slots[0].width); CHECK(interGap == 16); CHECK(intraGap == 4); CHECK(interGap > intraGap); } // --- Label sub-rect: full-height single row ------------------------------------ // L6: the label rect spans the full button height — no binding sub-row split. The label is // inset horizontally by hpad(4) on each side; horizontally it shares the same inner band. static void testLabelFullHeight() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 0, 600, 34}; auto slots = computeBarSlots(bar, clusters, spec); CHECK(!slots.empty()); const ActionBarSlot& s = slots[0]; // Label spans the full button height. CHECK(s.labelH == s.height); CHECK(s.labelY == s.y); // Inset horizontally. CHECK(s.labelX > s.x); CHECK(s.labelX + s.labelW < s.x + s.width); } // Short buttons also get the full-height label (no min-height split threshold anymore). static void testLabelFullHeightWhenShort() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 0, 600, 18}; // btnH = 18 - 6 = 12 — very short auto slots = computeBarSlots(bar, clusters, spec); CHECK(!slots.empty()); const ActionBarSlot& s = slots[0]; CHECK(s.labelH == s.height); CHECK(s.labelY == s.y); } // --- Overflow / hiding on a narrow panel -------------------------------------- // A bar wide enough for only the 2 capture buttons drops the rest WHOLE. The visible buttons // keep their full width (never clipped), and the frequent capture cluster survives. // // Exact fit for 2 capture buttons: sidePad(8) + 2*100 + 1*4 + sidePad(8) = 220. // A 3rd button (Maintenance) needs clusterGap(16)+100 = 116 more -> 336. So 220 fits exactly 2. static void testOverflowDropsTrailingWhole() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 0, 220, 34}; BarFit fit = computeBarFit(bar, clusters, spec); CHECK(fit.visibleCount == 2); CHECK(fit.hiddenCount == 4); auto slots = computeBarSlots(bar, clusters, spec); CHECK(slots.size() == 2); for (const auto& s : slots) { CHECK(s.width == spec.buttonWidth); // never clipped below full width CHECK(s.cluster == ActionCluster::Capture);// the surviving cluster is the frequent one } // The last visible button's right edge stays within the usable bound. CHECK(slots.back().x + slots.back().width <= bar.x + bar.width - spec.sidePad); } // A bar too narrow for even one button lays out nothing (all hidden) — no sub-minimum clipped // button; the shell draws an empty bar. static void testTooNarrowForAny() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 0, 60, 34}; // sidePad*2 + one 100-wide button won't fit BarFit fit = computeBarFit(bar, clusters, spec); CHECK(fit.visibleCount == 0); CHECK(fit.hiddenCount == 6); CHECK(computeBarSlots(bar, clusters, spec).empty()); } // --- Overflow reserve: the bar never tiles under the More (...) button -------- // The panel hands action_bar the top band MINUS overflow_menu's reserve (panel_layout's // topToolbarActionRect). Reproduce that composition and assert the invariant it exists to // buy: across every width, no visible button's right edge crosses into the reserved strip // where the More button is drawn — including the widths where the reserve is what pushes // the trailing "Insert as FX" button into overflow. static void testOverflowReserveHonouredAcrossWidths() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); const MenuButtonSpec menuSpec; // the panel's kMenuBtnSpec defaults bool sawReserveCostAButton = false; for (int w = 40; w <= 900; w += 3) { const MenuBarRect band{0, 0, w, 34}; const int reserve = menuButtonReserve(band, menuSpec); CHECK(reserve >= 0); ActionBarRect action{0, 0, w - reserve, 34}; if (action.width < 0) action.width = 0; const MenuButtonRect more = computeMenuButton(band, menuSpec); for (const auto& s : computeBarSlots(action, clusters, spec)) { // Fully clear of the reserved strip at the band's right. CHECK(s.x + s.width <= w - reserve); // And so, transitively, clear of the More button itself when one is drawn. if (!more.empty()) CHECK(s.x + s.width <= more.x); } // The reserve genuinely costs buttons somewhere in this range, so the assertions // above are exercised against a bar the reserve actually narrowed. const int full = computeBarFit(ActionBarRect{0, 0, w, 34}, clusters, spec).visibleCount; const int reserved = computeBarFit(action, clusters, spec).visibleCount; CHECK(reserved <= full); if (reserved < full) sawReserveCostAButton = true; } CHECK(sawReserveCostAButton); } // --- Reserve against the REAL panel spec: names the button and the exact width ----- // roundSpec() above uses clusterGap=16 for hand-checkable pixel math; the panel's actual // bar (shell/panel/panel_state.h's kBarSpec) uses clusterGap=24. Reproduced literally here // because this test target is REAPER-free and cannot include that shell header -- keep // the two in sync by hand if either spec ever changes. static ActionBarSpec realBarSpec() { ActionBarSpec s; s.buttonWidth = 108; s.buttonGap = 4; s.clusterGap = 24; s.sidePad = 8; s.verticalInset = 3; return s; } // At the real spec plus the real 40 px reserve (28 + 2*6, panel_state.h's kMenuBtnSpec -- // all default-constructed here since its defaults already match), 764 is the narrowest // band that still shows all six buttons; one pixel narrower drops Insert as FX (flat index // 5, the trailing Placement button) alone into overflow, leaving its five neighbours intact. static void testReserveDropsInsertAsFxAtRealWidth() { const auto clusters = inventory(); const ActionBarSpec spec = realBarSpec(); const MenuButtonSpec menuSpec; const int reserve = menuButtonReserve(MenuBarRect{0, 0, 900, 34}, menuSpec); CHECK(reserve == 40); const int allSixWidth = 764; { ActionBarRect action{0, 0, allSixWidth - reserve, 34}; const auto slots = computeBarSlots(action, clusters, spec); CHECK(slots.size() == 6); CHECK(slots.back().index == 5); // Insert as FX still visible } { ActionBarRect action{0, 0, allSixWidth - 1 - reserve, 34}; const auto slots = computeBarSlots(action, clusters, spec); CHECK(slots.size() == 5); // Insert as FX alone dropped CHECK(slots.back().index == 4); // Insert Conform is now the trailing visible button } } // --- Degenerate -------------------------------------------------------------- static void testDegenerate() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); CHECK(computeBarSlots(ActionBarRect{0, 0, 0, 34}, clusters, spec).empty()); CHECK(computeBarSlots(ActionBarRect{0, 0, 600, 0}, clusters, spec).empty()); CHECK(computeBarSlots(ActionBarRect{0, 0, 600, 34}, {}, spec).empty()); ActionBarSpec badW = spec; badW.buttonWidth = 0; CHECK(computeBarSlots(ActionBarRect{0, 0, 600, 34}, clusters, badW).empty()); // Empty clusters in the list contribute no buttons and no gaps. std::vector withEmpty = { {ActionCluster::Capture, 2}, {ActionCluster::Placement, 0}, // empty — skipped {ActionCluster::Maintenance, 1}, }; auto slots = computeBarSlots(ActionBarRect{0, 0, 600, 34}, withEmpty, spec); CHECK(slots.size() == 3); CHECK(slots[0].cluster == ActionCluster::Capture); CHECK(slots[1].cluster == ActionCluster::Capture); CHECK(slots[2].cluster == ActionCluster::Maintenance); // The cluster gap sits between the Capture and Maintenance buttons (Placement emitted none). const int gap = slots[2].x - (slots[1].x + slots[1].width); CHECK(gap == spec.clusterGap); } // --- Hit-test: hits, gaps, and misses ----------------------------------------- static void testHitTestHitsButtons() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 40, 600, 34}; auto slots = computeBarSlots(bar, clusters, spec); // A point in the middle of each button returns that button's flat index. for (const auto& s : slots) { const int cx = s.x + s.width / 2; const int cy = s.y + s.height / 2; CHECK(hitTestActionBar(cx, cy, bar, clusters, spec) == s.index); } } // A point in an intra-cluster gap and a point in an inter-cluster gap are both clean misses // (real gaps, unlike an equal-tiled strip — no nearest-button snapping). static void testHitTestGapsAreMisses() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 40, 600, 34}; auto slots = computeBarSlots(bar, clusters, spec); // Intra-cluster gap between button 0 (right edge 108) and button 1 (left 112): x in [108,112). CHECK(hitTestActionBar(110, 50, bar, clusters, spec) == -1); // Inter-cluster gap between button 1 (right 212) and button 2 (left 228): x in [212,228). CHECK(hitTestActionBar(220, 50, bar, clusters, spec) == -1); } static void testHitTestMissesOutsideBand() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{10, 40, 400, 34}; CHECK(hitTestActionBar(9, 50, bar, clusters, spec) == -1); // left of bar CHECK(hitTestActionBar(410, 50, bar, clusters, spec) == -1); // right edge (excluded) CHECK(hitTestActionBar(50, 39, bar, clusters, spec) == -1); // above the band CHECK(hitTestActionBar(50, 74, bar, clusters, spec) == -1); // below the band } // On a narrow bar the point past the last visible button (in the overflow dead-zone) misses. static void testHitTestOverflowDeadZone() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); ActionBarRect bar{0, 0, 220, 34}; // only 2 capture buttons visible // x well past the 2nd button's right edge but still inside the bar band. CHECK(hitTestActionBar(215, 10, bar, clusters, spec) == -1); } static void testHitTestDegenerate() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 0, 34}, clusters, spec) == -1); CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 600, 0}, clusters, spec) == -1); CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 600, 34}, {}, spec) == -1); } // --- Resize sweep: no overlap, no cut-off, hit-test matches layout ------------ // Across a representative width range: every visible slot is fully inside the bar's usable // area, no two slots overlap, and every point that hit-tests to a button lands inside that // button's drawn rect (hit-test and layout agree — the load-bearing consistency invariant). static void testResizeSweepNoOverlapNoCutoff() { const auto clusters = inventory(); const ActionBarSpec spec = roundSpec(); for (int w = 60; w <= 1000; w += 7) { ActionBarRect bar{0, 0, w, 34}; auto slots = computeBarSlots(bar, clusters, spec); int prevRight = bar.x + spec.sidePad - 1; for (const auto& s : slots) { // Inside the bar band. CHECK(s.x >= bar.x); CHECK(s.x + s.width <= bar.x + bar.width - spec.sidePad); CHECK(s.y >= bar.y); CHECK(s.y + s.height <= bar.y + bar.height); // No overlap with the previous slot (strictly increasing, non-overlapping). CHECK(s.x > prevRight); prevRight = s.x + s.width - 1; // Label rect stays inside the box. CHECK(s.labelX >= s.x && s.labelX + s.labelW <= s.x + s.width); CHECK(s.labelY >= s.y && s.labelY + s.labelH <= s.y + s.height); } // Hit-test agrees with layout for a mid-height row across the whole band. for (int px = bar.x; px < bar.x + bar.width; px += 3) { const int hit = hitTestActionBar(px, bar.y + bar.height / 2, bar, clusters, spec); if (hit >= 0) { bool found = false; for (const auto& s : slots) if (s.index == hit && px >= s.x && px < s.x + s.width) found = true; CHECK(found); } } } } int main() { testAllFitRectsAndGaps(); testLabelFullHeight(); testLabelFullHeightWhenShort(); testOverflowDropsTrailingWhole(); testTooNarrowForAny(); testOverflowReserveHonouredAcrossWidths(); testReserveDropsInsertAsFxAtRealWidth(); testDegenerate(); testHitTestHitsButtons(); testHitTestGapsAreMisses(); testHitTestMissesOutsideBand(); testHitTestOverflowDeadZone(); testHitTestDegenerate(); testResizeSweepNoOverlapNoCutoff(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }