Merge Ψ-W1-T4: resolve drop targets per move, not once
# Conflicts: # src/shell/actions/CLAUDE.md
This commit is contained in:
+19
-10
@@ -76,14 +76,20 @@ L7 sub-pass, 2026-07-27):
|
||||
geometry modules; the kit's *draw* half is shell, its *geometry* half is pure,
|
||||
even where a WDL piece is reused. Look-and-feel work never touches capture,
|
||||
placement, or bank data ownership.
|
||||
- **L7 drag-gesture precedence is a pure decision helper.** The rule — leave
|
||||
client rect → OS drag-out; else drop on a tab/other bank → move/copy; else
|
||||
same-bank grid → reorder-to-slot (empty slot = place, occupied + no modifier =
|
||||
insert-before-and-shift, occupied + Alt = replace) — is "encoded in a pure
|
||||
decision helper (mirror `drag_out::decideGesture`)"; the shell only reads live
|
||||
pointer/focus/client-rect/modifier state and calls it, then maps the resolved
|
||||
gesture to a cursor via `SetCursor`. No cue or precedence logic belongs in the
|
||||
shell.
|
||||
- **Drag-gesture precedence is a pure decision helper, on both sides of the client
|
||||
rect.** Inside: drop on a tab/other bank → move/copy; else same-bank grid →
|
||||
reorder-to-slot (empty slot = place, occupied + no modifier =
|
||||
insert-before-and-shift, occupied + Alt = replace). Outside: `decideDropClass`
|
||||
resolves the surface under the cursor. The shell only reads live
|
||||
pointer/focus/client-rect/modifier state and calls these, then maps the resolved
|
||||
cue to a cursor via `SetCursor`. No cue or precedence logic belongs in the shell.
|
||||
- **The drag-out law is per-move and stateless.** The class is resolved from the
|
||||
current pointer on every move and again at the release point; nothing is latched
|
||||
between evaluations. That is what makes every transition reversible and what
|
||||
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. Leaving
|
||||
REAPER entirely is the one irreversible transition, because the OS hand-off goes
|
||||
modal. A first-move class lock and a drag-lifetime "cannot hand off" latch both
|
||||
existed here and were removed — do not reintroduce either.
|
||||
|
||||
## Modules
|
||||
|
||||
@@ -91,7 +97,7 @@ L7 sub-pass, 2026-07-27):
|
||||
- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
|
||||
- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
|
||||
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
|
||||
- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag.
|
||||
- `drag_out` — the pure drag-out gesture law plus path-list assembly. Owns the `ReaperSurface` vocabulary (OffReaper / TrackPanel / FxSurface / FxEmbed / Arrange / Other — `core/wire/instrument_drop` classifies REAPER's info token INTO it), `decideDropClass` (surface × single-vs-multi payload → Internal / InstrumentDrop / ArrangeInsert / Refuse / OsHandoff / None), and `cueForDropClass`. **No `DropClass` means "nothing happens"**: a surface with no defined outcome for the payload resolves to `Refuse`, which the shell shows as a cursor, so "no silent no-op release" is a property of the enumeration rather than of any call site.
|
||||
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + the pastel accent system, the keyboard strip's spectral ramp, WCAG contrast-floor helpers + `compositeOver` (the effective color of a translucent fill, so alpha overlays are testable). Only the ramp's MID stop is its own constant; lo/hi are still aliases of `accent/primary`/`accent/tertiary`, so a categorical accent move CAN still reorder the ramp — `testSpectralRampLuminanceIsMonotonic` is the build-time catch, not the structure.
|
||||
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
|
||||
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
|
||||
@@ -119,9 +125,12 @@ L7 sub-pass, 2026-07-27):
|
||||
worked example. A prior revision wrote the threshold ~25% low and let 15px
|
||||
semibold clear a floor it was not entitled to.
|
||||
- `card_drag`'s precedence order must stay a pure decision helper mirroring
|
||||
`drag_out::decideGesture` — don't let a shell reimplement gesture precedence
|
||||
`drag_out::decideDropClass` — don't let a shell reimplement gesture precedence
|
||||
ad hoc; the cursor-cue mapping in the shell must stay a thin lookup over the
|
||||
pure result.
|
||||
- `drag_out` is deliberately **dependency-free**, and `instrument_drop` links it
|
||||
rather than the reverse. Inverting that edge would drag the instrument's state
|
||||
serializer into `card_drag` and into every `drag_out` consumer.
|
||||
- `rect`'s prior role names survive only as `using` aliases at their old call
|
||||
sites — changing `rect.h` itself ripples across every directory that aliases
|
||||
it (e.g. `editor_geometry::Rect`); check all alias sites, not just this one.
|
||||
|
||||
@@ -15,14 +15,49 @@ bool insideClient(int px, int py, const PanelClientRect& c) {
|
||||
|
||||
} // namespace
|
||||
|
||||
DragGesture decideGesture(int px, int py, const PanelClientRect& client,
|
||||
const DragState& state) {
|
||||
if (!state.dragging || !state.hasArmedSamples) return DragGesture::None;
|
||||
if (insideClient(px, py, client)) return DragGesture::Internal;
|
||||
// Outside the client: a single-capture drag still over REAPER's own UI is an instrument
|
||||
// drop; anything else (multi-capture, or pointer off REAPER entirely) is an OS drag-out.
|
||||
if (state.singleCapture && state.overReaperUi) return DragGesture::InstrumentDrop;
|
||||
return DragGesture::OsDrag;
|
||||
bool needsSurfaceProbe(int px, int py, const PanelClientRect& client) {
|
||||
return !insideClient(px, py, client);
|
||||
}
|
||||
|
||||
DropClass decideDropClass(int px, int py, const PanelClientRect& client,
|
||||
const DropContext& ctx) {
|
||||
if (!ctx.drag.dragging || !ctx.drag.hasArmedSamples) return DropClass::None;
|
||||
if (insideClient(px, py, client)) return DropClass::Internal;
|
||||
|
||||
switch (ctx.surface) {
|
||||
case ReaperSurface::OffReaper:
|
||||
return DropClass::OsHandoff;
|
||||
|
||||
case ReaperSurface::TrackPanel:
|
||||
case ReaperSurface::FxSurface:
|
||||
// One instance holds one capture, so a multi payload names no instrument to build —
|
||||
// it refuses with a cue rather than falling through to some other surface's outcome.
|
||||
return (ctx.singlePayload && ctx.haveTrack) ? DropClass::InstrumentDrop
|
||||
: DropClass::Refuse;
|
||||
|
||||
case ReaperSurface::Arrange:
|
||||
// GetThingFromPoint may return a null track with a valid info string (the SDK says
|
||||
// so): over the arrange that is the region below the last track, which names no lane
|
||||
// to place on. Refuse rather than guess a track.
|
||||
return ctx.haveTrack ? DropClass::ArrangeInsert : DropClass::Refuse;
|
||||
|
||||
case ReaperSurface::FxEmbed:
|
||||
case ReaperSurface::Other:
|
||||
return DropClass::Refuse;
|
||||
}
|
||||
return DropClass::Refuse; // an unclassifiable surface still refuses visibly, never silently
|
||||
}
|
||||
|
||||
DropCue cueForDropClass(DropClass cls) {
|
||||
switch (cls) {
|
||||
case DropClass::Internal: return DropCue::Internal;
|
||||
case DropClass::InstrumentDrop: return DropCue::Instrument;
|
||||
case DropClass::ArrangeInsert: return DropCue::ArrangeInsert;
|
||||
case DropClass::Refuse: return DropCue::Refuse;
|
||||
case DropClass::OsHandoff: return DropCue::OsOwned;
|
||||
case DropClass::None: return DropCue::None;
|
||||
}
|
||||
return DropCue::None;
|
||||
}
|
||||
|
||||
PathList assemblePathList(const std::vector<ResolvedSample>& resolved) {
|
||||
|
||||
+59
-24
@@ -1,15 +1,15 @@
|
||||
#pragma once
|
||||
#include "core/ui/rect.h"
|
||||
// drag_out — decision logic behind the bank_panel's native OS drag-out. OLE/SWELL initiation and
|
||||
// the panel gesture hook stay in the shell (drag_out_win.* + shell/panel/panel_drag.cpp).
|
||||
// drag_out — the pure drag-out gesture law plus the OS hand-off's path-list assembly. REAPER
|
||||
// hit-testing, cursor setting, and outcome execution stay in the shell (panel_drag.cpp +
|
||||
// instrument_drop_win / arrange_drop_win / drag_out_win).
|
||||
//
|
||||
// Gesture boundary: the panel's own internal drag (press a selected cell, drop onto a pool/bank
|
||||
// region or tab) lives entirely inside the panel client rect. The moment the pointer LEAVES that
|
||||
// rect while a drag is armed with samples, the gesture becomes OS-bound — dragged out to another
|
||||
// window/Explorer/DAW. A single-capture drag that leaves the rect but is still over REAPER's own
|
||||
// UI is instead an InstrumentDrop (heading for a track's FX button); do not regress this boundary.
|
||||
// THE LAW: the class is resolved from what is under the cursor on EVERY move; every transition
|
||||
// is reversible until release or until the pointer leaves REAPER entirely; the OS hand-off is
|
||||
// reserved for leaving REAPER, and every REAPER-internal target executes natively on release.
|
||||
// Do not reintroduce a first-move class lock or a drag-lifetime "blocked" latch.
|
||||
//
|
||||
// Path-list assembly: turns armed sample ids into the absolute path list the OS drop carries
|
||||
// Path-list assembly: turns armed sample ids into the absolute path list an OS drop carries
|
||||
// (Windows CF_HDROP / macOS file-list pasteboard) — set algebra only; the shell resolves each id
|
||||
// to its on-disk bank file. No temp files; copy-only is enforced at the OS layer (drag_out_win).
|
||||
|
||||
@@ -18,34 +18,69 @@
|
||||
|
||||
namespace reasampler::ui {
|
||||
|
||||
// --- Gesture boundary ---------------------------------------------------------
|
||||
// --- Gesture law --------------------------------------------------------------
|
||||
|
||||
// The panel's client rect, own client coords, top-left origin. Half-open: [x, x+width) x
|
||||
// [y, y+height).
|
||||
using PanelClientRect = Rect;
|
||||
|
||||
// Live drag state reduced to what the boundary decision needs. Pre-threshold "armed but not yet
|
||||
// dragging" is not a drag for this decision.
|
||||
// Live drag state reduced to what every gesture decision needs. Pre-threshold "armed but not
|
||||
// yet dragging" is not a drag. Shared with card_drag's in-grid precedence decision.
|
||||
struct DragState {
|
||||
bool dragging = false; // threshold crossed; a drag is in progress
|
||||
bool hasArmedSamples = false; // payload holds >= 1 sample id
|
||||
bool singleCapture = false; // payload holds EXACTLY one sample (arms InstrumentDrop)
|
||||
bool overReaperUi = false; // pointer is over REAPER's own UI (shell-supplied)
|
||||
};
|
||||
|
||||
// What the shell should do with the drag given the current pointer position.
|
||||
enum class DragGesture {
|
||||
None, // no drag under way, or an empty payload
|
||||
Internal, // dragging inside the panel — bank-to-bank move/copy
|
||||
InstrumentDrop, // single-capture drag left the panel but is over REAPER's UI — shell
|
||||
// hover-tracks the TCP FX button; on release adds a preloaded instance
|
||||
OsDrag, // dragging with samples, pointer left REAPER entirely — hand to the OS
|
||||
// What REAPER reports under the pointer, reduced to the surfaces the law distinguishes.
|
||||
// Produced from GetThingFromPoint's info token by wire::classifyReaperSurface.
|
||||
enum class ReaperSurface {
|
||||
OffReaper, // not over REAPER at all — the one irreversible exit
|
||||
TrackPanel, // TCP/MCP, ANY sub-element: the WHOLE panel is the instrument hotspot
|
||||
FxSurface, // fx_* — the FX chain and floating-FX windows
|
||||
FxEmbed, // tcp.fxembed / mcp.fxembed — an instance already draws there
|
||||
Arrange, // the timeline
|
||||
Other, // ruler, transport, spacers, docker chrome, unknown future tokens
|
||||
Count, // sentinel, NOT a real surface — tests/test_drag_out.cpp's kAllSurfaces is
|
||||
// pinned against this via static_assert so a 7th surface can't silently skip
|
||||
// the exhaustiveness matrix
|
||||
};
|
||||
|
||||
// Decides the gesture for a drag at pointer (px, py) over `client`, given `state`. Position-only
|
||||
// + state-only (no hidden state), so re-entry back inside always returns Internal.
|
||||
DragGesture decideGesture(int px, int py, const PanelClientRect& client,
|
||||
const DragState& state);
|
||||
// The resolved target class. Every value is a DEFINED outcome the shell executes or visibly
|
||||
// refuses — there is deliberately no "nothing happens" member, which is what makes "no silent
|
||||
// no-op release" a property of the enumeration rather than of any one call site.
|
||||
enum class DropClass {
|
||||
None, // no drag under way, or an empty payload — nothing to resolve
|
||||
Internal, // inside the panel client — the bank-to-bank drag, unchanged
|
||||
InstrumentDrop, // a track's panel or FX surface — add ReaSampler 9000 preloaded
|
||||
ArrangeInsert, // the timeline — place items at the pointer's track and time
|
||||
Refuse, // a REAPER surface with no defined outcome for this payload — cue it
|
||||
OsHandoff, // the pointer left REAPER — hand the file list to the OS
|
||||
};
|
||||
|
||||
// Everything the law reads. Nothing here is remembered between evaluations: two evaluations at
|
||||
// the same point with the same payload resolve identically, whatever happened in between.
|
||||
struct DropContext {
|
||||
DragState drag;
|
||||
bool singlePayload = false; // payload holds EXACTLY one capture (arms InstrumentDrop)
|
||||
ReaperSurface surface = ReaperSurface::OffReaper; // only read outside the client rect
|
||||
bool haveTrack = false; // GetThingFromPoint returned a non-null MediaTrack*
|
||||
};
|
||||
|
||||
// Resolves the class for a drag at pointer (px, py) over `client`. Position-and-context only,
|
||||
// so re-entry into the client always returns Internal and a surface transition always reverses.
|
||||
DropClass decideDropClass(int px, int py, const PanelClientRect& client, const DropContext& ctx);
|
||||
|
||||
// True outside the panel client rect — the same half-open test decideDropClass gates the SDK
|
||||
// hit-test on. Exported so the shell reads this one pure predicate instead of reimplementing the
|
||||
// inside-client math inline (core/ui/CLAUDE.md forbids hit-test geometry living in shell code).
|
||||
bool needsSurfaceProbe(int px, int py, const PanelClientRect& client);
|
||||
|
||||
// The pointer cue for a class, so "will this work" is visible BEFORE release. Internal defers
|
||||
// to card_drag's own cue; OsOwned means the OS drag loop draws the cursor and we must not fight
|
||||
// it.
|
||||
enum class DropCue { None, Internal, Instrument, ArrangeInsert, Refuse, OsOwned };
|
||||
|
||||
DropCue cueForDropClass(DropClass cls);
|
||||
|
||||
// --- Path-list assembly -------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user