Files
reasampler/src/action_bar.h
T
daniel 54f37be0bb L2: task-grouped action bar + kit-drawn dock panel
New pure action_bar module (clusters, keybinding sub-labels, overflow, hit-test) supersedes the flat M11 strip; bank_panel chrome/buttons/tabs/grid now draw through the L1 kit by role with hover. Expose draw_kit::toLice in header to fix drawThumbnail forward-refs. CTest 27/27.
2026-07-26 21:02:37 -04:00

156 lines
8.7 KiB
C++

#pragma once
// action_bar — the REAPER-free, LICE-free layout + hit-test math behind the bank_panel's
// TASK-GROUPED action bar (Phase L, L2). L2's dock-panel layout redesign (DS-3: a thorough
// layout, not a re-skin) groups the M11 action-trigger button inventory BY TASK — a compact
// bar of clusters (capture / placement / maintenance) 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 capture cluster survives.
//
// 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.
//
// NAME NOTE (brief §name-collision): ButtonRect / ButtonStripRect / ActionButtonRect /
// SegmentRect / CellRect / FooterRect / KitButtonBox are already owned in this namespace, so
// this module's types are ActionBarRect / ActionBarSlot / ActionCluster — grep-checked free
// before minting. They are a distinct concept (a task-grouped multi-cluster bar with text
// sub-rects) from the flat action_buttons strip, so the separate names are correct, not merely
// non-colliding.
//
// SCOPE: the destructive PRUNE button is NOT in this bar — it stays set-apart in the footer,
// warn-marked, owned by prune_button (L2 keeps prune deliberately away from the frequent
// action cluster). This module lays out only the non-destructive capture/placement/maintenance
// actions.
//
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library only.
#include <vector>
namespace reasampler {
// The task cluster a button belongs to (the L2 "group by task" mandate). Capture is the
// primary/frequent gesture (leftmost), then placement, then the rarer maintenance actions.
// The order here IS the left-to-right cluster order in the bar.
enum class ActionCluster {
Capture, // capture item / track / realtime / batch — the primary gesture
Placement, // insert at cursor / insert-conform — placing a bank sample on the timeline
Maintenance, // re-capture from source / cancel realtime — rarer upkeep actions
};
// The bar the clusters are drawn into, top-left origin (SWELL/LICE convention). (x, y) is the
// top-left corner; width/height are the bar extents. The panel reserves this as a fixed-height
// band (its own judgment where — above the tail footer, below the split body).
struct ActionBarRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const ActionBarRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
// One visible button's placement within the bar, top-left origin. `index` is the button's
// 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.
struct ActionBarSlot {
int index = 0;
ActionCluster cluster = ActionCluster::Capture;
int x = 0;
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).
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;
}
};
// One cluster's button count, in the caller's flat action-list order. The caller passes these
// in ActionCluster order (Capture, Placement, Maintenance); a cluster with count 0 is skipped
// (no gap emitted for it). The flat action index a slot carries is the running sum across
// clusters (cluster 0's buttons are indices [0, counts[0]), etc.), so the shell's flat action
// table lines up with the slots by index.
struct ClusterSpec {
ActionCluster cluster = ActionCluster::Capture;
int count = 0;
};
// Layout inputs for the bar, in pixels. Defaults are the bank_panel action-bar metrics; the
// shell passes its own so draw and hit-test share ONE source of truth.
// * buttonWidth — each button's fixed width (buttons never render narrower; overflow drops
// whole trailing buttons instead of shrinking below this).
// * buttonGap — horizontal gap between buttons WITHIN a cluster.
// * clusterGap — horizontal gap between adjacent clusters (wider than buttonGap so the
// 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`.
// Split from slot tiling so the shell can size an overflow affordance / count without
// re-deriving it. Trailing buttons that do not fit are the overflow (dropped whole). A
// non-positive bar width, or a bar too narrow for even one button, yields 0. Clamps to
// [0, total-button-count].
struct BarFit {
int visibleCount = 0; // buttons that fit (laid out), counted from the front
int hiddenCount = 0; // total - visibleCount (the overflow, dropped whole)
};
BarFit computeBarFit(const ActionBarRect& bar, const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec);
// 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.
std::vector<ActionBarSlot> computeBarSlots(const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec);
// The flat action index the point (px, py) (SWELL/LICE top-left client coords) lands on, or -1
// for a miss: outside the bar band, in an inter-button / inter-cluster gap, or past the last
// visible button (the narrow-panel overflow dead-zone — a harmless no-op the shell ignores).
// Half-open bounds [x, x+width) x [y, y+height) match computeBarSlots so no pixel is double-
// claimed and the hit maps to the button drawn there. Unlike an equal-tiled strip, the bar has
// real gaps, so a gap point is a clean miss (not the nearest button).
int hitTestActionBar(int px, int py, const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters, const ActionBarSpec& spec);
} // namespace reasampler