L6 toolbar polish: single-row faces, Cancel RT -> overflow, Re-capture between groups

Button faces now show only the short label (keybinding moved to hover tooltip as
"name — binding"). Cancel RT joins the overflow menu alongside Capture RT. Re-capture
sits between the capture and placement clusters. Toolbar heights 40->28.
This commit is contained in:
2026-07-27 00:38:32 -04:00
parent 7961458edd
commit acda259ab6
4 changed files with 149 additions and 187 deletions
+76 -84
View File
@@ -1,15 +1,14 @@
// 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.
// task-grouped action-bar layout, overflow-on-narrow, and hit-testing directly.
//
// Covers (L2 brief §test cases):
// 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; 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).
// * 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;
@@ -28,12 +27,14 @@ 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.
// The panel's real inventory shape (L6): 2 capture, 1 maintenance (Re-capture), 2 placement = 5
// buttons. Cluster order: Capture -> Maintenance -> Placement (Re-capture sits between the two
// capture verbs and the placement verbs — the L6 bar ordering).
static std::vector<ClusterSpec> inventory() {
return {
{ActionCluster::Capture, 4},
{ActionCluster::Capture, 2},
{ActionCluster::Maintenance, 1},
{ActionCluster::Placement, 2},
{ActionCluster::Maintenance, 2},
};
}
@@ -45,28 +46,32 @@ static ActionBarSpec roundSpec() {
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).
// A wide bar fits all 5 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)
// Minimum bar width: 548 + sidePad(8) = 556. Give it 600.
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};
ActionBarRect bar{0, 40, 600, 34};
BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 8);
CHECK(fit.visibleCount == 5);
CHECK(fit.hiddenCount == 0);
auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 8);
CHECK(slots.size() == 5);
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
CHECK(slots[0].x == 8);
@@ -80,89 +85,79 @@ static void testAllFitRectsAndGaps() {
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 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 4 (first placement): cluster gap of 16 after 420 -> 436.
CHECK(slots[4].x == 436);
// 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);
// 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);
// 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);
// 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 --------------------------------------------
// --- Label sub-rect: full-height single row ------------------------------------
// 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() {
// 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, 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
ActionBarRect bar{0, 0, 600, 34};
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.
// Label spans the full button height.
CHECK(s.labelH == s.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.
// Inset horizontally.
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() {
// 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, 900, 24}; // btnH = 24 - 6 = 18 < minSplitHeight(30)
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.bindingEmpty());
CHECK(s.labelH == s.height); // label fills the whole interior height
CHECK(s.labelH == s.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).
// 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();
// 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};
ActionBarRect bar{0, 0, 220, 34};
BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 4);
CHECK(fit.hiddenCount == 4);
CHECK(fit.visibleCount == 2);
CHECK(fit.hiddenCount == 3);
auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 4);
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
@@ -179,7 +174,7 @@ static void testTooNarrowForAny() {
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(fit.hiddenCount == 5);
CHECK(computeBarSlots(bar, clusters, spec).empty());
}
@@ -189,10 +184,10 @@ 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());
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, 900, 34}, clusters, badW).empty());
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 = {
@@ -200,7 +195,7 @@ static void testDegenerate() {
{ActionCluster::Placement, 0}, // empty — skipped
{ActionCluster::Maintenance, 1},
};
auto slots = computeBarSlots(ActionBarRect{0, 0, 900, 34}, withEmpty, spec);
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);
@@ -215,7 +210,7 @@ static void testDegenerate() {
static void testHitTestHitsButtons() {
const auto clusters = inventory();
const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 900, 34};
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) {
@@ -230,12 +225,12 @@ static void testHitTestHitsButtons() {
static void testHitTestGapsAreMisses() {
const auto clusters = inventory();
const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 900, 34};
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 3 (right 420) and button 4 (left 436): x in [420,436).
CHECK(hitTestActionBar(428, 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() {
@@ -252,17 +247,17 @@ static void testHitTestMissesOutsideBand() {
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);
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, 900, 0}, clusters, spec) == -1);
CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 900, 34}, {}, 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 ------------
@@ -286,12 +281,9 @@ static void testResizeSweepNoOverlapNoCutoff() {
// 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.
// Label rect stays 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);
}
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) {
@@ -308,8 +300,8 @@ static void testResizeSweepNoOverlapNoCutoff() {
int main() {
testAllFitRectsAndGaps();
testSubRectsSplit();
testSubRectsNoSplitWhenShort();
testLabelFullHeight();
testLabelFullHeightWhenShort();
testOverflowDropsTrailingWhole();
testTooNarrowForAny();
testDegenerate();