54f37be0bb
New pure action_bar module (clusters, keybinding sub-labels, overflow, hit-test) supersedes the flat M11 strip; bank_panel chrome/buttons/tabs/grid now draw through the L1 kit by role with hover. Expose draw_kit::toLice in header to fix drawThumbnail forward-refs. CTest 27/27.
326 lines
14 KiB
C++
326 lines
14 KiB
C++
// Standalone tests for reasampler::action_bar — no REAPER, no test framework. Same fast loop
|
|
// as the sibling pure tests (action_buttons / mode_switch / prune_button): assert the
|
|
// task-grouped action-bar layout, its keybinding sub-label sub-rects, overflow-on-narrow, and
|
|
// hit-testing directly.
|
|
//
|
|
// Covers (L2 brief §test cases):
|
|
// * 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; mirrors action_buttons suppression).
|
|
// * Keybinding sub-label sub-rects correct (label row + micro binding row split; too-short
|
|
// button collapses to label-only with an empty binding rect).
|
|
// * 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/action_bar.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)
|
|
|
|
// The panel's real inventory shape: 4 capture, 2 placement, 2 maintenance = 8 buttons.
|
|
static std::vector<ClusterSpec> inventory() {
|
|
return {
|
|
{ActionCluster::Capture, 4},
|
|
{ActionCluster::Placement, 2},
|
|
{ActionCluster::Maintenance, 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;
|
|
s.bindingHeight = 11;
|
|
s.minSplitHeight = 30;
|
|
return s;
|
|
}
|
|
|
|
// --- Layout: all fit, correct rects + gaps ------------------------------------
|
|
|
|
// A wide bar fits all 8 buttons. Verify the first few rects, the intra-cluster gap, and the
|
|
// (wider) inter-cluster gap between button 3 (last capture) and button 4 (first placement).
|
|
static void testAllFitRectsAndGaps() {
|
|
const auto clusters = inventory();
|
|
const ActionBarSpec spec = roundSpec();
|
|
// Usable width: sidePad(8) + 8*100 + 6 intra gaps*4 + 2 cluster gaps*16 + sidePad(8)
|
|
// = 8 + 800 + 24 + 32 + 8 = 872. Give it 900.
|
|
ActionBarRect bar{0, 40, 900, 34};
|
|
|
|
BarFit fit = computeBarFit(bar, clusters, spec);
|
|
CHECK(fit.visibleCount == 8);
|
|
CHECK(fit.hiddenCount == 0);
|
|
|
|
auto slots = computeBarSlots(bar, clusters, spec);
|
|
CHECK(slots.size() == 8);
|
|
|
|
// 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 3 is the last capture button. Its right edge:
|
|
// b0 8..108, +4 -> b1 112..212, +4 -> b2 216..316, +4 -> b3 320..420.
|
|
CHECK(slots[3].x == 320);
|
|
CHECK(slots[3].cluster == ActionCluster::Capture);
|
|
|
|
// Button 4 (first placement): cluster gap of 16 after 420 -> 436.
|
|
CHECK(slots[4].x == 436);
|
|
CHECK(slots[4].cluster == ActionCluster::Placement);
|
|
CHECK(slots[4].index == 4);
|
|
|
|
// Inter-cluster gap (436 - 420 = 16) is wider than the intra-cluster gap (4) — the task
|
|
// grouping is structurally visible in the geometry.
|
|
const int interGap = slots[4].x - (slots[3].x + slots[3].width);
|
|
const int intraGap = slots[1].x - (slots[0].x + slots[0].width);
|
|
CHECK(interGap == 16);
|
|
CHECK(intraGap == 4);
|
|
CHECK(interGap > intraGap);
|
|
|
|
// Button 6 (first maintenance): b4 436..536, +4 -> b5 540..640, +16 -> b6 656..756.
|
|
CHECK(slots[6].x == 656);
|
|
CHECK(slots[6].cluster == ActionCluster::Maintenance);
|
|
CHECK(slots[6].index == 6);
|
|
}
|
|
|
|
// --- Keybinding sub-label sub-rects --------------------------------------------
|
|
|
|
// A tall-enough button splits into a label row (top) and a micro binding row (bottom); the two
|
|
// abut, cover the button height, and sit inside the horizontal text inset.
|
|
static void testSubRectsSplit() {
|
|
const auto clusters = inventory();
|
|
const ActionBarSpec spec = roundSpec();
|
|
ActionBarRect bar{0, 0, 900, 34}; // btnH = 34 - 6 = 28 >= minSplitHeight? 28 < 30
|
|
// 28 < minSplitHeight(30) -> NOT split. Bump the bar so btnH >= 30.
|
|
bar.height = 40; // btnH = 40 - 6 = 34 >= 30 -> split
|
|
auto slots = computeBarSlots(bar, clusters, spec);
|
|
CHECK(!slots.empty());
|
|
const ActionBarSlot& s = slots[0];
|
|
CHECK(!s.bindingEmpty());
|
|
// Binding row is the bottom bindingHeight(11); label is the remainder (34 - 11 = 23).
|
|
CHECK(s.bindH == 11);
|
|
CHECK(s.labelH == s.height - 11);
|
|
// The two rows abut with no gap/overlap and together span the button height.
|
|
CHECK(s.labelY == s.y);
|
|
CHECK(s.bindY == s.labelY + s.labelH);
|
|
CHECK(s.bindY + s.bindH == s.y + s.height);
|
|
// Both inset horizontally (text clears the button edge) and share the same inner width.
|
|
CHECK(s.labelX > s.x);
|
|
CHECK(s.labelX == s.bindX);
|
|
CHECK(s.labelW == s.bindW);
|
|
CHECK(s.labelX + s.labelW < s.x + s.width);
|
|
}
|
|
|
|
// A short button (height below minSplitHeight) is NOT split: the label fills the interior and
|
|
// the binding sub-rect is empty (the shell draws only the label — graceful, no clipped micro).
|
|
static void testSubRectsNoSplitWhenShort() {
|
|
const auto clusters = inventory();
|
|
const ActionBarSpec spec = roundSpec();
|
|
ActionBarRect bar{0, 0, 900, 24}; // btnH = 24 - 6 = 18 < minSplitHeight(30)
|
|
auto slots = computeBarSlots(bar, clusters, spec);
|
|
CHECK(!slots.empty());
|
|
const ActionBarSlot& s = slots[0];
|
|
CHECK(s.bindingEmpty());
|
|
CHECK(s.labelH == s.height); // label fills the whole interior height
|
|
CHECK(s.labelY == s.y);
|
|
}
|
|
|
|
// --- Overflow / hiding on a narrow panel --------------------------------------
|
|
|
|
// A bar wide enough for only the 4 capture buttons + a couple placement drops the rest WHOLE.
|
|
// The visible buttons keep their full width (never clipped), and the frequent capture cluster
|
|
// survives (overflow drops from the END).
|
|
static void testOverflowDropsTrailingWhole() {
|
|
const auto clusters = inventory();
|
|
const ActionBarSpec spec = roundSpec();
|
|
// Room for exactly the 4 capture buttons: sidePad(8) + 4*100 + 3*4 = 420, +sidePad(8) = 428.
|
|
// A 5th button needs cluster gap 16 -> 428 + 16 + 100 = 544 > 430. So 430 fits exactly 4.
|
|
ActionBarRect bar{0, 0, 430, 34};
|
|
BarFit fit = computeBarFit(bar, clusters, spec);
|
|
CHECK(fit.visibleCount == 4);
|
|
CHECK(fit.hiddenCount == 4);
|
|
|
|
auto slots = computeBarSlots(bar, clusters, spec);
|
|
CHECK(slots.size() == 4);
|
|
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 == 8);
|
|
CHECK(computeBarSlots(bar, clusters, spec).empty());
|
|
}
|
|
|
|
// --- 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, 900, 0}, clusters, spec).empty());
|
|
CHECK(computeBarSlots(ActionBarRect{0, 0, 900, 34}, {}, spec).empty());
|
|
ActionBarSpec badW = spec; badW.buttonWidth = 0;
|
|
CHECK(computeBarSlots(ActionBarRect{0, 0, 900, 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, 900, 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, 900, 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, 900, 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 3 (right 420) and button 4 (left 436): x in [420,436).
|
|
CHECK(hitTestActionBar(428, 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, 430, 34}; // only 4 capture buttons visible
|
|
// x well past the 4th button's right edge but still inside the bar band.
|
|
CHECK(hitTestActionBar(425, 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, 900, 0}, clusters, spec) == -1);
|
|
CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 900, 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;
|
|
// Sub-rects stay inside the box.
|
|
CHECK(s.labelX >= s.x && s.labelX + s.labelW <= s.x + s.width);
|
|
if (!s.bindingEmpty()) {
|
|
CHECK(s.bindX >= s.x && s.bindX + s.bindW <= s.x + s.width);
|
|
CHECK(s.bindY + s.bindH <= 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();
|
|
testSubRectsSplit();
|
|
testSubRectsNoSplitWhenShort();
|
|
testOverflowDropsTrailingWhole();
|
|
testTooNarrowForAny();
|
|
testDegenerate();
|
|
testHitTestHitsButtons();
|
|
testHitTestGapsAreMisses();
|
|
testHitTestMissesOutsideBand();
|
|
testHitTestOverflowDeadZone();
|
|
testHitTestDegenerate();
|
|
testResizeSweepNoOverlapNoCutoff();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|