// action_bar — pure implementation. See action_bar.h. NO REAPER / SWELL / LICE / vendor. #include "action_bar.h" #include namespace reasampler { namespace { // The total button count across all clusters (empty clusters contribute nothing). int totalButtons(const std::vector& clusters) { int n = 0; for (const ClusterSpec& c : clusters) if (c.count > 0) n += c.count; return n; } // Fills a slot's label rect from its box. The label spans the full button height — a single-row // short label (L6: keybinding sub-row removed from the face; binding is in the hover tooltip). // Insets horizontally so text clears the button edge. 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 label rect empty s.labelX = innerX; s.labelY = s.y; s.labelW = innerW; s.labelH = s.height; } // 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 tile(const ActionBarRect& bar, const std::vector& clusters, const ActionBarSpec& spec, int visible) { std::vector slots; if (visible <= 0) return slots; slots.reserve(static_cast(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& 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& 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 computeBarSlots(const ActionBarRect& bar, const std::vector& 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& 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 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