Files
reasampler/tests/test_action_bar.cpp
T

399 lines
18 KiB
C++

// 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 <cstddef>
#include <cstdio>
#include <vector>
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, 2 placement (Insert / Insert as FX) = 4
// buttons. Re-capture and Insert Conform moved to the overflow menu, so the top bar's
// Maintenance cluster is now empty. Cluster order: Capture -> Placement.
static std::vector<ClusterSpec> inventory() {
return {
{ActionCluster::Capture, 2},
{ActionCluster::Placement, 2},
};
}
// 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 4 buttons. Verify the first few rects, the intra-cluster gap, and the
// (wider) inter-cluster gap between button 1 (last capture) and button 2 (first placement).
//
// 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 (Placement): x=228, right=328 (clusterGap = 16 after btn1's right)
// btn3 (Placement): x=332, right=432 (intraGap = 4 after btn2's right)
// Minimum bar width: 432 + sidePad(8) = 440. Give it 500.
static void testAllFitRectsAndGaps() {
const auto clusters = inventory();
const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 500, 34};
BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 4);
CHECK(fit.hiddenCount == 0);
auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 4);
// 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 (first placement): cluster gap of 16 after 212 -> 228.
CHECK(slots[2].x == 228);
CHECK(slots[2].cluster == ActionCluster::Placement);
CHECK(slots[2].index == 2);
// Button 3 (Insert as FX — second placement): intra-cluster gap of 4 after 328 -> 332.
// It joins the Placement cluster, so no cluster gap opens before it.
CHECK(slots[3].x == 332);
CHECK(slots[3].cluster == ActionCluster::Placement);
CHECK(slots[3].index == 3);
CHECK(slots[3].x - (slots[2].x + slots[2].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 (Placement) 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 == 2);
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 == 4);
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), 520 is the narrowest
// band that still shows all four buttons; one pixel narrower drops Insert as FX (flat index
// 3, the trailing Placement button) alone into overflow, leaving its three 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 allFourWidth = 520;
{
ActionBarRect action{0, 0, allFourWidth - reserve, 34};
const auto slots = computeBarSlots(action, clusters, spec);
CHECK(slots.size() == 4);
CHECK(slots.back().index == 3); // Insert as FX still visible
}
{
ActionBarRect action{0, 0, allFourWidth - 1 - reserve, 34};
const auto slots = computeBarSlots(action, clusters, spec);
CHECK(slots.size() == 3); // Insert as FX alone dropped
CHECK(slots.back().index == 2); // Insert 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<ClusterSpec> 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;
}