Merge pL-w2-dock-panel-layout: L2 dock-panel layout redesign (task-grouped action bar + kit-drawn panel)
This commit is contained in:
+21
-1
@@ -355,6 +355,21 @@ target_include_directories(theme PUBLIC src)
|
||||
add_library(component_geometry STATIC src/component_geometry.cpp)
|
||||
target_include_directories(component_geometry PUBLIC src)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2o) Pure action_bar library — NO REAPER, NO SWELL, NO LICE. The Phase L (L2)
|
||||
# dock-panel layout redesign core: the TASK-GROUPED action bar geometry that
|
||||
# supersedes the flat M11 action_buttons strip for the panel's action inventory —
|
||||
# clusters (capture / placement / maintenance) tile the bar at a fixed button
|
||||
# width with intra/inter-cluster gaps, each button carrying a label + keybinding
|
||||
# micro sub-rect, whole trailing buttons dropped (never clipped) on a narrow panel,
|
||||
# and point -> flat action index hit-test. Split out so the layout + hit-test math
|
||||
# is unit-tested outside the DAW; the bank_panel L1-kit draw + NamedCommandLookup/
|
||||
# Main_OnCommand dispatch + kbd_getTextFromCmd query are DAW-verified. Mirror of
|
||||
# mode_switch / action_buttons / prune_button.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_library(action_bar STATIC src/action_bar.cpp)
|
||||
target_include_directories(action_bar PUBLIC src)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -465,6 +480,10 @@ add_executable(component_geometry_tests tests/test_component_geometry.cpp)
|
||||
target_link_libraries(component_geometry_tests PRIVATE component_geometry)
|
||||
add_test(NAME component_geometry_tests COMMAND component_geometry_tests)
|
||||
|
||||
add_executable(action_bar_tests tests/test_action_bar.cpp)
|
||||
target_link_libraries(action_bar_tests PRIVATE action_bar)
|
||||
add_test(NAME action_bar_tests COMMAND action_bar_tests)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -508,8 +527,9 @@ add_library(reaper_reasampler MODULE
|
||||
src/bank_book.cpp
|
||||
src/owned_manifest.cpp
|
||||
src/drag_out_win.cpp
|
||||
src/action_bar.cpp
|
||||
)
|
||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance action_buttons drag_out theme component_geometry)
|
||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance action_buttons drag_out theme component_geometry action_bar)
|
||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
||||
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+438
-286
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
// 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.
|
||||
//
|
||||
// Covers (L2 brief §test cases):
|
||||
// * 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).
|
||||
// * 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;
|
||||
// degenerate/too-narrow bar handled without crash or overlap.
|
||||
// * Resize: no inventory item cut off or overlapping across a representative width range.
|
||||
|
||||
#include "../src/action_bar.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
|
||||
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.
|
||||
static std::vector<ClusterSpec> inventory() {
|
||||
return {
|
||||
{ActionCluster::Capture, 4},
|
||||
{ActionCluster::Placement, 2},
|
||||
{ActionCluster::Maintenance, 2},
|
||||
};
|
||||
}
|
||||
|
||||
// A spec with round numbers so expected pixels are hand-checkable.
|
||||
static ActionBarSpec roundSpec() {
|
||||
ActionBarSpec s;
|
||||
s.buttonWidth = 100;
|
||||
s.buttonGap = 4;
|
||||
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).
|
||||
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};
|
||||
|
||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||
CHECK(fit.visibleCount == 8);
|
||||
CHECK(fit.hiddenCount == 0);
|
||||
|
||||
auto slots = computeBarSlots(bar, clusters, spec);
|
||||
CHECK(slots.size() == 8);
|
||||
|
||||
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
|
||||
CHECK(slots[0].x == 8);
|
||||
CHECK(slots[0].y == 43);
|
||||
CHECK(slots[0].width == 100);
|
||||
CHECK(slots[0].height == 28);
|
||||
CHECK(slots[0].cluster == ActionCluster::Capture);
|
||||
CHECK(slots[0].index == 0);
|
||||
|
||||
// Button 1: intra-cluster gap of 4 after button 0's right edge (8+100=108) -> 112.
|
||||
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 4 (first placement): cluster gap of 16 after 420 -> 436.
|
||||
CHECK(slots[4].x == 436);
|
||||
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);
|
||||
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 --------------------------------------------
|
||||
|
||||
// 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() {
|
||||
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
|
||||
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.
|
||||
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.
|
||||
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() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
ActionBarRect bar{0, 0, 900, 24}; // btnH = 24 - 6 = 18 < minSplitHeight(30)
|
||||
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.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).
|
||||
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};
|
||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||
CHECK(fit.visibleCount == 4);
|
||||
CHECK(fit.hiddenCount == 4);
|
||||
|
||||
auto slots = computeBarSlots(bar, clusters, spec);
|
||||
CHECK(slots.size() == 4);
|
||||
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
|
||||
}
|
||||
// The last visible button's right edge stays within the usable bound.
|
||||
CHECK(slots.back().x + slots.back().width <= bar.x + bar.width - spec.sidePad);
|
||||
}
|
||||
|
||||
// A bar too narrow for even one button lays out nothing (all hidden) — no sub-minimum clipped
|
||||
// button; the shell draws an empty bar.
|
||||
static void testTooNarrowForAny() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
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(computeBarSlots(bar, clusters, spec).empty());
|
||||
}
|
||||
|
||||
// --- Degenerate --------------------------------------------------------------
|
||||
|
||||
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());
|
||||
ActionBarSpec badW = spec; badW.buttonWidth = 0;
|
||||
CHECK(computeBarSlots(ActionBarRect{0, 0, 900, 34}, clusters, badW).empty());
|
||||
|
||||
// Empty clusters in the list contribute no buttons and no gaps.
|
||||
std::vector<ClusterSpec> withEmpty = {
|
||||
{ActionCluster::Capture, 2},
|
||||
{ActionCluster::Placement, 0}, // empty — skipped
|
||||
{ActionCluster::Maintenance, 1},
|
||||
};
|
||||
auto slots = computeBarSlots(ActionBarRect{0, 0, 900, 34}, withEmpty, spec);
|
||||
CHECK(slots.size() == 3);
|
||||
CHECK(slots[0].cluster == ActionCluster::Capture);
|
||||
CHECK(slots[1].cluster == ActionCluster::Capture);
|
||||
CHECK(slots[2].cluster == ActionCluster::Maintenance);
|
||||
// The cluster gap sits between the Capture and Maintenance buttons (Placement emitted none).
|
||||
const int gap = slots[2].x - (slots[1].x + slots[1].width);
|
||||
CHECK(gap == spec.clusterGap);
|
||||
}
|
||||
|
||||
// --- Hit-test: hits, gaps, and misses -----------------------------------------
|
||||
|
||||
static void testHitTestHitsButtons() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
ActionBarRect bar{0, 40, 900, 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) {
|
||||
const int cx = s.x + s.width / 2;
|
||||
const int cy = s.y + s.height / 2;
|
||||
CHECK(hitTestActionBar(cx, cy, bar, clusters, spec) == s.index);
|
||||
}
|
||||
}
|
||||
|
||||
// A point in an intra-cluster gap and a point in an inter-cluster gap are both clean misses
|
||||
// (real gaps, unlike an equal-tiled strip — no nearest-button snapping).
|
||||
static void testHitTestGapsAreMisses() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
ActionBarRect bar{0, 40, 900, 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);
|
||||
}
|
||||
|
||||
static void testHitTestMissesOutsideBand() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
ActionBarRect bar{10, 40, 400, 34};
|
||||
CHECK(hitTestActionBar(9, 50, bar, clusters, spec) == -1); // left of bar
|
||||
CHECK(hitTestActionBar(410, 50, bar, clusters, spec) == -1); // right edge (excluded)
|
||||
CHECK(hitTestActionBar(50, 39, bar, clusters, spec) == -1); // above the band
|
||||
CHECK(hitTestActionBar(50, 74, bar, clusters, spec) == -1); // below the band
|
||||
}
|
||||
|
||||
// On a narrow bar the point past the last visible button (in the overflow dead-zone) misses.
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// --- Resize sweep: no overlap, no cut-off, hit-test matches layout ------------
|
||||
|
||||
// Across a representative width range: every visible slot is fully inside the bar's usable
|
||||
// area, no two slots overlap, and every point that hit-tests to a button lands inside that
|
||||
// button's drawn rect (hit-test and layout agree — the load-bearing consistency invariant).
|
||||
static void testResizeSweepNoOverlapNoCutoff() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
for (int w = 60; w <= 1000; w += 7) {
|
||||
ActionBarRect bar{0, 0, w, 34};
|
||||
auto slots = computeBarSlots(bar, clusters, spec);
|
||||
int prevRight = bar.x + spec.sidePad - 1;
|
||||
for (const auto& s : slots) {
|
||||
// Inside the bar band.
|
||||
CHECK(s.x >= bar.x);
|
||||
CHECK(s.x + s.width <= bar.x + bar.width - spec.sidePad);
|
||||
CHECK(s.y >= bar.y);
|
||||
CHECK(s.y + s.height <= bar.y + bar.height);
|
||||
// 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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
const int hit = hitTestActionBar(px, bar.y + bar.height / 2, bar, clusters, spec);
|
||||
if (hit >= 0) {
|
||||
bool found = false;
|
||||
for (const auto& s : slots)
|
||||
if (s.index == hit && px >= s.x && px < s.x + s.width) found = true;
|
||||
CHECK(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testAllFitRectsAndGaps();
|
||||
testSubRectsSplit();
|
||||
testSubRectsNoSplitWhenShort();
|
||||
testOverflowDropsTrailingWhole();
|
||||
testTooNarrowForAny();
|
||||
testDegenerate();
|
||||
testHitTestHitsButtons();
|
||||
testHitTestGapsAreMisses();
|
||||
testHitTestMissesOutsideBand();
|
||||
testHitTestOverflowDeadZone();
|
||||
testHitTestDegenerate();
|
||||
testResizeSweepNoOverlapNoCutoff();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user