Merge Insert as FX: load ReaSampler 9000 onto the selected track from the panel
# Conflicts: # src/shell/actions/CLAUDE.md
This commit is contained in:
+106
-12
@@ -6,7 +6,8 @@
|
||||
// * 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).
|
||||
// clipped; earlier frequent clusters survive), and the More-button reserve the panel
|
||||
// subtracts before tiling (composed here with overflow_menu, as panel_layout does).
|
||||
// * 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
|
||||
@@ -16,6 +17,7 @@
|
||||
// * Resize: no inventory item cut off or overlapping across a representative width range.
|
||||
|
||||
#include "../src/core/ui/action_bar.h"
|
||||
#include "../src/core/ui/overflow_menu.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
@@ -28,14 +30,15 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// The panel's real inventory shape (L6): 2 capture, 1 maintenance (Re-capture), 2 placement = 5
|
||||
// buttons. Cluster order: Capture -> Maintenance -> Placement (Re-capture sits between the two
|
||||
// capture verbs and the placement verbs — the L6 bar ordering).
|
||||
// The panel's real inventory shape: 2 capture, 1 maintenance (Re-capture), 3 placement
|
||||
// (Insert / Insert Conform / Insert as FX) = 6 buttons. Cluster order:
|
||||
// Capture -> Maintenance -> Placement (Re-capture sits between the two capture verbs and the
|
||||
// placement verbs).
|
||||
static std::vector<ClusterSpec> inventory() {
|
||||
return {
|
||||
{ActionCluster::Capture, 2},
|
||||
{ActionCluster::Maintenance, 1},
|
||||
{ActionCluster::Placement, 2},
|
||||
{ActionCluster::Placement, 3},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ static ActionBarSpec roundSpec() {
|
||||
|
||||
// --- Layout: all fit, correct rects + gaps ------------------------------------
|
||||
|
||||
// A wide bar fits all 5 buttons. Verify the first few rects, the intra-cluster gap, and the
|
||||
// A wide bar fits all 6 buttons. Verify the first few rects, the intra-cluster gap, and the
|
||||
// (wider) inter-cluster gap between button 1 (last capture) and button 2 (maintenance).
|
||||
//
|
||||
// Pixel walk with roundSpec and bar at (0,40):
|
||||
@@ -61,18 +64,19 @@ static ActionBarSpec roundSpec() {
|
||||
// btn2 (Maintenance): x=228, right=328 (clusterGap = 16 after btn1's right)
|
||||
// btn3 (Placement): x=344, right=444 (clusterGap = 16 after btn2's right)
|
||||
// btn4 (Placement): x=448, right=548 (intraGap = 4 after btn3's right)
|
||||
// Minimum bar width: 548 + sidePad(8) = 556. Give it 600.
|
||||
// btn5 (Placement): x=552, right=652 (intraGap = 4 after btn4's right)
|
||||
// Minimum bar width: 652 + sidePad(8) = 660. Give it 700.
|
||||
static void testAllFitRectsAndGaps() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
ActionBarRect bar{0, 40, 600, 34};
|
||||
ActionBarRect bar{0, 40, 700, 34};
|
||||
|
||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||
CHECK(fit.visibleCount == 5);
|
||||
CHECK(fit.visibleCount == 6);
|
||||
CHECK(fit.hiddenCount == 0);
|
||||
|
||||
auto slots = computeBarSlots(bar, clusters, spec);
|
||||
CHECK(slots.size() == 5);
|
||||
CHECK(slots.size() == 6);
|
||||
|
||||
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
|
||||
CHECK(slots[0].x == 8);
|
||||
@@ -101,6 +105,13 @@ static void testAllFitRectsAndGaps() {
|
||||
CHECK(slots[4].cluster == ActionCluster::Placement);
|
||||
CHECK(slots[4].index == 4);
|
||||
|
||||
// Button 5 (Insert as FX — third placement): intra-cluster gap of 4 after 548 -> 552.
|
||||
// It joins the Placement cluster, so no cluster gap opens before it.
|
||||
CHECK(slots[5].x == 552);
|
||||
CHECK(slots[5].cluster == ActionCluster::Placement);
|
||||
CHECK(slots[5].index == 5);
|
||||
CHECK(slots[5].x - (slots[4].x + slots[4].width) == spec.buttonGap);
|
||||
|
||||
// Inter-cluster gap (slot[2].x - slot[1].right = 228 - 212 = 16) is wider than the
|
||||
// intra-cluster gap (slot[1].x - slot[0].right = 112 - 108 = 4) — task grouping is
|
||||
// structurally visible in the geometry.
|
||||
@@ -155,7 +166,7 @@ static void testOverflowDropsTrailingWhole() {
|
||||
ActionBarRect bar{0, 0, 220, 34};
|
||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||
CHECK(fit.visibleCount == 2);
|
||||
CHECK(fit.hiddenCount == 3);
|
||||
CHECK(fit.hiddenCount == 4);
|
||||
|
||||
auto slots = computeBarSlots(bar, clusters, spec);
|
||||
CHECK(slots.size() == 2);
|
||||
@@ -175,10 +186,91 @@ static void testTooNarrowForAny() {
|
||||
ActionBarRect bar{0, 0, 60, 34}; // sidePad*2 + one 100-wide button won't fit
|
||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||
CHECK(fit.visibleCount == 0);
|
||||
CHECK(fit.hiddenCount == 5);
|
||||
CHECK(fit.hiddenCount == 6);
|
||||
CHECK(computeBarSlots(bar, clusters, spec).empty());
|
||||
}
|
||||
|
||||
// --- Overflow reserve: the bar never tiles under the More (...) button --------
|
||||
|
||||
// The panel hands action_bar the top band MINUS overflow_menu's reserve (panel_layout's
|
||||
// topToolbarActionRect). Reproduce that composition and assert the invariant it exists to
|
||||
// buy: across every width, no visible button's right edge crosses into the reserved strip
|
||||
// where the More button is drawn — including the widths where the reserve is what pushes
|
||||
// the trailing "Insert as FX" button into overflow.
|
||||
static void testOverflowReserveHonouredAcrossWidths() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = roundSpec();
|
||||
const MenuButtonSpec menuSpec; // the panel's kMenuBtnSpec defaults
|
||||
|
||||
bool sawReserveCostAButton = false;
|
||||
for (int w = 40; w <= 900; w += 3) {
|
||||
const MenuBarRect band{0, 0, w, 34};
|
||||
const int reserve = menuButtonReserve(band, menuSpec);
|
||||
CHECK(reserve >= 0);
|
||||
|
||||
ActionBarRect action{0, 0, w - reserve, 34};
|
||||
if (action.width < 0) action.width = 0;
|
||||
|
||||
const MenuButtonRect more = computeMenuButton(band, menuSpec);
|
||||
for (const auto& s : computeBarSlots(action, clusters, spec)) {
|
||||
// Fully clear of the reserved strip at the band's right.
|
||||
CHECK(s.x + s.width <= w - reserve);
|
||||
// And so, transitively, clear of the More button itself when one is drawn.
|
||||
if (!more.empty()) CHECK(s.x + s.width <= more.x);
|
||||
}
|
||||
|
||||
// The reserve genuinely costs buttons somewhere in this range, so the assertions
|
||||
// above are exercised against a bar the reserve actually narrowed.
|
||||
const int full = computeBarFit(ActionBarRect{0, 0, w, 34}, clusters, spec).visibleCount;
|
||||
const int reserved = computeBarFit(action, clusters, spec).visibleCount;
|
||||
CHECK(reserved <= full);
|
||||
if (reserved < full) sawReserveCostAButton = true;
|
||||
}
|
||||
CHECK(sawReserveCostAButton);
|
||||
}
|
||||
|
||||
// --- Reserve against the REAL panel spec: names the button and the exact width -----
|
||||
|
||||
// roundSpec() above uses clusterGap=16 for hand-checkable pixel math; the panel's actual
|
||||
// bar (shell/panel/panel_state.h's kBarSpec) uses clusterGap=24. Reproduced literally here
|
||||
// because this test target is REAPER-free and cannot include that shell header -- keep
|
||||
// the two in sync by hand if either spec ever changes.
|
||||
static ActionBarSpec realBarSpec() {
|
||||
ActionBarSpec s;
|
||||
s.buttonWidth = 108;
|
||||
s.buttonGap = 4;
|
||||
s.clusterGap = 24;
|
||||
s.sidePad = 8;
|
||||
s.verticalInset = 3;
|
||||
return s;
|
||||
}
|
||||
|
||||
// At the real spec plus the real 40 px reserve (28 + 2*6, panel_state.h's kMenuBtnSpec --
|
||||
// all default-constructed here since its defaults already match), 764 is the narrowest
|
||||
// band that still shows all six buttons; one pixel narrower drops Insert as FX (flat index
|
||||
// 5, the trailing Placement button) alone into overflow, leaving its five neighbours intact.
|
||||
static void testReserveDropsInsertAsFxAtRealWidth() {
|
||||
const auto clusters = inventory();
|
||||
const ActionBarSpec spec = realBarSpec();
|
||||
const MenuButtonSpec menuSpec;
|
||||
const int reserve = menuButtonReserve(MenuBarRect{0, 0, 900, 34}, menuSpec);
|
||||
CHECK(reserve == 40);
|
||||
|
||||
const int allSixWidth = 764;
|
||||
{
|
||||
ActionBarRect action{0, 0, allSixWidth - reserve, 34};
|
||||
const auto slots = computeBarSlots(action, clusters, spec);
|
||||
CHECK(slots.size() == 6);
|
||||
CHECK(slots.back().index == 5); // Insert as FX still visible
|
||||
}
|
||||
{
|
||||
ActionBarRect action{0, 0, allSixWidth - 1 - reserve, 34};
|
||||
const auto slots = computeBarSlots(action, clusters, spec);
|
||||
CHECK(slots.size() == 5); // Insert as FX alone dropped
|
||||
CHECK(slots.back().index == 4); // Insert Conform is now the trailing visible button
|
||||
}
|
||||
}
|
||||
|
||||
// --- Degenerate --------------------------------------------------------------
|
||||
|
||||
static void testDegenerate() {
|
||||
@@ -305,6 +397,8 @@ int main() {
|
||||
testLabelFullHeightWhenShort();
|
||||
testOverflowDropsTrailingWhole();
|
||||
testTooNarrowForAny();
|
||||
testOverflowReserveHonouredAcrossWidths();
|
||||
testReserveDropsInsertAsFxAtRealWidth();
|
||||
testDegenerate();
|
||||
testHitTestHitsButtons();
|
||||
testHitTestGapsAreMisses();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Standalone tests for reasampler::ui::insert_fx_enable — no REAPER, no test framework.
|
||||
// Exhaustive over the fold's whole input space (track selected x capture-count class),
|
||||
// plus the two properties the shell leans on: the button bit is the fold restricted to
|
||||
// the payload axis, and every refusal carries a non-empty sentence (a silent refusal is
|
||||
// the failure mode this verb must not have).
|
||||
|
||||
#include "../src/core/ui/insert_fx_enable.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace reasampler::ui;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// All six combinations of {no track, track} x {0, 1, many captures}. The payload axis is
|
||||
// checked BEFORE the destination axis, so a press with neither a capture nor a track
|
||||
// reports the capture problem — the one the user can fix inside the panel they clicked.
|
||||
static void testEveryCombination() {
|
||||
CHECK(insertFxRefusal(0, false) == InsertFxRefusal::NoCapture);
|
||||
CHECK(insertFxRefusal(0, true) == InsertFxRefusal::NoCapture);
|
||||
CHECK(insertFxRefusal(1, false) == InsertFxRefusal::NoTrack);
|
||||
CHECK(insertFxRefusal(1, true) == InsertFxRefusal::None);
|
||||
CHECK(insertFxRefusal(3, false) == InsertFxRefusal::MultiCapture);
|
||||
CHECK(insertFxRefusal(3, true) == InsertFxRefusal::MultiCapture);
|
||||
}
|
||||
|
||||
// GetSelectedTrack ignores the master track, so "only the master is selected" is a
|
||||
// distinct refusal from a plain empty selection -- both are !trackSelected, but the
|
||||
// message differs so a master-only user isn't told "select a track" when they did.
|
||||
static void testMasterOnlySelectedIsDistinctFromNoTrack() {
|
||||
CHECK(insertFxRefusal(1, false, /*masterOnlySelected=*/true) ==
|
||||
InsertFxRefusal::MasterOnlySelected);
|
||||
CHECK(insertFxRefusal(1, false, /*masterOnlySelected=*/false) == InsertFxRefusal::NoTrack);
|
||||
// The payload axis still runs first -- a bad capture count refuses on ITS reason
|
||||
// even with the master selected.
|
||||
CHECK(insertFxRefusal(0, false, /*masterOnlySelected=*/true) == InsertFxRefusal::NoCapture);
|
||||
}
|
||||
|
||||
// Exactly one capture is the only count that can run; two is already too many.
|
||||
static void testOnlyASingleCaptureRuns() {
|
||||
CHECK(insertFxRefusal(2, true) == InsertFxRefusal::MultiCapture);
|
||||
for (int n = 0; n <= 8; ++n) {
|
||||
const bool ready = insertFxRefusal(n, true) == InsertFxRefusal::None;
|
||||
CHECK(ready == (n == 1));
|
||||
}
|
||||
}
|
||||
|
||||
// A negative count can only come from a caller bug; it must fold to a refusal, never to
|
||||
// Ready (fail closed — this verb writes to the project).
|
||||
static void testNegativeCountRefuses() {
|
||||
CHECK(insertFxRefusal(-1, true) == InsertFxRefusal::NoCapture);
|
||||
CHECK(!insertFxButtonEnabled(-1));
|
||||
}
|
||||
|
||||
// The button bit is the fold with the destination axis held satisfied: it goes dead for
|
||||
// exactly the two panel-local refusals and stays live otherwise, whatever the project's
|
||||
// track selection happens to be.
|
||||
static void testButtonBitIsThePayloadAxisOfTheFold() {
|
||||
for (int n = -1; n <= 4; ++n) {
|
||||
const bool live = insertFxButtonEnabled(n);
|
||||
CHECK(live == (insertFxRefusal(n, true) == InsertFxRefusal::None));
|
||||
// A dead button always corresponds to a capture-side refusal, never to NoTrack.
|
||||
if (!live) {
|
||||
const InsertFxRefusal r = insertFxRefusal(n, false);
|
||||
CHECK(r == InsertFxRefusal::NoCapture || r == InsertFxRefusal::MultiCapture);
|
||||
}
|
||||
}
|
||||
CHECK(insertFxButtonEnabled(1));
|
||||
CHECK(!insertFxButtonEnabled(0));
|
||||
CHECK(!insertFxButtonEnabled(2));
|
||||
}
|
||||
|
||||
// Every refusal says something, and each says something DIFFERENT — a shared sentence
|
||||
// would leave the user unable to tell which condition they hit. None says nothing.
|
||||
static void testEveryRefusalHasItsOwnSentence() {
|
||||
const std::string none = insertFxRefusalMessage(InsertFxRefusal::None);
|
||||
const std::string noCap = insertFxRefusalMessage(InsertFxRefusal::NoCapture);
|
||||
const std::string multi = insertFxRefusalMessage(InsertFxRefusal::MultiCapture);
|
||||
const std::string noTrk = insertFxRefusalMessage(InsertFxRefusal::NoTrack);
|
||||
const std::string masterOnly = insertFxRefusalMessage(InsertFxRefusal::MasterOnlySelected);
|
||||
|
||||
CHECK(none.empty());
|
||||
CHECK(!noCap.empty());
|
||||
CHECK(!multi.empty());
|
||||
CHECK(!noTrk.empty());
|
||||
CHECK(!masterOnly.empty());
|
||||
CHECK(noCap != multi);
|
||||
CHECK(noCap != noTrk);
|
||||
CHECK(multi != noTrk);
|
||||
CHECK(masterOnly != noTrk);
|
||||
CHECK(masterOnly != noCap);
|
||||
CHECK(masterOnly != multi);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testEveryCombination();
|
||||
testMasterOnlySelectedIsDistinctFromNoTrack();
|
||||
testOnlyASingleCaptureRuns();
|
||||
testNegativeCountRefuses();
|
||||
testButtonBitIsThePayloadAxisOfTheFold();
|
||||
testEveryRefusalHasItsOwnSentence();
|
||||
|
||||
if (g_fail == 0) std::printf("insert_fx_enable: all tests passed\n");
|
||||
else std::printf("insert_fx_enable: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user