Hand a card dragged onto a plugin's own editor to the OS as a file drop; withdraw the OLE drag when the pointer returns to REAPER

A code-ownership proof (window class procedure's module, via the pure window_origin) plus REAPER's FX-surface token routes a plugin editor to OLE; FX chrome keeps the instrument drop. The OLE loop re-asks the law each turn, so passing a plugin editor stays reversible.
This commit is contained in:
2026-09-25 13:39:39 -04:00
parent 9ff34b0a9e
commit fe1aec5925
14 changed files with 720 additions and 91 deletions
+24 -11
View File
@@ -86,16 +86,28 @@ L7 sub-pass, 2026-07-27):
- **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 — so it must be PROVEN (a window-ownership test in the shell), never inferred
from a hit-test token REAPER could not name; an unrecognised surface *inside* REAPER
refuses. That proof is necessary but NOT sufficient: a plugin bridged into
`reaper_host*.exe` fails the process test while still being REAPER's own UI, so the
hand-off additionally requires REAPER's hit-test to have named nothing — a recognised
token can only ever veto the hand-off, never produce one. A first-move class lock and
a drag-lifetime "cannot hand off" latch both existed here and were removed — do not
reintroduce either.
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. The OS
hand-off goes modal, so it must be PROVEN by a window test in the shell, never
inferred from a hit-test token REAPER could not name; an unrecognised surface
*inside* REAPER refuses. There are exactly two proofs, each necessary but NOT
sufficient, each paired with a hit-test condition:
- **Leaving REAPER** (process ownership). A plugin bridged into `reaper_host*.exe`
fails the process test while still being inside REAPER, so this route also requires
REAPER's hit-test to have named nothing — here a recognised token can only VETO.
- **A plugin's own editor** (code ownership, `window_origin`). The proof alone would
hand off over any extension window docked in REAPER, so this route also REQUIRES the
FX-surface token; within an FX window the proof is what separates the plugin's editor
(OS hand-off — the plugin takes the files as it would from Explorer) from REAPER's
chrome (instrument drop, unchanged).
Once handed off, the law keeps running: on Windows the OS drag loop re-asks it every
turn (`decideOsDragTurn`) and WITHDRAWS the moment it stops answering `OsHandoff`,
giving the gesture back to the panel — or, at a release, committing REAPER's own
outcome at that point. So neither a pass over a plugin's editor nor a return from
outside REAPER costs a REAPER-internal outcome. SWELL's file drag exposes no such hook,
so off Windows the hand-off stays one-way (and there is no plugin-editor route there).
A first-move class lock and a drag-lifetime "cannot hand off" latch both existed here
and were removed — do not reintroduce either.
## Modules
@@ -103,7 +115,8 @@ 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` — the pure drag-out gesture law plus path-list assembly. Owns the `ReaperSurface` vocabulary (TrackPanel / FxSurface / FxEmbed / Arrange / Other — `core/wire/instrument_drop` classifies REAPER's info token INTO it; there is deliberately no off-REAPER member, since no token can carry that fact), `decideDropClass` (surface × single-vs-multi payload → Internal / InstrumentDrop / ArrangeInsert / Refuse / OsHandoff / None), and `cueForDropClass`. `OsHandoff` is reachable ONLY through `DropContext::pointerOffHost` — a positive window-ownership proof the shell owes the law (`drag_out_win::pointerOverHostWindow`) — AND only where REAPER's own hit-test named nothing (`Other`, no track), gated ahead of the surface switch, so neither evidence source can give a live drag away to OLE on its own. **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.
- `drag_out` — the pure drag-out gesture law plus path-list assembly. Owns the `ReaperSurface` vocabulary (TrackPanel / FxSurface / FxEmbed / Arrange / Other — `core/wire/instrument_drop` classifies REAPER's info token INTO it; there is deliberately no off-REAPER member, since no token can carry that fact), `decideDropClass` (surface × single-vs-multi payload → Internal / InstrumentDrop / ArrangeInsert / Refuse / OsHandoff / None), and `cueForDropClass`. `OsHandoff` is reachable ONLY through the two proof members of `DropContext` — `pointerOffHost` (`drag_out_win::pointerOverHostWindow`) where REAPER's hit-test named nothing (`Other`, no track), and `pointerOverPluginEditor` (`drag_out_win::pointerOverPluginEditor`) where it named `FxSurface` — both gated ahead of the surface switch, so no evidence source can give a live drag away to OLE on its own (the invariant above states why each pairing). Also owns `decideOsDragTurn`, the OS drag loop's per-turn Continue / Drop / Cancel / Withdraw / WithdrawAtRelease verdict. **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.
- `window_origin` — whose code implements a window (Host / System / Plugin / Unknown, from the owning process and the module holding the window CLASS's procedure) and `isPluginEditorChain`, the rule behind `pointerOverPluginEditor`: any Plugin window from the hit window up to its root. Unknown is never proof, and without the OS directory every non-host module degrades to Unknown — both keep the failure direction toward REAPER's own outcome, never toward the modal hand-off. The shell reads the facts; the known misses are listed at `drag_out_win.h`'s `pointerOverPluginEditor`.
- `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).