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:
@@ -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
|
||||
Reference in New Issue
Block a user