36bdef742a
New pure action_buttons module (strip layout/hit-test + label format, CTest-covered). Panel strip fires capture/insert/provenance actions via NamedCommandLookup + Main_OnCommand; labels from kbd_getTextFromCmd.
126 lines
6.9 KiB
C++
126 lines
6.9 KiB
C++
#pragma once
|
|
// action_buttons — the REAPER-free layout + label-format math behind the bank_panel's
|
|
// action-trigger button strip (Milestone 11). A row of LICE-drawn buttons in the docked
|
|
// panel fires the capture / insert / provenance actions THROUGH the existing command-id
|
|
// contract (the shell resolves each button's command id at runtime via NamedCommandLookup
|
|
// and dispatches with Main_OnCommand — this module never touches REAPER), and each button
|
|
// surfaces the action's current key binding as a reminder label.
|
|
//
|
|
// What is NOT DAW-bound lives here so it is unit-tested outside the DAW (CLAUDE.md
|
|
// §load-bearing split), and it is two mirror-of-mode_switch concerns in one pure module:
|
|
//
|
|
// 1. Button-strip layout + hit-test. Unlike the mode switch (which tiles N EQUAL
|
|
// segments at any width), the button strip must degrade gracefully on a narrow
|
|
// panel: buttons never shrink below a minimum readable width — instead only as many
|
|
// as fit are laid out (equally sharing the strip) and the rest are reported hidden.
|
|
// That is the "overflow handling for narrow panels — no clipped garbage" requirement.
|
|
// 2. Label-text formatting: (action name + the SDK's binding string) -> the label drawn
|
|
// on a button, with the unbound / empty / blank cases degraded to an explicit marker
|
|
// and an over-long binding truncated so it never blows the button's text budget.
|
|
//
|
|
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library only.
|
|
// Builds and unit-tests without REAPER. Mirror of mode_switch / tab_strip.
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace reasampler {
|
|
|
|
// The strip the buttons are drawn into, top-left origin (SWELL/LICE convention).
|
|
// (x, y) is the top-left corner; width/height are the strip extents. The panel reserves
|
|
// this as a fixed-height band (its own judgment where — above the tail footer).
|
|
struct ButtonStripRect {
|
|
int x = 0;
|
|
int y = 0;
|
|
int width = 0;
|
|
int height = 0;
|
|
|
|
bool operator==(const ButtonStripRect& o) const {
|
|
return x == o.x && y == o.y && width == o.width && height == o.height;
|
|
}
|
|
};
|
|
|
|
// One button's pixel rectangle within the strip, top-left origin, plus the index of the
|
|
// action it drives in the caller's list (button order). The shell draws the button here,
|
|
// labels it, and — on a hit at this index — resolves that action's command id and fires.
|
|
// Only VISIBLE buttons get a rect (a button that does not fit the strip is omitted, never
|
|
// returned with a clipped/zero width), so every returned rect is fully drawable.
|
|
struct ActionButtonRect {
|
|
int index = 0;
|
|
int x = 0;
|
|
int y = 0;
|
|
int width = 0;
|
|
int height = 0;
|
|
|
|
bool operator==(const ActionButtonRect& o) const {
|
|
return index == o.index && x == o.x && y == o.y &&
|
|
width == o.width && height == o.height;
|
|
}
|
|
};
|
|
|
|
// How many of `buttonCount` buttons fit `strip` at a minimum button width of
|
|
// `minButtonWidth`. This is the overflow decision, split from the rect tiling so the
|
|
// shell can size an "overflow" affordance / count without re-deriving it. Clamps to
|
|
// [0, buttonCount]; a non-positive strip width or minButtonWidth yields 0. When all fit,
|
|
// visibleCount == buttonCount and hiddenCount == 0.
|
|
struct ButtonFit {
|
|
int visibleCount = 0; // buttons that fit (and will be laid out)
|
|
int hiddenCount = 0; // buttonCount - visibleCount (the overflow)
|
|
};
|
|
|
|
ButtonFit computeButtonFit(const ButtonStripRect& strip, int buttonCount,
|
|
int minButtonWidth);
|
|
|
|
// Tiles the VISIBLE buttons (per computeButtonFit) left-to-right across `strip`, sharing
|
|
// its full width EQUALLY (exact tiling, rounding absorbed at boundaries so buttons abut
|
|
// with no gap/overlap and the last visible button reaches strip.x + strip.width — the
|
|
// same boundary discipline as mode_switch). Buttons never render narrower than
|
|
// minButtonWidth: when not all fit, the visible ones each get width >= minButtonWidth by
|
|
// construction (fewer buttons over the same strip). Returns exactly visibleCount rects in
|
|
// button-index order (indices 0..visibleCount-1). buttonCount <= 0, non-positive strip
|
|
// width, or non-positive minButtonWidth -> empty.
|
|
std::vector<ActionButtonRect> computeButtonRects(const ButtonStripRect& strip, int buttonCount,
|
|
int minButtonWidth);
|
|
|
|
// Hit-tests a point (SWELL/LICE top-left client coords) against the strip laid out for
|
|
// `buttonCount` buttons at `minButtonWidth`. Returns the index of the button containing
|
|
// the point, or -1 for a miss: outside the strip band entirely, or in the strip band but
|
|
// past the last visible button (the overflow dead-zone on a narrow panel — a harmless
|
|
// no-op the shell ignores). Half-open bounds [x, x+width) x [y, y+height) match
|
|
// computeButtonRects so no pixel is double-claimed and the hit maps to the button drawn
|
|
// there.
|
|
int hitTestButton(int px, int py, const ButtonStripRect& strip, int buttonCount,
|
|
int minButtonWidth);
|
|
|
|
// --- Label formatting ---------------------------------------------------------
|
|
//
|
|
// The SDK's kbd_getTextFromCmd returns the binding text for a command in the main
|
|
// section (e.g. "Ctrl+Shift+C"), or an empty / whitespace-only / null string when the
|
|
// action is unbound. The shell reads that raw string; this function turns
|
|
// (short action name + raw binding) into the label drawn on the button, with every
|
|
// degenerate binding collapsed to ONE explicit unbound marker so an unbound button reads
|
|
// clearly rather than showing a stray separator or a blank tail.
|
|
|
|
// The marker appended when an action has no key binding. An em-dash-free ASCII marker so
|
|
// it renders in any SWELL font; the label reads e.g. "Capture Item (unbound)".
|
|
inline constexpr const char* kUnboundMarker = "unbound";
|
|
|
|
// Max characters of the binding string kept in the label. A pathological binding (a long
|
|
// multi-chord custom binding) is truncated with a trailing ellipsis so it never overruns
|
|
// the button's text budget; DrawText's own DT_END_ELLIPSIS is a per-pixel backstop, but
|
|
// bounding the string here keeps the label deterministic and testable.
|
|
inline constexpr int kMaxBindingChars = 24;
|
|
|
|
// Formats a button label from the action's short name and the raw SDK binding string.
|
|
// * Bound: "<name> <binding>" (two spaces separate name and binding).
|
|
// * Unbound: "<name> (unbound)" when `rawBinding` is empty, or contains only
|
|
// whitespace (spaces/tabs), or is otherwise blank — all collapse to the
|
|
// single marker.
|
|
// * A binding longer than kMaxBindingChars is truncated to kMaxBindingChars-1 chars
|
|
// plus a "~" ellipsis (kept ASCII, single-width) so the whole label stays bounded.
|
|
// Leading/trailing whitespace on a non-blank binding is trimmed before formatting.
|
|
// `name` is passed through verbatim (already short — the shell supplies a terse label).
|
|
std::string formatButtonLabel(const std::string& name, const std::string& rawBinding);
|
|
|
|
} // namespace reasampler
|