diff --git a/src/shell/panel/draw_kit.cpp b/src/shell/panel/draw_kit.cpp index c734d01..4f7c9bd 100644 --- a/src/shell/panel/draw_kit.cpp +++ b/src/shell/panel/draw_kit.cpp @@ -1,17 +1,14 @@ // draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h. -// -// Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that -// touches LICE + SWELL. All colors come from the pure `theme` module; all geometry from -// the pure `component_geometry` module. DAW-verified, not unit-tested. +// SHELL: the only kit file that touches LICE + SWELL. Colors come from `theme`; +// geometry from `component_geometry`. DAW-verified, not unit-tested. #include "shell/panel/draw_kit.h" #include -#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure) +#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve -// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is -// provided by the host. Mirrors the panel TUs' (shell/panel/) include discipline. +// On Windows use native Win32 (windows.h first); on mac/linux SWELL is provided by the host. #ifdef _WIN32 #include #else @@ -23,7 +20,6 @@ namespace reasampler { -// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired). using audio::ChannelEnvelope; using audio::columnMinMax; using audio::MinMax; @@ -32,24 +28,18 @@ using ui::roleColor; using ui::roleColorState; using ui::spectralColor; -// --- KitColor <-> LICE boundary ---------------------------------------------- - -// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a) -// (lice.h:57). The theme owns the color; the shell owns the packing. Declared in draw_kit.h -// so shell translation units (bank_panel) can use it without duplicating the LICE_RGBA pack. +// The one place a pure KitColor becomes a LICE_pixel (LICE_RGBA(r,g,b,a), verified against +// lice.h). The theme owns the color; the shell owns the packing. LICE_pixel toLice(const KitColor& c) { return LICE_RGBA(c.r, c.g, c.b, c.a); } namespace { -// The draw alpha the LICE primitives take (0..1), from the KitColor's 8-bit alpha. Used so -// a disabled surface (alpha 0.4) composites at the right opacity — LICE_FillRect etc. take -// a float alpha argument separate from the pixel's own alpha byte. +// LICE_FillRect etc. take a float alpha (0..1) separate from the pixel's own alpha byte — +// this converts a KitColor's 8-bit alpha so a disabled surface composites at the right opacity. float drawAlpha(const KitColor& c) { return c.a / 255.0f; } -// --- Font set (owned by the kit) --------------------------------------------- - struct KitFonts { LICE_CachedFont title; LICE_CachedFont label; @@ -92,8 +82,8 @@ UINT alignFlag(Align a) { return DT_LEFT; } -// A 1px inner highlight on the top edge and shadow on the bottom edge — the vwnd trick -// that gives a flat fill dimension (§2.2). Lightens the top row, darkens the bottom row. +// A 1px inner highlight on the top edge and shadow on the bottom edge gives a flat fill +// dimension without a border. void innerEdges(LICE_IBitmap* bmp, const KitBox& b, float alpha) { if (b.width < 2 || b.height < 2) return; const LICE_pixel hi = LICE_RGBA(255, 255, 255, 255); @@ -111,9 +101,8 @@ void fillGradient(LICE_IBitmap* bmp, const KitBox& b, const KitColor& top, const KitColor& bottom) { if (b.empty()) return; const float a = drawAlpha(top); - // LICE_GradRect wants initial R/G/B/A (0..1) and per-axis deltas. Verified signature - // lice.h:466 — ir..ia are the top-left color; drdy..dady ramp DOWN the height so the - // bottom row reaches `bottom`. No horizontal ramp (drdx.. = 0). + // LICE_GradRect (lice.h) takes initial R/G/B/A plus per-axis deltas: ir..ia are the + // top-left color, drdy..dady ramp DOWN the height so the bottom row reaches `bottom`. const float ir = top.r / 255.0f, ig = top.g / 255.0f, ib = top.b / 255.0f; const float dr = (bottom.r - top.r) / 255.0f; const float dg = (bottom.g - top.g) / 255.0f; @@ -145,12 +134,8 @@ RECT toRect(const KitBox& b) { } // namespace -// --- Font lifecycle ---------------------------------------------------------- - void kitFontsInit() { if (g_fonts.ready) return; // idempotent - // §3.1 type scale: title ~15px semibold, label ~12px, value-mono ~12px tabular, - // micro ~10px. Segoe UI (universal on the Windows target); Consolas for numerics. loadFont(g_fonts.title, 15, FW_SEMIBOLD, "Segoe UI"); loadFont(g_fonts.label, 12, FW_NORMAL, "Segoe UI"); loadFont(g_fonts.valueMono, 12, FW_NORMAL, "Consolas"); @@ -160,11 +145,8 @@ void kitFontsInit() { void kitFontsShutdown() { if (!g_fonts.ready) return; // idempotent - // LICE_CachedFont's destructor frees its OWNS_HFONT HFONT. Re-assigning an empty font - // via SetFromHFont(nullptr) would leak nothing but also do nothing useful; instead we - // mark not-ready and let the fonts release their HFONTs when g_fonts is reset. Because - // g_fonts is a static instance (not re-created), free the HFONTs explicitly by handing - // each a null font, which OWNS semantics clean up the prior HFONT (lice_text.h:41). + // g_fonts is a static instance, never re-created, so free the HFONTs explicitly: + // handing each a null font with OWNS_HFONT cleans up the prior HFONT (lice_text.h). g_fonts.title.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT); g_fonts.label.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT); g_fonts.valueMono.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT); @@ -172,8 +154,6 @@ void kitFontsShutdown() { g_fonts.ready = false; } -// --- Text -------------------------------------------------------------------- - void text(LICE_IBitmap* bmp, const KitBox& box, const char* str, Font font, const KitColor& color, Align align) { if (!bmp || !str || box.empty()) return; @@ -191,8 +171,6 @@ void text(LICE_IBitmap* bmp, const KitBox& box, const char* str, text(bmp, box, str, font, roleColor(role), align); } -// --- Surfaces + components ---------------------------------------------------- - void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state) { if (!bmp || box.empty()) return; const KitColor base = roleColorState(role, state); @@ -211,9 +189,8 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label KitColor top, bottom; gradientPair(base, top, bottom); - // Rounded surface: fill the interior gradient, then an AA rounded border. Corner - // radius scales gently with height, clamped so tiny buttons stay legible. fillGradient(bmp, b, top, bottom); + // Corner radius scales with height, clamped so tiny buttons stay legible. const int radius = b.height >= 20 ? 5 : (b.height >= 12 ? 3 : 2); const KitColor borderCol = (state == InteractionState::Active || state == InteractionState::Focus) @@ -224,9 +201,7 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label radius, toLice(borderCol), drawAlpha(borderCol), 0, true); if (label && *label) { - // Active fill is the accent — draw its label in the base bg for contrast; else - // text/primary (disabled dims via the state on the surface, label stays primary - // but the whole control reads recessed). + // Active fill is the accent — label goes in bg/base for contrast; else text/primary. const Role textRole = (state == InteractionState::Active) ? Role::BgBase : Role::TextPrimary; @@ -237,10 +212,8 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state) { if (!bmp || geom.track.empty()) return; - // Track groove: the cell surface, recessed (pressed-ish) so it reads as a channel. fillSurface(bmp, geom.track, Role::BgCell, InteractionState::Pressed); - // Filled portion up to the handle: the accent (hover/dragging brighten it). if (!geom.filled.empty()) { const InteractionState fillState = (state == InteractionState::Hover || state == InteractionState::Dragging) @@ -251,7 +224,6 @@ void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState fillGradient(bmp, geom.filled, top, bottom); } - // Handle: a raised knob honoring state. if (!geom.handle.empty()) { const KitButtonBox knob{geom.handle}; drawButton(bmp, knob, nullptr, state, /*warn=*/false); @@ -263,18 +235,16 @@ void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label, const KitBox& b = row.box; if (!bmp || b.empty()) return; - // Row surface: bg/cell transformed by state (hover lightens, active = accent). fillSurface(bmp, b, Role::BgCell, state); - // Focus ring: a 1px text/primary rectangle, distinct from the accent selection fill. + // Focus ring is text/primary, distinct from the accent selection fill. if (state == InteractionState::Focus) { const KitColor ring = roleColor(Role::TextPrimary); LICE_DrawRect(bmp, b.x, b.y, b.width - 1, b.height - 1, toLice(ring), drawAlpha(ring), 0); } - // Label in the width after the reserved thumbnail inset. Active rows draw the label in - // bg/base for contrast against the accent fill; else text/primary. + // Active rows draw the label in bg/base for contrast against the accent fill. if (label && *label) { const int inset = thumbWidth > 0 ? thumbWidth + 6 : 6; KitBox labelBox{b.x + inset, b.y, b.width - inset - 6, b.height}; @@ -314,13 +284,7 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) { if (bins.empty() || innerW <= 0) continue; - // Render one filled vertical span per pixel column. peaks::columnMinMax merges - // all bins that project to column `col` under the exact same partition as - // computeEnvelope used to build the envelope, so every pixel column is covered - // with no gaps regardless of the bins-to-pixels ratio. With one bin per column - // (kWaveformOversample == 1) each span covers the true min/max of exactly the - // frames that fall in that column. Same dB display compression everywhere - // (bank_grid, pure). + // One filled span per pixel column (see draw_kit.h — gap-free via columnMinMax). for (int col = 0; col < innerW; ++col) { const MinMax mm = columnMinMax(bins, innerW, col); const int x = box.x + 2 + col; diff --git a/src/shell/panel/draw_kit.h b/src/shell/panel/draw_kit.h index 311d308..e747276 100644 --- a/src/shell/panel/draw_kit.h +++ b/src/shell/panel/draw_kit.h @@ -1,37 +1,29 @@ #pragma once -// draw_kit — the LICE-facing SHELL half of the shared drawing kit (Phase L, L1). This is -// the ONE source of drawing for the whole system: every surface (bank_panel now; the VST -// editor + embed strip at L3) fills, buttons, rows, sliders, waveforms, and — above all — -// draws TEXT through this kit, so a control looks identical everywhere because it is the -// same kit function. It replaces the flat LICE_FillRect blocks and raw-GDI DrawTextA with -// gradient/AA surfaces (the vwnd micro-gradient + inner highlight/shadow trick) and cached -// anti-aliased text (LICE_CachedFont), honoring the interaction-state model. +// draw_kit — the LICE-facing SHELL half of the shared drawing kit: the ONE source of +// drawing for the whole system (bank_panel, the VST editor, and the embed strip all fill, +// button, row, slider, waveform, and draw TEXT through this same kit, so a control looks +// identical everywhere). // -// PURE/SHELL SPLIT (CLAUDE.md §load-bearing): this file is SHELL — it touches LICE and -// SWELL (HFONT). All palette decisions come from the pure `theme` module (role -> KitColor); -// all layout/hit-test from the pure `component_geometry` / mode_switch / etc. modules. This -// file only turns those pure answers into LICE calls. It is DAW-verified, not unit-tested. +// SHELL: touches LICE and SWELL (HFONT). Palette decisions come from the pure `theme` +// module (role -> KitColor); layout/hit-test from the pure `component_geometry` / +// mode_switch / etc. modules. This file only turns those pure answers into LICE calls. +// DAW-verified, not unit-tested. // -// FONT LIFECYCLE (owned here): the kit holds a small set of LICE_CachedFonts (title / label -// / value-mono / micro). kitFontsInit() creates them once (from HFONTs handed off with -// LICE_FONT_FLAG_OWNS_HFONT, so the cached font frees the HFONT itself — verified in -// lice_text.h §SetFromHFont doc: "OWNS means LICE_IFont will clean up hfont on font change -// or exit"). kitFontsShutdown() deletes the cached fonts. The consumer calls init on panel -// open and shutdown on close/teardown. text() no-ops safely before init (defensive), so a -// draw that races construction never crashes. +// Fonts: kitFontsInit() hands each LICE_CachedFont an HFONT with +// LICE_FONT_FLAG_OWNS_HFONT, so the cached font frees the HFONT itself on shutdown/ +// reassignment. text() no-ops safely before init, so a draw that races construction +// never crashes. // -// DOUBLE-BUFFER DISCIPLINE (§3.5 "zero-jank"): every function here draws into the caller's -// offscreen LICE_IBitmap; the caller BitBlt's once. Nothing here draws direct-to-DC. +// Every function draws into the caller's offscreen LICE_IBitmap; the caller BitBlt's +// once. Nothing here draws direct-to-DC. -#include "core/ui/component_geometry.h" // KitBox / SliderGeometry — the pure geometry the shell draws -#include "core/audio/peaks.h" // Envelope — the waveform primitive's input +#include "core/ui/component_geometry.h" // KitBox / SliderGeometry +#include "core/audio/peaks.h" // Envelope #include "core/ui/theme.h" // Role / InteractionState / KitColor / TextClass -// LICE types at the boundary (this is the shell half). LICE_IBitmap is forward-declared -// to keep the header light. LICE_pixel is a typedef (unsigned int) — not forward-declarable -// — so the full lice.h is included only for the toLice() declaration; on Windows lice.h -// pulls in , which is fine since draw_kit.h is shell-only and never included by -// a pure module. +// LICE_pixel is a typedef (unsigned int), not forward-declarable, so the full lice.h is +// included for the toLice() declaration; lice.h pulls in on Windows, which is +// fine since this header is shell-only and never included by a pure module. #ifdef _WIN32 #include #endif @@ -40,10 +32,7 @@ class LICE_IBitmap; namespace reasampler { -// Real-namespace-home using-declarations (Q-W6: the interim core/namespaces.h shim -// is retired; the kit's pure vocabulary names its Q-W1 homes explicitly). These are -// deliberate re-exports: every draw_kit consumer speaks these types at the call -// boundary, so they surface here exactly as panel_state.h surfaces the panel's. +// Re-exports: every draw_kit consumer speaks these types at the call boundary. using audio::Envelope; using ui::InteractionState; using ui::KitBox; @@ -53,8 +42,8 @@ using ui::ListRowBox; using ui::Role; using ui::SliderGeometry; -// The kit's four cached fonts (§3.1 type scale). Consumers pass a Font to text() to pick -// the size/weight; the kit maps it to the matching LICE_CachedFont. +// The kit's four cached fonts. Consumers pass a Font to text() to pick the size/weight; +// the kit maps it to the matching LICE_CachedFont. enum class Font { Title, // ~15px semibold — region titles, headings Label, // ~12px regular — labels, body @@ -66,90 +55,59 @@ enum class Font { // single-line convention); a caller wanting multi-line composes rows itself. enum class Align { Left, Center, Right }; -// --- KitColor → LICE_pixel conversion ---------------------------------------- - -// The one place a pure KitColor becomes a LICE_pixel. Declared here so any shell -// translation unit that already includes draw_kit.h can use it without duplicating -// the LICE_RGBA packing. Defined in draw_kit.cpp. +// The one place a pure KitColor becomes a LICE_pixel. Defined in draw_kit.cpp. LICE_pixel toLice(const KitColor& c); -// --- Font lifecycle (owned by the kit) --------------------------------------- - -// Creates the four cached fonts once. Idempotent: a second call before shutdown is a no-op -// (the kit already holds live fonts). Safe to call on every panel open. Uses the platform -// UI sans (Segoe UI) for title/label/micro and a tabular mono (Consolas) for value-mono; -// the exact HFONT is created here, so a face change is a one-line edit. NO-OP-SAFE: if font -// creation fails, text() degrades to drawing nothing rather than crashing. +// Creates the four cached fonts once; idempotent. Segoe UI for title/label/micro, +// Consolas (tabular) for value-mono. No-op-safe: if font creation fails, text() +// draws nothing rather than crashing. void kitFontsInit(); -// Deletes the cached fonts (which free their owned HFONTs — LICE_FONT_FLAG_OWNS_HFONT). -// Idempotent. The consumer calls this on panel close / extension shutdown. +// Frees the owned HFONTs. Idempotent. Call on panel close / extension shutdown. void kitFontsShutdown(); -// --- Text (the single biggest "temple os -> modern" lever) ------------------- - -// Draws a single line of AA cached-font text in `color` inside `box`, horizontally aligned -// per `align` and vertically centered, clipped with an end-ellipsis. This REPLACES the -// GDI SetTextColor + DrawText path. No-op (safe) before kitFontsInit() or on a null bitmap. +// Draws a single line of AA cached-font text in `color` inside `box`, horizontally +// aligned per `align` and vertically centered, clipped with an end-ellipsis. No-op +// before kitFontsInit() or on a null bitmap. void text(LICE_IBitmap* bmp, const KitBox& box, const char* str, Font font, const KitColor& color, Align align); -// Convenience overload: text in a palette ROLE's color (the common case — the shell almost -// always wants text/primary or text/dim, not a raw color). +// Convenience overload: text in a palette ROLE's color. void text(LICE_IBitmap* bmp, const KitBox& box, const char* str, Font font, Role role, Align align); -// --- Surfaces + components ---------------------------------------------------- - -// The kit's foundational fill: a micro-gradient (a few percent lighter at the top, via -// LICE_GradRect) plus a 1px inner top-highlight and bottom-shadow — the vwnd trick that -// kills the flat look (§2.2). Every button/row/cell fills through this so elevation reads -// without a border. `role` picks the surface color; `state` transforms it per the -// interaction model (hover lightens, pressed darkens, disabled desaturates, etc.). +// The kit's foundational fill: a micro-gradient plus a 1px inner top-highlight/ +// bottom-shadow, so elevation reads without a border. `state` transforms the role +// color (hover lightens, pressed darkens, disabled desaturates). void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state); -// A rounded, gradient-filled button with the inner highlight/shadow and a centered label, -// honoring the interaction state. `warn == true` swaps the surface to the warn role (for -// byte-deleting verbs like prune/delete) — the only place warn is drawn. A degenerate box -// is a no-op. +// A rounded, gradient-filled button with a centered label. `warn == true` swaps the +// surface to the warn role — the only place warn is drawn. Degenerate box is a no-op. void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label, InteractionState state, bool warn); -// A horizontal slider: the track groove, the accent-filled portion up to the handle, and -// the handle (a raised knob honoring state — hover/dragging brighten it). `geom` is the -// pure SliderGeometry the caller computed; the kit only draws it. Degenerate geom is a no-op. +// A horizontal slider: track groove, accent-filled portion up to the handle, and the +// handle itself. `geom` is the pure SliderGeometry the caller computed. void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state); -// A selectable list row: the row surface (rest/hover/active/focus via state), an optional -// leading thumbnail area reserved at `thumbWidth` px (0 for none — the caller draws the -// thumbnail into the returned-by-convention left inset), and a left-aligned label in the -// remaining width. Focus draws a 1px text/primary ring distinct from the accent selection -// fill. A degenerate row is a no-op. +// A selectable list row: row surface, an optional leading thumbnail inset +// (`thumbWidth`, 0 for none), and a left-aligned label. Focus draws a 1px ring +// distinct from the accent selection fill. void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label, int thumbWidth, InteractionState state); -// waveformColumnCount — declared in component_geometry.h (already included above). Returns -// the drawable column count inside `box` (box.width minus the fixed 2px insets each side). -// Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope; -// overbinning (more bins than columns) costs memory and CPU without changing a rendered -// pixel — peaks::columnMinMax's exact partition already makes the draw gap-free. - -// Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing -// call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of -// 1 means they request exactly one bin per column, which is correct. The gap-free render -// comes from peaks::columnMinMax's exact partition, NOT from extra bins. +// Callers pass waveformColumnCount(box) as computeEnvelope's `binCount` — overbinning +// costs memory/CPU without changing a rendered pixel, since columnMinMax's exact +// partition already makes the draw gap-free at any bins-to-pixels ratio. Kept at 1 (no +// oversampling); present so existing call sites `kWaveformOversample * +// waveformColumnCount(box)` compile unchanged. inline constexpr int kWaveformOversample = 1; -// A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per -// channel and one accent vertical span PER PIXEL COLUMN, each column covering the true -// extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any -// bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope -// does, so every pixel column is always covered). The ONE waveform shape in the system: -// the dock-panel thumbnail, the browser cards, and the editor hero all render through -// this. `box` is the draw region; `env` is the per-channel min/max envelope from -// peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count). -// An empty env draws just the midline. The caller fills the surface first (or passes a -// box already filled); this draws only the wave + midline. +// A waveform drawn as a min/max plot: a midline per channel and one accent vertical +// span per pixel column (peaks::columnMinMax — gap-free at any bins-to-pixels ratio). +// The ONE waveform shape in the system: dock-panel thumbnail, browser cards, and +// editor hero all render through this. `env` is sized to waveformColumnCount(box) +// bins (clamped to frame count); an empty env draws just the midline. void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env); } // namespace reasampler diff --git a/src/shell/panel/panel_audition.cpp b/src/shell/panel/panel_audition.cpp index d05136f..407563a 100644 --- a/src/shell/panel/panel_audition.cpp +++ b/src/shell/panel/panel_audition.cpp @@ -1,19 +1,14 @@ -// panel_audition.cpp — the audition/preview engine seam of the docked bank panel -// (Q-W2 split of bank_panel.cpp; M5 Wave B). HOT PATH GUARDRAIL (T4-28 / Q-W2): the -// preview path stays a DIRECT free-function call-through — no interface, no virtual -// dispatch, no added header->TU indirection; the idle path is unchanged in shape. +// panel_audition.cpp — the audition/preview engine seam of the docked bank panel. +// Hot-path guardrail: the preview path stays a direct free-function call-through — +// no interface, no virtual dispatch, no added header->TU indirection. // -// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested. +// DAW-verified, not unit tested. main.cpp owns the API pointers; here they are extern. #include #include "shell/panel/panel_state.h" -// Stock preview API (verified against reaper_plugin.h / reaper_plugin_functions.h): -// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the -// STOCK symbols (not SWS-only) — see the audition section below. +// PlayPreview/StopPreview (stock, not SWS-only) drive a caller-owned preview_register_t. #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_PlayPreview #define REAPERAPI_WANT_StopPreview @@ -23,26 +18,18 @@ namespace reasampler::panel { -// --- Audition preview --------------------------------------------------------- +// Audition is preview playback only — never inserts into the arrange or mutates +// the project/bank. PlayPreview streams a caller-owned PCM_source through +// REAPER's preview bus. // -// READ-ONLY / NON-DESTRUCTIVE (load-bearing principle): audition is PREVIEW -// playback only. It NEVER inserts into the arrange, creates items/tracks, or -// mutates the project or bank. PlayPreview streams a caller-owned PCM_source -// through REAPER's preview bus and touches nothing in the project. -// -// FLAGGED RUNTIME ASSUMPTIONS (header does not specify these; verified only by -// signature/struct, not semantics — DAW-verify): -// 1. REAPER's audio thread reads the preview_register_t by POINTER while the -// preview is active (the struct's own comment mandates a cs/mutex we init), -// so the register must outlive playback — we hold it in g_panel (static), -// never on the stack. -// 2. StopPreview is assumed to detach the source from the audio thread BEFORE it -// returns, making it safe to PCM_Source_Destroy the source immediately after. -// This is the conventional contract (SWS' preview helpers rely on it) but is -// NOT documented in the header — flagged. If a rare race surfaced, the fix is -// a StartPreviewFade + deferred free; not done now (YAGNI, no evidence). -// 3. m_out_chan == 0 routes to the first hardware output pair (stereo). We do not -// set mono (&1024). volume 1.0, loop false, curpos 0. +// Runtime assumptions not documented in the SDK header (DAW-verify, not asserted): +// 1. The audio thread reads preview_register_t by pointer while active, so it +// must outlive playback — held in g_panel (static), never on the stack. +// 2. StopPreview is assumed to detach the source before returning, so +// PCM_Source_Destroy immediately after is safe (SWS' preview helpers rely on +// the same contract). If a race ever surfaces, the fix is a StartPreviewFade +// + deferred free. +// 3. m_out_chan == 0 routes to the first hardware output pair (stereo, not mono). void initPreview() { if (g_panel.previewInited) return; @@ -76,8 +63,7 @@ void deinitPreview() { g_panel.previewInited = false; } -// Auditions the sample at selection ordinal `idx` of the FOCUSED region's displayed bank. -// L7: `idx` is a DISPLAY-order (slot) ordinal, resolved through orderedIds, not a raw +// `idx` is a display-order (slot) ordinal, resolved through orderedIds, not a raw // BankModel position. void startAudition(int idx) { stopAudition(); diff --git a/src/shell/panel/panel_bank_ops.cpp b/src/shell/panel/panel_bank_ops.cpp index 9b4fce0..0ce2fbb 100644 --- a/src/shell/panel/panel_bank_ops.cpp +++ b/src/shell/panel/panel_bank_ops.cpp @@ -1,14 +1,12 @@ -// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel -// (Q-W2 split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the -// promptless bank verbs live in shell/bank_ops (model op + persistBankOp, taking -// ReaSamplerSession&); this TU is the panel's THIN UX SKIN over them — the menu -// handlers (prompts / confirms / message boxes / panel-state nudges / repaint), -// the book/bank accessors, the popup menus that drive them, and the selection-id / -// OS-drag path resolvers. The bindable bank_actions family is the sibling skin. +// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel. The +// promptless bank verbs live in shell/bank_ops (bankOp* + persistBankOp); this TU is +// the panel's THIN UX SKIN over them — menu handlers, book/bank accessors, popup +// menus, and the selection-id / OS-drag path resolvers. `bank_actions` is the +// sibling bindable-action skin. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested. +// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers. DAW-verified, not +// unit-tested. #include #include @@ -18,7 +16,7 @@ #include "shell/panel/panel_state.h" #include "shell/panel/panel_bank_ops.h" -#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (the Q-W6 non-UI seam) +#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs #include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate #define REAPERAPI_MINIMAL @@ -32,7 +30,7 @@ namespace reasampler::panel { namespace fs = std::filesystem; -// --- Current-project directory (mirrors the persist shell's derivation, ext_state_io.cpp) +// Mirrors the persist shell's derivation (ext_state_io.cpp). std::string currentProjectDir() { std::vector buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(buf.size())); @@ -41,8 +39,6 @@ std::string currentProjectDir() { return normalizeSlashes(fs::path(rpp).parent_path().string()); } -// --- Book / bank accessors ---------------------------------------------------- - BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; } // The BankModel a region currently displays. Pool region -> the pool; banks region -> @@ -72,17 +68,9 @@ std::vector namedBanks() { return out; } -// --- Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs) ------ -// -// Q-W4/Q-W6: each handler here owns only the panel's UX (prompts / confirms / -// message boxes / panel-state nudges / repaint); the model op + persist is the -// shared bankOp* inner verb (shell/bank_ops), which takes the live session by -// reference — the book() check answers the one session-liveness question per -// handler. After a STRUCTURAL mutation (create/delete/evacuate) any -// Bank*/BankModel& is invalid — we resolve fresh, pass ids, and let the next -// refreshFingerprint repaint. On an unsaved project the empty-close discard in -// persistBankOp ensures no stale state survives (matches the capture/B3 -// quiet-persist idiom). +// Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs). After a +// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we +// resolve fresh, pass ids, and let the next refreshFingerprint repaint. void doCreateBank() { if (!book()) return; @@ -116,9 +104,9 @@ void doRenameBank(const std::string& bankId) { invalidatePanel(); } -// Delete with the RICHER confirm-on-non-empty affordance (B4): the confirm names the -// member count AND offers evacuate as the one-click alternative (Yes=delete anyway, -// No=evacuate-then-keep, Cancel=abort) — richer than B3's basic YESNO. +// Delete with a confirm-on-non-empty affordance: the confirm names the member count +// and offers evacuate as the one-click alternative (Yes=delete anyway, +// No=evacuate-then-keep, Cancel=abort). void doDeleteBank(const std::string& bankId) { if (!book()) return; const Bank* bk = book()->bank(bankId); @@ -144,13 +132,10 @@ void doDeleteBank(const std::string& bankId) { } // r == 6 (Yes) falls through to a plain delete (drops members). } - // S9: bump when the bank held samples (either the Yes-drop path or the No-evacuate-then- - // delete path moved/dropped members) — both change what a live instance could play. An - // empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides - // (the No-path evacuated them moments ago, but the membership still changed). + // Bump generation when the bank held samples — an empty-bank delete is purely + // organizational. The ORIGINAL member count decides (the No-path already evacuated them). if (!bankOpDelete(*g_panel.session, bankId, /*bumpGeneration=*/members > 0)) return; - // shownBankId is reconciled by the next fingerprint pass. If no named banks remain, - // nudge focus to the pool so the selection has a valid home. + // If no named banks remain, nudge focus to the pool so the selection has a valid home. if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool; invalidatePanel(); } @@ -172,40 +157,33 @@ void doActivateBank(const std::string& bankId) { } // namespace // Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Thin panel -// skin over bankOpTransfer (the one-home verb owns the loop, the verb-aware no-op -// guardrail, and the undo-batched persist); this layer clears the stale selection -// and repaints on an actual mutation. +// skin over bankOpTransfer; clears the stale selection and repaints on an actual mutation. void transferSamples(const std::vector& sampleIds, const std::string& srcBankId, const std::string& destBankId, bool copy) { if (!book()) return; // no live session — nothing to transfer within if (!bankOpTransfer(*g_panel.session, sampleIds, srcBankId, destBankId, copy)) return; // nothing changed — no persist, no undo point - // The selection indexed into the source; after a move those indices are stale, so - // clear it (the fingerprint pass will also clear, but do it now for immediacy). + // Selection indexed into the source; after a move those indices are stale. g_panel.selection = Selection{}; invalidatePanel(); } // Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Thin panel skin -// over bankOpRemove — see the verb for the never-deletes-bytes / silent-remove / -// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal. +// over bankOpRemove. Clears the stale selection and repaints on an actual removal. void removeSamples(const std::vector& sampleIds, const std::string& srcBankId) { if (!book()) return; // no live session — nothing to remove from if (!bankOpRemove(*g_panel.session, sampleIds, srcBankId)) return; // nothing changed — no persist, no undo point - // The selection indexed into the source; after a remove those indices are stale, so - // clear it (the fingerprint pass will also clear, but do it now for immediacy). g_panel.selection = Selection{}; invalidatePanel(); } // The selection's sample ids resolved against the FOCUSED region's bank (source of a -// move/copy). Returns ids in bank order; empty when nothing selected. +// move/copy). Selection ordinals index the DISPLAY (slot) order, not BankModel +// insertion order. Returns ids in bank order; empty when nothing selected. std::vector focusedSelectionIds() { - // L7: selection ordinals index the DISPLAY (slot) order, not BankModel insertion order. - // orderedIds[i] is the id at selection ordinal i. std::vector ids; const RegionDisplay disp = focusedDisplay(); const int count = disp.occupiedCount(); @@ -214,14 +192,11 @@ std::vector focusedSelectionIds() { return ids; } -// Resolves the ARMED drag payload (g_panel.dragSampleIds, from g_panel.dragSourceBankId) to -// the absolute, existing-file path list for a native OS drag-out (M11). Reuses the SAME M4 -// path machinery the panel uses for audition/insert (resolveBankFile over the current -// project dir) — no temp copies; the drag points straight at the on-disk bank files. Each -// id is looked up in its SOURCE bank's index (the payload's origin, not the focused region, -// which can differ once the pointer roams), resolved, stat'd, then handed to the pure -// drag_out::assemblePathList for dedupe + skip-missing/unresolved policy. Read-only: no -// mutation of sample / index / selection (invariant #2). +// Resolves the ARMED drag payload to the absolute, existing-file path list for a native +// OS drag-out. Reuses resolveBankFile (audition/insert's path machinery) — no temp +// copies. Each id is looked up in its SOURCE bank's index (not the focused region, which +// can differ once the pointer roams), then handed to drag_out::assemblePathList for +// dedupe + skip-missing/unresolved policy. Read-only. std::vector resolveDragPathsForOs() { std::vector resolved; BankBook* b = book(); @@ -242,19 +217,13 @@ std::vector resolveDragPathsForOs() { return assemblePathList(resolved).paths; } -// --- Popup menus -------------------------------------------------------------- -// -// SWELL/Win32 both expose CreatePopupMenu / InsertMenu (SWELL aliases SWELL_InsertMenu -// -> InsertMenu) / TrackPopupMenu(TPM_RETURNCMD) / DestroyMenu. We build a menu of -// (label -> small int command), track it at screen coords, and switch on the return. -// Menu command ids are LOCAL to the popup (not REAPER action ids) — TPM_RETURNCMD -// hands the chosen id straight back, so no hookcommand routing is involved. +// SWELL/Win32 both expose CreatePopupMenu / InsertMenu / TrackPopupMenu(TPM_RETURNCMD) / +// DestroyMenu. Menu command ids below are LOCAL to the popup (not REAPER action ids) — +// TPM_RETURNCMD hands the chosen id straight back, so no hookcommand routing is involved. namespace { -// Appends a string item (id) to `menu` at its end. Portable over Win32/SWELL: both -// accept InsertMenu(menu, pos, MF_BYPOSITION|MF_STRING, id, text) with a negative -// position appending. Win32 and SWELL both treat pos < 0 as an append. +// Win32 and SWELL both treat pos < 0 as append. void menuAppend(HMENU menu, unsigned int id, const char* text, bool grayed = false) { UINT flags = MF_BYPOSITION | MF_STRING; if (grayed) flags |= MF_GRAYED; @@ -272,7 +241,7 @@ enum : unsigned int { kMenuDelete, kMenuEvacuate, kMenuCreate, - kMenuRemove, // remove selected sample(s) from the source bank (B5) + kMenuRemove, // remove selected sample(s) from the source bank kMenuMoveBase = 1000, // move-to-bank: kMenuMoveBase + destination index kMenuCopyBase = 2000, // copy-to-bank: kMenuCopyBase + destination index }; @@ -313,11 +282,10 @@ void showTabMenu(int screenX, int screenY, const std::string& bankId) { } } -// Opens the top-toolbar overflow ("⋯" More) popup at the button's screen position and fires the -// chosen rare-capture variant's command (L5 refinement 1). Menu ids are LOCAL to the popup -// (1-based ordinal into overflowMenuRows); TPM_RETURNCMD hands the chosen id back, then we -// resolve + fire the corresponding registered command id via the SAME contract the visible -// buttons use. Defined here (after menuAppend/menuSeparator); forward-declared above. +// Opens the top-toolbar overflow ("⋯" More) popup and fires the chosen rare-capture +// variant's command. Menu ids are LOCAL to the popup (1-based ordinal into +// overflowMenuRows); we resolve + fire the corresponding registered command id via +// the same contract the visible buttons use. void showMoreMenu() { if (!g_panel.hwnd) return; const std::vector rows = overflowMenuRows(); @@ -349,9 +317,8 @@ void showMoreMenu() { } // Shows the move/copy menu for the current selection (the SOURCE is the focused -// region's bank). Lists every OTHER bank (pool + named) as a move destination, then a -// copy submenu-free flat list (copy entries follow the move block). Move is the -// default (listed first); copy is the deliberate secondary act. +// region's bank). Lists every OTHER bank as a move destination, then the same list +// as a copy destination. Move is the default (listed first); copy is secondary. void showSelectionMenu(int screenX, int screenY) { const std::vector sel = focusedSelectionIds(); if (sel.empty()) return; @@ -401,18 +368,14 @@ void showSelectionMenu(int screenX, int screenY) { } // namespace reasampler::panel -// --- Public API (panel_bank_ops.h) --------------------------------------------- - namespace reasampler { -// One home (Q-W4) for the former actions/panel byte-identical twins. -// COMMA GUARD: GetUserInputs splits returned values on a separator defaulting to ',', -// so the return separator is overridden to \x1f (un-typeable) via the documented -// `separator=X` trailing pseudo-caption (SDK ~3806) — any printable name round-trips. +// GetUserInputs splits returned values on a separator defaulting to ',', so the +// separator is overridden to \x1f (un-typeable) via the documented `separator=X` +// trailing pseudo-caption — any printable name round-trips. bool promptBankName(const char* title, const char* caption, const std::string& initial, std::string& out) { std::vector buf(512, '\0'); - // Pre-fill: GetUserInputs seeds the field from the retvals buffer's initial value. std::snprintf(buf.data(), buf.size(), "%s", initial.c_str()); const std::string captions = std::string(caption) + ",separator=\x1f"; if (!GetUserInputs(title, 1, captions.c_str(), buf.data(), @@ -424,8 +387,6 @@ bool promptBankName(const char* title, const char* caption, const std::string& i return true; } -// --- Selection read seam -------------------------------------------------------- - std::vector bankPanelSelectedSampleIds() { return panel::focusedSelectionIds(); } diff --git a/src/shell/panel/panel_bank_ops.h b/src/shell/panel/panel_bank_ops.h index 73877d0..8179b8e 100644 --- a/src/shell/panel/panel_bank_ops.h +++ b/src/shell/panel/panel_bank_ops.h @@ -1,55 +1,25 @@ #pragma once -// panel_bank_ops — the bank-CRUD-UX + selection-read seam of the bank panel (Q-W2 -// split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the -// promptless bank verbs themselves live in the NON-UI shell/bank_ops seam -// (bankOp* + persistBankOp, taking ReaSamplerSession&); this TU is the panel's -// thin UX skin over them — prompts / confirms / message boxes / panel-state -// nudges / repaints — plus the popup menus that drive them. The bank_actions -// bindable family is the sibling skin over the same verbs. This header carries -// the shared prompt helper and the panel's public selection-read surface. -// -// The selection reads are REAPER-free; the prompt helper is REAPER-facing (stock -// dialogs) but SDK-free in this header. +// panel_bank_ops — bank-CRUD-UX + selection-read seam of the bank panel. The +// promptless verbs themselves live in shell/bank_ops (bankOp* + persistBankOp); +// this TU is the panel's thin prompt/confirm/repaint skin over them, plus the +// popup menus that drive them. `bank_actions` is the sibling skin. #include #include namespace reasampler { -// Prompts the user for a single line of text via REAPER's stock input dialog -// (GetUserInputs). `initial` pre-fills the field. Returns false (leaving `out` -// untouched) on cancel or an empty entry. COMMA GUARD: the return separator is -// overridden to \x1f (un-typeable) via the documented `separator=X` pseudo-caption, -// so any printable name — commas included — round-trips whole (SDK ~3806/3808). -// One home (Q-W4) for the former actions/panel byte-identical twins; shared by the -// panel menus and the bank_actions bindable family. +// Via GetUserInputs. Returns false (leaving `out` untouched) on cancel or empty +// entry. Return separator is overridden to \x1f so any printable name round-trips. bool promptBankName(const char* title, const char* caption, const std::string& initial, std::string& out); -// The stable ids of the currently-selected samples, in bank (insertion) order. -// Empty when nothing is selected or the panel has never opened. This is the clean -// seam the `insert` action reads to know WHAT to place — it returns ids (not grid -// indices) so the caller resolves against the live bank and is unaffected by the -// panel's internal index bookkeeping. READ of panel state only; no mutation. -// -// Note: the panel's selection is cleared on a bank change (capture / project -// load), so a returned id always names a sample present in the current bank at -// the moment of the call; the caller still tolerates an absent id gracefully. -// -// Phase B4 (vertical split): the selection lives in whichever REGION the user last -// interacted with (the pool grid on top or a named-bank grid below), which is NOT -// necessarily the active/capture-target bank. The returned ids therefore name -// samples in the FOCUSED region's displayed bank — the bank the user visibly -// selected in. Pair with bankPanelSelectedSourceBankId() to know which bank those -// ids belong to (the move/copy source). +// Ids in bank (insertion) order, not grid indices. Empty when nothing is selected. +// Pair with bankPanelSelectedSourceBankId() to know which bank these belong to. std::vector bankPanelSelectedSampleIds(); -// The bank id the current selection belongs to — the displayed bank of the region -// the user last interacted with (pool region -> the pool id; named-banks region -> -// the shown tab's bank id). This is the SOURCE bank for a move/copy of the current -// selection, and it is distinct from the active/capture-target bank (active ≠ shown). -// Returns the pool id when nothing is selected or the panel has never opened (a safe -// default source). READ of panel state only; no mutation. +// The displayed bank of the region the user last interacted with — the SOURCE +// bank for a move/copy. Returns the pool id as a safe default. std::string bankPanelSelectedSourceBankId(); } // namespace reasampler diff --git a/src/shell/panel/panel_drag.cpp b/src/shell/panel/panel_drag.cpp index 8edda00..9f0d5db 100644 --- a/src/shell/panel/panel_drag.cpp +++ b/src/shell/panel/panel_drag.cpp @@ -1,18 +1,13 @@ -// panel_drag.cpp — the card-drag / hover state machine seam of the docked bank panel -// (Q-W2 split of bank_panel.cpp; the T4-01 NEW seam; M11/L7/S17). Owns WM_MOUSEMOVE -// (hover resolution + tooltip timing + the live drag), the drop-target/gesture -// classification, the cursor cues, button-up drop dispatch (reorder / replace / -// move / copy / instrument-drop / OS drag-out), and right-click menu routing. Its -// PURE mirror is core/ui/card_drag (gesture precedence + slot hit-test) with -// core/ui/drag_out owning the OS-drag boundary decision — this shell supplies only -// the live rects, modifier state, and side effects. -// -// PER-MOUSE-MOVE GUARDRAIL (T4-28): everything on the move path stays plain +// panel_drag.cpp — the card-drag / hover state machine seam of the docked bank panel: +// WM_MOUSEMOVE (hover + tooltip timing + the live drag), drop-target/gesture +// classification, cursor cues, button-up drop dispatch, and right-click menu routing. +// Its PURE mirror is core/ui/card_drag (gesture precedence + slot hit-test), with +// core/ui/drag_out owning the OS-drag boundary decision — this shell supplies only the +// live rects, modifier state, and side effects. Per-mouse-move work stays plain // free-function calls — no interface, no virtual dispatch. // // Compiled into the reaper_reasampler MODULE. No REAPER API functions are called -// directly here (the FX-hotspot / OS-drag / instrument-drop shells own theirs); -// REAPER SDK types arrive via panel_state.h. +// directly here; REAPER SDK types arrive via panel_state.h. #include // std::abs (drag threshold) #include @@ -20,14 +15,12 @@ #include "shell/panel/panel_state.h" -#include "shell/bank_ops/bank_ops.h" // persistBankOp — shared undo-block wrapper (R-B) -#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11) -#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17) +#include "shell/bank_ops/bank_ops.h" // persistBankOp — shared undo-block wrapper +#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam +#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop namespace reasampler::panel { -// --- Drag (move between regions/onto a tab) ----------------------------------- - constexpr int kDragThreshold = 5; // px the pointer must move to begin a drag namespace { @@ -55,9 +48,7 @@ void updateDropTarget(int x, int y) { g_panel.dropBankId = tabs[static_cast(hit.index)]->id; return; } - // Tab takes precedence over the region; if the point is in the banks region but - // not on a specific tab, treat the whole grid as a drop zone for the shown bank. - // No valid target when there are no named banks or no shown bank. + // Tab takes precedence; otherwise the whole grid is a drop zone for the shown bank. if (!g_panel.shownBankId.empty() && book() && book()->bank(g_panel.shownBankId)) { if (x >= br.left && x < br.right && y >= br.top && y < br.bottom) { g_panel.dropKind = DropKind::BanksRegion; @@ -76,9 +67,8 @@ void updateDropTarget(int x, int y) { } } -// The destination bank id under the current drop target (pool id for PoolRegion; the tab/ -// shown-bank id for Tab/BanksRegion; "" for no target). Derived from updateDropTarget's -// dropKind/dropBankId — the single source of "what bank is under the pointer". +// The destination bank id under the current drop target (pool id for PoolRegion; the +// tab/shown-bank id for Tab/BanksRegion; "" for no target). std::string dropTargetBankId() { switch (g_panel.dropKind) { case DropKind::PoolRegion: return std::string(kPoolBankId); @@ -89,13 +79,12 @@ std::string dropTargetBankId() { return {}; } -// L7: classifies the live in-grid drag into a pure CardGesture and (for a same-bank drop) -// the target slot, updating g_panel.cardGesture / dragTargetSlot. Call AFTER updateDropTarget -// so dropKind/dropBankId are current. The pure card_drag::decideCardGesture owns the -// precedence (leave-client -> OS; other-bank -> move/copy; same-bank grid -> reorder/replace); -// the shell only supplies the region verdict, the same-bank target slot + occupancy, and the -// live modifier state. The OS-drag-out boundary is handled by the existing decideGesture path -// in onMouseMove BEFORE this runs, so here the pointer is always inside the client. +// Classifies the live in-grid drag into a pure CardGesture and (for a same-bank drop) +// the target slot. Call AFTER updateDropTarget so dropKind/dropBankId are current. +// card_drag::decideCardGesture owns the precedence (other-bank -> move/copy; same-bank +// grid -> reorder/replace); this only supplies the region verdict, target slot + +// occupancy, and modifier state (the OS-drag-out boundary is handled earlier, in +// onMouseMove, so here the pointer is always inside). void classifyCardDrag(int x, int y) { g_panel.cardGesture = CardGesture::None; g_panel.dragTargetSlot = -1; @@ -111,10 +100,9 @@ void classifyCardDrag(int x, int y) { mods.alt = altDown(); if (!destBank.empty() && destBank == g_panel.dragSourceBankId) { - // Same-bank grid: a reorder/replace target. Resolve the slot the pointer sits over - // in the SOURCE bank's own region display + whether it is occupied. - // Uses computeSlotRectsForDrop (one trailing row past maxSlot) so a drop beyond - // the last occupied card resolves to a valid trailing slot, not a -1 miss. + // Same-bank grid: a reorder/replace target. Resolve the slot + occupancy in the + // SOURCE bank's display. computeSlotRectsForDrop adds one trailing row past + // maxSlot so a drop beyond the last card resolves to a valid slot, not a -1 miss. mods.region = DropRegion::SameBankGrid; const bool isBanks = g_panel.dragSourceRegion == Region::Banks; const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h); @@ -141,16 +129,11 @@ void classifyCardDrag(int x, int y) { g_panel.cardGesture = decideCardGesture(x, y, client, st, mods); } -// Maps the pure L7 cursor cue to a SWELL stock cursor and sets it. The cue DECISION is pure -// (card_drag::cursorForGesture); the shell owns only this SetCursor call + the resource choice. -// Stock SWELL cursors (vendor/WDL/WDL/swell/swell-types.h:1320-1329, mirroring the Win32 OCR_* -// set): Reorder -> IDC_SIZEALL (four-way move, the file-manager reorder idiom); Move -> -// IDC_HAND (grab-and-place to another bank/tab); Copy -> IDC_UPARROW (no stock copy cursor -// exists cross-platform — this is the closest distinct stock cue; a bespoke copy cursor would -// need a resource file, deliberately NOT added); Replace -> IDC_SIZEWE (a distinct "swap -// occupant" cue, shown ONLY when the pure result is Replace, i.e. Alt over an occupied slot); -// OsDragOut -> the OS drag loop owns the cursor once handed off, so leave it (arrow here is -// never seen — the handoff happens before this runs); Default/None -> IDC_ARROW. +// Maps the pure cursor cue to a SWELL stock cursor (vendor/WDL/WDL/swell/swell-types.h, +// mirroring Win32 OCR_*) and sets it; the cue decision is pure (card_drag::cursorForGesture). +// Copy has no stock cross-platform cursor, so IDC_UPARROW is the closest distinct stock cue +// (a bespoke resource was deliberately not added). OsDragOut leaves the cursor alone — the +// OS drag loop owns it once handed off, and this branch is never actually seen. void applyDragCursor(CardGesture g) { const char* idc = IDC_ARROW; switch (cursorForGesture(g)) { @@ -164,26 +147,23 @@ void applyDragCursor(CardGesture g) { SetCursor(LoadCursor(nullptr, idc)); } -// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback, mirroring -// handleClick's precedence exactly (so the element that lights on hover is the one a click -// would hit). Returns HoverKind::None for the grid / dead space / a point outside the client -// (the grid cells carry their own selection/focus chrome, not a kit hover surface). Pure -// resolution over the same pure geometry the click path uses. +// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback, +// mirroring handleClick's precedence exactly (so the element that lights on hover is +// the one a click would hit). Returns HoverKind::None for the grid / dead space (the +// grid cells carry their own selection/focus chrome, not a kit hover surface). Hover resolveHover(int x, int y) { if (!g_panel.hwnd) return Hover{}; RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; - // TOP toolbar: the far-right More button, then the frequent buttons (matching the click - // order — first zone top-to-bottom). + // TOP toolbar, then footer, then BOTTOM toolbar, matching handleClick's precedence. { const MenuButtonRect mb = topMenuButtonRect(w); if (hitTestMenuButton(x, y, mb)) return Hover{HoverKind::MoreButton, -1}; const int hit = toolbarHit(x, y, topToolbarActionRect(w), topBarRows()); if (hit >= 0) return Hover{HoverKind::TopBarButton, hit}; } - // Footer: mode-toggle segments, Tail button, then Prune (matching the click order). { const int seg = footerToggleSegmentHit(x, y, w, h); if (seg >= 0) return Hover{HoverKind::ModeSegment, seg}; @@ -192,12 +172,10 @@ Hover resolveHover(int x, int y) { const ButtonRect pb = pruneButtonRectFor(w, h); if (hitTestPruneButton(x, y, pb)) return Hover{HoverKind::PruneButton, -1}; } - // BOTTOM toolbar buttons. { const int hit = toolbarHit(x, y, bottomToolbarRect(w, h), bottomBarRows()); if (hit >= 0) return Hover{HoverKind::BottomBarButton, hit}; } - // Region chrome: full-height toggles, create button, tabs. if (poolShown()) { const RECT pr = poolRegionRect(w, h); const RECT ftb = fullHtBtnRect(pr); @@ -222,10 +200,10 @@ Hover resolveHover(int x, int y) { } // Updates the live hover element and repaints ONLY on a change (sub-frame feedback, no -// per-move jank — the "speed is the selling point" repaint discipline). L5: a hover CHANGE also -// resets the tooltip timer (hoverSinceTick) and hides any shown tooltip, so the tooltip only -// appears after the pointer rests kTooltipDelayMs on ONE element (the delay is applied by the -// poll tick in maybeShowTooltip). A move within the SAME element leaves the timer running. +// per-move jank). A hover CHANGE also resets the tooltip timer (hoverSinceTick) and hides +// any shown tooltip, so the tooltip only appears after the pointer rests kTooltipDelayMs +// on ONE element (applied by the poll tick in maybeShowTooltip); a move within the SAME +// element leaves the timer running. void updateHover(int x, int y) { const Hover next = resolveHover(x, y); if (next != g_panel.hovered) { @@ -239,10 +217,9 @@ void updateHover(int x, int y) { } // namespace // Applies the tooltip hover-delay: if a tooltip-bearing element has been hovered past -// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from the -// OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated timer; -// WM_MOUSEMOVE's updateHover resets the timer, so a moving pointer never trips it. No-op when the -// current hover has no tooltip (grid / chrome / the More button). +// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from +// the OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated +// timer; updateHover resets the timer on every move, so a moving pointer never trips it. void maybeShowTooltip() { if (g_panel.tooltipShown) return; const HoverKind k = g_panel.hovered.kind; @@ -255,9 +232,8 @@ void maybeShowTooltip() { } void onMouseMove(int x, int y) { - // Hover feedback (L2): resolve + repaint-on-change, but NOT during a drag (the drag owns - // the visual feedback then — a drop-target highlight, not a hover). Cleared to None when - // the pointer is over the grid / dead space. + // Hover feedback: resolve + repaint-on-change, but NOT during a drag (the drag owns + // the visual feedback then — a drop-target highlight, not a hover). if (!g_panel.dragging && !g_panel.dragArmed) updateHover(x, y); if (g_panel.dragArmed && !g_panel.dragging) { @@ -267,9 +243,9 @@ void onMouseMove(int x, int y) { g_panel.dragging = true; g_panel.dragSourceBankId = bankIdForRegion(g_panel.dragSourceRegion); g_panel.dragSampleIds = focusedSelectionIds(); - // The single card actually grabbed = the focus ordinal's id. This is the L7 - // in-grid reorder/replace subject (see onLBtnUp) — "drag a card" is a single-card - // gesture, distinct from the multi-select move/copy payload in dragSampleIds. + // The single card actually grabbed = the focus ordinal's id — the in-grid + // reorder/replace subject (see onLBtnUp), distinct from the multi-select + // move/copy payload in dragSampleIds. { const RegionDisplay disp = focusedDisplay(); const int f = g_panel.selection.focus; @@ -283,13 +259,9 @@ void onMouseMove(int x, int y) { } } if (g_panel.dragging) { - // M11 gesture boundary, REFINED by S17. While a drag with samples is under way and the - // pointer is INSIDE the client rect it stays the internal bank-to-bank drag (invariant - // #4, byte-identical). Once it LEAVES the client rect the pure drag_out::decideGesture - // splits the outside case three ways: a single-capture drag over REAPER's OWN UI is an - // InstrumentDrop (hover-track the FX button, drop on release); a multi-capture drag OR a - // pointer that has left REAPER entirely is the unchanged M11 OsDrag; inside stays - // Internal. The shell supplies the "over REAPER's UI" predicate via GetThingFromPoint. + // Inside the client rect it stays the internal bank-to-bank drag. Once it LEAVES, + // drag_out::decideGesture splits three ways: single-capture over REAPER's OWN UI -> + // InstrumentDrop; multi-capture or fully outside REAPER -> OsDrag; inside -> Internal. RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const PanelClientRect client{cr.left, cr.top, cr.right - cr.left, cr.bottom - cr.top}; @@ -298,10 +270,8 @@ void onMouseMove(int x, int y) { DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()}; st.singleCapture = (g_panel.dragSampleIds.size() == 1); - // Resolve the FX drop target only when OUTSIDE the client rect (the S17 middle case can - // only arise there) and only for a single-capture payload — the SDK hit-test is skipped - // on the common internal-drag path so it costs nothing there. The screen conversion is - // Windows-only (D5); resolveFxDropTarget owns the REAPER hit query. + // Only resolved OUTSIDE the client rect and for a single-capture payload, so the SDK + // hit-test costs nothing on the common internal-drag path. FxDropTarget fx; if (!inside && st.singleCapture) { POINT sp{x, y}; @@ -313,11 +283,9 @@ void onMouseMove(int x, int y) { const DragGesture gesture = decideGesture(x, y, client, st); if (gesture == DragGesture::InstrumentDrop) { - // Track the FX hotspot for the release; the highlight is REAPER's own FX-button - // hover feedback under the pointer (the drop is driven on button-up). We keep the - // internal-drag capture alive so we keep receiving moves (unlike OsDrag, this does - // NOT hand off to a modal OS loop). Clear any internal drop-target highlight so the - // panel does not also paint a bank-drop cue while the drag is out over a track. + // Track the FX hotspot for the release. Unlike OsDrag this does NOT hand off to + // a modal OS loop, so the internal-drag capture stays alive; clear any bank + // drop-target highlight so the panel doesn't paint that cue too. g_panel.instrumentDropTrack = fx.valid() ? fx.track : nullptr; g_panel.dropKind = DropKind::None; g_panel.dropBankId.clear(); @@ -333,11 +301,8 @@ void onMouseMove(int x, int y) { // drag state (the resolver reads dragSourceBankId / dragSampleIds). const std::vector paths = resolveDragPathsForOs(); - // Reset internal drag state and release capture NOW: DoDragDrop runs its own - // modal loop and takes over mouse capture, so the internal drag must be fully - // wound down first (no stale dragging/dropKind, no lingering SetCapture). A - // cancelled/empty OS drag therefore leaves the panel in a clean, no-op state - // (invariant #2 — nothing mutated). + // DoDragDrop runs its own modal loop and takes over mouse capture, so the internal + // drag must be fully wound down first. if (GetCapture() == g_panel.hwnd) ReleaseCapture(); g_panel.dragArmed = false; g_panel.dragging = false; @@ -353,9 +318,9 @@ void onMouseMove(int x, int y) { initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows return; } - // Inside the client: classify the in-grid gesture (L7 reorder/replace vs the existing - // move/copy) and reflect it as a cursor cue. updateDropTarget first so dropKind/ - // dropBankId are current for classifyCardDrag's same-vs-other-bank decision. + // Inside the client: classify the in-grid gesture (reorder/replace vs move/copy) and + // reflect it as a cursor cue. updateDropTarget first so dropKind/dropBankId are + // current for classifyCardDrag's same-vs-other-bank decision. updateDropTarget(x, y); classifyCardDrag(x, y); applyDragCursor(g_panel.cardGesture); @@ -363,12 +328,10 @@ void onMouseMove(int x, int y) { } } -// L7 in-grid REORDER drop: move the grabbed card to targetSlot within its bank (gap- -// preserving; onto a gap = place there, onto an occupant = insert-before-and-shift — the pure -// BankBook::reorderSample owns the semantics). One drop = one Ctrl-Z (persistBankOp opens the -// batched undo point + saves). A no-op reorder (already at the target, model returns false) -// opens no undo point. Selection reasons over slot order, so it is cleared after — the -// fingerprint pass rebuilds it against the new order. +// In-grid REORDER drop: move the grabbed card to targetSlot within its bank (gap- +// preserving; onto a gap = place there, onto an occupant = insert-before-and-shift — the +// pure BankBook::reorderSample owns the semantics). One drop = one Ctrl-Z. A no-op reorder +// opens no undo point. Selection reasons over slot order, so it is cleared after. namespace { void doReorderDrop(const std::string& id, const std::string& bankId, int targetSlot) { @@ -379,10 +342,9 @@ void doReorderDrop(const std::string& id, const std::string& bankId, int targetS invalidatePanel(); } -// L7 Alt-REPLACE drop: the grabbed card `newId` takes the occupant `oldId`'s slot; `oldId` is -// removed from the bank's index (index-only, file untouched — pool guard enforced in the pure -// BankBook::replaceSample). Rejected (pool guard / absent) = a true NO-OP: no fallback insert, -// no undo point (per spec). One drop = one Ctrl-Z on success. +// Alt-REPLACE drop: the grabbed card `newId` takes the occupant `oldId`'s slot; `oldId` is +// removed from the bank's index (index-only, file untouched — pool guard enforced in the +// pure BankBook::replaceSample). Rejected = a true NO-OP: no fallback insert, no undo point. void doReplaceDrop(const std::string& newId, const std::string& oldId, const std::string& bankId) { if (!book() || newId.empty() || oldId.empty() || bankId.empty()) return; @@ -409,24 +371,22 @@ void resetDragState() { } // Commits (or abandons) a drag on button-up. The resolved pure CardGesture decides: -// * Reorder / Replace -> in-grid, within the source bank (L7); one Ctrl-Z each. -// * Move / Copy -> the EXISTING cross-bank transfer (unchanged; Ctrl = copy). +// * Reorder / Replace -> in-grid, within the source bank; one Ctrl-Z each. +// * Move / Copy -> the cross-bank transfer (Ctrl = copy). // * None -> a drop over dead space / the source-bank gap = no-op. // OsDragOut is never seen here: the pointer-left-client handoff happens live in onMouseMove. void onLBtnUp(int x, int y) { if (g_panel.dragging) { - // S17 drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a + // Drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a // ReaSampler 9000 on that track preloaded with the dragged capture — NOT a bank move, - // NOT an OS drag, NEVER a timeline insert. Takes priority over the L7 in-grid / cross-bank - // drop (the pointer is out over a track, not over a bank region). Single-capture only (the - // gesture never armed for a multi payload), so dragSampleIds.front() is the capture. + // NOT an OS drag, NEVER a timeline insert. Takes priority over the in-grid / cross-bank + // drop. Single-capture only, so dragSampleIds.front() is the capture. if (g_panel.instrumentDropTrack && g_panel.dragSampleIds.size() == 1) { const std::string sampleId = g_panel.dragSampleIds.front(); performInstrumentDrop(g_panel.instrumentDropTrack, buildInstrumentDropPreset(sampleId)); - // Read-only over the bank + arrange: the ONLY mutations are the new FX instance + - // its state (both undoable in performInstrumentDrop). No book change, no ext-state, - // no dirty-mark here. + // Read-only over the bank + arrange: the only mutations are the new FX instance + + // its state (both undoable in performInstrumentDrop). } else { updateDropTarget(x, y); classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed) @@ -481,7 +441,6 @@ void handleRightClick(int x, int y) { GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; - // Tab management menu. if (banksShown()) { const RECT br = banksRegionRect(w, h); const TabStripRect strip = banksTabStripRect(br); diff --git a/src/shell/panel/panel_input.cpp b/src/shell/panel/panel_input.cpp index 8a2a1b4..2fbdf91 100644 --- a/src/shell/panel/panel_input.cpp +++ b/src/shell/panel/panel_input.cpp @@ -1,13 +1,9 @@ -// panel_input.cpp — the input + detection seam of the docked bank panel (Q-W2 split -// of bank_panel.cpp). Owns left-click / wheel / keyboard routing (plain free-function -// calls on the per-event path — T4-28), the accelerator registration, the tail-setting -// read/mutate helpers, and the timer-driven new-content auto-tag detection (D2 Wave 2). -// Mouse-MOVE (hover + the card-drag state machine) lives in panel_drag; the bank-change +// panel_input.cpp — the input + detection seam of the docked bank panel: left-click / +// wheel / keyboard routing, accelerator registration, tail-setting read/mutate helpers, +// and timer-driven new-content auto-tag detection. Mouse-MOVE lives in panel_drag; the // fingerprint pass lives in panel_thumbnails (it owns the cache it invalidates). -// -// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested. +// Compiled into the reaper_reasampler MODULE, without REAPERAPI_IMPLEMENT (main.cpp +// owns the API pointers). DAW-verified, not unit-tested. #include #include @@ -17,15 +13,15 @@ #include "shell/panel/panel_state.h" #include "shell/panel/panel_input.h" -#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch (R3) +#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch #include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation -#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag (D2 Wave 2) -#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B) -#include "shell/capture/track_guid.h" // guidString — canonical track GUID key (D2 Wave 2) -#include "shell/view/view.h" // applyMode / mintManagedLanes — mode activation (D2/D4) +#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag +#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam +#include "shell/capture/track_guid.h" // guidString — canonical track GUID key +#include "shell/view/view.h" // applyMode / mintManagedLanes — mode activation -// New-content detection (D2 Wave 2): enumerate live tracks + items and read fixed-lane -// state to classify an item's lane as managed vs manual. +// New-content detection: enumerate live tracks + items and read fixed-lane state to +// classify an item's lane as managed vs manual. #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountTracks #define REAPERAPI_WANT_GetTrack @@ -50,23 +46,17 @@ TailSetting currentTail() { namespace { -// Commits the current tail setting to ext state and marks the active project dirty -// so the change travels inside the .rpp on Ctrl+S. saveToActiveProject() is the only -// path that calls SetProjExtState for the tail key — calling it here closes the gap -// where toggle/scroll would dirty the project but the new value was never written. -// On an unsaved project saveToActiveProject() no-ops cleanly (documented in persist.h). -// MarkProjectDirty runs unconditionally so REAPER knows a save is owed either way. -// NON-DESTRUCTIVE: touches nothing in the bank/arrange. +// Commits the current tail setting to ext state and marks the active project dirty so the +// change travels inside the .rpp on Ctrl+S — closes the gap where toggle/scroll would dirty +// the project but never write the new value. No-ops cleanly on an unsaved project. void markTailDirty() { if (g_panel.session) g_panel.session->saveToActiveProject(); ReaProject* proj = EnumProjects(-1, nullptr, 0); if (proj) MarkProjectDirty(proj); } -// Routes a click in a toolbar to the hit button's action, fired through the command-id contract -// (Main_OnCommand — REAPER runs the SAME action a keybinding would). Returns true iff the click -// was inside the bar band (handled, or a harmless gap/overflow/unregistered no-op), so the -// caller stops before grid handling. `rows` is the toolbar's inventory. +// Routes a click in a toolbar to the hit button's action via Main_OnCommand. Returns true +// iff the click was inside the bar band, so the caller stops before grid handling. bool handleToolbarClick(int x, int y, const ActionBarRect& bar, const std::vector& rows) { if (bar.height <= 0) return false; @@ -78,46 +68,26 @@ bool handleToolbarClick(int x, int y, const ActionBarRect& bar, x >= bar.x && x < bar.x + bar.width; } const ActionBarRow& row = rows[static_cast(hit)]; - // A disabled button (L5 opposite-mode gate) is claimed but no-ops — the click never fires the - // action and never falls through to the grid (a dead button reads as inert, not absent). + // A disabled button (opposite-mode gate) is claimed but no-ops — the click never fires + // the action and never falls through to the grid (a dead button reads as inert, not absent). if (!row.enabled) return true; const int cmd = resolveBarCommandId(row); if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0); return true; } -// --- New-content detection (D2 Wave 2) ---------------------------------------- -// -// REAPER exposes no "item/track added" callback, so we diff live project state on the -// existing timer. Each tick: enumerate every track GUID and every item GUID, diff -// against the previous tick (GuidBaseline, first-poll-guarded), and auto-tag the new -// GUIDs into the active mode via the pure autoTagNewContent. An item on a MANUAL lane -// is exempt (design point #1) — its lane's durable name lacks the managed prefix. All -// enumeration is READ-ONLY on the project; the only mutation is to the in-memory -// membership index (persisted by persist on the next save, same as an action-driven tag). - -// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). The SDK value is verified -// in view.cpp (kFreeModeFixedLanes=2); reproduced here as a local constant so -// panel_input.cpp stays self-contained without pulling in view.cpp's private namespace. +// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). Value verified in view.cpp; +// reproduced locally so this file stays self-contained. constexpr int kFreeModeFixedLanes = 2; bool isFixedLaneTrack(MediaTrack* tr) { return static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes; } -// Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h): -// itemGuid(it) and itemLaneName(tr, it). panel_input.cpp no longer carries its own copies. - -// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set, -// baseline input) and, for each item, records whether it sits on a manual lane so a -// newly-detected item can be exempted from auto-tag without a second project walk. -// `trackItemGuids` additionally maps each track GUID to the item GUIDs it carries, so a -// newly-detected item's PRE-EXISTING siblings can be resolved (the adoption / strand -// guard) without a second project walk. -// -// Manual-lane classification uses the single pure predicate isOnManualLane(isFixedLaneTrack, -// laneName) from lane_keys — the same predicate the apply path consults — so the exemption -// rule is defined in exactly one place and is unit-tested there. +// Enumerates the live project's track + item GUIDs. Fills `allGuids` and, per item, +// whether it sits on a manual lane (exempt from auto-tag). `trackItemGuids` maps each +// track to its item GUIDs so a newly-detected item's PRE-EXISTING siblings resolve in +// one lookup. Manual-lane classification uses the single pure predicate isOnManualLane. void enumerateLiveGuids(ReaProject* proj, std::set& allGuids, std::map& itemOnManualLane, std::map>& trackItemGuids) { @@ -140,9 +110,8 @@ void enumerateLiveGuids(ReaProject* proj, std::set& allGuids, std::string ig = itemGuid(it); if (ig.empty()) continue; allGuids.insert(ig); - // Classify via the single shared predicate. For a fixed-lane track we read - // the item's lane name; for a normal track we pass "" (isOnManualLane returns - // false immediately for non-fixed-lane tracks regardless of name). + // "" for a normal track — isOnManualLane returns false immediately for a + // non-fixed-lane track regardless of name. const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{}; itemOnManualLane[ig] = isOnManualLane(fixedLane, ln); itemsOnTrack.push_back(ig); @@ -150,35 +119,26 @@ void enumerateLiveGuids(ReaProject* proj, std::set& allGuids, } } -// One detection tick: diff live GUIDs against the baseline and auto-tag the new ones -// into the active mode. Runs every timer tick regardless of panel open/close (content -// is created in the arrange). READ-ONLY on the project; mutates only the in-memory -// membership index. +// One detection tick: REAPER exposes no "item/track added" callback, so this diffs live +// GUIDs against the baseline and auto-tags the new ones into the active mode. Runs every +// timer tick regardless of panel open/close. READ-ONLY on the project; mutates only the +// in-memory membership index — deliberately OUTSIDE any Undo block (auto-tag is a +// background metadata update, not a destructive edit; an Undo block here would flood +// REAPER's history with an entry per tick that sees new content). // -// INTENTIONAL: membership mutation happens OUTSIDE any Undo block. Auto-tag is a -// background metadata update (like setting a label), not a destructive project edit. The -// persist shell (ext_state_io.cpp) writes it on the next project save alongside the bank -// and view state, the same way an action-driven tag is persisted. Wrapping this in an Undo -// block would flood the REAPER undo history with a new entry for every timer tick that sees -// new content. -// Returns true iff this tick tagged at least one new GUID into a mode — the signal the -// caller uses to decide whether to run the lane-minting pass (a track can only newly -// become multi-mode when auto-tag just placed content on it). No tag ⇒ nothing to mint. +// Returns true iff this tick tagged at least one new GUID — the signal the caller uses to +// decide whether to run the lane-minting pass. bool detectNewContent() { if (!g_panel.session) return false; ReaProject* proj = EnumProjects(-1, nullptr, 0); // A project (re)load re-arms the first-poll guard so we never diff across two - // projects. The signal is persist's — main.cpp calls bankPanelNotifyProjectLoaded() - // on the tick persist restores the project's membership + active mode, which sets - // reloadPending. Draining it here re-baselines against the fully-loaded set (that - // same tick's reapply-active-mode enumerated those tracks, so they are present), - // and the observe() below returns nothing new — pre-existing untagged tracks stay - // Arrange. GuidBaseline self-arms on its first observe() for the very first tick, so - // no separate first-tick handling is needed here. Using persist's GUID-primary load - // signal (not a local pointer compare) is what fixes the reload-mis-tag: the two - // identity checks can no longer diverge on a recycled ReaProject* address. + // projects. bankPanelNotifyProjectLoaded() sets reloadPending on the tick persist + // restores membership + active mode; draining it here re-baselines against the + // fully-loaded set, so pre-existing untagged tracks stay Arrange rather than getting + // mass-tagged. Using persist's load signal (not a local ReaProject* compare) is what + // fixes the reload-mis-tag bug: pointer identity can recycle across projects. if (g_panel.reloadPending) { g_panel.contentBaseline.reset(); g_panel.reloadPending = false; @@ -210,12 +170,9 @@ bool detectNewContent() { for (const auto& [trackGuid, items] : trackItemGuids) for (const std::string& ig : items) trackOfItem[ig] = trackGuid; - // The distinct modes the PRE-EXISTING (not-new-this-tick) MANAGED-ELIGIBLE items on - // `trackGuid` resolve to. Untagged siblings resolve to Arrange (leafBelongsToMode's - // default); new siblings are excluded; manual-lane siblings are EXEMPT — exactly as - // planLaneMinting ignores them when computing a track's own-item mode span, so the - // adoption guard's view of the track matches the split decision's. Drives the adoption - // / strand guard in autoTagNewContent. + // The distinct modes the PRE-EXISTING, managed-eligible items on `trackGuid` resolve + // to. Untagged siblings default to Arrange; new siblings excluded; manual-lane + // siblings EXEMPT — matching planLaneMinting's own-item mode span computation. const auto preExistingTrackModes = [&](const std::string& trackGuid) -> std::set { std::set modes; @@ -233,8 +190,8 @@ bool detectNewContent() { }; // Split the new GUIDs into tracks vs items so the pure decision can apply the - // manual-lane exemption to items only. A GUID present in the item-lane map is an - // item; otherwise it is a track (track GUIDs never appear in that map). + // manual-lane exemption to items only. A GUID in the item-lane map is an item; + // otherwise it's a track. std::vector newTracks; std::vector newItems; for (const std::string& g : added) { @@ -256,28 +213,21 @@ bool detectNewContent() { return !tags.empty(); } -// The item count the SELECTION reasons over — the focused region's occupied-cell count. -// L7: selection/navigation traverse OCCUPIED cells only (empty slots are gaps, not -// selectable). Occupied count == index size by construction: every index member maps to -// exactly one occupied slot (gaps are empty slots, which the index never backs), so the -// raw index size IS the dense selection-space extent. +// The item count the SELECTION reasons over. Occupied count == index size by +// construction, so the raw index size IS the dense selection-space extent. int focusedItemCount() { const BankModel* idx = indexForRegion(g_panel.focusedRegion); return idx ? static_cast(idx->size()) : 0; } -// --- Click routing ------------------------------------------------------------ - // Handles a header/tab-strip/button click for the banks region. Returns true if the // click was consumed (a region-chrome hit), false to fall through to grid selection. bool handleBanksChromeClick(int x, int y, const RECT& region) { - // Full-height toggle button. const RECT ftb = fullHtBtnRect(region); if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom) { bankPanelToggledBanksFullHeight(); return true; } - // "+" create button. const RECT cb = createBtnRect(region); if (x >= cb.left && x < cb.right && y >= cb.top && y < cb.bottom) { doCreateBank(); @@ -326,16 +276,15 @@ bool handlePoolChromeClick(int x, int y, const RECT& region) { // Applies a left-click at (x, y): route to top toolbar / footer (toggle / Tail / Prune) / // bottom toolbar / region chrome / grid selection, and arm a potential drag when the click -// lands on a selected cell. L4 order mirrors the three-zone layout top-to-bottom. +// lands on a selected cell. Order mirrors the three-zone layout top-to-bottom. void handleClick(int x, int y) { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); const int w = cr.right - cr.left, h = cr.bottom - cr.top; - // TOP toolbar: the far-right More button first (its rect sits in the band's reserved right - // strip, outside the action rect), then the frequent capture/placement buttons. A button - // fires its registered action via the command-id contract; the band is claimed whole (a - // gap/overflow miss is a harmless no-op, never a fall-through). Capture never auto-inserts. + // TOP toolbar: the far-right More button first, then the frequent capture/placement + // buttons. A button fires its registered action via the command-id contract; the band + // is claimed whole (a gap/overflow miss is a harmless no-op, never a fall-through). { const MenuButtonRect mb = topMenuButtonRect(w); if (hitTestMenuButton(x, y, mb)) { showMoreMenu(); return; } @@ -345,10 +294,8 @@ void handleClick(int x, int y) { // the More button) so a click there is inert chrome, never a fall-through to the grid. if (y >= 0 && y < kTopToolbarHeight && x >= 0 && x < w) return; - // Footer: mode toggle (left) -> Tail button -> Prune (right). The narrow [Arrange|Design] - // toggle activates that mode; the Tail button cycles the tail setting (L4 §4 — was a - // click-zone); Prune fires the guarded prune command. Checked before the bottom toolbar / - // grid so a footer click never selects a cell. + // Footer: mode toggle (left) -> Tail button -> Prune (right). Checked before the bottom + // toolbar / grid so a footer click never selects a cell. { const int seg = footerToggleSegmentHit(x, y, w, h); if (seg >= 0) { @@ -363,9 +310,7 @@ void handleClick(int x, int y) { const FooterBarLayout fb = footerBarLayoutFor(w, h); if (g_panel.session && hitTestFooterBar(x, y, fb) == FooterHit::Tail) { - // Tail button click cycles the tail mode (None -> Auto -> Manual -> None). Mutates - // the SESSION's tail setting (capture reads it; persist saves it with the project) - // and marks the project dirty — touches NOTHING in the bank/arrange. + // Cycles None -> Auto -> Manual -> None; touches NOTHING in the bank/arrange. TailSetting& tail = g_panel.session->tail(); tail.mode = cycleTailMode(tail.mode); markTailDirty(); @@ -373,10 +318,9 @@ void handleClick(int x, int y) { return; } - // Prune button (R3): fires the "Prune bank folder" action THROUGH its registered - // command id (fork R-E: dispatch the command, not the session directly) so the panel - // affordance and the bindable action share the one guarded dry-run/confirm/delete path - // in doBankPruneFolder. A 0 id (pre-registration) no-ops. + // Fires through its registered command id (not the session directly) so the panel + // affordance and the bindable action share the one guarded dry-run/confirm/delete + // path in doBankPruneFolder. const ButtonRect pb = pruneButtonRectFor(w, h); if (hitTestPruneButton(x, y, pb)) { const int cmd = bankPruneCommandId(); @@ -385,11 +329,8 @@ void handleClick(int x, int y) { } } - // BOTTOM toolbar (Design-View verbs): a button fires its registered action via the - // command-id contract. Claimed whole like the top toolbar. if (handleToolbarClick(x, y, bottomToolbarRect(w, h), bottomBarRows())) return; - // Region chrome (headers, tab strip, buttons). if (poolShown()) { const RECT pr = poolRegionRect(w, h); if (y >= pr.top && y < regionGridRect(pr, false).top) { @@ -403,15 +344,13 @@ void handleClick(int x, int y) { } } - // Grid selection. Resolve which region's grid the point is in. Region reg = Region::Pool; if (!regionAt(x, y, reg)) return; const bool isBanks = reg == Region::Banks; const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h); - // L7: hit-test the SPARSE slot layout, then map the slot to a selection ordinal. An - // empty (gap) slot hits selectionForSlot == -1, which reads as a grid miss below (a - // click on a gap clears selection, exactly like a click in the margin) — empty slots - // are decorative, not selectable. + // Hit-test the SPARSE slot layout, then map the slot to a selection ordinal. An empty + // (gap) slot hits selectionForSlot == -1, which reads as a grid miss below (a click on + // a gap clears selection, exactly like a click in the margin). const RegionDisplay disp = regionDisplay(region, isBanks, reg); const int hitSlot = hitTestSlot(x, y, disp.slotRects); const int hit = hitSlot < 0 ? -1 : disp.selectionForSlot(hitSlot); @@ -434,19 +373,12 @@ void handleClick(int x, int y) { } // Drag-arm disambiguation for plain (no ctrl, no shift) presses on a grid cell: - // - // • Already-selected cell: defer the selection change to LBUTTONUP so a plain - // press on a multi-selection doesn't collapse it before we know whether a drag - // will happen. Arm the drag with the current (multi-)selection as the payload - // candidate; only the caret moves immediately. - // - // • Unselected cell: apply the plain-click selection immediately (collapses to - // the single pressed cell) THEN arm a drag from it — so the user can press-and- - // drag in one gesture without a prior selecting click. The selection is set - // before arming so that focusedSelectionIds() resolves the right payload when - // the threshold is crossed in onMouseMove. - // - // ctrl / shift presses are selection-only gestures — no drag arm in either case. + // already-selected cell defers the selection change to LBUTTONUP (so a plain press on + // a multi-selection doesn't collapse it before we know whether a drag will happen; only + // the caret moves immediately); unselected cell applies the plain-click selection now + // (collapses to the single pressed cell) so a press-and-drag works without a prior + // selecting click and focusedSelectionIds() resolves the right payload once the + // threshold is crossed. ctrl/shift presses are selection-only — no drag arm. const bool onSelected = g_panel.selection.contains(hit); if (!ctrlDown() && !shiftDown()) { if (!onSelected) { @@ -461,12 +393,10 @@ void handleClick(int x, int y) { g_panel.dragStartX = x; g_panel.dragStartY = y; g_panel.dragSourceRegion = reg; - // Capture the mouse NOW so WM_MOUSEMOVE is delivered even when the pointer leaves the - // panel client rect before the drag threshold is crossed. Without capture, outside moves - // are not delivered, so a fast straight-out drag never transitions dragArmed → dragging - // and the OS drag-out never fires on the first pass. The capture is released on button-up - // (no drag: onLBtnUp dragArmed branch; drag: OsDrag path or onLBtnUp dragging branch) - // and on WM_CAPTURECHANGED (stolen or external release — already calls resetDragState). + // Capture the mouse NOW so WM_MOUSEMOVE is still delivered once the pointer leaves the + // client rect before the drag threshold is crossed — without capture, a fast + // straight-out drag never transitions dragArmed -> dragging. Released on button-up or + // WM_CAPTURECHANGED (which already calls resetDragState). SetCapture(g_panel.hwnd); invalidatePanel(); return; @@ -477,13 +407,10 @@ void handleClick(int x, int y) { invalidatePanel(); } -// Handles a scroll-wheel notch over client (x, y) with signed wheel delta `delta`. -// Fine-adjusts the Manual tail length in kManualStepMs steps ONLY when the cursor is -// over the footer strip AND the mode is Manual — wheel up lengthens, down shortens, -// clamped to [0, kMaxTailMs]. In Off/Auto (or off the footer) it does nothing (returns -// false so the caller can let REAPER/the docker handle the wheel normally). On a real -// change it mutates the SESSION's tail setting, marks the project dirty (so it saves), -// and repaints the live length. Returns true iff the wheel was consumed. +// Fine-adjusts the Manual tail length in kManualStepMs steps ONLY when the cursor is over +// the footer strip AND the mode is Manual — wheel up lengthens, down shortens, clamped to +// [0, kMaxTailMs]. Otherwise does nothing (returns false so the caller can let REAPER/the +// docker handle the wheel normally). Returns true iff the wheel was consumed. bool handleWheel(int x, int y, int delta) { if (!g_panel.session) return false; if (!pointInFooter(x, y)) return false; @@ -542,8 +469,7 @@ bool handleKey(int vk) { stopAudition(); return true; case VK_DELETE: { - // Remove the focused-region selection (B5). Silent; a no-op when nothing - // is selected. + // Remove the focused-region selection. Silent; a no-op when nothing is selected. const std::vector sel = focusedSelectionIds(); if (sel.empty()) return false; // nothing selected — let the key fall through removeSamples(sel, bankIdForRegion(g_panel.focusedRegion)); @@ -580,35 +506,27 @@ void unregisterAccel() { } // namespace reasampler::panel -// --- Public API (the timer + tail read seam — panel_input.h) ------------------- - namespace reasampler { void bankPanelNotifyProjectLoaded() { - // Persist restored a project's membership + active mode this tick (main.cpp calls - // this from the same consumeLoadSignal() branch that reapplies the active mode). - // Arm the new-content detector to re-baseline on its next tick so the just-loaded - // project's pre-existing content is treated as the baseline (nothing new) rather - // than diffed against the previous project and mass-tagged into the active mode. - // A flag (not an inline reset) because detectNewContent owns the baseline and runs - // later in the SAME OnTimer tick — it drains this and re-baselines against the live - // set in one place, keeping the reset and the observe() adjacent and ordered. + // Arms the new-content detector to re-baseline on its next tick so the just-loaded + // project's pre-existing content is the baseline (nothing new) rather than diffed + // against the previous project and mass-tagged. A flag, not an inline reset, because + // detectNewContent owns the baseline and runs later in the SAME OnTimer tick. panel::g_panel.reloadPending = true; } void bankPanelRefresh() { // New-content auto-tag detection runs EVERY tick regardless of panel open/close: - // tracks/items are created in the arrange view, not the panel, so detection must - // not be gated on the dock being visible. READ-ONLY on the project; only mutates - // the in-memory membership index (persist saves it like any action-driven tag). + // tracks/items are created in the arrange view, not the panel. READ-ONLY on the + // project; only mutates the in-memory membership index. const bool tagged = panel::detectNewContent(); - // Lane minting (D2 Wave 3) runs ONLY when detection just tagged new content — a - // track can only newly become multi-mode when auto-tag placed content on it. Unlike - // the invisible membership tag above, minting is a visible structural mutation + // Lane minting runs ONLY when detection just tagged new content — a track can only + // newly become multi-mode when auto-tag placed content on it. Unlike the invisible + // membership tag above, minting is a visible structural mutation // (I_FREEMODE/I_FIXEDLANE/P_LANENAME), so mintManagedLanes wraps it in its own Undo - // block and only mints for tracks that hold >1 mode's content — a single-mode track - // is left to D1 whole-track parking. Managed lanes only; manual lanes untouched. + // block and only mints for tracks that hold >1 mode's content. Managed lanes only. if (tagged && panel::g_panel.session) { ReaProject* proj = EnumProjects(-1, nullptr, 0); mintManagedLanes(panel::g_panel.session->view(), proj); @@ -616,8 +534,8 @@ void bankPanelRefresh() { if (!panel::g_panel.open || !panel::g_panel.hwnd) return; - // L5: the custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if a - // toolbar button has rested under the pointer past the delay, latch + repaint the tooltip. + // The custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if + // a toolbar button has rested under the pointer past the delay, latch + repaint it. panel::maybeShowTooltip(); if (panel::refreshFingerprint()) @@ -625,10 +543,9 @@ void bankPanelRefresh() { } capture::TailSetting bankPanelTailSetting() { - // The authoritative setting lives in the session (session->tail()) so it travels - // inside the .rpp: it loads per project and saves with the project. This stays the - // read seam for the capture actions. manualMs is clamped here so a caller always - // receives a within-cap length regardless of what was stored/scrolled. + // The authoritative setting lives in the session so it travels inside the .rpp; this + // is the read seam for the capture actions. manualMs is clamped here so a caller + // always receives a within-cap length regardless of what was stored/scrolled. capture::TailSetting s = panel::currentTail(); s.manualMs = capture::clampManualMs(s.manualMs); return s; diff --git a/src/shell/panel/panel_input.h b/src/shell/panel/panel_input.h index 9f08280..efb3abd 100644 --- a/src/shell/panel/panel_input.h +++ b/src/shell/panel/panel_input.h @@ -1,43 +1,24 @@ #pragma once -// panel_input — the input + detection seam of the bank panel (Q-W2 split of -// bank_panel.h). The .cpp owns mouse-click / wheel / keyboard routing (plain -// free-function calls per T4-28 — no interface on the per-event path) plus the -// timer-driven detection passes: new-content auto-tag (D2 Wave 2) and the -// hover-delay tooltip latch. This header carries the timer/lifecycle surface -// main.cpp drives and the tail-setting read seam the capture actions consume. -// -// REAPER-free as practical: TailSetting is the pure capture-side type. +// panel_input — input + detection seam of the bank panel: mouse/wheel/keyboard +// routing plus timer-driven passes (new-content auto-tag, tooltip hover-delay +// latch). REAPER-free as practical: TailSetting is the pure capture-side type. -#include "core/capture/tail_control.h" // capture::TailSetting — the panel's tail-mode toggle state +#include "core/capture/tail_control.h" namespace reasampler { -// Requests a repaint if the bank changed since the last paint (generation bump). -// Cheap when nothing changed. Driven by the timer so a capture / project load is -// reflected without the panel diffing the bank itself. Also hosts the every-tick -// new-content auto-tag detection (runs whether or not the dock is visible) and the -// tooltip hover-delay latch. +// Repaints if the bank changed since last paint (generation bump); cheap no-op +// otherwise. Also drives the auto-tag detector and tooltip hover latch each tick. void bankPanelRefresh(); -// Notifies the panel that persist just (re)loaded a project's view model (membership + -// active mode). main.cpp calls this on the exact tick it drains persist's load signal -// and reapplies the active mode. It re-arms the new-content detector so the just-loaded -// project's PRE-EXISTING content is taken as the baseline (reported as nothing new), -// never diffed against the previously-open project and mass-tagged into the active mode. -// This coordinates the detector's project-identity signal with persist's authoritative -// (GUID-primary) one — the two can no longer diverge on a recycled ReaProject* address, -// which is what caused a project opened in Design to mis-tag its Arrange tracks. READ/ -// arm of panel state only; no project or bank mutation. +// Call on the exact tick persist's project-load signal drains, before reapplying the +// active mode. Re-arms the new-content detector so the just-loaded project's existing +// content is the baseline, not diffed against the prior project and mass-tagged. void bankPanelNotifyProjectLoaded(); -// The panel's current tail-mode setting (mode + Manual length), read by the plain -// CAPTURE_ITEM / CAPTURE_TRACK actions when building a CaptureRequest so a capture -// applies whatever the panel toggle is set to. Default None (exact bounds) — a -// capture with no explicit choice stays byte-identical to today. The authoritative -// setting lives in ReaSamplerSession (it travels inside the .rpp); the panel mutates -// it via the footer Tail button (cycle) and scroll-wheel (Manual fine-adjust), both -// owned by this input seam. Safe to call before the panel has ever opened (returns -// the default). READ of panel state only. +// Read by CAPTURE_ITEM/CAPTURE_TRACK to apply the panel's tail toggle to a +// CaptureRequest. Default None (exact bounds, byte-identical to no tail). Safe +// before the panel has ever opened. capture::TailSetting bankPanelTailSetting(); } // namespace reasampler diff --git a/src/shell/panel/panel_layout.cpp b/src/shell/panel/panel_layout.cpp index 34d9311..1a3de78 100644 --- a/src/shell/panel/panel_layout.cpp +++ b/src/shell/panel/panel_layout.cpp @@ -1,18 +1,11 @@ -// panel_layout.cpp — the geometry-glue seam of the docked bank panel (Q-W2 split of -// bank_panel.cpp; the T4-01 NEW seam). Owns the toolbar/footer/menu rects, the -// toolbar row/cluster builders, the vertical-split geometry + region rects, and the -// L7 slot-order display bridge (regionDisplay/focusedDisplay). Every rect is derived -// from the client size + fullHeight state, and BOTH paint (panel_render) and -// hit-testing (panel_input / panel_drag) call these so they never drift. +// panel_layout.cpp — geometry-glue seam of the docked bank panel: toolbar/footer/menu +// rects, toolbar row/cluster builders, vertical-split geometry + region rects, and +// the slot-order display bridge. Every rect is derived from client size + fullHeight +// state, and both paint and hit-testing call these so they never drift. Also home of +// the public split-state seam (panel_layout.h). // -// Also home of the public split-state seam (panel_layout.h): the B3-owned -// BankPanelFullHeight toggles the render derives the region rects from. -// -// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested (the PURE tiling / -// hit-test math lives in action_bar / footer_bar / prune_button / overflow_menu / -// tab_strip / mode_switch / bank_grid / card_drag, unit-tested outside the DAW). +// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested +// (the pure tiling/hit-test math lives in action_bar / footer_bar / etc.). #include #include @@ -20,12 +13,11 @@ #include "shell/panel/panel_state.h" #include "shell/panel/panel_layout.h" -#include "shell/persist/session.h" // ReaSamplerSession — mode/view reads -#include "core/view/view_mode_model.h" // ViewModeModel — modes()/activeModeId() +#include "shell/persist/session.h" +#include "core/view/view_mode_model.h" -// Action-trigger buttons (M11): resolve each button's command id at runtime from the -// composed named-command string and read its current key binding for the tooltip. -// All main-section (SectionFromUniqueID(0)). +// Resolves each button's command id at runtime from the composed named-command +// string, and reads its current key binding for the tooltip. Main-section only. #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_NamedCommandLookup #define REAPERAPI_WANT_kbd_getTextFromCmd @@ -34,20 +26,14 @@ namespace reasampler::panel { -// --- Mode toggle (D5; relocated to the footer at L4) -------------------------- - int modeCount() { if (!g_panel.session) return 0; return static_cast(g_panel.session->view().modes().size()); } -// --- Top toolbar band (L4; L5 overflow-menu reserve) -------------------------- -// -// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate -// (height 0) when the client is too short to host it above the split body. The WHOLE band -// (topToolbarRect) is what the far-right More button anchors into; the action_bar's frequent -// buttons tile into the band MINUS the menu reserve (topToolbarActionRect), so they never run -// under the menu button (L5 refinement 1). +// topToolbarRect is what the far-right More button anchors into; the action_bar +// buttons tile into the band minus the menu reserve (topToolbarActionRect) so +// they never run under it. namespace { @@ -60,25 +46,21 @@ ActionBarRect topToolbarRect(int w) { return s; } -// The band the More button occupies (the whole top toolbar band as a MenuBarRect). MenuBarRect topMenuBarRect(int w) { const ActionBarRect bar = topToolbarRect(w); return MenuBarRect{bar.x, bar.y, bar.width, bar.height}; } -// The More button's rect (right-anchored in the top band). Empty when the band is too narrow -// to place it clear of its left inset — the three variants stay reachable via their bindable -// commands (graceful suppression). } // namespace +// Empty when the band is too narrow to place the button clear of its left +// inset — suppressed gracefully; still reachable via its bindable command. MenuButtonRect topMenuButtonRect(int w) { return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec); } -// The rect the TOP toolbar's action_bar tiles into: the whole band MINUS the reserve for the -// far-right More button, so the frequent buttons never overlap it. When the More button is -// suppressed (band too narrow) the reserve is still subtracted (the reserve is 0 only for a -// degenerate band), which keeps draw and hit-test consistent whether or not the button shows. +// Reserve is still subtracted even when the button is suppressed (0 only for a +// degenerate band), keeping draw and hit-test consistent either way. ActionBarRect topToolbarActionRect(int w) { ActionBarRect bar = topToolbarRect(w); const int reserve = menuButtonReserve(topMenuBarRect(w), kMenuBtnSpec); @@ -87,8 +69,6 @@ ActionBarRect topToolbarActionRect(int w) { return bar; } -// --- Footer (L4) -------------------------------------------------------------- - RECT panelFooter(int w, int h) { RECT rc{}; rc.left = 0; @@ -100,8 +80,8 @@ RECT panelFooter(int w, int h) { return rc; } -// The footer LEFT-group layout (mode toggle + count + Tail button), derived from the client -// size. SINGLE source of truth for draw and hit-test. All-empty when the footer is degenerate. +// Left-group layout (mode toggle + count + Tail button). Single source of truth +// for draw and hit-test. FooterBarLayout footerBarLayoutFor(int w, int h) { const RECT f = panelFooter(w, h); if (f.top >= f.bottom) return FooterBarLayout{}; @@ -109,22 +89,18 @@ FooterBarLayout footerBarLayoutFor(int w, int h) { return computeFooterBar(footer, FooterBarSpec{}); } -// The prune button's rect within the footer, derived from the client size. SINGLE source -// of truth for both draw and hit-test (they never drift). Empty when the footer is degenerate -// or too narrow to place the button clear of the footer-left group / version readout — the -// action stays reachable via its bindable command, so a suppressed button is graceful. Kept -// set apart at the RIGHT (footer_bar reserves the matching space at its right so the two -// groups never overlap). See prune_button.h §Placement contract. +// Empty when the footer is degenerate or too narrow to clear the left group / +// version readout — reachable via its bindable command regardless. Set apart at +// the right (footer_bar reserves matching space so the groups never overlap). ButtonRect pruneButtonRectFor(int w, int h) { const RECT f = panelFooter(w, h); - if (f.top >= f.bottom) return ButtonRect{}; // degenerate footer -> no button + if (f.top >= f.bottom) return ButtonRect{}; const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top}; return computePruneButton(footer, PruneButtonSpec{}); } -// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip. Used by the -// scroll-wheel (Manual tail fine-adjust) so a wheel notch over the footer is claimed. The -// Tail-cycle CLICK no longer uses this — it now hits the Tail button rect (footer_bar). +// Used by the scroll-wheel (Manual tail fine-adjust); the Tail-cycle click hits +// the Tail button rect (footer_bar) instead. bool pointInFooter(int x, int y) { if (!g_panel.hwnd) return false; RECT cr{}; @@ -133,37 +109,11 @@ bool pointInFooter(int x, int y) { return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom; } -// === Task-grouped toolbars (Phase L, L2 + L4) ================================= -// -// L4 re-homes the button inventory around frequency and intent (DS-3 layout, not a re-skin) -// across TWO toolbars, BOTH drawn through the pure action_bar module: -// * the TOP toolbar (Capture + Placement) sits at the very top where the eye lands — the -// two acts the tool exists for (L4 §1); -// * the BOTTOM toolbar (the Design-View verbs: Tagging then Switching) sits above the -// footer, in the space capture/placement vacated (L4 §2). -// Each button is drawn with its action name (Font::Label) and live key binding on a Micro -// sub-row (the L2 contract). action_bar owns the cluster tiling, the label/binding sub-rects, -// the whole-trailing-button overflow, and the hit-test; only the kit draw + SDK binding query -// + the NamedCommandLookup/Main_OnCommand dispatch live here. -// -// Each button resolves its command id at RUNTIME from the composed named-command string -// (NamedCommandLookup on "_" + channelCommandId(suffix)), so it is channel-correct on stable -// and beta and adds NO second registration. A cmd of 0 (action not registered on this channel) -// draws Disabled and no-ops on click. L4 is layout-only: the SAME existing actions fire via the -// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts. - -// The TOP toolbar inventory (L6 refinement): the FREQUENT acts only — Capture (item / track) -// then Re-capture (Maintenance, set between the two capture verbs and the placement verbs) then -// Placement (insert / insert-conform). The FOUR RARE variants (Batch Items / Batch Razor / -// Capture RT / Cancel RT) are ALL in the far-right "⋯" overflow menu (overflowMenuRows) — -// same registered actions, same command-id contract, just a different home. Capture scopes come -// from captureActionTable() (render_settings, pure); the rest are the registered M11/M10/M8 -// commands. Built once per draw/click. Each row carries its full (prefix-stripped) action name -// for the hover tooltip. +// 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. std::vector topBarRows() { std::vector rows; - // Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track"; - // the tooltip carries the full descriptionPhrase the action was registered with. for (const CaptureActionDef& def : captureActionTable()) { std::string label = def.commandSuffix; if (label == "CAPTURE_ITEM") label = "Capture Item"; @@ -171,12 +121,8 @@ std::vector topBarRows() { rows.push_back({def.commandSuffix, label, def.descriptionPhrase, ActionCluster::Capture, true}); } - // Maintenance cluster — Re-capture from source (M10), placed BETWEEN the capture group and - // the placement group so its position reads "refine the last capture before placing it". - // Cancel RT lives in the overflow menu (both realtime verbs share that home — L6). rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", "re-capture from source", ActionCluster::Maintenance, true}); - // Placement cluster — the second act (still a distinct on-demand act; no auto-insert). rows.push_back({"INSERT_SELECTED", "Insert", "insert selected sample at edit cursor", ActionCluster::Placement, true}); rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", @@ -185,12 +131,8 @@ std::vector topBarRows() { return rows; } -// The TOP-toolbar OVERFLOW menu inventory (L6): four items pulled off the visible bar into the -// far-right "⋯" menu button's popup — the three rare batch/realtime capture variants plus -// Cancel RT (both realtime verbs share the menu home). Each fires the SAME existing registered -// command id via the SAME NamedCommandLookup/Main_OnCommand contract — no action changes. The -// fullName is the popup entry text (the terse shortLabel is unused for menu items; the popup has -// room for the full name). Batch entries first, then the two realtime verbs. +// 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). std::vector overflowMenuRows() { return { {"CAPTURE_BATCH_ITEMS", "Batch Items", @@ -206,8 +148,6 @@ std::vector overflowMenuRows() { namespace { -// The active mode id the opposite-mode gate + footer toggle both read (ONE source of truth for -// "which mode is active"). Empty when no session (every button then falls to fail-open live). std::string activeModeIdOrEmpty() { if (!g_panel.session) return {}; return g_panel.session->view().activeModeId(); @@ -215,26 +155,16 @@ std::string activeModeIdOrEmpty() { } // namespace -// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons -// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from -// design_view_actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves; -// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not -// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag). -// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the -// footer [Arrange|Design] toggle owns mode switching. -// -// OPPOSITE-MODE ENABLEMENT (L5): a tag button is LIVE only for the OPPOSITE of the active mode -// (you tag into the mode you are not in). The pure mode_enable::tagButtonEnabled decides it from -// the active mode id; Show Both is unconditional (not a tag target). enabled=false rows draw -// Disabled and no-op on click. The Item/Track axis is display-only here — both the Item and the -// Track button for a target share the target's enablement. +// Four Item/Track x Arrange/Design tag buttons then a set-apart Show Both. A tag +// button is live only for the OPPOSITE of the active mode (tag into the mode +// you're not in) — decided by the pure mode_enable::tagButtonEnabled; disabled +// rows draw Disabled and no-op on click. Show Both is unconditional. std::vector bottomBarRows() { const std::string active = activeModeIdOrEmpty(); const bool arrangeLive = tagButtonEnabled(active, TagTarget::Arrange); const bool designLive = tagButtonEnabled(active, TagTarget::Design); std::vector rows; - // Tagging cluster — the four Item/Track x Arrange/Design tag buttons. rows.push_back({"VIEW_MOVE_ITEMS_ARRANGE", "Item: Arrange", "move selected items -> Arrange", ActionCluster::Tagging, arrangeLive}); rows.push_back({"VIEW_MOVE_ITEMS_DESIGN", "Item: Design", @@ -243,18 +173,14 @@ std::vector bottomBarRows() { "tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive}); rows.push_back({"VIEW_TAG_DESIGN", "Track: Design", "tag selected tracks -> Design", ActionCluster::Tagging, designLive}); - // Switching cluster — Show Both, set apart (the only survivor of the old switching group). rows.push_back({"VIEW_SHOW_BOTH", "Show Both", "show both for selected tracks", ActionCluster::Switching, true}); return rows; } -// The cluster button-count specs for a given row set, in the row list's cluster order (so the -// pure action_bar's flat index lines up with the row list). Handles all five cluster kinds; -// empty clusters contribute a 0-count spec (action_bar skips them, emitting no gap). The spec -// order follows each toolbar's fixed layout order (top: Capture, Maintenance, Placement — -// Re-capture sits between the two capture verbs and the placement verbs; bottom: Tagging, -// Switching). The bottom bar's Maintenance count is 0, so the order change is transparent there. +// Cluster button-count specs in the row list's cluster order, so action_bar's +// flat index lines up with the row list. Empty clusters contribute a 0-count +// spec (action_bar skips them, no gap). std::vector actionBarClusters(const std::vector& rows) { int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0; for (const ActionBarRow& r : rows) { @@ -275,8 +201,8 @@ std::vector actionBarClusters(const std::vector& rows }; } -// The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split -// body). Degenerate (height 0) when the client is too short to host it above the footer. +// Fixed-height band directly above the footer; degenerate (height 0) when too +// short to host it above the footer. ActionBarRect bottomToolbarRect(int w, int h) { ActionBarRect s; const RECT footer = panelFooter(w, h); @@ -285,12 +211,10 @@ ActionBarRect bottomToolbarRect(int w, int h) { s.width = w; s.height = kBottomToolbarHeight; s.y = footerTop - kBottomToolbarHeight; - // Keep the bar below the top toolbar; if the client is too short, collapse it. if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; } return s; } -// Resolves a row's composed named command to its runtime command id (0 if not registered). // The named-command lookup string is "_" + the channel-qualified id (REAPER's convention). int resolveBarCommandId(const ActionBarRow& row) { if (!NamedCommandLookup) return 0; @@ -300,8 +224,7 @@ int resolveBarCommandId(const ActionBarRow& row) { namespace { -// The current key binding string for a command in the MAIN section, or "" (unbound / not -// registered). Queried via kbd_getTextFromCmd (SectionFromUniqueID(0)). +// "" when unbound or not registered. std::string barBindingText(int cmd) { if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) { const char* t = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0)); @@ -318,16 +241,9 @@ int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector rows; @@ -343,8 +259,8 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a } if (hv.index < 0 || hv.index >= static_cast(rows.size())) return false; - // The hovered button's slot rect (the anchor). computeBarSlots is the same layout the draw + - // hit-test use, so the anchor matches the drawn button exactly. + // computeBarSlots is the same layout draw + hit-test use, so the anchor + // matches the drawn button exactly. const std::vector clusters = actionBarClusters(rows); const std::vector slots = computeBarSlots(bar, clusters, kBarSpec); const ActionBarSlot* slot = nullptr; @@ -352,11 +268,9 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a if (s.index == hv.index) { slot = &s; break; } if (!slot) return false; - // The full name is stored already prefix-free, but strip defensively in case a source ever - // carries the "ReaSampler:" display prefix (the tooltip must never show it — L5 refinement 2). - // L6: the keybinding sub-row was removed from the button face, so the tooltip now carries - // both the name AND the binding (when bound) — e.g. "capture selected item — F5". When the - // action is unbound the tooltip shows only the name (no "(unbound)" noise in the tooltip). + // fullName is stored prefix-free; strip defensively in case a source ever + // carries the display prefix. Tooltip carries name + binding when bound + // (e.g. "capture selected item — F5"), name only when unbound. const std::string phrase = stripActionPrefix(rows[static_cast(hv.index)].fullName, actionDisplayPrefix()); const int cmd = resolveBarCommandId(rows[static_cast(hv.index)]); @@ -366,14 +280,9 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a return true; } -// --- Split geometry ----------------------------------------------------------- -// -// Every rect below is derived from the client size + fullHeight state, and BOTH paint -// and hit-testing call these so they never drift. All are top-left origin. - -// The body band between the TOP toolbar and the BOTTOM toolbar (L4). Its top edge is below the -// top toolbar; its bottom edge is the bottom toolbar's top. When the bottom bar collapses on a -// short client, bottomToolbarRect returns its y at the footer top, so the body still ends there. +// Between the top and bottom toolbars. When the bottom bar collapses on a short +// client, bottomToolbarRect returns its y at the footer top, so the body still +// ends there. RECT splitBody(int w, int h) { RECT rc{}; rc.left = 0; @@ -385,24 +294,20 @@ RECT splitBody(int w, int h) { return rc; } -// True when both regions are shown (the split is live). Otherwise one region fills -// the body. bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; } bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; } -// The pool region's rect (whole-region: header band + grid). Empty when hidden. +// Empty when hidden. RECT poolRegionRect(int w, int h) { const RECT body = splitBody(w, h); if (!poolShown()) return RECT{0, 0, 0, 0}; - if (!banksShown()) return body; // pool full-height: the whole body - // Split: pool gets the top half (minus the divider). + if (!banksShown()) return body; RECT rc = body; rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2; if (rc.bottom < rc.top) rc.bottom = rc.top; return rc; } -// The named-banks region's rect (whole-region: header band + tab strip + grid). RECT banksRegionRect(int w, int h) { const RECT body = splitBody(w, h); if (!banksShown()) return RECT{0, 0, 0, 0}; @@ -414,7 +319,6 @@ RECT banksRegionRect(int w, int h) { return rc; } -// A region's header band (the top kRegionHeaderHeight of the region). RECT regionHeaderRect(const RECT& region) { RECT rc = region; rc.bottom = region.top + kRegionHeaderHeight; @@ -422,7 +326,6 @@ RECT regionHeaderRect(const RECT& region) { return rc; } -// The named-banks region's tab strip (below its header band). TabStripRect banksTabStripRect(const RECT& region) { const RECT hdr = regionHeaderRect(region); TabStripRect s; @@ -445,7 +348,6 @@ RECT regionGridRect(const RECT& region, bool isBanks) { return rc; } -// The full-height toggle button rect inside a region header (right-aligned). RECT fullHtBtnRect(const RECT& region) { const RECT hdr = regionHeaderRect(region); RECT rc = hdr; @@ -456,8 +358,6 @@ RECT fullHtBtnRect(const RECT& region) { return rc; } -// The "+" create-bank button rect inside the named-banks region header (left of the -// full-height button). RECT createBtnRect(const RECT& region) { RECT ft = fullHtBtnRect(region); RECT rc = ft; @@ -466,10 +366,8 @@ RECT createBtnRect(const RECT& region) { return rc; } -// Resolves a region's display for the currently-shown bank. Empty (no bank / no width) -// yields an empty display. orderedSampleIds reconciles the bank's SlotMap against live -// membership, so a freshly-migrated or out-of-band-mutated bank always yields a complete -// order (trailing empties are trimmed by the model — maxSlot walks only live occupants). +// orderedSampleIds reconciles the bank's SlotMap against live membership, so a +// freshly-migrated or out-of-band-mutated bank always yields a complete order. RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) { RegionDisplay d; BankBook* b = book(); @@ -490,8 +388,6 @@ RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) { return d; } -// The FOCUSED region's display (the slot-order bridge for the region holding the live -// selection). Mirrors columnsForRegion's client read. RegionDisplay focusedDisplay() { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); @@ -501,8 +397,6 @@ RegionDisplay focusedDisplay() { return regionDisplay(region, isBanks, g_panel.focusedRegion); } -// Which region (if any) contains client point (x, y); returns false via `out` set to -// Pool by default when the point is in neither region body. bool regionAt(int x, int y, Region& out) { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); @@ -522,8 +416,6 @@ bool regionAt(int x, int y, Region& out) { return false; } -// The footer mode-toggle segment (Arrange|Design) under (x, y), or -1. Segments are tiled by -// mode_switch inside footer_bar's toggle box, so both draw and hit-test use the same box. int footerToggleSegmentHit(int x, int y, int w, int h) { if (!g_panel.session) return -1; const FooterBarLayout fb = footerBarLayoutFor(w, h); @@ -532,7 +424,6 @@ int footerToggleSegmentHit(int x, int y, int w, int h) { return hitTestSegment(x, y, th, modeCount()); } -// The column count for a region's current grid width (nav needs the layout's wrap). int columnsForRegion(Region reg) { RECT cr{}; GetClientRect(g_panel.hwnd, &cr); @@ -545,8 +436,6 @@ int columnsForRegion(Region reg) { } // namespace reasampler::panel -// --- Public API (the split-state seam — panel_layout.h) ------------------------ - namespace reasampler { BankPanelFullHeight bankPanelFullHeight() { diff --git a/src/shell/panel/panel_layout.h b/src/shell/panel/panel_layout.h index 72ac6ea..a244a0f 100644 --- a/src/shell/panel/panel_layout.h +++ b/src/shell/panel/panel_layout.h @@ -1,43 +1,25 @@ #pragma once -// panel_layout — the vertical-split layout STATE seam of the bank panel (Q-W2 split of -// bank_panel.h; Phase B3/B4). The panel window splits vertically — pool on top, -// named-banks region below — and two toggles collapse the split. This header carries -// that public state surface; the geometry derivation itself (toolbar/footer/menu rects, -// row/cluster builders, region rects, the L7 slot-order display bridge) is internal to -// panel_layout.cpp (see panel_state.h for the intra-panel seam). -// -// REAPER-free: main.cpp / bank_actions.cpp drive these through plain free functions. +// panel_layout — vertical-split layout state seam of the bank panel (pool on top, +// named-banks region below, two toggles collapse the split). Geometry derivation +// itself is internal to panel_layout.cpp. REAPER-free. namespace reasampler { -// The vertical-split full-height layout state (Phase B). The bank window splits -// vertically — pool on top, named-banks region below — and two toggles collapse the -// split: pool full-height (hide the named-banks region) and banks full-height (hide -// the pool). The two are mutually exclusive with the default (both regions shown), -// so one enum captures the whole state. -// -// This bit is B3-owned (the actions flip it); B4's panel RENDERS from it. It lives -// beside the tail setting — the other session-level view-layout bit the panel -// reads — NOT in the persisted ReaSamplerSession: it is a UI-layout preference, not -// project state, so it must not travel with the .rpp. In-memory for the extension's -// lifetime; resets to Split on unload. +// UI-layout preference, not project state — deliberately not persisted in +// ReaSamplerSession (must not travel with the .rpp). Resets to Split on unload. enum class BankPanelFullHeight { - Split, // default: pool region on top, named-banks region below - PoolOnly, // pool full-height — named-banks region hidden - BanksOnly, // banks full-height — pool region hidden + Split, + PoolOnly, + BanksOnly, }; -// The current full-height layout state (default Split). READ by B4's panel to decide -// which region(s) to draw. Safe before the panel has ever opened. +// Safe before the panel has ever opened. BankPanelFullHeight bankPanelFullHeight(); -// Toggles pool full-height: Split <-> PoolOnly. From PoolOnly returns to Split; from -// either other state (Split or BanksOnly) enters PoolOnly. Bound to the "pool -// full-height" action. Requests a repaint so an open panel reflects the change. +// Split <-> PoolOnly; from BanksOnly also enters PoolOnly. Requests a repaint. void bankPanelToggledPoolFullHeight(); -// Toggles banks full-height: Split <-> BanksOnly, symmetric to the pool toggle. -// Bound to the "banks full-height" action. Requests a repaint. +// Split <-> BanksOnly, symmetric to the pool toggle. void bankPanelToggledBanksFullHeight(); } // namespace reasampler diff --git a/src/shell/panel/panel_render.cpp b/src/shell/panel/panel_render.cpp index e111169..9a0bd3f 100644 --- a/src/shell/panel/panel_render.cpp +++ b/src/shell/panel/panel_render.cpp @@ -1,49 +1,35 @@ -// panel_render.cpp — the LICE draw seam of the docked bank panel (Q-W2 split of -// bank_panel.cpp; M5 Wave A/B + Phase B4 + Phase L). Owns WM_PAINT's full paint: -// the VERTICAL SPLIT (pool grid region on top, named-banks tab-page region below), -// the region headers + tab strip, the two task-grouped toolbars + More button, the -// footer (mode toggle + count + Tail + Prune), the hover-delay tooltip overlay, and -// the per-card thumbnail/metadata draw — everything through the L1 kit by palette -// role (draw_kit), double-buffered, BitBlt'd once. -// -// READ-ONLY: reads panel + session state; the input/drag seams mutate it. All rect -// derivation comes from panel_layout (the single source both draw and hit-test use). -// -// Compiled into the reaper_reasampler MODULE. No REAPER API functions are called -// here (LICE/Win32 only); REAPER SDK types arrive via panel_state.h. +// panel_render.cpp — LICE draw seam of the docked bank panel. Owns WM_PAINT's full +// paint: the vertical split, region headers + tab strip, the two toolbars + More +// button, the footer, the tooltip overlay, and per-card thumbnail/metadata draw — +// everything through the kit by palette role, double-buffered, BitBlt'd once. +// Read-only: reads panel + session state; input/drag seams mutate it. All rect +// derivation comes from panel_layout (the single source both draw and hit-test +// use). No REAPER API functions are called here. #include #include #include "shell/panel/panel_state.h" -#include "shell/panel/draw_kit.h" // kit text()/fillSurface/drawButton/drawWaveform (L1) -#include "shell/persist/session.h" // ReaSamplerSession — mode/view/tail reads -#include "core/view/view_mode_model.h" // ViewModeModel / Mode — the footer toggle's model +#include "shell/panel/draw_kit.h" +#include "shell/persist/session.h" +#include "core/view/view_mode_model.h" namespace reasampler::panel { namespace { -// --- Drawing: thumbnails (via the kit's shared drawWaveform since FA3) --------- - -// Draws the L7 decorative metadata overlay on a card: bars.beats.subdivisions bottom-LEFT -// (musical, from the capture-time tempo + meter stamp) and seconds.milliseconds bottom-RIGHT -// (wall-clock). Decorative + non-interactive (no hit-test, no hover). Drawn in the kit's -// Micro / ValueMono classes in text/dim, subordinate to the waveform. A blank musical -// read-out (unstamped meter / unknown tempo) simply omits the bottom-left string. +// Bars.beats.subdivisions bottom-left (musical), seconds.milliseconds bottom-right. +// Decorative, non-interactive; a blank musical readout omits the left string. void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) { MusicalLength ml; ml.lengthSeconds = s.lengthSeconds; ml.tempoBpm = s.captureTempo; ml.timeSigNum = s.captureTimeSigNum; ml.timeSigDenom = s.captureTimeSigDenom; - const std::string bars = formatBarsBeats(ml); // "" when unstamped/no-tempo + const std::string bars = formatBarsBeats(ml); const std::string secs = formatSecondsMs(s.lengthSeconds); - // A short strip along the card's bottom edge. Left/right halves; text/dim so the - // waveform stays the centerpiece. Micro on the left (musical), ValueMono on the right - // (tabular numbers that must not jitter). const int stripH = 12; const int pad = 3; const int y = rect.y + rect.height - stripH; @@ -57,17 +43,13 @@ void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) { void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env, bool selected, bool focused, bool hovered, const Sample* sample) { - // Cell surface through the kit (L7 selection restyle): a selected card draws the NORMAL - // cell surface (Rest, or Hover when hovered) — NOT the inverted accent-fill. Selection is - // marked purely by an accent/tertiary (pastel purple) border below; hover stays a fill- - // state change orthogonal to that border, so a hovered selected card still reads selected. + // Selected cards draw the normal cell surface, not an inverted fill — selection + // is marked purely by the border below, kept orthogonal to hover state. const KitBox cell{rect.x, rect.y, rect.width, rect.height}; const InteractionState state = hovered ? InteractionState::Hover : InteractionState::Rest; fillSurface(bmp, cell, Role::BgCell, state); - // Border (L7): accent/TERTIARY purple when selected (the sole selection signal), else - // hairline. Focus is a distinct text/primary inner ring so a focused-AND-selected card - // reads BOTH — the purple outer border + the inner focus ring — kept visually separate. + // Focus is a distinct inner ring so a focused-AND-selected card reads both. const KitColor border = selected ? roleColor(Role::AccentTertiary) : roleColor(Role::LineHairline); LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(border), 1.0f, 0); if (focused) { @@ -75,44 +57,22 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env, LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0); } - // Waveform plot through the kit's shared primitive (FA3): the SAME per-pixel-column - // min/max envelope draw the VST editor hero + browser cards use — one algorithm, one - // look, everywhere. The oversampled env (see drawRegionGrid's binWidth) collapses per - // column via peaks::columnMinMax inside the kit; an empty env draws just the midline. drawWaveform(bmp, cell, env); - // L7 decorative metadata overlay, drawn last so it sits over the waveform. if (sample) drawCardMeta(bmp, rect, *sample); } -// --- Kit draw adapters (Phase L) ---------------------------------------------- -// -// All panel text draws through the kit's cached AA font (draw_kit::text), NOT raw GDI DrawText -// (retired at L1). L2 re-roles every color through the pure `theme` module and draws surfaces -// via the kit (fillSurface / drawButton). These thin adapters bridge the panel's RECT-based -// geometry helpers to the kit's KitBox and give the panel a KitColor->LICE_pixel boundary for -// the few raw borders it still draws over kit surfaces. The kit owns the font lifecycle -// (kitFontsInit/Shutdown, wired at panel open/close below). - KitBox toKitBox(const RECT& r) { return KitBox{r.left, r.top, r.right - r.left, r.bottom - r.top}; } -// KitColor -> LICE_pixel: all sites use the kit's toLice() from draw_kit.h — the single -// conversion boundary the kit enforces. No local alias needed. - -// L2 role/font-aware text: draws through the kit in a palette ROLE color and a chosen kit -// Font (the action bar uses Micro for the keybinding sub-label, Label for the name, Title for -// region headings). Takes a KitBox directly (the pure geometry the L2 modules return). void kitText(LICE_IBitmap* bmp, const KitBox& box, const char* txt, Font font, Role role, Align align) { text(bmp, box, txt, font, role, align); } -// The per-mode membership count that travels with the toggle (L4 §3): the number of leaves -// tagged into the currently ACTIVE mode. A compact readout beside the toggle. 0 when no -// session. (The Arrange default — untagged — is not counted; membership tracks tagged leaves.) -// A display-only tally over the model's public membership map — no model semantics duplicated. +// Number of leaves tagged into the currently active mode; 0 when no session. The +// Arrange default (untagged) is not counted — membership tracks tagged leaves only. int activeModeMemberCount() { if (!g_panel.session) return 0; const ViewModeModel& view = g_panel.session->view(); @@ -124,10 +84,9 @@ int activeModeMemberCount() { return n; } -// Draws the footer: the band + top divider, then the LEFT group (the narrow [Arrange|Design] -// toggle drawn as mode_switch segments over footer_bar's toggle box, the per-mode count, and -// the Tail BUTTON — L4 §4), the right-aligned version readout, and finally the Prune button -// set apart at the far right (warn). READ-ONLY: reads session state; input handlers mutate it. +// Draws the footer: band + top divider, LEFT group ([Arrange|Design] toggle, per-mode +// count, Tail BUTTON), the version readout, and the Prune button at the far right. +// READ-ONLY: reads session state; input handlers mutate it. void drawFooter(LICE_IBitmap* bmp, int w, int h) { const RECT f = panelFooter(w, h); if (f.top >= f.bottom) return; @@ -140,8 +99,7 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { const FooterBarLayout fb = footerBarLayoutFor(w, h); - // [Arrange|Design] toggle — drawn as N mode_switch segments inside footer_bar's toggle box - // (the segment geometry stays owned by the pure mode_switch; footer_bar owns the box). The + // [Arrange|Design] toggle — N mode_switch segments inside footer_bar's toggle box. The // active mode's segment carries the accent; others hover-or-rest bg/cell. if (!fb.toggle.empty() && g_panel.session) { const ViewModeModel& view = g_panel.session->view(); @@ -166,8 +124,7 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { } } - // Per-mode member count, a compact dim readout beside the toggle (L4 §3 — "the count - // travels with the toggle"). Passive text, not a control. + // Per-mode member count, a compact dim readout beside the toggle. Passive text, not a control. if (!fb.count.empty()) { const int members = activeModeMemberCount(); const std::string countLabel = @@ -176,8 +133,8 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { countLabel.c_str(), Font::Micro, Role::TextDim, Align::Center); } - // Tail BUTTON (L4 §4) — a real kit button with rest/hover states; its click cycles the - // tail mode exactly as the old click-zone did. Label is the pure tailToggleLabel. + // Tail BUTTON — a real kit button with rest/hover states; its click cycles the tail + // mode. Label is the pure tailToggleLabel. if (!fb.tail.empty()) { const InteractionState state = hoverState(g_panel.hovered, HoverKind::TailButton, -1); const std::string label = tailToggleLabel(currentTail()); @@ -185,17 +142,13 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { drawButton(bmp, box, label.c_str(), state, /*warn=*/false); } - // Version/channel readout (Phase V, V3/V4), right-aligned, unobtrusive. appVersion() - // renders the configured version string on stable and that string plus "-beta" on beta, - // so a beta panel self-identifies. It sits inside the space footer_bar reserves at the - // right (rightReserve) and clears the prune button (prune_button::rightInset). Dim, - // passive identification (V3). + // Version/channel readout. appVersion() renders the configured version string on + // stable and that string plus "-beta" on beta, so a beta panel self-identifies. kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top}, appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right); - // Prune button — set apart at the far RIGHT (the ONLY warn-colored, byte-deleting control), - // honoring hover. No-op when suppressed (footer too narrow). Order reads left (benign, - // frequent) -> right (destructive, rare) per the L4 footer contract. + // Prune button — the ONLY warn-colored, byte-deleting control. No-op when suppressed + // (footer too narrow). const ButtonRect pb = pruneButtonRectFor(w, h); if (!pb.empty()) { const InteractionState state = hoverState(g_panel.hovered, HoverKind::PruneButton, -1); @@ -204,14 +157,12 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { } } -// Draws one task-grouped toolbar through the L1 kit: a bg/panel band, then each visible button -// as a kit drawButton (rest/hover/disabled) with the action short label on the single-row face. -// Overflow drops WHOLE trailing buttons (the pure layout returns only the buttons that fit), so -// nothing is drawn clipped. `hoverKind` selects which HoverKind this bar's buttons use -// (TopBarButton / BottomBarButton) so the two toolbars' hover states never cross. `topDivider` -// draws a hairline at the band's top edge (the bottom toolbar's elevation over the split body); -// the top toolbar draws it at its bottom edge instead. Key binding help is in the hover tooltip -// (L6), not on the button face — the face shows only shortLabel. +// Draws one task-grouped toolbar through the kit. Overflow drops WHOLE trailing buttons +// (the pure layout returns only the buttons that fit), so nothing is drawn clipped. +// `hoverKind` selects which HoverKind this bar's buttons use so the two toolbars' hover +// states never cross. `topDivider` draws the hairline at the band's top edge (bottom +// toolbar) vs. bottom edge (top toolbar). Key binding help is in the hover tooltip, not +// on the button face. void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, const std::vector& rows, HoverKind hoverKind, bool topDivider) { if (bar.height <= 0 || bar.width <= 0) return; @@ -230,17 +181,14 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, const ActionBarRow& row = rows[static_cast(s.index)]; const int cmd = resolveBarCommandId(row); - // State: Disabled when the action is not registered on this channel OR the row is gated - // off (L5 opposite-mode enablement — the tag buttons for the ACTIVE mode); else Hover - // when hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.) + // Disabled when unregistered on this channel or gated off (opposite-mode + // enablement); else Hover when hovered, else Rest — these are stateless triggers. InteractionState state = InteractionState::Rest; if (cmd == 0 || !row.enabled) state = InteractionState::Disabled; else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index) state = InteractionState::Hover; - // The button surface (drawButton draws the micro-gradient + rounded border + honors - // the state). The label is drawn separately so the text role tracks the state correctly; - // pass no label to drawButton. + // Label drawn separately (not passed to drawButton) so its text role tracks state. const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}}; drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); @@ -251,31 +199,22 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar, } } -// --- Top-toolbar overflow ("⋯" More) menu (L5 refinement 1) ------------------- -// -// The three rare capture variants live only in this popup. The button is drawn kit-style (rest/ -// hover) at the far right of the top band; a click opens a REAPER/host TrackPopupMenu listing the -// variants, each firing its existing registered command id via NamedCommandLookup/Main_OnCommand -// (the SAME contract the visible buttons use — no action changes). A transient OS menu is fine -// for panel-external chrome (brief §1); only the button geometry (overflow_menu) is pure. - -// Draws the far-right More button (rest/hover). No-op when suppressed (band too narrow). +// Draws the far-right More button (rest/hover) — the entry to the top-toolbar overflow +// popup listing the rare capture variants. No-op when suppressed (band too narrow). void drawMoreButton(LICE_IBitmap* bmp, int w) { const MenuButtonRect mb = topMenuButtonRect(w); if (mb.empty()) return; const InteractionState state = hoverState(g_panel.hovered, HoverKind::MoreButton, -1); const KitButtonBox box{KitBox{mb.x, mb.y, mb.width, mb.height}}; drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false); - // The glyph: three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text - // path). Drawn as text so it picks up the kit font + AA. Reads as the conventional "More". + // Three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text path). kitText(bmp, KitBox{mb.x, mb.y, mb.width, mb.height}, "...", Font::Label, Role::TextPrimary, Align::Center); } -// Draws the hover-delay tooltip over the given anchor button, if a tooltip is due (the current -// hover is a toolbar button AND it has been hovered past kTooltipDelayMs). Drawn LAST in the -// paint so it overlays the toolbars. The box is placed by the pure tooltip module (below the -// anchor, flipping above near the bottom edge, clamped to the client). +// Draws the hover-delay tooltip over the given anchor button, if due. Drawn LAST so it +// overlays the toolbars. Box placement (below anchor, flip above near the bottom edge, +// clamp to client) is the pure tooltip module's. void drawTooltip(LICE_IBitmap* bmp, int w, int h) { if (!g_panel.tooltipShown) return; std::string txt; @@ -295,8 +234,6 @@ void drawTooltip(LICE_IBitmap* bmp, int w, int h) { kitText(bmp, box, txt.c_str(), Font::Label, Role::TextPrimary, Align::Center); } -// --- Drawing: a grid region --------------------------------------------------- - // Draws one region's grid of thumbnails (or an empty-state line) clipped to its // viewport. `selectionOwner` is true when this region holds the live selection, so // its cells show selection/focus chrome; the other region draws plain. @@ -311,14 +248,12 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks, return; } - // L7: iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense + // Iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense // BankModel insertion order. Selection/focus are keyed by the occupied-ordinal (selection // space); a slot maps back to its ordinal via selectionForSlot. const RegionDisplay disp = regionDisplay(region, isBanks, reg); - // FA3 gap-free: request one bin per drawn pixel column; drawWaveform's - // peaks::columnMinMax exact partition makes every column gap-free — overbinning - // produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps - // the request to the frame count. + // Request one bin per drawn pixel column; drawWaveform's peaks::columnMinMax exact + // partition makes every column gap-free regardless. computeThumbnail clamps to frame count. const int binWidth = kWaveformOversample * waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight}); for (const SlotCellRect& r : disp.slotRects) { @@ -327,9 +262,8 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks, const std::string id = disp.idAtSlot(r.slot); if (id.empty()) { // Interior gap slot: a subtle empty-slot treatment through the kit — a hairline - // outline on bg/cell, clearly NOT a card (decorative, per the L7 spec). No - // selection/focus/waveform, and not a hover or hit target (the grid never tracks - // cell hover; a click on an empty slot clears selection like any grid miss). + // outline on bg/cell, clearly NOT a card. No selection/focus/waveform, and not a + // hover or hit target (a click on an empty slot clears selection like any grid miss). fillSurface(bmp, KitBox{rect.x, rect.y, rect.width, rect.height}, Role::BgCell, InteractionState::Rest); LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, @@ -349,12 +283,11 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks, } } -// L7: draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but +// Draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but // ONLY when a same-bank in-grid drag (Reorder or Replace) is live over THIS region (the drag -// source region). An accent/HOT outline (distinct from the accent/tertiary purple selection -// border, per the spec's "must not be confusable" constraint); Replace draws a doubled outline -// so an Alt-over-occupied replace reads as a stronger "swap" cue than a plain reorder. No-op -// for a move/copy/OS drag or when the pointer is off any slot (dragTargetSlot < 0). +// source region). An accent/HOT outline, distinct from the accent/tertiary purple selection +// border so it is never confusable; Replace draws a doubled outline so an Alt-over-occupied +// replace reads as a stronger "swap" cue than a plain reorder. void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Region reg) { if (!g_panel.dragging) return; if (g_panel.cardGesture != CardGesture::Reorder && @@ -365,8 +298,8 @@ void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Reg const RECT grid = regionGridRect(region, isBanks); const RegionDisplay disp = regionDisplay(region, isBanks, reg); - // Use the drop rects (includes the trailing row past maxSlot) so a beyond-extent - // target slot gets a visible highlight cue, not silence. + // The drop rects include a trailing row past maxSlot so a beyond-extent target + // slot gets a visible highlight cue, not silence. const int gridW = grid.right - grid.left; const int maxSlot = disp.bank ? disp.bank->slots.maxSlot() : -1; std::vector dropRects = computeSlotRectsForDrop(maxSlot, gridW, kGrid); @@ -386,26 +319,24 @@ void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Reg void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title, const std::string& activeName, bool poolBtnIsPool) { const RECT hdr = regionHeaderRect(region); - // Region header band (kit bg/panel — a raised region title bar). A hairline underline. fillSurface(bmp, KitBox{hdr.left, hdr.top, hdr.right - hdr.left, hdr.bottom - hdr.top}, Role::BgPanel, InteractionState::Rest); LICE_Line(bmp, hdr.left, hdr.bottom - 1, hdr.right, hdr.bottom - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0, false); - // Title, left (Font::Title — a region heading). The two regions are distinct KINDS of - // container, so the title carries a CATEGORICAL accent (DS-2 revised: secondary/tertiary - // mark kinds, never intensity) — Pool = secondary teal, Banks = tertiary purple. This is - // a category mark, NOT the "what's live" signal (that stays the primary-lime "Active:" - // readout beside it), keeping primary reserved for the live/active layer. + // Title, left. The two regions are distinct KINDS of container, so the title carries a + // CATEGORICAL accent (secondary/tertiary mark kinds, never intensity) — Pool = secondary + // teal, Banks = tertiary purple. This is a category mark, NOT the "what's live" signal + // (that stays the primary-lime "Active:" readout beside it). RECT titleRc = hdr; titleRc.left += 8; titleRc.right = titleRc.left + 120; const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary; kitText(bmp, toKitBox(titleRc), title, Font::Title, titleRole, Align::Left); - // Active-bank readout — the UNMISTAKABLE indicator (settled B4 constraint), in the PRIMARY - // accent role in BOTH region headers so the active/capture-target bank is legible even when - // it is not the shown tab and even when it is the pool. Primary = "what's live" (DS-2). + // Active-bank readout — the UNMISTAKABLE indicator, in the PRIMARY accent role in BOTH + // region headers so the active/capture-target bank is legible even when it is not the + // shown tab and even when it is the pool. Primary = "what's live". const std::string readout = "Active: " + activeName; RECT actRc = hdr; actRc.left = titleRc.right + 6; @@ -413,8 +344,8 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title, if (actRc.right > actRc.left) kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::AccentPrimary, Align::Left); - // Full-height toggle button: an arrow glyph. In split it means "maximize this region"; - // when this region is already full it means "restore the split". Kit drawButton + hover. + // Arrow glyph: in split it means "maximize this region"; when already full it means + // "restore the split". const RECT btn = fullHtBtnRect(region); const bool thisFull = poolBtnIsPool ? (g_panel.fullHeight == BankPanelFullHeight::PoolOnly) @@ -434,7 +365,6 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title, void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) { const TabStripRect strip = banksTabStripRect(region); if (strip.height <= 0) return; - // Tab strip band (kit bg/base — recessed relative to the region header above it). fillSurface(bmp, KitBox{strip.x, strip.y, strip.width, strip.height}, Role::BgBase, InteractionState::Rest); @@ -474,9 +404,8 @@ void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) { const bool hovered = g_panel.hovered.kind == HoverKind::Tab && g_panel.hovered.index == tr.index; - // Surface state: the ACTIVE bank (capture target) carries the accent (Active); a drag - // drop-target reads Dragging; the SHOWN (browsed) tab reads Pressed (recessed-lit); - // else hover-or-rest bg/cell. + // The ACTIVE bank (capture target) carries the accent; a drag drop-target reads + // Dragging; the SHOWN (browsed) tab reads Pressed; else hover-or-rest bg/cell. const KitBox tb{tr.x, tr.y, tr.width, tr.height}; InteractionState state = InteractionState::Rest; if (active) state = InteractionState::Active; @@ -510,8 +439,6 @@ std::string activeBankName() { } // namespace -// --- Full paint --------------------------------------------------------------- - void paintPanel(HWND hwnd, HDC hdc) { RECT cr{}; GetClientRect(hwnd, &cr); @@ -525,16 +452,14 @@ void paintPanel(HWND hwnd, HDC hdc) { const std::string projectDir = currentProjectDir(); const std::string activeName = activeBankName(); - // Pool region (top). if (poolShown()) { const RECT region = poolRegionRect(w, h); drawRegionHeader(&bmp, region, "Pool", activeName, /*poolBtnIsPool=*/true); drawRegionGrid(&bmp, region, /*isBanks=*/false, indexForRegion(Region::Pool), "No samples in the pool yet. Capture one to see it here.", g_panel.focusedRegion == Region::Pool, projectDir, Region::Pool); - // Drop-target highlight for the pool region during a MOVE/COPY drag (a whole-grid - // outline signalling "drop here to move/copy into this bank"). Suppressed for a - // same-bank reorder (that shows a per-SLOT highlight below, not the whole grid). + // Whole-grid drop-target outline for a MOVE/COPY drag; a same-bank reorder shows a + // per-SLOT highlight instead (drawCardDropTarget below). if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion && (g_panel.cardGesture == CardGesture::Move || g_panel.cardGesture == CardGesture::Copy)) { @@ -543,13 +468,9 @@ void paintPanel(HWND hwnd, HDC hdc) { grid.right - grid.left - 2, grid.bottom - grid.top - 2, toLice(roleColor(Role::AccentHot)), 1.0f, 0); } - // L7 per-slot reorder/replace target highlight (source = pool). An accent/hot outline - // on the target slot's cell — distinct from the accent/tertiary purple selection - // border, so it is never confusable with a selected card. drawCardDropTarget(&bmp, region, /*isBanks=*/false, Region::Pool); } - // Split divider. if (poolShown() && banksShown()) { const RECT body = splitBody(w, h); const int dy = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2; @@ -557,7 +478,6 @@ void paintPanel(HWND hwnd, HDC hdc) { toLice(roleColor(Role::BgBase)), 1.0f, 0); } - // Named-banks region (bottom). if (banksShown()) { const RECT region = banksRegionRect(w, h); drawRegionHeader(&bmp, region, "Banks", activeName, /*poolBtnIsPool=*/false); @@ -575,9 +495,8 @@ void paintPanel(HWND hwnd, HDC hdc) { ? "Select or create a named bank." : "This bank is empty. Move samples here from the pool.", g_panel.focusedRegion == Region::Banks, projectDir, Region::Banks); - // Drop-target highlight for the banks region during a drag. BanksRegion fires - // when the pointer is in the grid but not on a specific tab; Tab draws its own - // highlight on the individual tab (drawTabStrip above handles that case). + // BanksRegion fires when the pointer is in the grid but not on a specific tab; + // Tab draws its own highlight on the individual tab (drawTabStrip above). if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion && (g_panel.cardGesture == CardGesture::Move || g_panel.cardGesture == CardGesture::Copy)) { @@ -586,16 +505,12 @@ void paintPanel(HWND hwnd, HDC hdc) { grid.right - grid.left - 2, grid.bottom - grid.top - 2, toLice(roleColor(Role::AccentHot)), 1.0f, 0); } - // L7 per-slot reorder/replace target highlight (source = banks region). drawCardDropTarget(&bmp, region, /*isBanks=*/true, Region::Banks); } - // L4 three-zone chrome + L5 refinements: TOP toolbar (frequent capture + placement) tiles - // into the band MINUS the far-right More-button reserve; the More button is drawn over the - // band's reserved right strip; the BOTTOM toolbar (four opposite-mode tag buttons + Show - // Both); then the footer (mode toggle + count + Tail button + Prune). Drawn last so they sit - // over the split body's edges. drawToolbar fills only its passed (action) rect, so fill the - // WHOLE top band first — otherwise the reserved right strip behind the More button is bare. + // Toolbars + footer drawn last so they sit over the split body's edges. drawToolbar + // fills only its passed (action) rect, so fill the WHOLE top band first — otherwise + // the reserved right strip behind the More button is bare. fillSurface(&bmp, KitBox{0, 0, w, kTopToolbarHeight}, Role::BgPanel, InteractionState::Rest); drawToolbar(&bmp, topToolbarActionRect(w), topBarRows(), HoverKind::TopBarButton, /*topDivider=*/false); @@ -604,7 +519,6 @@ void paintPanel(HWND hwnd, HDC hdc) { /*topDivider=*/true); drawFooter(&bmp, w, h); - // The custom hover-delay tooltip overlays everything (L5 refinement 2). drawTooltip(&bmp, w, h); BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY); diff --git a/src/shell/panel/panel_state.h b/src/shell/panel/panel_state.h index 1cc153c..f169dbf 100644 --- a/src/shell/panel/panel_state.h +++ b/src/shell/panel/panel_state.h @@ -1,36 +1,22 @@ #pragma once -// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel -// (Q-W2: bank_panel.cpp split into eight TUs under shell/panel/). Included ONLY by the -// panel's own translation units (panel_render / panel_thumbnails / panel_audition / -// panel_input / panel_layout / panel_drag / panel_bank_ops / panel_window) — consumers -// outside the panel use the per-seam public headers (panel_window.h / panel_input.h / -// panel_bank_ops.h / panel_layout.h). +// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel. +// Included ONLY by the panel's own TUs; outside consumers use the per-seam public +// headers (panel_window.h / panel_input.h / panel_bank_ops.h / panel_layout.h). +// Cross-seam calls are plain free functions — direct call-through, no virtual dispatch +// (audition and per-mouse-move paths must stay direct calls). // -// What lives here: -// * PanelState (the one shared state blob, defined in panel_window.cpp) + the small -// enums/structs the seams speak (Region / DropKind / Hover / RegionDisplay / -// ActionBarRow) and the shared layout constants. -// * The cross-seam free-function declarations, grouped by OWNING TU. Everything is a -// plain free function — direct call-through, no interface, no virtual dispatch -// (T4-28: the audition path and the per-mouse-move path must stay direct calls). -// * Explicit using-declarations pulling the pure modules' symbols into -// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces). The -// interim core/namespaces.h shim is GONE (deleted in Q-W6 with the last split); -// every symbol below names its true home. -// -// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural -// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector, -// so a BankModel& / Bank* must NEVER be cached across one. Every seam resolves fresh -// AFTER any mutation and passes bank IDS (not references) into the model ops. +// REFERENCE-INVALIDATION GUARDRAIL: a bank-structural mutation (create/delete/ +// evacuate/activate/move) can reallocate the book's vector, so a BankModel& / Bank* +// must NEVER be cached across one. Every seam resolves fresh after any mutation and +// passes bank IDS (not references) into the model ops. #include #include #include #include -// SWELL / platform types (HWND, RECT, HMENU). On macOS/Linux SWELL is provided by the -// host (SWELL_PROVIDED_BY_APP); on Windows we use native Win32 (windows.h first, then -// swell.h no-ops on _WIN32). +// On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP); on Windows +// we use native Win32 (windows.h first, then swell.h no-ops on _WIN32). #ifdef _WIN32 #include #else @@ -39,37 +25,35 @@ #include "wdltypes.h" #include "swell/swell.h" -// REAPER SDK types only (preview_register_t, MediaTrack, ReaProject). The API function -// POINTERS are declared per-TU (REAPERAPI_MINIMAL + per-TU WANT list) — main.cpp owns -// the definitions (CLAUDE.md §contract). +// REAPER API function pointers are declared per-TU; main.cpp owns the definitions. #include "reaper_plugin.h" -#include "core/audio/peaks.h" // audio::Envelope — thumbnail cache payload -#include "core/capture/capture_paths.h" // capture::resolveBankFile / normalizeSlashes -#include "core/capture/render_settings.h" // capture::CaptureActionDef / captureActionTable -#include "core/capture/tail_control.h" // capture::TailSetting — the tail toggle state -#include "core/model/bank_book.h" // BankBook / Bank / SlotMap (flat reasampler until its split wave) -#include "core/model/bank_model.h" // model::BankModel / model::Sample -#include "core/ui/action_bar.h" // ui::ActionBarRect / slots / clusters -#include "core/ui/bank_grid.h" // ui::GridSpec / Selection / ThumbnailKey / CellRect -#include "core/ui/card_drag.h" // ui::CardGesture / SlotCellRect / gesture decisions -#include "core/ui/card_meta.h" // ui::MusicalLength / formatters -#include "core/ui/component_geometry.h" // ui::KitBox / KitButtonBox / waveformColumnCount -#include "core/ui/drag_out.h" // ui::DragState / PanelClientRect / decideGesture -#include "core/ui/footer_bar.h" // ui::FooterBarLayout / computeFooterBar -#include "core/ui/mode_enable.h" // ui::tagButtonEnabled / TagTarget -#include "core/ui/overflow_menu.h" // ui::MenuButtonSpec / computeMenuButton -#include "core/ui/prune_button.h" // ui::ButtonRect / computePruneButton -#include "core/ui/tab_strip.h" // ui::TabStripSpec / layout / hit-test -#include "core/ui/theme.h" // ui::Role / InteractionState / KitColor -#include "core/ui/tooltip.h" // ui::TooltipBox / computeTooltip / stripActionPrefix -#include "core/version/app_version.h" // version::channelCommandId / appVersion / dock identity -#include "core/view/guid_diff.h" // view::GuidBaseline — new-content detection -#include "core/view/lane_keys.h" // view::isOnManualLane -#include "core/view/mode_switch.h" // view::SegmentRect / computeSegmentRects -#include "core/wire/instrument_drop.h" // wire::buildInstrumentDropPreset (S17) +#include "core/audio/peaks.h" +#include "core/capture/capture_paths.h" +#include "core/capture/render_settings.h" +#include "core/capture/tail_control.h" +#include "core/model/bank_book.h" +#include "core/model/bank_model.h" +#include "core/ui/action_bar.h" +#include "core/ui/bank_grid.h" +#include "core/ui/card_drag.h" +#include "core/ui/card_meta.h" +#include "core/ui/component_geometry.h" +#include "core/ui/drag_out.h" +#include "core/ui/footer_bar.h" +#include "core/ui/mode_enable.h" +#include "core/ui/overflow_menu.h" +#include "core/ui/prune_button.h" +#include "core/ui/tab_strip.h" +#include "core/ui/theme.h" +#include "core/ui/tooltip.h" +#include "core/version/app_version.h" +#include "core/view/guid_diff.h" +#include "core/view/lane_keys.h" +#include "core/view/mode_switch.h" +#include "core/wire/instrument_drop.h" -#include "shell/panel/panel_layout.h" // BankPanelFullHeight — the split-state enum +#include "shell/panel/panel_layout.h" namespace reasampler { class ReaSamplerSession; @@ -77,14 +61,6 @@ class ReaSamplerSession; namespace reasampler::panel { -// --- Real-namespace-home using-declarations ----------------------------------- -// -// The panel's pre-split internals reference the pure modules' symbols unqualified; -// these explicit per-symbol usings keep those references valid while documenting -// each symbol's Q-W1 home. Flat-`reasampler` symbols (BankBook / ViewModeModel / -// the draw_kit shell / the shell/bank_ops verbs / ...) resolve via the enclosing -// namespace and need no using. - // core/ui using ui::ActionBarRect; using ui::ActionBarSlot; @@ -200,73 +176,46 @@ using version::dockTitle; // core/wire using wire::buildInstrumentDropPreset; -// --- Layout constants --------------------------------------------------------- -// -// L2: every panel COLOR comes from the pure `theme` module by ROLE (drawn through the -// L1 kit — fillSurface / drawButton / kit text). Only the pixel LAYOUT metrics (band -// heights, grid/tab specs, insets) live here, shared by the layout/render/input/drag -// seams so draw and hit-test can never drift. +// All color comes from `theme` by role, drawn through the kit. Only pixel layout +// metrics live here, shared by layout/render/input/drag so draw and hit-test can +// never drift. inline const GridSpec kGrid{/*cellWidth=*/140, /*cellHeight=*/84, /*gap=*/10}; -// --- Footer (Phase L, L4) ----------------------------------------------------- -// The footer carries a task-cluster of small persistent controls: the narrowed -// [Arrange|Design] mode toggle, a compact per-mode count, the Tail BUTTON (L4 §4 — -// a real kit button, no longer a click-zone), and the set-apart Prune button at the -// right. Taller than the L2 footer to host the toggle segments + button chrome cleanly. -// Layout is the pure footer_bar (left group) + prune_button (right); this is the band height. +// Footer: [Arrange|Design] toggle, per-mode count, Tail button, Prune button +// (footer_bar left group + prune_button right). inline constexpr int kFooterHeight = 30; -// --- Toolbars (Phase L, L4) --------------------------------------------------- -// TWO task-grouped toolbars, both drawn through the pure action_bar module: -// * kTopToolbarHeight — the TOP toolbar (capture + placement clusters) at the very top of -// the client, where the eye lands (L4 §1). Replaces the L2 mode-switch header there. -// * kBottomToolbarHeight — the BOTTOM toolbar (Design-View tag/switch verbs) directly above -// the footer (L4 §2). This is the L2 action-bar band, repurposed. -inline constexpr int kTopToolbarHeight = 28; // single-row label face (L6: keybinding sub-row removed) -inline constexpr int kBottomToolbarHeight = 28; // same shape — both bars consistent +// Two task-grouped toolbars drawn through action_bar: top = capture + placement, +// bottom = Design-View tag/switch verbs, directly above the footer. +inline constexpr int kTopToolbarHeight = 28; +inline constexpr int kBottomToolbarHeight = 28; -// --- Tooltip (Phase L, L5) ---------------------------------------------------- -// The custom hover-delay tooltip's timing + approximate text metrics. The delay matches the -// platform convention (~0.5 s) so the tooltip is deliberate, not twitchy; it is driven off the -// OnTimer poll (bankPanelRefresh) + WM_MOUSEMOVE, so no dedicated timer is added. The kit font -// is AA and proportional, so the width is estimated from a per-char average (the tooltip box is -// generous — a slight over/under-estimate only pads the box, never clips the text). +// Hover-delay tooltip timing, driven off the OnTimer poll (no dedicated timer). Kit +// font is proportional, so char width is a generous estimate (pads, never clips). inline constexpr unsigned int kTooltipDelayMs = 500; -inline constexpr int kTooltipCharPx = 7; // approx px per char at Font::Label (generous) -inline constexpr int kTooltipTextH = 14; // approx line height at Font::Label +inline constexpr int kTooltipCharPx = 7; +inline constexpr int kTooltipTextH = 14; -// --- Vertical split + region headers + tab strip (Phase B4; L4 re-home) ------- -// -// The client area, top to bottom (L4): TOP toolbar (kTopToolbarHeight, capture + placement) | -// split body | BOTTOM toolbar (kBottomToolbarHeight, Design-View verbs) | footer -// (kFooterHeight — mode toggle + count + Tail button + Prune). The split body holds the pool -// region (top) and the named-banks region (bottom). Each region opens with a REGION HEADER -// band: a title, the active-bank readout, and a full-height toggle button. The named-banks -// region's header ALSO hosts the LICE tab strip and a "+" create button. -inline constexpr int kRegionHeaderHeight = 24; // per-region title/toggle band -inline constexpr int kTabStripHeight = 26; // the named-banks tab strip band -inline constexpr int kSplitDividerHeight = 3; // the horizontal divider between regions -inline constexpr int kFullHtBtnWidth = 22; // the square full-height toggle button -inline constexpr int kCreateBtnWidth = 22; // the "+" create-bank button +// Client area top to bottom: top toolbar | split body | bottom toolbar | footer. +inline constexpr int kRegionHeaderHeight = 24; +inline constexpr int kTabStripHeight = 26; +inline constexpr int kSplitDividerHeight = 3; +inline constexpr int kFullHtBtnWidth = 22; +inline constexpr int kCreateBtnWidth = 22; -// Tab strip metrics (the pure tab_strip owns the math; these are its inputs). inline const TabStripSpec kTabSpec{/*tabWidth=*/96, /*chevronWidth=*/20}; -// The spec for the far-right More ("⋯") overflow-menu button. One source of truth for its -// geometry + the reserve the action_bar leaves for it (L5). +// Far-right More ("...") overflow-menu button: one source of truth for its +// geometry + the reserve action_bar leaves for it. inline const MenuButtonSpec kMenuBtnSpec{/*buttonWidth=*/28, /*rightInset=*/6, /*verticalInset=*/3, /*minLeftInset=*/40}; -// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared -// by both toolbars' draw and hit-test (identical button shape top and bottom). L5 refinement 5: -// clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups. L6: -// bindingHeight / minSplitHeight removed — buttons are single-row label-only faces now. +// Shared by both toolbars' draw and hit-test (identical button shape top and +// bottom). clusterGap is wider than buttonGap so semantic groups read as groups. inline const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24, /*sidePad=*/8, /*verticalInset=*/3}; -// --- Panel state -------------------------------------------------------------- - struct CachedThumbnail { Envelope envelope; int width = 0; @@ -276,32 +225,28 @@ struct CachedThumbnail { // input. The move/copy source is the focused region's displayed bank. enum class Region { Pool, Banks }; -// What a drag is dropping onto, resolved live under the pointer during a drag. -// BanksRegion fires when the pointer is anywhere in the named-banks grid that is NOT -// on a specific tab (tab takes precedence — more specific wins). The resolved bank is -// always shownBankId. +// What a drag is dropping onto, resolved live under the pointer. BanksRegion fires +// when the pointer is anywhere in the named-banks grid that is NOT on a specific tab +// (tab takes precedence); the resolved bank is always shownBankId. enum class DropKind { None, PoolRegion, Tab, BanksRegion }; -// --- Hover model (Phase L, L2) ------------------------------------------------ -// // The hovered interactive element, resolved live in WM_MOUSEMOVE so the kit draws its -// hover state on that element only (the "hover on every interactive element" + "sub-frame -// feedback = the perception of speed" L2 constraint). SWELL exposes no WM_MOUSELEAVE (grep -// of vendor/WDL/WDL/swell — none), so hover is cleared by a move that resolves to None -// rather than a leave message; the panel is Windows-only (D5) but this stays portable-safe. +// hover state on that element only. SWELL exposes no WM_MOUSELEAVE (confirmed: no hit in +// vendor/WDL/WDL/swell), so hover is cleared by a move that resolves to None rather than a +// leave message; the panel is Windows-only but this stays portable-safe. // `index` disambiguates within a kind (action-bar button index, tab index); -1 when N/A. enum class HoverKind { None, - TopBarButton, // a button in the TOP toolbar (index = flat action index into topBarRows) - BottomBarButton, // a button in the BOTTOM toolbar (index = flat action index into bottomBarRows) - MoreButton, // the TOP toolbar's far-right "⋯" overflow-menu button (L5) + TopBarButton, // index = flat action index into topBarRows + BottomBarButton, // index = flat action index into bottomBarRows + MoreButton, PruneButton, - FullHtPool, // pool region full-height toggle - FullHtBanks, // banks region full-height toggle - CreateBank, // the "+" create-bank button - Tab, // a named-bank tab (index = tab ordinal) - TailButton, // the footer Tail button (L4 §4 — a real button, was a click-zone) - ModeSegment, // a footer mode-toggle segment (index = segment ordinal) + FullHtPool, + FullHtBanks, + CreateBank, + Tab, // index = tab ordinal + TailButton, + ModeSegment, // index = segment ordinal }; struct Hover { @@ -312,9 +257,8 @@ struct Hover { bool operator!=(const Hover& o) const { return !(*this == o); } }; -// The kit interaction state for an interactive element: Hover when this (kind,index) is the -// live hovered element, else Rest. Active/Pressed are decided per-element by the caller (e.g. -// an active tab draws Active regardless of hover); this is the base rest/hover resolver. +// Base rest/hover resolver; Active/Pressed are decided per-element by the caller +// (e.g. an active tab draws Active regardless of hover). inline InteractionState hoverState(const Hover& hovered, HoverKind kind, int index) { return (hovered.kind == kind && hovered.index == index) ? InteractionState::Hover : InteractionState::Rest; @@ -331,135 +275,83 @@ struct PanelState { std::unordered_map cache; - // --- Selection (per focused region) --------------------------------------- // One live selection, scoped to `focusedRegion`. Switching regions moves the - // selection with the focus (a click in the other region reseeds it there). + // selection with the focus. Selection selection; int selItemCount = 0; Region focusedRegion = Region::Pool; - // --- Hover (Phase L, L2) -------------------------------------------------- - // The live hovered interactive element (WM_MOUSEMOVE resolves it; the kit draws its - // hover state). Repaint fires only when this changes (sub-frame, no per-move jank). + // Resolved on WM_MOUSEMOVE; repaint fires only when this changes. Hover hovered; - // --- Tooltip (Phase L, L5) ------------------------------------------------ - // A custom LICE-kit hover-delay tooltip (NOT the native Win32/SWELL tooltip control): when a - // TOOLTIP-capable element (a toolbar button) stays hovered past kTooltipDelayMs, the panel - // draws a small overlay carrying the full, prefix-stripped action name. hoverSinceTick is the - // GetTickCount() at which the CURRENT hovered element was first entered (reset on every hover - // change); tooltipShown latches once the delay elapses so the OnTimer poll repaints exactly - // once when the tooltip appears. The last-seen pointer pos anchors nothing (the anchor is the - // hovered button's rect), but is kept so the OnTimer path can re-resolve without a live event. + // Custom hover-delay tooltip. hoverSinceTick resets on every hover change; + // tooltipShown latches once the delay elapses so the OnTimer poll repaints once. unsigned int hoverSinceTick = 0; bool tooltipShown = false; - // --- Vertical-split state ------------------------------------------------- BankPanelFullHeight fullHeight = BankPanelFullHeight::Split; - // The named bank whose grid the banks region shows (the SHOWN tab) — DISTINCT - // from the active/capture-target bank (book().activeBankId()). Empty when there - // are no named banks. Reconciled each fingerprint pass so it always names a live - // named bank (or is empty). + // The named bank the banks region shows — distinct from the active/capture-target + // bank. Reconciled each fingerprint pass so it always names a live bank (or empty). std::string shownBankId; - // Tab-strip horizontal scroll offset (px), clamped to the strip's max each frame. int tabScroll = 0; - // --- Drag (sample move between regions/onto a tab) ------------------------ - // A drag begins only after the pointer moves past a threshold from a press that - // landed on a SELECTED grid cell — this is how it is disambiguated from the M5 - // multi-select drag (which begins immediately on any grid press). See handleClick/ - // onMouseMove. dragging is true once the threshold is crossed. - bool dragArmed = false; // pressed on a selected cell; watching for threshold - bool dragging = false; // threshold crossed; a move-drag is in progress + // A drag begins only after the pointer moves past a threshold from a press + // that landed on a selected grid cell — disambiguates from the multi-select + // drag (which begins immediately on any grid press). + bool dragArmed = false; + bool dragging = false; int dragStartX = 0, dragStartY = 0; Region dragSourceRegion = Region::Pool; - std::string dragSourceBankId; // the bank the dragged samples come from - std::vector dragSampleIds;// snapshot of the selection at drag start - std::string dragPrimaryId; // the single card grabbed (the focus) — the L7 - // reorder/replace subject (see onLBtnUp dispatch) - DropKind dropKind = DropKind::None; // live drop target under the pointer + std::string dragSourceBankId; + std::vector dragSampleIds; + std::string dragPrimaryId; // the single card grabbed — the reorder/replace subject + DropKind dropKind = DropKind::None; std::string dropBankId; // destination bank id when dropKind==Tab - // --- L7 in-grid reorder/replace drag -------------------------------------- - // The live card gesture resolved by the pure card_drag::decideCardGesture each mouse- - // move (drives the cursor cue AND the drop dispatch), plus the same-bank target slot the - // pointer sits over (>= 0 only for a Reorder/Replace over the source bank's own grid; -1 - // otherwise). A Reorder highlights dragTargetSlot's cell; Replace + a live cursor cue - // signal the Alt-over-occupied case. Reset with the rest of the drag state on drop/cancel. + // Resolved each mouse-move by card_drag::decideCardGesture. dragTargetSlot >= 0 + // only for a Reorder/Replace over the source bank's own grid. CardGesture cardGesture = CardGesture::None; int dragTargetSlot = -1; - // --- S17 drop-and-load (InstrumentDrop) ----------------------------------- - // While a SINGLE-capture drag is over REAPER's own UI outside the panel, the drag is an - // InstrumentDrop heading for a track's TCP FX button. The shell hover-tracks the FX - // hotspot; on release over a valid target it adds a ReaSampler 9000 preloaded with the - // dragged capture (no OS drag, no timeline insert). instrumentDropTrack is the last - // resolved FX-hotspot track (null when the pointer is not over an FX button) — read on - // release. Only set/used on Windows (D5); the M11 OsDrag and internal drag are untouched. + // While a single-capture drag is over REAPER's own UI, heading for a track's TCP FX + // button: on release this adds a ReaSampler 9000 preloaded with the capture. Null + // when the pointer is not over an FX button. MediaTrack* instrumentDropTrack = nullptr; - // --- Tail-mode toggle ----------------------------------------------------- - // The authoritative tail setting lives in ReaSamplerSession (session->tail()), - // NOT in panel state, so it travels inside the .rpp (persist serializes it on save, - // restores it on project load). The panel reads it for drawing and mutates it via - // the footer click (cycle mode) and scroll-wheel (Manual fine-adjust), marking the - // project dirty so the choice saves. bankPanelTailSetting is the read seam for the - // capture actions. Held here only through the session pointer above. + // Authoritative tail setting lives in ReaSamplerSession, not here; panel reads it for + // drawing and mutates via footer click / scroll-wheel. bankPanelTailSetting is the + // capture actions' read seam. - // --- Audition preview ----------------------------------------------------- preview_register_t preview{}; PCM_source* previewSrc = nullptr; bool previewActive = false; bool previewInited = false; // guards double init / deinit - // --- New-content detection (D2 Wave 2) ------------------------------------ - // // Each timer tick diffs the live track+item GUID set against the previous tick to - // auto-tag content created SINCE the last tick into the then-active mode. The - // baseline carries the first-poll-after-open guard (GuidBaseline self-arms on its - // first observe()) so pre-existing content is never mass-tagged (it stays Arrange). - // - // Project-load re-arm is driven by persist's AUTHORITATIVE load lifecycle, NOT by a - // pointer compare here. main.cpp calls bankPanelNotifyProjectLoaded() on the exact - // tick persist restores a project's membership + active mode (the same tick it - // reapplies the active mode); that sets reloadPending so the NEXT detect tick this - // same tick re-baselines against the fully-loaded set and reports nothing new. This - // replaces the former `proj != lastProject` re-arm, which used a WEAKER signal than - // persist (pointer-only vs persist's GUID-primary identity) and so missed a load onto - // a RECYCLED ReaProject* address — the just-loaded project's pre-existing tracks then - // diffed against the previous project's stale baseline and were mass-tagged into the - // active mode (the reload-mis-tag bug). Coordinating with persist's signal makes the - // two identity checks agree by construction. - // - // Lives for the extension's lifetime alongside the session, independent of panel - // open/close — detection must run whether or not the dock is visible (content is - // created in the arrange, not the panel). + // auto-tag new content. GuidBaseline self-arms on first observe() so pre-existing + // content is never mass-tagged. Project-load re-arm is driven by persist's load + // signal, not a ReaProject* compare — a recycled address previously mis-tagged tracks. GuidBaseline contentBaseline; bool reloadPending = false; // set by bankPanelNotifyProjectLoaded; drained next detect tick }; -// The one shared panel state blob. Defined in panel_window.cpp (the lifecycle owner). +// Defined in panel_window.cpp (the lifecycle owner). extern PanelState g_panel; -// --- L7 slot-order display bridge --------------------------------------------- -// -// L7 re-maps cell index <-> sample identity: the grid draws in the bank's persisted -// SlotMap order (sparse, gap-preserving), NOT BankModel insertion order. regionDisplay -// (panel_layout.cpp) is the single place that resolves a region's display, composed -// purely from bank_book's slot order (orderedSampleIds) + card_drag's sparse slot rects -// (computeSlotRects) — the shell adds no layout math of its own. +// The grid draws in the bank's persisted SlotMap order (sparse, gap-preserving), +// NOT BankModel insertion order. regionDisplay (panel_layout.cpp) is the single +// place that resolves a region's display. // // TWO INDEX SPACES the whole panel must keep straight: -// * SLOT — a display position 0..maxSlot; gaps are empty slots that draw as empty -// cells and are valid drop targets. This is what pixels/hit-tests speak. -// * SELECTION — the DENSE occupied-ordinal [0, occupied) space the pure Selection / -// applyClick / navigate reason in. Selection index i <-> orderedIds[i]. -// Keyboard navigation therefore traverses ONLY occupied cells and SKIPS -// gaps (spec: skip-vs-land-on-gap is unspecified -> skip, documented here). -// RegionDisplay carries both plus the translation between them, resolved FRESH each call -// (never cached across a mutation, per the reference-invalidation guardrail). +// * SLOT — display position 0..maxSlot; gaps are empty, valid drop targets. +// What pixels/hit-tests speak. +// * SELECTION — the dense occupied-ordinal [0, occupied) space the pure +// Selection/applyClick/navigate reason in. Selection index i <-> +// orderedIds[i]. Keyboard nav therefore skips gaps. +// RegionDisplay carries both plus the translation, resolved fresh each call (never +// cached across a mutation, per the reference-invalidation guardrail). struct RegionDisplay { std::vector orderedIds; // occupied ids in slot order (selection space) std::vector slotRects; // one rect per slot 0..maxSlot, viewport coords @@ -486,27 +378,18 @@ struct RegionDisplay { int occupiedCount() const { return static_cast(orderedIds.size()); } }; -// --- Toolbar row vocabulary (Phase L, L4/L5/L6) -------------------------------- -// -// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel prefix -// at fire time — never a hardcoded numeric id), its terse on-button FACE label, its full action -// NAME for the hover tooltip (already prefix-stripped — the "ReaSampler:" display prefix is -// dropped at build), and the task cluster it belongs to. The order of a toolbar's row list IS -// the flat action index the pure action_bar slots carry, so each list is built cluster-by-cluster -// in its toolbar's cluster order. Built by panel_layout (topBarRows / bottomBarRows / -// overflowMenuRows); consumed by the render draw, the input click routing, and the drag hover. +// One action button. Row order IS the flat action index the pure action_bar slots +// carry. Built by panel_layout; consumed by render, input, drag. struct ActionBarRow { std::string suffix; std::string shortLabel; std::string fullName; ActionCluster cluster = ActionCluster::Capture; - bool enabled = true; // L5: opposite-mode gate for the bottom-bar tag buttons; always true - // for the top bar (its actions are unconditional triggers). + bool enabled = true; // opposite-mode gate for bottom-bar tag buttons; always true + // for the top bar (unconditional triggers) }; -// --- Shared one-liner helpers -------------------------------------------------- - -// Modifier state at event time. Alt = the L7 replace modifier. +// Modifier state at event time. Alt = the replace modifier. inline bool ctrlDown() { return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; } inline bool shiftDown() { return (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; } inline bool altDown() { return (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; } @@ -515,7 +398,7 @@ inline void invalidatePanel() { if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE); } -// --- Cross-seam contract (grouped by OWNING TU; all plain free functions) ------ +// Cross-seam contract, grouped by owning TU; all plain free functions. // panel_bank_ops.cpp — book/bank accessors + the bank-CRUD verbs + menus. BankBook* book(); @@ -536,7 +419,7 @@ void showSelectionMenu(int screenX, int screenY); void showMoreMenu(); // panel_layout.cpp — toolbar/footer/menu rects, row/cluster builders, split geometry, -// region rects, the L7 display bridge. Draw and hit-test both call these so they never drift. +// region rects, the display bridge. Draw and hit-test both call these so they never drift. int modeCount(); MenuButtonRect topMenuButtonRect(int w); ActionBarRect topToolbarActionRect(int w); @@ -579,8 +462,8 @@ const Envelope& thumbnailFor(const Sample& sample, int width, const std::string& bool refreshFingerprint(); void reconcileShownBank(); -// panel_audition.cpp — the preview engine (HOT PATH: direct call-through, never -// virtual, no added header->TU indirection — T4-28 / Q-W2 guardrail). +// panel_audition.cpp — the preview engine (hot path: direct call-through, never +// virtual, no added header->TU indirection). void initPreview(); void deinitPreview(); void stopAudition(); @@ -595,7 +478,7 @@ void registerAccel(); void unregisterAccel(); // panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag). -// Per-mouse-move work stays plain free-function calls (T4-28). +// Per-mouse-move work stays plain free-function calls. void onMouseMove(int x, int y); void onLBtnUp(int x, int y); void handleRightClick(int x, int y); diff --git a/src/shell/panel/panel_thumbnails.cpp b/src/shell/panel/panel_thumbnails.cpp index 65bc1f5..4babe0b 100644 --- a/src/shell/panel/panel_thumbnails.cpp +++ b/src/shell/panel/panel_thumbnails.cpp @@ -1,16 +1,10 @@ -// panel_thumbnails.cpp — thumbnail compute + cache seam of the docked bank panel -// (Q-W2 split of bank_panel.cpp; M5/FA3). Owns the per-sample PCM read via PCM_source -// fed to peaks::computeEnvelope (one bin per drawn pixel column) and the in-memory -// thumbnail cache keyed by (sample id, bin width, bank generation) — plus the -// bank-change fingerprint pass that OWNS that generation key: refreshFingerprint bumps -// the generation, clears the cache, resets selection/audition, and reconciles the -// shown bank on any book mutation. (The fingerprint pass lives here rather than in -// panel_input because the cache + generation it invalidates are this seam's state — -// a Q-W2 placement judgment; the T4-01 audit lumped it under the input seam's range.) +// panel_thumbnails.cpp — thumbnail compute + cache seam of the docked bank panel. +// Owns the per-sample PCM read fed to peaks::computeEnvelope and the in-memory +// thumbnail cache keyed by (sample id, bin width, bank generation), plus the +// bank-change fingerprint pass that owns that generation key — it lives here +// because the cache + generation it invalidates are this seam's state. // -// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested. +// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested. #include #include @@ -29,8 +23,7 @@ namespace { constexpr int kMaxThumbnailFrames = 1 << 20; // ~1M frames (~22s @ 48k) -// --- Thumbnail computation (M5; `width` is a BIN count since FA3 oversampling) -- - +// `width` is a BIN count. Envelope computeThumbnail(const std::string& absPath, int width) { if (width <= 0 || absPath.empty()) return {}; @@ -98,13 +91,11 @@ const Envelope& thumbnailFor(const Sample& sample, int width, return ins.first->second.envelope; } -// --- Bank-change detection ---------------------------------------------------- - namespace { // A fingerprint of the WHOLE BOOK: for each bank, its id + display name + active flag // + per-sample id/path. Catches every mutation the panel must redraw for: capture, -// project load, and B4's own create/rename/delete/move/activate. +// project load, and create/rename/delete/move/activate. std::string bookFingerprint() { BankBook* b = book(); if (!b) return {}; diff --git a/src/shell/panel/panel_window.cpp b/src/shell/panel/panel_window.cpp index d64a9b4..00b4ce7 100644 --- a/src/shell/panel/panel_window.cpp +++ b/src/shell/panel/panel_window.cpp @@ -1,16 +1,11 @@ -// panel_window.cpp — the window-lifecycle seam of the docked bank panel (Q-W2 split -// of bank_panel.cpp; M5 Wave A). Owns the SWELL dialog (IDD_BANK_PANEL) docked via -// DockWindowAddEx / undocked via DockWindowRemove, the dialog proc that routes -// messages to the input/drag/render/audition seams, the S8 OS drop-target opt-in -// (WM_DROPFILES -> ingest), and the shared PanelState blob's definition. +// panel_window.cpp — window-lifecycle seam of the docked bank panel. Owns the SWELL +// dialog (docked via DockWindowAddEx / undocked via DockWindowRemove), the dialog +// proc routing to the input/drag/render/audition seams, the OS drop-target opt-in +// (WM_DROPFILES -> ingest), and the shared PanelState blob's definition. The panel +// never inserts into the arrange. Dock title + persisted-position identstr both +// come from app_version (channel-qualified). // -// READ-ONLY of the TIMELINE (load-bearing principle): the panel never inserts into -// the arrange. Channel-qualified dock identity (Phase V, V4): title + persisted- -// position identstr both come from app_version. -// -// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h -// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are -// extern (CLAUDE.md §contract). DAW-verified, not unit tested. +// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested. #include #include @@ -18,12 +13,12 @@ #include "shell/panel/panel_state.h" #include "shell/panel/panel_window.h" -#include "shell/panel/draw_kit.h" // kitFontsInit/Shutdown — the kit's cached AA fonts (L1) -#include "ingest.h" // ingestDroppedFiles — S8 drop-onto-panel ingest +#include "shell/panel/draw_kit.h" +#include "ingest.h" #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux) -#include // DragAcceptFiles / DragQueryFile / DragFinish — S8 drop ingest +#include // DragAcceptFiles / DragQueryFile / DragFinish #endif #include "resource.h" @@ -40,26 +35,20 @@ extern REAPER_PLUGIN_HINSTANCE g_hInst; namespace reasampler::panel { -// The one shared panel state blob (declared extern in panel_state.h). Defined here — -// the lifecycle seam owns the state's lifetime, mirroring the old single-TU global. +// Defined here — the lifecycle seam owns the state's lifetime. PanelState g_panel; -// --- Dialog proc + docking ---------------------------------------------------- - namespace { -// Decodes a WM_DROPFILES HDROP into the dropped file paths (absolute, OS-native) and hands -// them to the S8 ingest path. Multi-file drop: ingestDroppedFiles imports all into the active -// bank (bank-fill only — no assignment to any live instance). Always DragFinish's the HDROP -// (frees the shell-allocated drop buffer) on every path. DragQueryFile(hDrop, 0xFFFFFFFF, ...) -// returns the file count; then each path is queried by index. Both Win32 and SWELL expose -// DragQueryFile/DragFinish with this contract. +// DragQueryFile(hDrop, 0xFFFFFFFF, ...) returns the file count; each path is then +// queried by index (length first, excludes NUL, then a sized buffer). DragFinish +// always frees the shell-allocated drop buffer. Multi-file drop imports all into +// the active bank (bank-fill only — no assignment to any live instance). void handleDropFiles(HDROP hDrop) { std::vector paths; const UINT count = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0); paths.reserve(count); for (UINT i = 0; i < count; ++i) { - // Query the required length first (excludes the NUL), then read into a sized buffer. const UINT len = DragQueryFile(hDrop, i, nullptr, 0); if (len == 0) continue; std::vector buf(static_cast(len) + 1, '\0'); @@ -74,8 +63,6 @@ void handleDropFiles(HDROP hDrop) { WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DROPFILES: - // S8 drop-onto-panel ingest: OS file drop on the docked panel HWND -> import - // into the active bank (bank-fill only). wParam is the HDROP. handleDropFiles(reinterpret_cast(wParam)); return 0; case WM_PAINT: { @@ -101,10 +88,8 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { handleRightClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_CAPTURECHANGED: - // Capture lost (pointer left window pre-threshold and released outside, or another - // window stole capture mid-drag) — cancel the whole drag as a NO-OP so no stale - // state lingers, mirroring onLBtnUp's reset (peer-path symmetry). Nothing is - // mutated on a cancel; the cursor is restored to the arrow. + // Capture lost (pointer left pre-threshold, or another window stole it + // mid-drag) — cancel the drag as a no-op, mirroring onLBtnUp's reset. if (g_panel.dragArmed || g_panel.dragging) { resetDragState(); SetCursor(LoadCursor(nullptr, IDC_ARROW)); @@ -112,13 +97,9 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { } return 0; case WM_MOUSEWHEEL: { - // Fine-adjust the Manual tail length when the wheel is over the footer. - // UNLIKE the button messages, WM_MOUSEWHEEL carries SCREEN coordinates in - // lParam (Win32 and SWELL agree — swell-generic-gdk.cpp §WM_MOUSEWHEEL), so - // convert to client space before hit-testing the footer. The signed wheel - // delta is the HIWORD of wParam (SWELL packs it as (delta<<16), delta=+/-120, - // matching GET_WHEEL_DELTA_WPARAM). Consume (return 1) only when the footer - // handler acts, so scrolling elsewhere in the dock still behaves normally. + // Unlike button messages, WM_MOUSEWHEEL carries SCREEN coords in lParam + // (Win32 and SWELL agree), so convert to client space first. Wheel delta + // is the HIWORD of wParam. Consume (return 1) only when the footer acts. POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; ScreenToClient(hwnd, &pt); const int delta = static_cast(HIWORD(wParam)); @@ -147,30 +128,23 @@ void openPanel() { } initPreview(); - // Create the kit's cached AA fonts before the first paint (Phase L, L1). Idempotent, so - // a reopen after closePanel (which leaves the fonts alive) is a cheap no-op; the fonts - // are torn down once at bankPanelShutdown. All panel text draws through these. + // Idempotent — a reopen after closePanel (fonts left alive) is a cheap no-op; + // torn down once at bankPanelShutdown. kitFontsInit(); g_panel.hwnd = CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), GetMainHwnd(), dlgProc, 0); if (!g_panel.hwnd) return; - // Channel-qualified dock identity (Phase V, V4). The title and the persisted-position - // identstr both come from app_version, so a beta panel is distinguishable ("ReaSampler - // Bank beta") and does not fight over stable's saved dock slot (the identstr is a - // REAPER-global collision surface — it keys the persisted dock position). + // identstr is a REAPER-global collision surface keying the persisted dock + // position — channel-qualified so beta doesn't fight over stable's slot. DockWindowAddEx(g_panel.hwnd, dockTitle().c_str(), dockIdent().c_str(), true); DockWindowActivate(g_panel.hwnd); g_panel.open = true; - // S8: accept OS file drops on the panel HWND (WM_DROPFILES routes to handleDropFiles). - // DragAcceptFiles is a native Win32 shell call (shellapi.h); SWELL does NOT expose it, - // so the opt-in is Windows-only here. The primary/shipped platform is Windows (the VST3 - // instrument the drop assigns to is Windows-only, D5); a mac/linux drop-registration - // surface is out of scope for this dispatch. WM_DROPFILES handling itself uses - // DragQueryFile/DragFinish, which SWELL DOES provide, so a drop delivered by other means - // would still ingest — only the accept opt-in is gated. + // DragAcceptFiles is native Win32 (shellapi.h); SWELL doesn't expose it, so the + // accept opt-in is Windows-only. DragQueryFile/DragFinish ARE SWELL-provided, + // so a drop delivered by other means would still ingest. #ifdef _WIN32 DragAcceptFiles(g_panel.hwnd, TRUE); #endif @@ -199,27 +173,20 @@ void closePanel() { } // namespace reasampler::panel -// --- Public API (the lifecycle seam — panel_window.h) -------------------------- - namespace reasampler { void bankPanelInit(ReaSamplerSession* session) { panel::g_panel.session = session; } -// Returns true only when the panel window is actually visible to the user right now. -// IsWindowVisible() returns false when the docker is hidden via Alt+D even though the -// HWND and g_panel.open are still live — the live query is the source of truth for -// toggle decisions and the Actions-list checkmark (OnToggleAction in main.cpp). +// Alt+D hides the docker without destroying the window, leaving HWND/g_panel.open +// live but IsWindowVisible false — the live query is the source of truth for +// toggle decisions and the Actions-list checkmark. static bool panelEffectivelyVisible() { return panel::g_panel.hwnd && IsWindowVisible(panel::g_panel.hwnd); } void bankPanelToggle() { - // Decide from live visibility, not the cached g_panel.open flag. - // Alt+D hides the docker without destroying the window, leaving g_panel.open - // stale (true) while the panel is gone. Using IsWindowVisible avoids the - // double-fire needed to re-show the panel after a docker hide. if (panelEffectivelyVisible()) panel::closePanel(); else @@ -227,8 +194,6 @@ void bankPanelToggle() { } bool bankPanelIsOpen() { - // Derive from live window state so the Actions-list checkmark stays honest - // even after Alt+D hides the docker without notifying the extension. return panelEffectivelyVisible(); } @@ -239,7 +204,7 @@ void bankPanelInvalidate() { void bankPanelShutdown() { panel::closePanel(); panel::deinitPreview(); - kitFontsShutdown(); // free the kit's cached AA fonts + their owned HFONTs (L1) + kitFontsShutdown(); panel::g_panel.cache.clear(); panel::g_panel.session = nullptr; } diff --git a/src/shell/panel/panel_window.h b/src/shell/panel/panel_window.h index 654727c..1143aa3 100644 --- a/src/shell/panel/panel_window.h +++ b/src/shell/panel/panel_window.h @@ -1,42 +1,29 @@ #pragma once -// panel_window — the window-lifecycle seam of the docked bank panel (Q-W2 split of -// bank_panel.h; M5, Wave A). REAPER-facing shell: the .cpp owns a SWELL dialog -// (IDD_BANK_PANEL) docked via DockWindowAddEx / undocked via DockWindowRemove, -// toggled open/closed, plus the OS drop-target opt-in (S8) and the dialog proc that -// routes messages to the input/drag/render seams. The panel itself NEVER inserts -// into the arrange or mutates the project (CONTEXT.md §load-bearing principle). -// -// The header is REAPER-free: main.cpp drives the panel through these free functions, -// passing the live session so the panel reads the current bank. All SWELL / LICE / -// PCM_source use is confined to the shell/panel/ .cpp seams. +// panel_window — window-lifecycle seam of the docked bank panel: owns the SWELL +// dialog (dock/undock, toggle), the drop-target opt-in, and the dialog proc that +// routes to the input/drag/render seams. Panel never inserts into the arrange or +// mutates the project. Header is REAPER-free; main.cpp drives it via these +// free functions. namespace reasampler { class ReaSamplerSession; -// Wires the panel into main.cpp's lifecycle. Called once after the API pointers -// are loaded, BEFORE the toggle action is registered. `session` must outlive the -// panel (it is the extension-lifetime g_session). Stores the session pointer the -// panel reads on every repaint; does not create the window yet. +// `session` must outlive the panel (extension-lifetime g_session). Call once +// after API pointers load, before the toggle action registers. void bankPanelInit(ReaSamplerSession* session); -// Toggles the docked window: creates+docks it if hidden, hides+undocks it if -// shown. Bound to the "toggle bank panel" action. Safe to call before the first -// timer tick. +// Creates+docks if hidden, hides+undocks if shown. void bankPanelToggle(); -// Whether the panel window is currently open/visible. Feeds the action's -// checked-state (toggleaction) so REAPER shows a tick next to the menu entry. +// Feeds the toggle action's checked-state. bool bankPanelIsOpen(); -// Requests an immediate repaint of the panel if it is open. A no-op when the panel -// is closed (safe to call unconditionally). Called by the actions layer after a -// mode change so the footer [Arrange|Design] toggle reflects the new mode without -// requiring a hide/reshow. +// No-op if closed. Called after a mode change so the footer reflects it without +// a hide/reshow. void bankPanelInvalidate(); -// Tears the panel down on extension unload: destroys the window and releases any -// cached thumbnails / PCM handles. Mirror of bankPanelInit; safe if never opened. +// Mirror of bankPanelInit; safe if never opened. void bankPanelShutdown(); } // namespace reasampler