From acda259ab693a5d73b1d89be28cf5196044a4890 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 00:38:32 -0400 Subject: [PATCH] L6 toolbar polish: single-row faces, Cancel RT -> overflow, Re-capture between groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/action_bar.cpp | 25 ++---- src/action_bar.h | 50 +++++------- src/bank_panel.cpp | 101 +++++++++++------------- tests/test_action_bar.cpp | 160 ++++++++++++++++++-------------------- 4 files changed, 149 insertions(+), 187 deletions(-) diff --git a/src/action_bar.cpp b/src/action_bar.cpp index 9ceaa1d..3f7535c 100644 --- a/src/action_bar.cpp +++ b/src/action_bar.cpp @@ -16,28 +16,15 @@ int totalButtons(const std::vector& clusters) { return n; } -// Fills a slot's label / binding sub-rects from its box per the spec. The binding is the -// bottom `bindingHeight` micro strip; the label is the remainder above it, both inset -// horizontally so text clears the button edge. A button shorter than minSplitHeight is not -// split: bindingBox stays empty and the label fills the interior (the shell draws only the -// label — graceful, no clipped micro row). -void fillTextRects(ActionBarSlot& s, const ActionBarSpec& spec) { +// Fills a slot's label rect from its box. The label spans the full button height — a single-row +// short label (L6: keybinding sub-row removed from the face; binding is in the hover tooltip). +// Insets horizontally so text clears the button edge. +void fillTextRects(ActionBarSlot& s, const ActionBarSpec& /*spec*/) { const int hpad = 4; // horizontal text inset inside the button const int innerX = s.x + hpad; const int innerW = s.width - 2 * hpad; - if (innerW <= 0) return; // too narrow for text; leave sub-rects empty - - if (s.height >= spec.minSplitHeight && spec.bindingHeight > 0 && - s.height - spec.bindingHeight > 0) { - const int bindH = spec.bindingHeight; - const int labelH = s.height - bindH; - s.labelX = innerX; s.labelY = s.y; s.labelW = innerW; s.labelH = labelH; - s.bindX = innerX; s.bindY = s.y + labelH; s.bindW = innerW; s.bindH = bindH; - } else { - // Too short to split — label fills the interior; no binding row. - s.labelX = innerX; s.labelY = s.y; s.labelW = innerW; s.labelH = s.height; - s.bindX = s.bindY = s.bindW = s.bindH = 0; - } + if (innerW <= 0) return; // too narrow for text; leave label rect empty + s.labelX = innerX; s.labelY = s.y; s.labelW = innerW; s.labelH = s.height; } // Tiles the first `visible` buttons into slots, cluster by cluster, left to right. This is the diff --git a/src/action_bar.h b/src/action_bar.h index 59c05c1..1b2b9d1 100644 --- a/src/action_bar.h +++ b/src/action_bar.h @@ -1,12 +1,13 @@ #pragma once // action_bar — the REAPER-free, LICE-free layout + hit-test math behind the bank_panel's -// TASK-GROUPED toolbars (Phase L, L2 + L4). L2's dock-panel layout redesign (DS-3: a thorough -// layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact bar of -// clusters instead of one flat equal-tiled strip (the M11 action_buttons row this supersedes -// for the panel's action inventory). Each button carries a label sub-rect and a keybinding-help -// MICRO sub-rect ("icon+label, keybinding as a micro sub-label" — the L2 contract), and the bar -// degrades gracefully on a narrow panel by dropping WHOLE trailing buttons (never clipping) so -// the frequent leading cluster survives. +// TASK-GROUPED toolbars (Phase L, L2 + L4 + L6). L2's dock-panel layout redesign (DS-3: a +// thorough layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact +// bar of clusters instead of one flat equal-tiled strip (the M11 action_buttons row this +// supersedes for the panel's action inventory). Each button carries a label sub-rect spanning +// its full height — a single-row short label (L6: the keybinding sub-row was on the button face +// through L5; L6 moves it to the hover tooltip instead). The bar degrades gracefully on a narrow +// panel by dropping WHOLE trailing buttons (never clipping) so the frequent leading cluster +// survives. // // L4 re-homes the inventory across TWO toolbars, BOTH driven by this one module: a TOP toolbar // (Capture + Placement — the two acts the tool exists for) and a BOTTOM toolbar (the Design-View @@ -17,8 +18,8 @@ // Why pure (CLAUDE.md §load-bearing split, DS-1 caution): the panel shell owns the SWELL // window, the L1-kit draws, and the NamedCommandLookup/Main_OnCommand dispatch — all // DAW-verified. What is NOT DAW-bound — how the clusters tile the bar, where each button and -// its two text sub-rects sit, and which button a click hits — lives HERE, unit-tested outside -// the DAW. Mirror of mode_switch / action_buttons / prune_button. +// its label sub-rect sit, and which button a click hits — lives HERE, unit-tested outside the +// DAW. Mirror of mode_switch / action_buttons / prune_button. // // NAME NOTE (brief §name-collision): ButtonRect / ButtonStripRect / ActionButtonRect / // SegmentRect / CellRect / FooterRect / KitButtonBox are already owned in this namespace, so @@ -74,10 +75,9 @@ struct ActionBarRect { // position in the caller's flat action list (the caller supplies actions in cluster order, so // index also selects the action to fire on a hit). `cluster` is the task group it was laid out // under (surfaced so a test can assert the grouping is structural, and the shell can tint a -// cluster). `box` is the whole button rect; `labelBox` and `bindingBox` split it into the -// action-name row (top) and the keybinding MICRO row (bottom) so the shell draws each with the -// matching kit font. Only VISIBLE buttons get a slot — a button that does not fit is omitted, -// never returned clipped, so every slot is fully drawable. +// cluster). `box` is the whole button rect; `labelBox` is the text area inset horizontally so +// text clears the button edge. Only VISIBLE buttons get a slot — a button that does not fit is +// omitted, never returned clipped, so every slot is fully drawable. struct ActionBarSlot { int index = 0; ActionCluster cluster = ActionCluster::Capture; @@ -85,22 +85,16 @@ struct ActionBarSlot { int y = 0; int width = 0; int height = 0; - // Text sub-rects (absolute, top-left origin), both inside `box`. bindingBox is the bottom - // micro strip; labelBox is the remainder above it. When the button is too short to split - // (height < a minimum), bindingBox is empty (width/height 0) and labelBox is the whole - // interior — the shell then draws only the label (graceful, no clipped micro row). + // Label rect (absolute, top-left origin), inside `box`. The label spans the full button + // height — a single-row short label only (L6: keybinding sub-row removed from the face; + // binding is surfaced in the hover tooltip instead). int labelX = 0, labelY = 0, labelW = 0, labelH = 0; - int bindX = 0, bindY = 0, bindW = 0, bindH = 0; - - bool bindingEmpty() const { return bindW <= 0 || bindH <= 0; } bool operator==(const ActionBarSlot& o) const { return index == o.index && cluster == o.cluster && x == o.x && y == o.y && width == o.width && height == o.height && labelX == o.labelX && labelY == o.labelY && - labelW == o.labelW && labelH == o.labelH && - bindX == o.bindX && bindY == o.bindY && - bindW == o.bindW && bindH == o.bindH; + labelW == o.labelW && labelH == o.labelH; } }; @@ -124,16 +118,12 @@ struct ClusterSpec { // task grouping reads visually; the 8px-grid density decision). // * sidePad — left/right inset from the bar edges to the first/last button. // * verticalInset — top/bottom gap inside the bar (buttons read as raised, not full-bleed). -// * bindingHeight — height of the keybinding MICRO sub-row at the button's bottom. -// * minSplitHeight— a button shorter than this is not split (bindingBox empty; label fills). struct ActionBarSpec { int buttonWidth = 108; int buttonGap = 4; int clusterGap = 16; int sidePad = 8; int verticalInset = 3; - int bindingHeight = 11; - int minSplitHeight = 30; }; // How many buttons (from the front, cluster by cluster) fit the bar at `spec.buttonWidth`. @@ -152,9 +142,9 @@ BarFit computeBarFit(const ActionBarRect& bar, const std::vector& c // Lays out the VISIBLE buttons (per computeBarFit) left-to-right in cluster order: buttons // pack at buttonWidth with buttonGap inside a cluster and clusterGap between clusters, starting // at bar.x + sidePad. Each slot carries its flat action index, its cluster, its box, and the -// label / keybinding sub-rects. Empty clusters emit no gap. Returns exactly visibleCount slots -// in ascending index order. A degenerate bar (width/height <= 0), an empty cluster list, or a -// non-positive buttonWidth yields empty. +// label sub-rect (full-height single row). Empty clusters emit no gap. Returns exactly +// visibleCount slots in ascending index order. A degenerate bar (width/height <= 0), an empty +// cluster list, or a non-positive buttonWidth yields empty. std::vector computeBarSlots(const ActionBarRect& bar, const std::vector& clusters, const ActionBarSpec& spec); diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index feabfba..2b6d9b7 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -47,7 +47,6 @@ #include #include "action_bar.h" // pure TASK-GROUPED action-bar layout + hit-test (L2) -#include "action_buttons.h" // pure label format (formatButtonLabel) — reused by the L2 bar (M11) #include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path) #include "drag_out.h" // pure gesture-boundary decision + path-list assembly (M11) #include "drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11) @@ -162,8 +161,8 @@ constexpr int kFooterHeight = 30; // * kBottomToolbarHeight — the BOTTOM toolbar (Design-View tag/switch verbs) directly above // the footer (L4 §2). This is the L2 action-bar band, repurposed. // The kBarSpec metrics the bars consume live near the draw below; only heights live here. -constexpr int kTopToolbarHeight = 40; // taller — hosts the label + keybinding micro sub-row -constexpr int kBottomToolbarHeight = 40; // same shape (label + keybinding sub-row) +constexpr int kTopToolbarHeight = 28; // single-row label face (L6: keybinding sub-row removed) +constexpr int kBottomToolbarHeight = 28; // same shape — both bars consistent // --- Tooltip (Phase L, L5) ---------------------------------------------------- // The custom hover-delay tooltip's timing + approximate text metrics. The delay matches the @@ -793,13 +792,14 @@ struct ActionBarRow { // for the top bar (its actions are unconditional triggers). }; -// The TOP toolbar inventory (L5 refinement 1): the FREQUENT acts only — Capture (item / track) -// then Placement (insert / insert-conform) then Maintenance (re-capture / cancel RT). The three -// RARE capture variants (Batch Items / Batch Razor / Capture RT) are re-homed OFF the visible -// bar into the far-right "⋯" overflow menu (overflowMenuRows) — same registered actions, same -// command-id contract, just a different home. Capture scopes come from captureActionTable() -// (render_settings, pure); the rest are the registered M11/M10/M8 commands. Built once per -// draw/click. Each row carries its full (prefix-stripped) action name for the hover tooltip. +// The TOP toolbar inventory (L6 refinement): the FREQUENT acts only — Capture (item / track) +// then Re-capture (Maintenance, set between the two capture verbs and the placement verbs) then +// Placement (insert / insert-conform). The FOUR RARE variants (Batch Items / Batch Razor / +// Capture RT / Cancel RT) are ALL in the far-right "⋯" overflow menu (overflowMenuRows) — +// same registered actions, same command-id contract, just a different home. Capture scopes come +// from captureActionTable() (render_settings, pure); the rest are the registered M11/M10/M8 +// commands. Built once per draw/click. Each row carries its full (prefix-stripped) action name +// for the hover tooltip. std::vector topBarRows() { std::vector rows; // Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track"; @@ -811,26 +811,26 @@ std::vector topBarRows() { rows.push_back({def.commandSuffix, label, def.descriptionPhrase, ActionCluster::Capture, true}); } + // Maintenance cluster — Re-capture from source (M10), placed BETWEEN the capture group and + // the placement group so its position reads "refine the last capture before placing it". + // Cancel RT lives in the overflow menu (both realtime verbs share that home — L6). + rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", + "re-capture from source", ActionCluster::Maintenance, true}); // Placement cluster — the second act (still a distinct on-demand act; no auto-insert). rows.push_back({"INSERT_SELECTED", "Insert", "insert selected sample at edit cursor", ActionCluster::Placement, true}); rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", "insert selected sample at edit cursor (conform to tempo)", ActionCluster::Placement, true}); - // Maintenance cluster — rarer upkeep: re-capture from source (M10) and cancel an - // in-flight realtime capture (M8). Capture-adjacent, so they live in the top toolbar. - rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", - "re-capture from source", ActionCluster::Maintenance, true}); - rows.push_back({"CANCEL_REALTIME_CAPTURE", "Cancel RT", - "cancel realtime capture", ActionCluster::Maintenance, true}); return rows; } -// The TOP-toolbar OVERFLOW menu inventory (L5 refinement 1): the three rare capture variants, -// pulled off the visible bar into the far-right "⋯" menu button's popup. Each fires the SAME -// existing registered command id via the SAME NamedCommandLookup/Main_OnCommand contract — no -// action changes. The fullName is the popup entry text (the terse shortLabel is unused for menu -// items; the popup has room for the full name). Order matches the L4 capture-cluster order. +// The TOP-toolbar OVERFLOW menu inventory (L6): four items pulled off the visible bar into the +// far-right "⋯" menu button's popup — the three rare batch/realtime capture variants plus +// Cancel RT (both realtime verbs share the menu home). Each fires the SAME existing registered +// command id via the SAME NamedCommandLookup/Main_OnCommand contract — no action changes. The +// fullName is the popup entry text (the terse shortLabel is unused for menu items; the popup has +// room for the full name). Batch entries first, then the two realtime verbs. std::vector overflowMenuRows() { return { {"CAPTURE_BATCH_ITEMS", "Batch Items", @@ -839,6 +839,8 @@ std::vector overflowMenuRows() { "batch capture razor areas (one per area)", ActionCluster::Capture, true}, {"CAPTURE_TRACK_REALTIME", "Capture RT", "capture selected track (realtime)", ActionCluster::Capture, true}, + {"CANCEL_REALTIME_CAPTURE", "Cancel RT", + "cancel realtime capture", ActionCluster::Maintenance, true}, }; } @@ -886,8 +888,9 @@ std::vector bottomBarRows() { // The cluster button-count specs for a given row set, in the row list's cluster order (so the // pure action_bar's flat index lines up with the row list). Handles all five cluster kinds; // empty clusters contribute a 0-count spec (action_bar skips them, emitting no gap). The spec -// order follows each toolbar's fixed layout order (top: Capture, Placement, Maintenance; -// bottom: Tagging, Switching). +// order follows each toolbar's fixed layout order (top: Capture, Maintenance, Placement — +// Re-capture sits between the two capture verbs and the placement verbs; bottom: Tagging, +// Switching). The bottom bar's Maintenance count is 0, so the order change is transparent there. std::vector actionBarClusters(const std::vector& rows) { int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0; for (const ActionBarRow& r : rows) { @@ -901,8 +904,8 @@ std::vector actionBarClusters(const std::vector& rows } return { {ActionCluster::Capture, nCap}, - {ActionCluster::Placement, nPlace}, {ActionCluster::Maintenance, nMaint}, + {ActionCluster::Placement, nPlace}, {ActionCluster::Tagging, nTag}, {ActionCluster::Switching, nSwitch}, }; @@ -910,11 +913,10 @@ std::vector actionBarClusters(const std::vector& rows // The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared // by both toolbars' draw and hit-test (identical button shape top and bottom). L5 refinement 5: -// clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups — the -// gap between clusters is visibly larger than the gap between buttons within a cluster. +// clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups. L6: +// bindingHeight / minSplitHeight removed — buttons are single-row label-only faces now. const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24, - /*sidePad=*/8, /*verticalInset=*/3, /*bindingHeight=*/11, - /*minSplitHeight=*/30}; + /*sidePad=*/8, /*verticalInset=*/3}; // The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split // body). Degenerate (height 0) when the client is too short to host it above the footer. @@ -950,12 +952,13 @@ std::string barBindingText(int cmd) { } // Draws one task-grouped toolbar through the L1 kit: a bg/panel band, then each visible button -// as a kit drawButton (rest/hover/disabled) with the action NAME on the label row and the key -// binding (or "unbound") on the Micro sub-row. Overflow drops WHOLE trailing buttons (the pure -// layout returns only the buttons that fit), so nothing is drawn clipped. `hoverKind` selects -// which HoverKind this bar's buttons use (TopBarButton / BottomBarButton) so the two toolbars' -// hover states never cross. `topDivider` draws a hairline at the band's top edge (the bottom -// toolbar's elevation over the split body); the top toolbar draws it at its bottom edge instead. +// as a kit drawButton (rest/hover/disabled) with the action short label on the single-row face. +// Overflow drops WHOLE trailing buttons (the pure layout returns only the buttons that fit), so +// nothing is drawn clipped. `hoverKind` selects which HoverKind this bar's buttons use +// (TopBarButton / BottomBarButton) so the two toolbars' hover states never cross. `topDivider` +// draws a hairline at the band's top edge (the bottom toolbar's elevation over the split body); +// the top toolbar draws it at its bottom edge instead. Key binding help is in the hover tooltip +// (L6), not on the button face — the face shows only shortLabel. void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, const std::vector& rows, HoverKind hoverKind, bool topDivider) { if (bar.height <= 0 || bar.width <= 0) return; @@ -983,8 +986,8 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, state = InteractionState::Hover; // The button surface (drawButton draws the micro-gradient + rounded border + honors - // the state). The label is drawn separately below so the binding sub-row can use the - // Micro font, so pass no label to drawButton. + // the state). The label is drawn separately so the text role tracks the state correctly; + // pass no label to drawButton. const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}}; drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); @@ -992,19 +995,6 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, (state == InteractionState::Disabled) ? Role::TextDim : Role::TextPrimary; const KitBox labelBox{s.labelX, s.labelY, s.labelW, s.labelH}; kitText(bmp, labelBox, row.shortLabel.c_str(), Font::Label, textRole, Align::Center); - - if (!s.bindingEmpty()) { - // The keybinding help sub-label, dim + Micro. formatButtonLabel's blank/unbound - // collapse is reused so an unbound action reads "(unbound)" cleanly; here we want - // just the binding token (name is already on the label row), so format the binding - // alone and strip the leading name-less case. - const std::string binding = barBindingText(cmd); - const std::string sub = formatButtonLabel("", binding); // "" + " (unbound)" / " " - std::size_t start = sub.find_first_not_of(' '); - const std::string shown = (start == std::string::npos) ? sub : sub.substr(start); - const KitBox bindBox{s.bindX, s.bindY, s.bindW, s.bindH}; - kitText(bmp, bindBox, shown.c_str(), Font::Micro, Role::TextDim, Align::Center); - } } } @@ -1098,11 +1088,14 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a // The full name is stored already prefix-free, but strip defensively in case a source ever // carries the "ReaSampler:" display prefix (the tooltip must never show it — L5 refinement 2). - // Intentionally distinct sources: the tooltip reads the registered phrase (fullName) while the - // keybinding sub-row in drawToolbar reads the live binding via kbd_getTextFromCmd. Do not - // unify them — each serves a different purpose and has a different lifetime. - textOut = stripActionPrefix(rows[static_cast(hv.index)].fullName, - actionDisplayPrefix()); + // L6: the keybinding sub-row was removed from the button face, so the tooltip now carries + // both the name AND the binding (when bound) — e.g. "capture selected item — F5". When the + // action is unbound the tooltip shows only the name (no "(unbound)" noise in the tooltip). + const std::string phrase = stripActionPrefix(rows[static_cast(hv.index)].fullName, + actionDisplayPrefix()); + const int cmd = resolveBarCommandId(rows[static_cast(hv.index)]); + const std::string binding = barBindingText(cmd); + textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding; // " — " (em dash, UTF-8) ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height; return true; } diff --git a/tests/test_action_bar.cpp b/tests/test_action_bar.cpp index 56e67cc..02b9b25 100644 --- a/tests/test_action_bar.cpp +++ b/tests/test_action_bar.cpp @@ -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 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 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();