Merge panel polish: crosshair arrange-drop cue, Re-capture and Insert Conform to the overflow menu

This commit is contained in:
2026-08-03 17:50:09 -04:00
4 changed files with 48 additions and 59 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ live in `shell/bank_ops`, a sibling directory, not here.
## Modules
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Maintenance → Placement via `action_bar`, short labels, More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Placement, frequent acts only, via `action_bar`, short labels; Maintenance and the rarer Placement/Capture variants sit in the More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
- `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in. The drop splits by extension: a `.rsbank` is a whole bank and routes to the package-import action (one NEW bank each), everything else keeps the audio-ingest route.
- `panel_layout` — toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read).
- `panel_render` — the WM_PAINT draw.
+1 -1
View File
@@ -157,7 +157,7 @@ void applyDropCue(DropCue cue) {
const char* idc = nullptr;
switch (cue) {
case DropCue::Instrument: idc = IDC_HAND; break;
case DropCue::ArrangeInsert: idc = IDC_IBEAM; break; // an insertion point on a timeline
case DropCue::ArrangeInsert: idc = IDC_CROSS; break; // I-beam read as text-edit, not a timeline placement point
case DropCue::Refuse: idc = IDC_NO; break;
case DropCue::OsOwned: return; // reached once per move resolving to OsHandoff,
// right before handOffToOs is attempted; the OS
+13 -10
View File
@@ -109,9 +109,9 @@ bool pointInFooter(int x, int y) {
return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
}
// Frequent acts only: Capture (item/track), Re-capture (between capture and
// placement), Placement (insert/insert-conform). The four rare variants live in
// the overflow menu (overflowMenuRows) — same actions, different home.
// Frequent acts only: Capture (item/track), Placement (insert/insert-as-fx).
// Re-capture and Insert Conform moved to the overflow menu (overflowMenuRows) —
// same actions, different home.
std::vector<ActionBarRow> topBarRows() {
std::vector<ActionBarRow> rows;
for (const CaptureActionDef& def : captureActionTable()) {
@@ -121,13 +121,8 @@ std::vector<ActionBarRow> topBarRows() {
rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
ActionCluster::Capture, true});
}
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
"re-capture from source", ActionCluster::Maintenance, true});
rows.push_back({"INSERT_SELECTED", "Insert",
"insert selected sample at edit cursor", ActionCluster::Placement, true});
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
"insert selected sample at edit cursor (conform to tempo)",
ActionCluster::Placement, true});
// The one top-bar row with a live/dead gate: it places the PLAYER, and one instance
// holds one capture. Only the payload half of the fold is knowable at draw time —
// core/ui/insert_fx_enable.h states why the track half stays a press-time message.
@@ -142,8 +137,11 @@ std::vector<ActionBarRow> topBarRows() {
return rows;
}
// The four rare batch/realtime capture variants pulled off the visible bar, plus
// Cancel RT. fullName is the popup entry text (shortLabel is unused for menu items).
// The rare batch/realtime capture variants and Cancel RT, plus Re-capture and
// Insert Conform (moved off the top bar to make room for the frequent-acts-only
// bar). fullName is the popup entry text (shortLabel is unused for menu items).
// Ordered by cluster (Capture -> Maintenance -> Placement), same convention as
// topBarRows.
std::vector<ActionBarRow> overflowMenuRows() {
return {
{"CAPTURE_BATCH_ITEMS", "Batch Items",
@@ -152,8 +150,13 @@ std::vector<ActionBarRow> overflowMenuRows() {
"batch capture razor areas (one per area)", ActionCluster::Capture, true},
{"CAPTURE_TRACK_REALTIME", "Capture RT",
"capture selected track (realtime)", ActionCluster::Capture, true},
{"RECAPTURE_FROM_SOURCE", "Re-capture",
"re-capture from source", ActionCluster::Maintenance, true},
{"CANCEL_REALTIME_CAPTURE", "Cancel RT",
"cancel realtime capture", ActionCluster::Maintenance, true},
{"INSERT_SELECTED_CONFORM", "Insert Conform",
"insert selected sample at edit cursor (conform to tempo)",
ActionCluster::Placement, true},
};
}
+33 -47
View File
@@ -30,15 +30,13 @@ 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: 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).
// The panel's real inventory shape: 2 capture, 2 placement (Insert / Insert as FX) = 4
// buttons. Re-capture and Insert Conform moved to the overflow menu, so the top bar's
// Maintenance cluster is now empty. Cluster order: Capture -> Placement.
static std::vector<ClusterSpec> inventory() {
return {
{ActionCluster::Capture, 2},
{ActionCluster::Maintenance, 1},
{ActionCluster::Placement, 3},
{ActionCluster::Placement, 2},
};
}
@@ -55,28 +53,26 @@ static ActionBarSpec roundSpec() {
// --- Layout: all fit, correct rects + gaps ------------------------------------
// 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).
// A wide bar fits all 4 buttons. Verify the first few rects, the intra-cluster gap, and the
// (wider) inter-cluster gap between button 1 (last capture) and button 2 (first placement).
//
// Pixel walk with roundSpec and bar at (0,40):
// btn0 (Capture): x=8, right=108
// btn1 (Capture): x=112, right=212 (intraGap = 4 after btn0's right)
// 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)
// btn5 (Placement): x=552, right=652 (intraGap = 4 after btn4's right)
// Minimum bar width: 652 + sidePad(8) = 660. Give it 700.
// btn0 (Capture): x=8, right=108
// btn1 (Capture): x=112, right=212 (intraGap = 4 after btn0's right)
// btn2 (Placement): x=228, right=328 (clusterGap = 16 after btn1's right)
// btn3 (Placement): x=332, right=432 (intraGap = 4 after btn2's right)
// Minimum bar width: 432 + sidePad(8) = 440. Give it 500.
static void testAllFitRectsAndGaps() {
const auto clusters = inventory();
const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 40, 700, 34};
ActionBarRect bar{0, 40, 500, 34};
BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 6);
CHECK(fit.visibleCount == 4);
CHECK(fit.hiddenCount == 0);
auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 6);
CHECK(slots.size() == 4);
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
CHECK(slots[0].x == 8);
@@ -90,27 +86,17 @@ static void testAllFitRectsAndGaps() {
CHECK(slots[1].x == 112);
CHECK(slots[1].cluster == ActionCluster::Capture);
// Button 2 (maintenance / Re-capture): cluster gap of 16 after 212 -> 228.
// Button 2 (first placement): cluster gap of 16 after 212 -> 228.
CHECK(slots[2].x == 228);
CHECK(slots[2].cluster == ActionCluster::Maintenance);
CHECK(slots[2].cluster == ActionCluster::Placement);
CHECK(slots[2].index == 2);
// Button 3 (first placement): cluster gap of 16 after 328 -> 344.
CHECK(slots[3].x == 344);
// Button 3 (Insert as FX — second placement): intra-cluster gap of 4 after 328 -> 332.
// It joins the Placement cluster, so no cluster gap opens before it.
CHECK(slots[3].x == 332);
CHECK(slots[3].cluster == ActionCluster::Placement);
CHECK(slots[3].index == 3);
// Button 4 (second placement): intra-cluster gap of 4 after 444 -> 448.
CHECK(slots[4].x == 448);
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);
CHECK(slots[3].x - (slots[2].x + slots[2].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
@@ -159,14 +145,14 @@ static void testLabelFullHeightWhenShort() {
// keep their full width (never clipped), and the frequent capture cluster survives.
//
// Exact fit for 2 capture buttons: sidePad(8) + 2*100 + 1*4 + sidePad(8) = 220.
// A 3rd button (Maintenance) needs clusterGap(16)+100 = 116 more -> 336. So 220 fits exactly 2.
// A 3rd button (Placement) needs clusterGap(16)+100 = 116 more -> 336. So 220 fits exactly 2.
static void testOverflowDropsTrailingWhole() {
const auto clusters = inventory();
const ActionBarSpec spec = roundSpec();
ActionBarRect bar{0, 0, 220, 34};
BarFit fit = computeBarFit(bar, clusters, spec);
CHECK(fit.visibleCount == 2);
CHECK(fit.hiddenCount == 4);
CHECK(fit.hiddenCount == 2);
auto slots = computeBarSlots(bar, clusters, spec);
CHECK(slots.size() == 2);
@@ -186,7 +172,7 @@ 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 == 6);
CHECK(fit.hiddenCount == 4);
CHECK(computeBarSlots(bar, clusters, spec).empty());
}
@@ -246,9 +232,9 @@ static ActionBarSpec realBarSpec() {
}
// 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.
// all default-constructed here since its defaults already match), 520 is the narrowest
// band that still shows all four buttons; one pixel narrower drops Insert as FX (flat index
// 3, the trailing Placement button) alone into overflow, leaving its three neighbours intact.
static void testReserveDropsInsertAsFxAtRealWidth() {
const auto clusters = inventory();
const ActionBarSpec spec = realBarSpec();
@@ -256,18 +242,18 @@ static void testReserveDropsInsertAsFxAtRealWidth() {
const int reserve = menuButtonReserve(MenuBarRect{0, 0, 900, 34}, menuSpec);
CHECK(reserve == 40);
const int allSixWidth = 764;
const int allFourWidth = 520;
{
ActionBarRect action{0, 0, allSixWidth - reserve, 34};
ActionBarRect action{0, 0, allFourWidth - reserve, 34};
const auto slots = computeBarSlots(action, clusters, spec);
CHECK(slots.size() == 6);
CHECK(slots.back().index == 5); // Insert as FX still visible
CHECK(slots.size() == 4);
CHECK(slots.back().index == 3); // Insert as FX still visible
}
{
ActionBarRect action{0, 0, allSixWidth - 1 - reserve, 34};
ActionBarRect action{0, 0, allFourWidth - 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
CHECK(slots.size() == 3); // Insert as FX alone dropped
CHECK(slots.back().index == 2); // Insert is now the trailing visible button
}
}