L5: dock-panel button refinements — overflow menu, tooltips, opposite-mode tag buttons
Top-bar rare-capture variants move to a right-anchored More popup; short faces gain hover-delay full-name tooltips (prefix stripped); bottom bar becomes four opposite-mode Item/Track tag buttons + Show Both (Toggle/Activates dropped); wider cluster gaps. New pure overflow_menu/mode_enable/tooltip modules, CTest-covered. Same actions, same contract.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Standalone tests for reasampler::mode_enable — no REAPER, no test framework. Asserts the
|
||||
// opposite-mode tag-button enablement predicate for BOTH active modes (the acceptance criterion:
|
||||
// covered for both, not only whichever a DAW pass sat in).
|
||||
//
|
||||
// The rule (L5 refinement 3): a tag button whose target is `t` is LIVE iff `t` != the active
|
||||
// mode — you tag INTO the mode you are not currently in. When Design is active, "…: Arrange"
|
||||
// buttons are live and "…: Design" buttons are dead; when Arrange is active, the reverse. An
|
||||
// unrecognized active id fails OPEN (every button live) so a future mode never dead-locks the bar.
|
||||
|
||||
#include "../src/mode_enable.h"
|
||||
#include "../src/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
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)
|
||||
|
||||
// When ARRANGE is active: only the Design-target buttons are live (tag into Design); the
|
||||
// Arrange-target buttons are dead (already in Arrange — nothing to do).
|
||||
static void testArrangeActive() {
|
||||
const std::string active = kArrangeModeId;
|
||||
CHECK(!tagButtonEnabled(active, TagTarget::Arrange)); // dead — already active mode
|
||||
CHECK(tagButtonEnabled(active, TagTarget::Design)); // live — opposite mode
|
||||
}
|
||||
|
||||
// When DESIGN is active: the reverse — only the Arrange-target buttons are live.
|
||||
static void testDesignActive() {
|
||||
const std::string active = kDesignModeId;
|
||||
CHECK(tagButtonEnabled(active, TagTarget::Arrange)); // live — opposite mode
|
||||
CHECK(!tagButtonEnabled(active, TagTarget::Design)); // dead — already active mode
|
||||
}
|
||||
|
||||
// Exactly one of the two targets is live in each mode (the buttons are complementary — a real
|
||||
// pair, never both-live or both-dead). This is the invariant the disabled/enabled visuals rely on.
|
||||
static void testExactlyOneTargetLivePerMode() {
|
||||
CHECK(tagButtonEnabled(kArrangeModeId, TagTarget::Arrange) !=
|
||||
tagButtonEnabled(kArrangeModeId, TagTarget::Design));
|
||||
CHECK(tagButtonEnabled(kDesignModeId, TagTarget::Arrange) !=
|
||||
tagButtonEnabled(kDesignModeId, TagTarget::Design));
|
||||
}
|
||||
|
||||
// An unrecognized active id (neither seed mode — e.g. empty when no session, or a future mode)
|
||||
// fails OPEN: every button live, so the user can always reach the action.
|
||||
static void testUnknownActiveFailsOpen() {
|
||||
CHECK(tagButtonEnabled("", TagTarget::Arrange));
|
||||
CHECK(tagButtonEnabled("", TagTarget::Design));
|
||||
CHECK(tagButtonEnabled("some-future-mode", TagTarget::Arrange));
|
||||
CHECK(tagButtonEnabled("some-future-mode", TagTarget::Design));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testArrangeActive();
|
||||
testDesignActive();
|
||||
testExactlyOneTargetLivePerMode();
|
||||
testUnknownActiveFailsOpen();
|
||||
|
||||
if (g_fail == 0) std::printf("mode_enable: all tests passed\n");
|
||||
else std::printf("mode_enable: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Standalone tests for reasampler::overflow_menu — no REAPER, no test framework. Same fast
|
||||
// loop as the sibling pure tests (prune_button / mode_switch): assert the top-toolbar More
|
||||
// ("⋯") button placement, the action-bar reserve, and hit-testing directly.
|
||||
//
|
||||
// Covers (L5 refinement 1 pure module): right-anchored layout in a wide band; the reserve the
|
||||
// action_bar must leave (width + 2*rightInset); vertical inset; thin-band height clamp;
|
||||
// SUPPRESSION (empty rect) when the band is too narrow to clear the left inset or is degenerate;
|
||||
// a degenerate band reserves nothing; hit-test in/out/edge (half-open bounds); an empty button
|
||||
// claims no point; draw and hit-test agree over the whole rect.
|
||||
|
||||
#include "../src/overflow_menu.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
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: wide band, right-anchored ---------------------------------------
|
||||
|
||||
// Band 400 wide at origin (0, 0), height 40. Default spec: buttonWidth 28, rightInset 6,
|
||||
// verticalInset 3, minLeftInset 40. Right edge = 0+400-6 = 394, left = 394-28 = 366
|
||||
// (>= 0+40, placed). Top = 0+3 = 3, height = 40-6 = 34.
|
||||
static void testWideBandRightAnchored() {
|
||||
MenuBarRect bar{0, 0, 400, 40};
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||
CHECK(!b.empty());
|
||||
CHECK((b == MenuButtonRect{366, 3, 28, 34}));
|
||||
CHECK(b.x + b.width == bar.x + bar.width - 6); // right edge at rightInset
|
||||
CHECK(b.x >= bar.x + 40); // clears the left inset
|
||||
}
|
||||
|
||||
// Origin offset honoured (button anchors to THIS band's right).
|
||||
static void testOffsetBandAnchors() {
|
||||
MenuBarRect bar{10, 5, 400, 40};
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||
CHECK(!b.empty());
|
||||
CHECK(b.x + b.width == bar.x + bar.width - 6); // = 10+400-6 = 404
|
||||
CHECK(b.y == 8); // 5 + 3
|
||||
}
|
||||
|
||||
// --- Reserve -----------------------------------------------------------------
|
||||
|
||||
// The reserve = buttonWidth + 2*rightInset so the frequent buttons get a right gap AND a left
|
||||
// breathing gap before the menu button. Default: 28 + 2*6 = 40.
|
||||
static void testReserve() {
|
||||
MenuBarRect bar{0, 0, 400, 40};
|
||||
CHECK(menuButtonReserve(bar, MenuButtonSpec{}) == 40);
|
||||
}
|
||||
|
||||
// A degenerate band reserves nothing (no button to reserve for).
|
||||
static void testDegenerateBandReservesNothing() {
|
||||
CHECK(menuButtonReserve(MenuBarRect{0, 0, 0, 40}, MenuButtonSpec{}) == 0);
|
||||
CHECK(menuButtonReserve(MenuBarRect{0, 0, 400, 0}, MenuButtonSpec{}) == 0);
|
||||
MenuButtonSpec zero{}; zero.buttonWidth = 0;
|
||||
CHECK(menuButtonReserve(MenuBarRect{0, 0, 400, 40}, zero) == 0);
|
||||
}
|
||||
|
||||
// --- Suppression: too narrow / degenerate ------------------------------------
|
||||
|
||||
// The button is suppressed when its left edge would fall past minLeftInset. left = x + width -
|
||||
// rightInset - buttonWidth. Need left < x + minLeftInset ->
|
||||
// width < rightInset + buttonWidth + minLeftInset = 6 + 28 + 40 = 74. Width 73 suppresses; 74
|
||||
// places (boundary).
|
||||
static void testNarrowBandSuppressed() {
|
||||
CHECK(computeMenuButton(MenuBarRect{0, 0, 73, 40}, MenuButtonSpec{}).empty());
|
||||
CHECK(!computeMenuButton(MenuBarRect{0, 0, 74, 40}, MenuButtonSpec{}).empty());
|
||||
}
|
||||
|
||||
static void testDegenerateBandSuppressed() {
|
||||
CHECK(computeMenuButton(MenuBarRect{0, 0, 0, 40}, MenuButtonSpec{}).empty());
|
||||
CHECK(computeMenuButton(MenuBarRect{0, 0, 400, 0}, MenuButtonSpec{}).empty());
|
||||
MenuButtonSpec zero{}; zero.buttonWidth = 0;
|
||||
CHECK(computeMenuButton(MenuBarRect{0, 0, 400, 40}, zero).empty());
|
||||
}
|
||||
|
||||
// A thin band (height <= 2*verticalInset) still places a button, clamped to the band's height.
|
||||
static void testThinBandClampsHeight() {
|
||||
MenuBarRect bar{0, 0, 400, 4}; // 4 <= 2*3, so height would be negative -> clamp
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||
CHECK(!b.empty());
|
||||
CHECK(b.y == bar.y);
|
||||
CHECK(b.height == bar.height);
|
||||
}
|
||||
|
||||
// --- Hit-test ----------------------------------------------------------------
|
||||
|
||||
static void testHitTestInside() {
|
||||
MenuBarRect bar{0, 0, 400, 40};
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{}); // {366,3,28,34}
|
||||
CHECK(hitTestMenuButton(b.x, b.y, b)); // top-left inclusive
|
||||
CHECK(hitTestMenuButton(b.x + b.width - 1, b.y + b.height - 1, b)); // bottom-right inclusive
|
||||
CHECK(hitTestMenuButton(b.x + b.width / 2, b.y + b.height / 2, b)); // centre
|
||||
}
|
||||
|
||||
static void testHitTestEdgesExcluded() {
|
||||
MenuBarRect bar{0, 0, 400, 40};
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||
CHECK(!hitTestMenuButton(b.x - 1, b.y, b)); // just left
|
||||
CHECK(!hitTestMenuButton(b.x + b.width, b.y, b)); // right edge excluded
|
||||
CHECK(!hitTestMenuButton(b.x, b.y - 1, b)); // just above
|
||||
CHECK(!hitTestMenuButton(b.x, b.y + b.height, b)); // bottom edge excluded
|
||||
}
|
||||
|
||||
// An empty/suppressed button claims no point — a click where it would be falls through.
|
||||
static void testEmptyButtonClaimsNothing() {
|
||||
MenuButtonRect empty{};
|
||||
CHECK(!hitTestMenuButton(0, 0, empty));
|
||||
const MenuButtonRect suppressed = computeMenuButton(MenuBarRect{0, 0, 50, 40}, MenuButtonSpec{});
|
||||
CHECK(suppressed.empty());
|
||||
CHECK(!hitTestMenuButton(30, 20, suppressed));
|
||||
}
|
||||
|
||||
// Draw/hit-test agreement over the whole rect + the four immediate outside neighbours.
|
||||
static void testHitTestMatchesLayout() {
|
||||
MenuBarRect bar{3, 7, 377, 38}; // awkward origin/size
|
||||
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||
CHECK(!b.empty());
|
||||
for (int py = b.y; py < b.y + b.height; ++py)
|
||||
for (int px = b.x; px < b.x + b.width; ++px)
|
||||
CHECK(hitTestMenuButton(px, py, b));
|
||||
CHECK(!hitTestMenuButton(b.x - 1, b.y, b));
|
||||
CHECK(!hitTestMenuButton(b.x + b.width, b.y, b));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testWideBandRightAnchored();
|
||||
testOffsetBandAnchors();
|
||||
testReserve();
|
||||
testDegenerateBandReservesNothing();
|
||||
testNarrowBandSuppressed();
|
||||
testDegenerateBandSuppressed();
|
||||
testThinBandClampsHeight();
|
||||
testHitTestInside();
|
||||
testHitTestEdgesExcluded();
|
||||
testEmptyButtonClaimsNothing();
|
||||
testHitTestMatchesLayout();
|
||||
|
||||
if (g_fail == 0) std::printf("overflow_menu: all tests passed\n");
|
||||
else std::printf("overflow_menu: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Standalone tests for reasampler::tooltip — no REAPER, no test framework. Asserts the custom
|
||||
// hover-delay tooltip's placement geometry and the action display-prefix strip helper.
|
||||
//
|
||||
// Covers (L5 refinement 2 pure module): prefix strip (present / absent / empty prefix / exact
|
||||
// match); placement BELOW the anchor centred; horizontal clamp at both client edges; the
|
||||
// bottom-edge flip to ABOVE; the both-clip clamp for a tall tooltip; degenerate inputs -> empty.
|
||||
|
||||
#include "../src/tooltip.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
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)
|
||||
|
||||
// --- Prefix strip ------------------------------------------------------------
|
||||
|
||||
static void testStripPresent() {
|
||||
CHECK(stripActionPrefix("ReaSampler: capture selected track(s)", "ReaSampler: ") ==
|
||||
"capture selected track(s)");
|
||||
}
|
||||
|
||||
static void testStripAbsent() {
|
||||
// No prefix present -> returned unchanged (defensive: a name from an unexpected source shows).
|
||||
CHECK(stripActionPrefix("capture selected track(s)", "ReaSampler: ") ==
|
||||
"capture selected track(s)");
|
||||
}
|
||||
|
||||
static void testStripEmptyPrefix() {
|
||||
CHECK(stripActionPrefix("anything", "") == "anything");
|
||||
}
|
||||
|
||||
static void testStripExactMatchLeavesEmpty() {
|
||||
// Name equals the prefix exactly -> the remainder is empty.
|
||||
CHECK(stripActionPrefix("ReaSampler: ", "ReaSampler: ").empty());
|
||||
}
|
||||
|
||||
static void testStripBetaPrefix() {
|
||||
CHECK(stripActionPrefix("ReaSampler beta: show version", "ReaSampler beta: ") ==
|
||||
"show version");
|
||||
}
|
||||
|
||||
// --- Placement: below, centred -----------------------------------------------
|
||||
|
||||
// Anchor button at (100, 10) size 108x34 in a 400x300 client. Text 80x14, spec defaults
|
||||
// (gap 4, padX 6, padY 3, margin 2). boxW = 80+12 = 92, boxH = 14+6 = 20.
|
||||
// Centre x = 100 + (108-92)/2 = 100 + 8 = 108. Below y = 10 + 34 + 4 = 48.
|
||||
static void testBelowCentred() {
|
||||
const TooltipBox tb = computeTooltip(100, 10, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||
CHECK(!tb.empty());
|
||||
CHECK((tb == TooltipBox{108, 48, 92, 20}));
|
||||
}
|
||||
|
||||
// --- Horizontal clamp --------------------------------------------------------
|
||||
|
||||
// A button near the RIGHT edge pushes the centred box past the client; it clamps to
|
||||
// clientW - margin - boxW. Client 400, boxW 92, margin 2 -> maxX = 400-2-92 = 306.
|
||||
static void testClampRight() {
|
||||
const TooltipBox tb = computeTooltip(360, 10, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||
CHECK(!tb.empty());
|
||||
CHECK(tb.x == 306);
|
||||
}
|
||||
|
||||
// A button near the LEFT edge clamps x to the left margin (2).
|
||||
static void testClampLeft() {
|
||||
const TooltipBox tb = computeTooltip(0, 10, 20, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||
CHECK(!tb.empty());
|
||||
CHECK(tb.x == 2);
|
||||
}
|
||||
|
||||
// --- Vertical flip -----------------------------------------------------------
|
||||
|
||||
// A button near the BOTTOM edge: below would clip, so the box flips ABOVE the anchor.
|
||||
// Anchor at y=270 h=34 in a 300-tall client. Below y = 270+34+4 = 308, box bottom 308+20=328 >
|
||||
// 300-2 -> flip above: y = 270 - 4 - 20 = 246 (>= margin, so used).
|
||||
static void testFlipAbove() {
|
||||
const TooltipBox tb = computeTooltip(100, 270, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||
CHECK(!tb.empty());
|
||||
CHECK(tb.y == 246);
|
||||
}
|
||||
|
||||
// A tall tooltip in a short client fits neither below nor above cleanly -> clamped to the
|
||||
// bottom margin (never negative). Client 40 tall, box 20 tall, anchor filling it.
|
||||
static void testBothClipClampsBottom() {
|
||||
// anchor 0..30 in a 40-tall client, text tall enough that above < margin too.
|
||||
const TooltipBox tb = computeTooltip(100, 5, 108, 30, 80, 30, 400, 40, TooltipSpec{});
|
||||
CHECK(!tb.empty());
|
||||
// boxH = 36; below y = 5+30+4=39 clips; above = 5-4-36 = -35 < margin; clamp to maxY =
|
||||
// 40-2-36 = 2 (>= margin).
|
||||
CHECK(tb.y == 2);
|
||||
}
|
||||
|
||||
// --- Degenerate --------------------------------------------------------------
|
||||
|
||||
static void testDegenerateEmpty() {
|
||||
CHECK(computeTooltip(0, 0, 100, 30, 0, 14, 400, 300, TooltipSpec{}).empty()); // no text width
|
||||
CHECK(computeTooltip(0, 0, 100, 30, 80, 0, 400, 300, TooltipSpec{}).empty()); // no text height
|
||||
CHECK(computeTooltip(0, 0, 100, 30, 80, 14, 0, 300, TooltipSpec{}).empty()); // no client width
|
||||
CHECK(computeTooltip(0, 0, 100, 30, 80, 14, 400, 0, TooltipSpec{}).empty()); // no client height
|
||||
}
|
||||
|
||||
int main() {
|
||||
testStripPresent();
|
||||
testStripAbsent();
|
||||
testStripEmptyPrefix();
|
||||
testStripExactMatchLeavesEmpty();
|
||||
testStripBetaPrefix();
|
||||
testBelowCentred();
|
||||
testClampRight();
|
||||
testClampLeft();
|
||||
testFlipAbove();
|
||||
testBothClipClampsBottom();
|
||||
testDegenerateEmpty();
|
||||
|
||||
if (g_fail == 0) std::printf("tooltip: all tests passed\n");
|
||||
else std::printf("tooltip: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user