feat(panel): add Insert as FX — ReaSampler 9000 onto the selected track, preloaded with the focused capture, over the existing instrument-drop body

This commit is contained in:
2026-08-03 16:06:22 -04:00
parent 5042e2709b
commit fcd1ed022c
14 changed files with 323 additions and 18 deletions
+63 -12
View File
@@ -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,49 @@ 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);
}
// --- Degenerate --------------------------------------------------------------
static void testDegenerate() {
@@ -305,6 +355,7 @@ int main() {
testLabelFullHeightWhenShort();
testOverflowDropsTrailingWhole();
testTooNarrowForAny();
testOverflowReserveHonouredAcrossWidths();
testDegenerate();
testHitTestHitsButtons();
testHitTestGapsAreMisses();