Merge pL-w6-toolbar-polish: single-row button faces, Cancel RT to overflow menu, Re-capture between groups

This commit is contained in:
2026-07-27 00:42:51 -04:00
4 changed files with 149 additions and 187 deletions
+6 -19
View File
@@ -16,28 +16,15 @@ int totalButtons(const std::vector<ClusterSpec>& clusters) {
return n; return n;
} }
// Fills a slot's label / binding sub-rects from its box per the spec. The binding is the // Fills a slot's label rect from its box. The label spans the full button height — a single-row
// bottom `bindingHeight` micro strip; the label is the remainder above it, both inset // short label (L6: keybinding sub-row removed from the face; binding is in the hover tooltip).
// horizontally so text clears the button edge. A button shorter than minSplitHeight is not // Insets horizontally so text clears the button edge.
// split: bindingBox stays empty and the label fills the interior (the shell draws only the void fillTextRects(ActionBarSlot& s, const ActionBarSpec& /*spec*/) {
// label — graceful, no clipped micro row).
void fillTextRects(ActionBarSlot& s, const ActionBarSpec& spec) {
const int hpad = 4; // horizontal text inset inside the button const int hpad = 4; // horizontal text inset inside the button
const int innerX = s.x + hpad; const int innerX = s.x + hpad;
const int innerW = s.width - 2 * hpad; const int innerW = s.width - 2 * hpad;
if (innerW <= 0) return; // too narrow for text; leave sub-rects empty 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;
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;
}
} }
// Tiles the first `visible` buttons into slots, cluster by cluster, left to right. This is the // Tiles the first `visible` buttons into slots, cluster by cluster, left to right. This is the
+20 -30
View File
@@ -1,12 +1,13 @@
#pragma once #pragma once
// action_bar — the REAPER-free, LICE-free layout + hit-test math behind the bank_panel's // 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 // TASK-GROUPED toolbars (Phase L, L2 + L4 + L6). L2's dock-panel layout redesign (DS-3: a
// layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact bar of // thorough layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact
// clusters instead of one flat equal-tiled strip (the M11 action_buttons row this supersedes // bar of clusters instead of one flat equal-tiled strip (the M11 action_buttons row this
// for the panel's action inventory). Each button carries a label sub-rect and a keybinding-help // supersedes for the panel's action inventory). Each button carries a label sub-rect spanning
// MICRO sub-rect ("icon+label, keybinding as a micro sub-label" — the L2 contract), and the bar // its full height — a single-row short label (L6: the keybinding sub-row was on the button face
// degrades gracefully on a narrow panel by dropping WHOLE trailing buttons (never clipping) so // through L5; L6 moves it to the hover tooltip instead). The bar degrades gracefully on a narrow
// the frequent leading cluster survives. // 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 // 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 // (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 // 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 // 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 // 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 // its label sub-rect sit, and which button a click hits — lives HERE, unit-tested outside the
// the DAW. Mirror of mode_switch / action_buttons / prune_button. // DAW. Mirror of mode_switch / action_buttons / prune_button.
// //
// NAME NOTE (brief §name-collision): ButtonRect / ButtonStripRect / ActionButtonRect / // NAME NOTE (brief §name-collision): ButtonRect / ButtonStripRect / ActionButtonRect /
// SegmentRect / CellRect / FooterRect / KitButtonBox are already owned in this namespace, so // 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 // 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 // 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 // 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 // cluster). `box` is the whole button rect; `labelBox` is the text area inset horizontally so
// action-name row (top) and the keybinding MICRO row (bottom) so the shell draws each with the // text clears the button edge. Only VISIBLE buttons get a slot — a button that does not fit is
// matching kit font. Only VISIBLE buttons get a slot — a button that does not fit is omitted, // omitted, never returned clipped, so every slot is fully drawable.
// never returned clipped, so every slot is fully drawable.
struct ActionBarSlot { struct ActionBarSlot {
int index = 0; int index = 0;
ActionCluster cluster = ActionCluster::Capture; ActionCluster cluster = ActionCluster::Capture;
@@ -85,22 +85,16 @@ struct ActionBarSlot {
int y = 0; int y = 0;
int width = 0; int width = 0;
int height = 0; int height = 0;
// Text sub-rects (absolute, top-left origin), both inside `box`. bindingBox is the bottom // Label rect (absolute, top-left origin), inside `box`. The label spans the full button
// micro strip; labelBox is the remainder above it. When the button is too short to split // height — a single-row short label only (L6: keybinding sub-row removed from the face;
// (height < a minimum), bindingBox is empty (width/height 0) and labelBox is the whole // binding is surfaced in the hover tooltip instead).
// interior — the shell then draws only the label (graceful, no clipped micro row).
int labelX = 0, labelY = 0, labelW = 0, labelH = 0; 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 { bool operator==(const ActionBarSlot& o) const {
return index == o.index && cluster == o.cluster && return index == o.index && cluster == o.cluster &&
x == o.x && y == o.y && width == o.width && height == o.height && x == o.x && y == o.y && width == o.width && height == o.height &&
labelX == o.labelX && labelY == o.labelY && labelX == o.labelX && labelY == o.labelY &&
labelW == o.labelW && labelH == o.labelH && labelW == o.labelW && labelH == o.labelH;
bindX == o.bindX && bindY == o.bindY &&
bindW == o.bindW && bindH == o.bindH;
} }
}; };
@@ -124,16 +118,12 @@ struct ClusterSpec {
// task grouping reads visually; the 8px-grid density decision). // task grouping reads visually; the 8px-grid density decision).
// * sidePad — left/right inset from the bar edges to the first/last button. // * 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). // * 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 { struct ActionBarSpec {
int buttonWidth = 108; int buttonWidth = 108;
int buttonGap = 4; int buttonGap = 4;
int clusterGap = 16; int clusterGap = 16;
int sidePad = 8; int sidePad = 8;
int verticalInset = 3; int verticalInset = 3;
int bindingHeight = 11;
int minSplitHeight = 30;
}; };
// How many buttons (from the front, cluster by cluster) fit the bar at `spec.buttonWidth`. // 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<ClusterSpec>& c
// Lays out the VISIBLE buttons (per computeBarFit) left-to-right in cluster order: buttons // 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 // 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 // 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 // label sub-rect (full-height single row). Empty clusters emit no gap. Returns exactly
// in ascending index order. A degenerate bar (width/height <= 0), an empty cluster list, or a // visibleCount slots in ascending index order. A degenerate bar (width/height <= 0), an empty
// non-positive buttonWidth yields empty. // cluster list, or a non-positive buttonWidth yields empty.
std::vector<ActionBarSlot> computeBarSlots(const ActionBarRect& bar, std::vector<ActionBarSlot> computeBarSlots(const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters, const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec); const ActionBarSpec& spec);
+47 -54
View File
@@ -47,7 +47,6 @@
#include <vector> #include <vector>
#include "action_bar.h" // pure TASK-GROUPED action-bar layout + hit-test (L2) #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 "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.h" // pure gesture-boundary decision + path-list assembly (M11)
#include "drag_out_win.h" // OLE / SWELL drag-out initiation seam (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 // * kBottomToolbarHeight — the BOTTOM toolbar (Design-View tag/switch verbs) directly above
// the footer (L4 §2). This is the L2 action-bar band, repurposed. // 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. // 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 kTopToolbarHeight = 28; // single-row label face (L6: keybinding sub-row removed)
constexpr int kBottomToolbarHeight = 40; // same shape (label + keybinding sub-row) constexpr int kBottomToolbarHeight = 28; // same shape — both bars consistent
// --- Tooltip (Phase L, L5) ---------------------------------------------------- // --- Tooltip (Phase L, L5) ----------------------------------------------------
// The custom hover-delay tooltip's timing + approximate text metrics. The delay matches the // 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). // for the top bar (its actions are unconditional triggers).
}; };
// The TOP toolbar inventory (L5 refinement 1): the FREQUENT acts only — Capture (item / track) // The TOP toolbar inventory (L6 refinement): the FREQUENT acts only — Capture (item / track)
// then Placement (insert / insert-conform) then Maintenance (re-capture / cancel RT). The three // then Re-capture (Maintenance, set between the two capture verbs and the placement verbs) then
// RARE capture variants (Batch Items / Batch Razor / Capture RT) are re-homed OFF the visible // Placement (insert / insert-conform). The FOUR RARE variants (Batch Items / Batch Razor /
// bar into the far-right "⋯" overflow menu (overflowMenuRows) — same registered actions, same // Capture RT / Cancel RT) are ALL in the far-right "⋯" overflow menu (overflowMenuRows) —
// command-id contract, just a different home. Capture scopes come from captureActionTable() // same registered actions, same command-id contract, just a different home. Capture scopes come
// (render_settings, pure); the rest are the registered M11/M10/M8 commands. Built once per // from captureActionTable() (render_settings, pure); the rest are the registered M11/M10/M8
// draw/click. Each row carries its full (prefix-stripped) action name for the hover tooltip. // commands. Built once per draw/click. Each row carries its full (prefix-stripped) action name
// for the hover tooltip.
std::vector<ActionBarRow> topBarRows() { std::vector<ActionBarRow> topBarRows() {
std::vector<ActionBarRow> rows; std::vector<ActionBarRow> rows;
// Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track"; // Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track";
@@ -811,26 +811,26 @@ std::vector<ActionBarRow> topBarRows() {
rows.push_back({def.commandSuffix, label, def.descriptionPhrase, rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
ActionCluster::Capture, true}); 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). // Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
rows.push_back({"INSERT_SELECTED", "Insert", rows.push_back({"INSERT_SELECTED", "Insert",
"insert selected sample at edit cursor", ActionCluster::Placement, true}); "insert selected sample at edit cursor", ActionCluster::Placement, true});
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
"insert selected sample at edit cursor (conform to tempo)", "insert selected sample at edit cursor (conform to tempo)",
ActionCluster::Placement, true}); 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; return rows;
} }
// The TOP-toolbar OVERFLOW menu inventory (L5 refinement 1): the three rare capture variants, // The TOP-toolbar OVERFLOW menu inventory (L6): four items pulled off the visible bar into the
// pulled off the visible bar into the far-right "⋯" menu button's popup. Each fires the SAME // far-right "⋯" menu button's popup — the three rare batch/realtime capture variants plus
// existing registered command id via the SAME NamedCommandLookup/Main_OnCommand contract — no // Cancel RT (both realtime verbs share the menu home). Each fires the SAME existing registered
// action changes. The fullName is the popup entry text (the terse shortLabel is unused for menu // command id via the SAME NamedCommandLookup/Main_OnCommand contract — no action changes. The
// items; the popup has room for the full name). Order matches the L4 capture-cluster order. // 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<ActionBarRow> overflowMenuRows() { std::vector<ActionBarRow> overflowMenuRows() {
return { return {
{"CAPTURE_BATCH_ITEMS", "Batch Items", {"CAPTURE_BATCH_ITEMS", "Batch Items",
@@ -839,6 +839,8 @@ std::vector<ActionBarRow> overflowMenuRows() {
"batch capture razor areas (one per area)", ActionCluster::Capture, true}, "batch capture razor areas (one per area)", ActionCluster::Capture, true},
{"CAPTURE_TRACK_REALTIME", "Capture RT", {"CAPTURE_TRACK_REALTIME", "Capture RT",
"capture selected track (realtime)", ActionCluster::Capture, true}, "capture selected track (realtime)", ActionCluster::Capture, true},
{"CANCEL_REALTIME_CAPTURE", "Cancel RT",
"cancel realtime capture", ActionCluster::Maintenance, true},
}; };
} }
@@ -886,8 +888,9 @@ std::vector<ActionBarRow> bottomBarRows() {
// The cluster button-count specs for a given row set, in the row list's cluster order (so the // 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; // 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 // 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; // order follows each toolbar's fixed layout order (top: Capture, Maintenance, Placement —
// bottom: Tagging, Switching). // 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<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows) { std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows) {
int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0; int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0;
for (const ActionBarRow& r : rows) { for (const ActionBarRow& r : rows) {
@@ -901,8 +904,8 @@ std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows
} }
return { return {
{ActionCluster::Capture, nCap}, {ActionCluster::Capture, nCap},
{ActionCluster::Placement, nPlace},
{ActionCluster::Maintenance, nMaint}, {ActionCluster::Maintenance, nMaint},
{ActionCluster::Placement, nPlace},
{ActionCluster::Tagging, nTag}, {ActionCluster::Tagging, nTag},
{ActionCluster::Switching, nSwitch}, {ActionCluster::Switching, nSwitch},
}; };
@@ -910,11 +913,10 @@ std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows
// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared // 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: // 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 // clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups. L6:
// gap between clusters is visibly larger than the gap between buttons within a cluster. // bindingHeight / minSplitHeight removed — buttons are single-row label-only faces now.
const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24, const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24,
/*sidePad=*/8, /*verticalInset=*/3, /*bindingHeight=*/11, /*sidePad=*/8, /*verticalInset=*/3};
/*minSplitHeight=*/30};
// The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split // 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. // 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 // 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 // as a kit drawButton (rest/hover/disabled) with the action short label on the single-row face.
// binding (or "unbound") on the Micro sub-row. Overflow drops WHOLE trailing buttons (the pure // Overflow drops WHOLE trailing buttons (the pure layout returns only the buttons that fit), so
// layout returns only the buttons that fit), so nothing is drawn clipped. `hoverKind` selects // nothing is drawn clipped. `hoverKind` selects which HoverKind this bar's buttons use
// which HoverKind this bar's buttons use (TopBarButton / BottomBarButton) so the two toolbars' // (TopBarButton / BottomBarButton) so the two toolbars' hover states never cross. `topDivider`
// hover states never cross. `topDivider` draws a hairline at the band's top edge (the bottom // draws a hairline at the band's top edge (the bottom toolbar's elevation over the split body);
// toolbar's elevation over the split body); the top toolbar draws it at its bottom edge instead. // 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, void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
const std::vector<ActionBarRow>& rows, HoverKind hoverKind, bool topDivider) { const std::vector<ActionBarRow>& rows, HoverKind hoverKind, bool topDivider) {
if (bar.height <= 0 || bar.width <= 0) return; if (bar.height <= 0 || bar.width <= 0) return;
@@ -983,8 +986,8 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
state = InteractionState::Hover; state = InteractionState::Hover;
// The button surface (drawButton draws the micro-gradient + rounded border + honors // 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 // the state). The label is drawn separately so the text role tracks the state correctly;
// Micro font, so pass no label to drawButton. // pass no label to drawButton.
const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}}; const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}};
drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); 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; (state == InteractionState::Disabled) ? Role::TextDim : Role::TextPrimary;
const KitBox labelBox{s.labelX, s.labelY, s.labelW, s.labelH}; const KitBox labelBox{s.labelX, s.labelY, s.labelW, s.labelH};
kitText(bmp, labelBox, row.shortLabel.c_str(), Font::Label, textRole, Align::Center); 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)" / " <bind>"
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 // 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). // 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 // L6: the keybinding sub-row was removed from the button face, so the tooltip now carries
// keybinding sub-row in drawToolbar reads the live binding via kbd_getTextFromCmd. Do not // both the name AND the binding (when bound) — e.g. "capture selected item — F5". When the
// unify them — each serves a different purpose and has a different lifetime. // action is unbound the tooltip shows only the name (no "(unbound)" noise in the tooltip).
textOut = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName, const std::string phrase = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName,
actionDisplayPrefix()); actionDisplayPrefix());
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(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; ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
return true; return true;
} }
+76 -84
View File
@@ -1,15 +1,14 @@
// Standalone tests for reasampler::action_bar — no REAPER, no test framework. Same fast loop // 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 // 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 // task-grouped action-bar layout, overflow-on-narrow, and hit-testing directly.
// 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 // * Layout: correct rects for each action button across representative panel widths; buttons
// pack at a fixed width with intra-cluster + inter-cluster gaps. // pack at a fixed width with intra-cluster + inter-cluster gaps.
// * Overflow/hiding when the bar is too narrow (whole trailing buttons dropped, never // * Overflow/hiding when the bar is too narrow (whole trailing buttons dropped, never
// clipped; earlier frequent clusters survive; mirrors action_buttons suppression). // clipped; earlier frequent clusters survive; mirrors action_buttons suppression).
// * Keybinding sub-label sub-rects correct (label row + micro binding row split; too-short // * Label sub-rect correct — spans full button height (L6: keybinding sub-row removed from
// button collapses to label-only with an empty binding rect). // the face; binding is in the hover tooltip instead).
// * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs // * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs
// across clusters; inter-cluster gaps are wider than intra-cluster gaps. // 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; // * 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)) { \ #define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) 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() { static std::vector<ClusterSpec> inventory() {
return { return {
{ActionCluster::Capture, 4}, {ActionCluster::Capture, 2},
{ActionCluster::Maintenance, 1},
{ActionCluster::Placement, 2}, {ActionCluster::Placement, 2},
{ActionCluster::Maintenance, 2},
}; };
} }
@@ -45,28 +46,32 @@ static ActionBarSpec roundSpec() {
s.clusterGap = 16; s.clusterGap = 16;
s.sidePad = 8; s.sidePad = 8;
s.verticalInset = 3; s.verticalInset = 3;
s.bindingHeight = 11;
s.minSplitHeight = 30;
return s; return s;
} }
// --- Layout: all fit, correct rects + gaps ------------------------------------ // --- Layout: all fit, correct rects + gaps ------------------------------------
// A wide bar fits all 8 buttons. Verify the first few rects, the intra-cluster gap, and the // A wide bar fits all 5 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). // (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() { static void testAllFitRectsAndGaps() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
// Usable width: sidePad(8) + 8*100 + 6 intra gaps*4 + 2 cluster gaps*16 + sidePad(8) ActionBarRect bar{0, 40, 600, 34};
// = 8 + 800 + 24 + 32 + 8 = 872. Give it 900.
ActionBarRect bar{0, 40, 900, 34};
BarFit fit = computeBarFit(bar, clusters, spec); BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 8); CHECK(fit.visibleCount == 5);
CHECK(fit.hiddenCount == 0); CHECK(fit.hiddenCount == 0);
auto slots = computeBarSlots(bar, clusters, spec); 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. // Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
CHECK(slots[0].x == 8); CHECK(slots[0].x == 8);
@@ -80,89 +85,79 @@ static void testAllFitRectsAndGaps() {
CHECK(slots[1].x == 112); CHECK(slots[1].x == 112);
CHECK(slots[1].cluster == ActionCluster::Capture); CHECK(slots[1].cluster == ActionCluster::Capture);
// Button 3 is the last capture button. Its right edge: // Button 2 (maintenance / Re-capture): cluster gap of 16 after 212 -> 228.
// b0 8..108, +4 -> b1 112..212, +4 -> b2 216..316, +4 -> b3 320..420. CHECK(slots[2].x == 228);
CHECK(slots[3].x == 320); CHECK(slots[2].cluster == ActionCluster::Maintenance);
CHECK(slots[3].cluster == ActionCluster::Capture); CHECK(slots[2].index == 2);
// Button 4 (first placement): cluster gap of 16 after 420 -> 436. // Button 3 (first placement): cluster gap of 16 after 328 -> 344.
CHECK(slots[4].x == 436); 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].cluster == ActionCluster::Placement);
CHECK(slots[4].index == 4); CHECK(slots[4].index == 4);
// Inter-cluster gap (436 - 420 = 16) is wider than the intra-cluster gap (4) — the task // Inter-cluster gap (slot[2].x - slot[1].right = 228 - 212 = 16) is wider than the
// grouping is structurally visible in the geometry. // intra-cluster gap (slot[1].x - slot[0].right = 112 - 108 = 4) — task grouping is
const int interGap = slots[4].x - (slots[3].x + slots[3].width); // 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); const int intraGap = slots[1].x - (slots[0].x + slots[0].width);
CHECK(interGap == 16); CHECK(interGap == 16);
CHECK(intraGap == 4); CHECK(intraGap == 4);
CHECK(interGap > intraGap); 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 // L6: the label rect spans the full button height — no binding sub-row split. The label is
// abut, cover the button height, and sit inside the horizontal text inset. // inset horizontally by hpad(4) on each side; horizontally it shares the same inner band.
static void testSubRectsSplit() { static void testLabelFullHeight() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 0, 900, 34}; // btnH = 34 - 6 = 28 >= minSplitHeight? 28 < 30 ActionBarRect bar{0, 0, 600, 34};
// 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); auto slots = computeBarSlots(bar, clusters, spec);
CHECK(!slots.empty()); CHECK(!slots.empty());
const ActionBarSlot& s = slots[0]; const ActionBarSlot& s = slots[0];
CHECK(!s.bindingEmpty()); // Label spans the full button height.
// Binding row is the bottom bindingHeight(11); label is the remainder (34 - 11 = 23). CHECK(s.labelH == s.height);
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.labelY == s.y);
CHECK(s.bindY == s.labelY + s.labelH); // Inset horizontally.
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.x);
CHECK(s.labelX == s.bindX);
CHECK(s.labelW == s.bindW);
CHECK(s.labelX + s.labelW < s.x + s.width); CHECK(s.labelX + s.labelW < s.x + s.width);
} }
// A short button (height below minSplitHeight) is NOT split: the label fills the interior and // Short buttons also get the full-height label (no min-height split threshold anymore).
// the binding sub-rect is empty (the shell draws only the label — graceful, no clipped micro). static void testLabelFullHeightWhenShort() {
static void testSubRectsNoSplitWhenShort() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); 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); auto slots = computeBarSlots(bar, clusters, spec);
CHECK(!slots.empty()); CHECK(!slots.empty());
const ActionBarSlot& s = slots[0]; const ActionBarSlot& s = slots[0];
CHECK(s.bindingEmpty()); CHECK(s.labelH == s.height);
CHECK(s.labelH == s.height); // label fills the whole interior height
CHECK(s.labelY == s.y); CHECK(s.labelY == s.y);
} }
// --- Overflow / hiding on a narrow panel -------------------------------------- // --- Overflow / hiding on a narrow panel --------------------------------------
// A bar wide enough for only the 4 capture buttons + a couple placement drops the rest WHOLE. // A bar wide enough for only the 2 capture buttons drops the rest WHOLE. The visible buttons
// The visible buttons keep their full width (never clipped), and the frequent capture cluster // keep their full width (never clipped), and the frequent capture cluster survives.
// survives (overflow drops from the END). //
// 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() { static void testOverflowDropsTrailingWhole() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
// Room for exactly the 4 capture buttons: sidePad(8) + 4*100 + 3*4 = 420, +sidePad(8) = 428. ActionBarRect bar{0, 0, 220, 34};
// 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); BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 4); CHECK(fit.visibleCount == 2);
CHECK(fit.hiddenCount == 4); CHECK(fit.hiddenCount == 3);
auto slots = computeBarSlots(bar, clusters, spec); auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 4); CHECK(slots.size() == 2);
for (const auto& s : slots) { for (const auto& s : slots) {
CHECK(s.width == spec.buttonWidth); // never clipped below full width CHECK(s.width == spec.buttonWidth); // never clipped below full width
CHECK(s.cluster == ActionCluster::Capture);// the surviving cluster is the frequent one 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 ActionBarRect bar{0, 0, 60, 34}; // sidePad*2 + one 100-wide button won't fit
BarFit fit = computeBarFit(bar, clusters, spec); BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 0); CHECK(fit.visibleCount == 0);
CHECK(fit.hiddenCount == 8); CHECK(fit.hiddenCount == 5);
CHECK(computeBarSlots(bar, clusters, spec).empty()); CHECK(computeBarSlots(bar, clusters, spec).empty());
} }
@@ -189,10 +184,10 @@ static void testDegenerate() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
CHECK(computeBarSlots(ActionBarRect{0, 0, 0, 34}, clusters, spec).empty()); 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, 600, 0}, clusters, spec).empty());
CHECK(computeBarSlots(ActionBarRect{0, 0, 900, 34}, {}, spec).empty()); CHECK(computeBarSlots(ActionBarRect{0, 0, 600, 34}, {}, spec).empty());
ActionBarSpec badW = spec; badW.buttonWidth = 0; 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. // Empty clusters in the list contribute no buttons and no gaps.
std::vector<ClusterSpec> withEmpty = { std::vector<ClusterSpec> withEmpty = {
@@ -200,7 +195,7 @@ static void testDegenerate() {
{ActionCluster::Placement, 0}, // empty — skipped {ActionCluster::Placement, 0}, // empty — skipped
{ActionCluster::Maintenance, 1}, {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.size() == 3);
CHECK(slots[0].cluster == ActionCluster::Capture); CHECK(slots[0].cluster == ActionCluster::Capture);
CHECK(slots[1].cluster == ActionCluster::Capture); CHECK(slots[1].cluster == ActionCluster::Capture);
@@ -215,7 +210,7 @@ static void testDegenerate() {
static void testHitTestHitsButtons() { static void testHitTestHitsButtons() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 900, 34}; ActionBarRect bar{0, 40, 600, 34};
auto slots = computeBarSlots(bar, clusters, spec); auto slots = computeBarSlots(bar, clusters, spec);
// A point in the middle of each button returns that button's flat index. // A point in the middle of each button returns that button's flat index.
for (const auto& s : slots) { for (const auto& s : slots) {
@@ -230,12 +225,12 @@ static void testHitTestHitsButtons() {
static void testHitTestGapsAreMisses() { static void testHitTestGapsAreMisses() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 900, 34}; ActionBarRect bar{0, 40, 600, 34};
auto slots = computeBarSlots(bar, clusters, spec); auto slots = computeBarSlots(bar, clusters, spec);
// Intra-cluster gap between button 0 (right edge 108) and button 1 (left 112): x in [108,112). // 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); 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). // Inter-cluster gap between button 1 (right 212) and button 2 (left 228): x in [212,228).
CHECK(hitTestActionBar(428, 50, bar, clusters, spec) == -1); CHECK(hitTestActionBar(220, 50, bar, clusters, spec) == -1);
} }
static void testHitTestMissesOutsideBand() { static void testHitTestMissesOutsideBand() {
@@ -252,17 +247,17 @@ static void testHitTestMissesOutsideBand() {
static void testHitTestOverflowDeadZone() { static void testHitTestOverflowDeadZone() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 0, 430, 34}; // only 4 capture buttons visible ActionBarRect bar{0, 0, 220, 34}; // only 2 capture buttons visible
// x well past the 4th button's right edge but still inside the bar band. // x well past the 2nd button's right edge but still inside the bar band.
CHECK(hitTestActionBar(425, 10, bar, clusters, spec) == -1); CHECK(hitTestActionBar(215, 10, bar, clusters, spec) == -1);
} }
static void testHitTestDegenerate() { static void testHitTestDegenerate() {
const auto clusters = inventory(); const auto clusters = inventory();
const ActionBarSpec spec = roundSpec(); const ActionBarSpec spec = roundSpec();
CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 0, 34}, clusters, spec) == -1); 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, 600, 0}, clusters, spec) == -1);
CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 900, 34}, {}, spec) == -1); CHECK(hitTestActionBar(5, 5, ActionBarRect{0, 0, 600, 34}, {}, spec) == -1);
} }
// --- Resize sweep: no overlap, no cut-off, hit-test matches layout ------------ // --- 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). // No overlap with the previous slot (strictly increasing, non-overlapping).
CHECK(s.x > prevRight); CHECK(s.x > prevRight);
prevRight = s.x + s.width - 1; 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); CHECK(s.labelX >= s.x && s.labelX + s.labelW <= s.x + s.width);
if (!s.bindingEmpty()) { CHECK(s.labelY >= s.y && s.labelY + s.labelH <= s.y + s.height);
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. // 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) { for (int px = bar.x; px < bar.x + bar.width; px += 3) {
@@ -308,8 +300,8 @@ static void testResizeSweepNoOverlapNoCutoff() {
int main() { int main() {
testAllFitRectsAndGaps(); testAllFitRectsAndGaps();
testSubRectsSplit(); testLabelFullHeight();
testSubRectsNoSplitWhenShort(); testLabelFullHeightWhenShort();
testOverflowDropsTrailingWhole(); testOverflowDropsTrailingWhole();
testTooNarrowForAny(); testTooNarrowForAny();
testDegenerate(); testDegenerate();