diff --git a/CMakeLists.txt b/CMakeLists.txt index 2394361..e87bcb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,20 +318,6 @@ target_include_directories(provenance PUBLIC src) add_library(assignment_request STATIC src/assignment_request.cpp) target_include_directories(assignment_request PUBLIC src) -# --------------------------------------------------------------------------- -# 2k) Pure action_buttons library — NO REAPER, NO SWELL. The Milestone 11 -# action-trigger button strip: strip rect + N buttons at a minimum width -> -# per-button rects (equal tiling; overflow HIDES excess on a narrow panel -# rather than clipping), point -> button hit-test, and the button label format -# (action name + SDK binding string -> label, with the unbound/blank case -# degraded to an explicit marker and an over-long binding truncated). Split out -# so the layout + label math is unit-tested outside the DAW; the bank_panel -# draw + NamedCommandLookup/Main_OnCommand dispatch + kbd_getTextFromCmd query -# are DAW-verified. Mirror of mode_switch / tab_strip. -# --------------------------------------------------------------------------- -add_library(action_buttons STATIC src/action_buttons.cpp) -target_include_directories(action_buttons PUBLIC src) - # --------------------------------------------------------------------------- # 2l) Pure drag_out library — NO REAPER, NO SWELL, NO OS/OLE. The Milestone 11 # native-OS-drag-out decision core: the gesture-boundary decision (drag state + @@ -341,7 +327,7 @@ target_include_directories(action_buttons PUBLIC src) # an explicit skip-missing / skip-unresolved policy). Split out so the boundary + # set algebra are unit-tested outside the DAW; the OLE DoDragDrop / SWELL file-list # initiation (drag_out_win) and the bank_panel gesture hook are DAW-verified. Mirror -# of action_buttons / mode_switch. +# of mode_switch. # --------------------------------------------------------------------------- add_library(drag_out STATIC src/drag_out.cpp) target_include_directories(drag_out PUBLIC src) @@ -378,7 +364,7 @@ target_include_directories(theme PUBLIC src) # generic component geometry the kit draws against: button box (inset + graceful # suppression), horizontal slider track/handle/filled geometry + value<->px inverse, # and list-row rect + hover hit-test. The kit-level primitives that don't already -# have a pure owner (bank_grid/mode_switch/tab_strip/action_buttons/prune_button +# have a pure owner (bank_grid/mode_switch/tab_strip/prune_button # stay the source of truth for THEIR surfaces). Names KitBox/KitButtonBox/ # SliderGeometry/ListRowBox avoid the existing ButtonRect/CellRect collisions. # Mirror of prune_button — pure, CTest-covered. @@ -388,15 +374,15 @@ 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 +# dock-panel layout redesign core: the TASK-GROUPED action bar geometry 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. +# mode_switch / prune_button. # --------------------------------------------------------------------------- add_library(action_bar STATIC src/action_bar.cpp) target_include_directories(action_bar PUBLIC src) @@ -604,10 +590,6 @@ add_executable(provenance_tests tests/test_provenance.cpp) target_link_libraries(provenance_tests PRIVATE provenance bank_model) add_test(NAME provenance_tests COMMAND provenance_tests) -add_executable(action_buttons_tests tests/test_action_buttons.cpp) -target_link_libraries(action_buttons_tests PRIVATE action_buttons) -add_test(NAME action_buttons_tests COMMAND action_buttons_tests) - add_executable(drag_out_tests tests/test_drag_out.cpp) target_link_libraries(drag_out_tests PRIVATE drag_out) add_test(NAME drag_out_tests COMMAND drag_out_tests) @@ -928,7 +910,7 @@ add_library(reaper_reasampler MODULE src/card_meta.cpp src/card_drag.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 instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync) +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 drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync) 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' diff --git a/src/action_bar.h b/src/action_bar.h index 1b2b9d1..8e48a7e 100644 --- a/src/action_bar.h +++ b/src/action_bar.h @@ -2,8 +2,7 @@ // action_bar — the REAPER-free, LICE-free layout + hit-test math behind the bank_panel's // TASK-GROUPED toolbars (Phase L, L2 + L4 + L6). L2's dock-panel layout redesign (DS-3: a // thorough layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact -// bar of clusters 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 spanning +// bar of clusters, each button carrying a label sub-rect spanning // its full height — a single-row short label (L6: the keybinding sub-row was on the button face // through L5; L6 moves it to the hover tooltip instead). The bar degrades gracefully on a narrow // panel by dropping WHOLE trailing buttons (never clipping) so the frequent leading cluster @@ -19,14 +18,13 @@ // 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 label sub-rect sit, and which button a click hits — lives HERE, unit-tested outside the -// DAW. Mirror of mode_switch / action_buttons / prune_button. +// DAW. Mirror of mode_switch / 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. +// sub-rects), 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 diff --git a/src/action_buttons.cpp b/src/action_buttons.cpp deleted file mode 100644 index fd8ca3d..0000000 --- a/src/action_buttons.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// action_buttons — pure implementation. See action_buttons.h. NO REAPER / SWELL / vendor. - -#include "action_buttons.h" - -#include - -namespace reasampler { - -namespace { - -// The left edge of button i when `count` buttons share a strip of the given x-origin and -// width. Boundary i is x + (i * width) / count, so button i spans [edge(i), edge(i+1)). -// Every boundary derives from the same formula, so consecutive buttons share an exact -// edge (no gap, no overlap) and edge(count) == x + width precisely. count assumed >= 1. -int buttonEdge(int x, int width, int i, int count) { - return x + (i * width) / count; -} - -// True for an ASCII space or tab (the whitespace the SDK binding string might carry). -bool isBlankChar(char c) { return c == ' ' || c == '\t'; } - -// The trimmed [first, last) view of `s` with leading/trailing blanks removed. Returns -// an empty range when `s` is all blanks. -std::string trimBlanks(const std::string& s) { - std::size_t b = 0; - std::size_t e = s.size(); - while (b < e && isBlankChar(s[b])) ++b; - while (e > b && isBlankChar(s[e - 1])) --e; - return s.substr(b, e - b); -} - -} // namespace - -ButtonFit computeButtonFit(const ButtonStripRect& strip, int buttonCount, - int minButtonWidth) { - ButtonFit fit; - if (buttonCount <= 0 || strip.width <= 0 || minButtonWidth <= 0) return fit; - - int fits = strip.width / minButtonWidth; // how many min-width buttons the strip holds - if (fits > buttonCount) fits = buttonCount; - if (fits < 0) fits = 0; - - fit.visibleCount = fits; - fit.hiddenCount = buttonCount - fits; - return fit; -} - -std::vector computeButtonRects(const ButtonStripRect& strip, int buttonCount, - int minButtonWidth) { - std::vector rects; - const ButtonFit fit = computeButtonFit(strip, buttonCount, minButtonWidth); - const int n = fit.visibleCount; - if (n <= 0) return rects; - - rects.reserve(static_cast(n)); - for (int i = 0; i < n; ++i) { - const int left = buttonEdge(strip.x, strip.width, i, n); - const int right = buttonEdge(strip.x, strip.width, i + 1, n); - ActionButtonRect r; - r.index = i; - r.x = left; - r.y = strip.y; - r.width = right - left; // absorbs rounding; visible buttons abut and fill the strip - r.height = strip.height; - rects.push_back(r); - } - return rects; -} - -int hitTestButton(int px, int py, const ButtonStripRect& strip, int buttonCount, - int minButtonWidth) { - if (strip.height <= 0) return -1; - // Reject anything outside the strip band first (half-open bounds match the rects). - if (px < strip.x || px >= strip.x + strip.width || - py < strip.y || py >= strip.y + strip.height) - return -1; - - const ButtonFit fit = computeButtonFit(strip, buttonCount, minButtonWidth); - const int n = fit.visibleCount; - if (n <= 0) return -1; - - // Inside the band: find the visible button whose [edge(i), edge(i+1)) contains px. - // A point past the last visible button's right edge (narrow-panel overflow dead-zone) - // falls through to -1. - for (int i = 0; i < n; ++i) { - const int left = buttonEdge(strip.x, strip.width, i, n); - const int right = buttonEdge(strip.x, strip.width, i + 1, n); - if (px >= left && px < right) return i; - } - return -1; -} - -std::string formatButtonLabel(const std::string& name, const std::string& rawBinding) { - const std::string binding = trimBlanks(rawBinding); - if (binding.empty()) { - return name + " (" + kUnboundMarker + ")"; - } - - std::string shown = binding; - if (static_cast(shown.size()) > kMaxBindingChars) { - // Keep the leading portion and mark the truncation with a single-width "~". - shown = shown.substr(0, static_cast(kMaxBindingChars - 1)) + "~"; - } - return name + " " + shown; -} - -} // namespace reasampler diff --git a/src/action_buttons.h b/src/action_buttons.h deleted file mode 100644 index 696bf1c..0000000 --- a/src/action_buttons.h +++ /dev/null @@ -1,125 +0,0 @@ -#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 -#include - -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 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: " " (two spaces separate name and binding). -// * Unbound: " (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 diff --git a/src/component_geometry.h b/src/component_geometry.h index d536932..ff8e30f 100644 --- a/src/component_geometry.h +++ b/src/component_geometry.h @@ -2,8 +2,8 @@ // component_geometry — the REAPER-free, LICE-free geometry + hit-test math for the shared // drawing kit's generic components (Phase L, L1): a button box, a slider's track/handle, // and a list row. These are the kit-level primitives that DON'T already have a pure owner: -// bank_grid / mode_switch / tab_strip / action_buttons / prune_button stay the source of -// truth for the surfaces THEY own; this module carries only the new, reusable component +// bank_grid / mode_switch / tab_strip / prune_button stay the source of truth for the +// surfaces THEY own; this module carries only the new, reusable component // shapes the kit's drawButton / drawSlider / drawListRow draw against. // // Why pure (CLAUDE.md §load-bearing split, DS-1 caution): even where the draw shell reuses @@ -47,8 +47,8 @@ bool hitTestBox(int px, int py, const KitBox& box); // // A button drawn inside a host cell, inset by a uniform padding so it reads as a raised // control rather than a full-bleed fill (the kit's drawButton draws the micro-gradient -// surface inside this box). Distinct from prune_button/action_buttons, which own their -// OWN placement within their strips — this is the generic "given a cell, where's the +// surface inside this box). Distinct from prune_button, which owns its OWN placement +// within its strip — this is the generic "given a cell, where's the // button" helper for new kit consumers. struct KitButtonBox { KitBox box; diff --git a/src/drag_out.h b/src/drag_out.h index 312b47e..eacf47f 100644 --- a/src/drag_out.h +++ b/src/drag_out.h @@ -24,7 +24,7 @@ // drag_out_win — never by relocating or copying bytes here). // // PURE MODULE: NO REAPER types, NO SWELL, NO OS/OLE, NO vendor/ includes. Standard library -// only. Builds and unit-tests without REAPER. Mirror of action_buttons / mode_switch. +// only. Builds and unit-tests without REAPER. Mirror of mode_switch. #include #include diff --git a/tests/test_action_bar.cpp b/tests/test_action_bar.cpp index 02b9b25..cda6d88 100644 --- a/tests/test_action_bar.cpp +++ b/tests/test_action_bar.cpp @@ -1,12 +1,12 @@ // 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 +// as the sibling pure tests (mode_switch / prune_button): assert the // task-grouped action-bar layout, overflow-on-narrow, and hit-testing directly. // // Covers (L2 brief §test cases, updated for L6 single-row face change): // * 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). +// clipped; earlier frequent clusters survive). // * Label sub-rect correct — spans full button height (L6: keybinding sub-row removed from // the face; binding is in the hover tooltip instead). // * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs diff --git a/tests/test_action_buttons.cpp b/tests/test_action_buttons.cpp deleted file mode 100644 index ef75376..0000000 --- a/tests/test_action_buttons.cpp +++ /dev/null @@ -1,236 +0,0 @@ -// Standalone tests for reasampler::action_buttons — no REAPER, no test framework. -// Same fast loop as the sibling pure tests (mode_switch / tab_strip et al.): assert the -// action-button strip layout + hit-testing and the label-format logic directly. -// -// Covers (M11 brief §test cases): -// * Layout: N buttons in a strip — all fit (exact equal tiling), overflow on a narrow -// panel (only the fitting count laid out, rest hidden, never clipped), zero-width edge. -// * Hit-test: inside each button, outside the band, half-open boundary, the narrow-panel -// overflow dead-zone, hit/layout agreement. -// * Label format: bound ("name binding"), unbound marker, empty / whitespace-only SDK -// return -> unbound, over-long binding truncation. - -#include "../src/action_buttons.h" - -#include -#include -#include -#include - -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) - -// --- Layout: all fit ---------------------------------------------------------- - -// Strip 300 wide at origin (0, 40), height 24, 3 buttons at min width 80: all fit -// (300/80 = 3), tiled equally -> boundaries floor(i*300/3) = 0,100,200,300, widths 100. -static void testAllButtonsFitEqualTiling() { - ButtonStripRect s{0, 40, 300, 24}; - ButtonFit fit = computeButtonFit(s, 3, 80); - CHECK(fit.visibleCount == 3); - CHECK(fit.hiddenCount == 0); - - auto rects = computeButtonRects(s, 3, 80); - CHECK(rects.size() == 3); - CHECK((rects[0] == ActionButtonRect{0, 0, 40, 100, 24})); - CHECK((rects[1] == ActionButtonRect{1, 100, 40, 100, 24})); - CHECK((rects[2] == ActionButtonRect{2, 200, 40, 100, 24})); - // Abut exactly; last reaches strip right edge. - CHECK(rects[1].x == rects[0].x + rects[0].width); - CHECK(rects[2].x + rects[2].width == s.x + s.width); -} - -// Uneven width absorbed at boundaries: 100 wide / 3 -> edges 0,33,66,100 -> 33,33,34. -static void testUnevenWidthTilesExactly() { - ButtonStripRect s{7, 3, 100, 16}; - auto rects = computeButtonRects(s, 3, 30); // 100/30 = 3 fit - CHECK(rects.size() == 3); - CHECK(rects[0].width == 33); - CHECK(rects[1].width == 33); - CHECK(rects[2].width == 34); - CHECK(rects.front().x == s.x); - CHECK(rects.back().x + rects.back().width == s.x + s.width); -} - -// --- Layout: overflow on a narrow panel --------------------------------------- - -// 5 buttons at min width 80 into a 200-wide strip: only 2 fit (200/80 = 2). The two -// visible buttons share the FULL strip (100 each — never clipped, never below min), and -// 3 are hidden (the overflow the shell degrades, not clipped garbage). -static void testOverflowHidesExcessNotClipped() { - ButtonStripRect s{0, 0, 200, 24}; - ButtonFit fit = computeButtonFit(s, 5, 80); - CHECK(fit.visibleCount == 2); - CHECK(fit.hiddenCount == 3); - - auto rects = computeButtonRects(s, 5, 80); - CHECK(rects.size() == 2); - CHECK(rects[0].width == 100); // >= min width 80, shares full strip - CHECK(rects[1].width == 100); - CHECK(rects[1].x + rects[1].width == s.x + s.width); -} - -// A strip too narrow for even one min-width button lays out nothing (all hidden). The -// shell draws an empty strip rather than a sub-minimum clipped button. -static void testStripTooNarrowForAny() { - ButtonStripRect s{0, 0, 50, 24}; - ButtonFit fit = computeButtonFit(s, 3, 80); - CHECK(fit.visibleCount == 0); - CHECK(fit.hiddenCount == 3); - CHECK(computeButtonRects(s, 3, 80).empty()); -} - -// --- Layout: degenerate -------------------------------------------------------- - -static void testLayoutDegenerate() { - CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 0, 80).empty()); - CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, -2, 80).empty()); - CHECK(computeButtonRects(ButtonStripRect{0, 0, 0, 24}, 3, 80).empty()); - CHECK(computeButtonRects(ButtonStripRect{0, 0, -5, 24}, 3, 80).empty()); - CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, 0).empty()); - CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, -10).empty()); - - ButtonFit fit = computeButtonFit(ButtonStripRect{0, 0, 0, 24}, 3, 80); - CHECK(fit.visibleCount == 0 && fit.hiddenCount == 0); -} - -// A single button fills the whole strip. -static void testSingleButtonFillsStrip() { - ButtonStripRect s{5, 5, 120, 24}; - auto rects = computeButtonRects(s, 1, 80); - CHECK(rects.size() == 1); - CHECK((rects[0] == ActionButtonRect{0, 5, 5, 120, 24})); -} - -// --- Hit-test: hits ----------------------------------------------------------- - -static void testHitTestHitsEachButton() { - ButtonStripRect s{0, 40, 300, 24}; // 3 buttons, 100 wide each - CHECK(hitTestButton(0, 40, s, 3, 80) == 0); // top-left of button 0 - CHECK(hitTestButton(50, 51, s, 3, 80) == 0); // middle of button 0 - CHECK(hitTestButton(99, 63, s, 3, 80) == 0); // last pixel of button 0 - CHECK(hitTestButton(100, 50, s, 3, 80) == 1); // first pixel of button 1 - CHECK(hitTestButton(299, 40, s, 3, 80) == 2); // last column of button 2 -} - -// The boundary pixel belongs to exactly ONE button (half-open): px==100 starts button 1. -static void testHitTestBoundaryHalfOpen() { - ButtonStripRect s{0, 0, 300, 24}; - CHECK(hitTestButton(99, 10, s, 3, 80) == 0); - CHECK(hitTestButton(100, 10, s, 3, 80) == 1); - CHECK(hitTestButton(199, 10, s, 3, 80) == 1); - CHECK(hitTestButton(200, 10, s, 3, 80) == 2); -} - -// --- Hit-test: misses --------------------------------------------------------- - -static void testHitTestMissesOutsideBand() { - ButtonStripRect s{10, 40, 200, 24}; - CHECK(hitTestButton(9, 50, s, 3, 60) == -1); // left of strip - CHECK(hitTestButton(210, 50, s, 3, 60) == -1); // right edge (== x+width, excluded) - CHECK(hitTestButton(50, 39, s, 3, 60) == -1); // above the band - CHECK(hitTestButton(50, 64, s, 3, 60) == -1); // below the band -} - -// On a narrow panel the visible buttons fill the whole strip, so there is no in-band -// dead-zone; but when buttonCount is 0 or the strip too narrow, every in-band point -// misses (the shell draws nothing and ignores the click). -static void testHitTestOverflowDeadZone() { - ButtonStripRect s{0, 0, 50, 24}; // too narrow for any 80-min button - CHECK(hitTestButton(25, 10, s, 3, 80) == -1); - CHECK(hitTestButton(0, 0, s, 0, 80) == -1); // no buttons -} - -static void testHitTestDegenerate() { - ButtonStripRect s{0, 0, 300, 24}; - CHECK(hitTestButton(50, 10, s, 0, 80) == -1); - CHECK(hitTestButton(50, 10, s, -2, 80) == -1); - CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 0, 24}, 3, 80) == -1); - CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 300, 0}, 3, 80) == -1); -} - -// Every point in the strip hit-tests to the button that DREW it (hit-test and layout -// agree — the load-bearing consistency invariant), across an awkward width and count. -static void testHitTestMatchesLayout() { - ButtonStripRect s{4, 2, 173, 22}; - const int count = 4, minW = 40; // 173/40 = 4 fit - auto rects = computeButtonRects(s, count, minW); - for (int px = s.x; px < s.x + s.width; ++px) { - const int b = hitTestButton(px, s.y + 1, s, count, minW); - CHECK(b >= 0); - const ActionButtonRect& r = rects[static_cast(b)]; - CHECK(px >= r.x && px < r.x + r.width); - } -} - -// --- Label format ------------------------------------------------------------- - -static void testLabelBound() { - CHECK(formatButtonLabel("Capture Item", "Ctrl+Shift+C") == - "Capture Item Ctrl+Shift+C"); - CHECK(formatButtonLabel("Insert", "F3") == "Insert F3"); -} - -static void testLabelUnboundEmpty() { - CHECK(formatButtonLabel("Capture Item", "") == "Capture Item (unbound)"); -} - -// Whitespace-only SDK returns (spaces, tabs) collapse to the unbound marker — the -// explicit blank-return handling the brief requires. -static void testLabelUnboundBlank() { - CHECK(formatButtonLabel("Capture Track", " ") == "Capture Track (unbound)"); - CHECK(formatButtonLabel("Insert", "\t") == "Insert (unbound)"); - CHECK(formatButtonLabel("Insert", " \t ") == "Insert (unbound)"); -} - -// Leading/trailing whitespace on a real binding is trimmed before formatting. -static void testLabelTrimsSurroundingBlanks() { - CHECK(formatButtonLabel("Insert", " F3 ") == "Insert F3"); -} - -// An over-long binding is truncated to kMaxBindingChars-1 chars + "~" so the label stays -// bounded (a pathological multi-chord custom binding never blows the button budget). -static void testLabelTruncatesLongBinding() { - const std::string longB(40, 'X'); // 40 > kMaxBindingChars (24) - const std::string label = formatButtonLabel("Cancel", longB); - // "Cancel " (8) + 23 'X' + "~" (kMaxBindingChars total in the binding portion). - const std::string expectedBinding = - std::string(kMaxBindingChars - 1, 'X') + "~"; - CHECK(label == "Cancel " + expectedBinding); - CHECK(static_cast(expectedBinding.size()) == kMaxBindingChars); -} - -// A binding exactly at the limit is NOT truncated (boundary: <= kMaxBindingChars kept). -static void testLabelAtLimitNotTruncated() { - const std::string atLimit(kMaxBindingChars, 'Y'); - CHECK(formatButtonLabel("X", atLimit) == "X " + atLimit); -} - -int main() { - testAllButtonsFitEqualTiling(); - testUnevenWidthTilesExactly(); - testOverflowHidesExcessNotClipped(); - testStripTooNarrowForAny(); - testLayoutDegenerate(); - testSingleButtonFillsStrip(); - - testHitTestHitsEachButton(); - testHitTestBoundaryHalfOpen(); - testHitTestMissesOutsideBand(); - testHitTestOverflowDeadZone(); - testHitTestDegenerate(); - testHitTestMatchesLayout(); - - testLabelBound(); - testLabelUnboundEmpty(); - testLabelUnboundBlank(); - testLabelTrimsSurroundingBlanks(); - testLabelTruncatesLongBinding(); - testLabelAtLimitNotTruncated(); - - if (g_fail == 0) std::printf("All tests passed.\n"); - return g_fail ? 1 : 0; -} diff --git a/tests/test_drag_out.cpp b/tests/test_drag_out.cpp index ac8420a..8da3977 100644 --- a/tests/test_drag_out.cpp +++ b/tests/test_drag_out.cpp @@ -1,5 +1,5 @@ // Standalone tests for reasampler::drag_out — no REAPER, no test framework. Same fast loop -// as the sibling pure tests (action_buttons / mode_switch et al.): assert the gesture- +// as the sibling pure tests (mode_switch et al.): assert the gesture- // boundary decision and the path-list assembly directly. // // Covers (M11 drag-out brief §test cases):