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.
This commit is contained in:
2026-07-26 20:52:37 -04:00
parent a20cb46d65
commit 54f37be0bb
7 changed files with 1128 additions and 292 deletions
+167
View File
@@ -0,0 +1,167 @@
// action_bar — pure implementation. See action_bar.h. NO REAPER / SWELL / LICE / vendor.
#include "action_bar.h"
#include <cstddef>
namespace reasampler {
namespace {
// The total button count across all clusters (empty clusters contribute nothing).
int totalButtons(const std::vector<ClusterSpec>& clusters) {
int n = 0;
for (const ClusterSpec& c : clusters)
if (c.count > 0) n += c.count;
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) {
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;
}
}
// Tiles the first `visible` buttons into slots, cluster by cluster, left to right. This is the
// ONE placement routine; both computeBarSlots and hitTestActionBar drive it so draw and
// hit-test can never drift. `visible` is assumed already clamped to [0, total]. Returns the
// slots in ascending flat-index order.
std::vector<ActionBarSlot> tile(const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec, int visible) {
std::vector<ActionBarSlot> slots;
if (visible <= 0) return slots;
slots.reserve(static_cast<std::size_t>(visible));
const int top = bar.y + spec.verticalInset;
const int btnH = bar.height - 2 * spec.verticalInset;
if (btnH <= 0) return slots;
int cursorX = bar.x + spec.sidePad;
int flatIndex = 0; // running flat action index across all clusters
int placed = 0; // buttons placed so far (stops at `visible`)
bool firstClusterEmitted = false;
for (const ClusterSpec& c : clusters) {
if (c.count <= 0) continue; // skip empty clusters (no gap emitted)
if (placed >= visible) break;
// Gap BEFORE this cluster (except the first non-empty one).
if (firstClusterEmitted) cursorX += spec.clusterGap;
firstClusterEmitted = true;
for (int i = 0; i < c.count; ++i, ++flatIndex) {
if (placed >= visible) return slots; // overflow cut — stop cleanly
if (i > 0) cursorX += spec.buttonGap; // gap between buttons in the cluster
ActionBarSlot s;
s.index = flatIndex;
s.cluster = c.cluster;
s.x = cursorX;
s.y = top;
s.width = spec.buttonWidth;
s.height = btnH;
fillTextRects(s, spec);
slots.push_back(s);
cursorX += spec.buttonWidth;
++placed;
}
}
return slots;
}
// The rightmost pixel the first `visible` buttons would occupy (bar.x + sidePad based). Used by
// computeBarFit to test whether a candidate visible-count fits within the bar's usable width.
// Mirrors tile()'s advance math exactly (gaps included) so fit and layout agree.
int rightEdgeFor(const ActionBarRect& bar, const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec, int visible) {
if (visible <= 0) return bar.x + spec.sidePad;
int cursorX = bar.x + spec.sidePad;
int placed = 0;
bool firstClusterEmitted = false;
for (const ClusterSpec& c : clusters) {
if (c.count <= 0) continue;
if (placed >= visible) break;
if (firstClusterEmitted) cursorX += spec.clusterGap;
firstClusterEmitted = true;
for (int i = 0; i < c.count; ++i) {
if (placed >= visible) return cursorX;
if (i > 0) cursorX += spec.buttonGap;
cursorX += spec.buttonWidth; // this button's right edge
++placed;
if (placed >= visible) return cursorX;
}
}
return cursorX;
}
} // namespace
BarFit computeBarFit(const ActionBarRect& bar, const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec) {
BarFit fit;
const int total = totalButtons(clusters);
if (total <= 0 || bar.width <= 0 || bar.height <= 0 || spec.buttonWidth <= 0) {
fit.hiddenCount = total > 0 ? total : 0;
return fit;
}
const int usableRight = bar.x + bar.width - spec.sidePad;
// Largest prefix of buttons whose right edge stays within the usable right bound. Buttons
// never shrink; trailing ones that do not fit are the overflow (dropped whole).
int visible = 0;
for (int cand = 1; cand <= total; ++cand) {
if (rightEdgeFor(bar, clusters, spec, cand) <= usableRight)
visible = cand;
else
break;
}
fit.visibleCount = visible;
fit.hiddenCount = total - visible;
return fit;
}
std::vector<ActionBarSlot> computeBarSlots(const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters,
const ActionBarSpec& spec) {
if (bar.width <= 0 || bar.height <= 0 || spec.buttonWidth <= 0) return {};
const BarFit fit = computeBarFit(bar, clusters, spec);
return tile(bar, clusters, spec, fit.visibleCount);
}
int hitTestActionBar(int px, int py, const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters, const ActionBarSpec& spec) {
if (bar.height <= 0 || bar.width <= 0) return -1;
// Reject outside the bar band first (half-open bounds match the slots).
if (px < bar.x || px >= bar.x + bar.width ||
py < bar.y || py >= bar.y + bar.height)
return -1;
const std::vector<ActionBarSlot> slots = computeBarSlots(bar, clusters, spec);
for (const ActionBarSlot& s : slots) {
if (px >= s.x && px < s.x + s.width && py >= s.y && py < s.y + s.height)
return s.index;
}
return -1; // inter-button/cluster gap or the overflow dead-zone — a clean miss
}
} // namespace reasampler
+155
View File
@@ -0,0 +1,155 @@
#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
+440 -286
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -23,16 +23,17 @@
namespace reasampler {
namespace {
// --- KitColor <-> LICE boundary ----------------------------------------------
// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a)
// (lice.h:57). The theme owns the color; the shell owns the packing.
// (lice.h:57). The theme owns the color; the shell owns the packing. Declared in draw_kit.h
// so shell translation units (bank_panel) can use it without duplicating the LICE_RGBA pack.
LICE_pixel toLice(const KitColor& c) {
return LICE_RGBA(c.r, c.g, c.b, c.a);
}
namespace {
// The draw alpha the LICE primitives take (0..1), from the KitColor's 8-bit alpha. Used so
// a disabled surface (alpha 0.4) composites at the right opacity — LICE_FillRect etc. take
// a float alpha argument separate from the pixel's own alpha byte.
+16 -2
View File
@@ -27,8 +27,15 @@
#include "peaks.h" // Envelope — the waveform primitive's input
#include "theme.h" // Role / InteractionState / KitColor / TextClass
// LICE + SWELL types at the boundary (this is the shell half). Forward-declared where
// possible to keep the header light; the .cpp includes the full LICE/SWELL headers.
// LICE types at the boundary (this is the shell half). LICE_IBitmap is forward-declared
// to keep the header light. LICE_pixel is a typedef (unsigned int) — not forward-declarable
// — so the full lice.h is included only for the toLice() declaration; on Windows lice.h
// pulls in <windows.h>, which is fine since draw_kit.h is shell-only and never included by
// a pure module.
#ifdef _WIN32
#include <windows.h>
#endif
#include "lice/lice.h"
class LICE_IBitmap;
namespace reasampler {
@@ -46,6 +53,13 @@ enum class Font {
// single-line convention); a caller wanting multi-line composes rows itself.
enum class Align { Left, Center, Right };
// --- KitColor → LICE_pixel conversion ----------------------------------------
// The one place a pure KitColor becomes a LICE_pixel. Declared here so any shell
// translation unit that already includes draw_kit.h can use it without duplicating
// the LICE_RGBA packing. Defined in draw_kit.cpp.
LICE_pixel toLice(const KitColor& c);
// --- Font lifecycle (owned by the kit) ---------------------------------------
// Creates the four cached fonts once. Idempotent: a second call before shutdown is a no-op