#pragma once // drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS // drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are // unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation // and the bank_panel gesture hook stay in the shell (drag_out_win.* + bank_panel.cpp). // // 1. GESTURE BOUNDARY (invariant #4 — do not regress the internal drag). The panel // already runs an INTERNAL drag: press a selected cell, cross a threshold, drop onto // a pool/banks region or a tab to move/copy the samples between banks. That drag lives // entirely INSIDE the panel client rect. The OS drag is a DISTINCT gesture with a // distinct, discoverable boundary: while a drag is armed with samples in the payload, // the moment the pointer LEAVES the panel client area the gesture becomes OS-bound — // the payload is being dragged out to another window / Explorer / another DAW. Inside // the client area it stays internal; with no armed samples there is no drag at all. // This function is that decision, pure over (drag state + pointer + panel rect). // // 2. PATH-LIST ASSEMBLY. The OS drop carries absolute file paths (Windows CF_HDROP / // macOS file-list pasteboard). Turning the armed sample ids into that path list — // resolving each id to its already-on-disk bank file, de-duping, and applying an // explicit skip-missing-file policy — is pure string work over a resolver the shell // supplies (the shell owns the REAPER project-dir read + resolveBankFile; this module // owns the set algebra and the result contract). NO temp files: the bank files already // exist; the list points straight at them (COPY-ONLY is enforced at the OS layer — see // drag_out_win — never by relocating or copying bytes here). // // PURE MODULE: NO REAPER types, NO SWELL, NO OS/OLE, NO vendor/ includes. Standard library // only. Builds and unit-tests without REAPER. Mirror of mode_switch. #include #include namespace reasampler { // --- Gesture boundary --------------------------------------------------------- // The panel's client rectangle in its own client coordinates (top-left origin, the SWELL/ // LICE convention). width/height are the extents; a point (px, py) is INSIDE when // x <= px < x + width and y <= py < y + height (half-open, matching the panel's other // hit-tests so the edge is claimed consistently). struct PanelClientRect { int x = 0; int y = 0; int width = 0; int height = 0; bool operator==(const PanelClientRect& o) const { return x == o.x && y == o.y && width == o.width && height == o.height; } }; // The live drag state the shell tracks, reduced to what the boundary decision needs: // whether a drag is currently active (threshold crossed) and whether the armed payload // carries at least one sample. (Pre-threshold "armed but not yet dragging" is NOT a drag // for this decision — the shell only asks once a drag is under way.) // // S17 (drop-and-load) adds two inputs that refine the OUTSIDE-the-panel decision without // touching the INSIDE decision (the internal bank-to-bank drag stays byte-identical): // * singleCapture — the payload holds EXACTLY ONE sample id. Only a single-capture drag // arms the InstrumentDrop gesture (per the S17 open-question lean: a multi-capture drag // over an FX button is NOT an instrument drop — it falls through to OsDrag, the natural // multi-file drag-out to Explorer/another DAW). REJECT, not load-first: the whole gesture // is "make ONE capture a playable instrument", so a multi payload is out of contract here. // * overReaperUi — a SHELL-SUPPLIED predicate: true when the pointer, though outside the // panel client rect, is still over REAPER's OWN window/UI (the shell owns the REAPER // hit query, e.g. GetThingFromPoint; the pure layer owns only the set/boundary algebra). // Both default false, so an M11-era caller that fills only {dragging, hasArmedSamples} gets // EXACTLY the M11 behavior: outside the client rect with overReaperUi=false -> OsDrag. struct DragState { bool dragging = false; // threshold crossed; a drag is in progress bool hasArmedSamples = false; // the drag payload holds >= 1 sample id bool singleCapture = false; // S17: payload holds EXACTLY one sample (arms InstrumentDrop) bool overReaperUi = false; // S17: 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 — do nothing Internal, // dragging inside the panel — the existing bank-to-bank move/copy drag InstrumentDrop, // S17: single-capture drag left the panel but is over REAPER's UI — // the shell hover-tracks the TCP FX button and, on release, adds a // ReaSampler 9000 instance preloaded with the dragged capture. OsDrag, // dragging with samples, pointer left REAPER entirely — hand off to the OS }; // Decides the gesture for a drag at pointer (px, py) over `client`, given `state`. // * Not dragging (or no armed samples): None — the shell ignores the move. // * Dragging with samples, pointer INSIDE the client rect: Internal — unchanged // bank-to-bank behavior (invariant #4: the internal drag stays byte-identical). // * Dragging OUTSIDE the client rect, SINGLE capture, over REAPER's UI: InstrumentDrop — // the drag is heading for a track's FX button (S17); the shell hover-tracks + highlights. // * Dragging OUTSIDE the client rect otherwise (multi-capture, OR the pointer has left // REAPER entirely): OsDrag — the samples are leaving to the OS; the shell initiates the // native OS drag with the resolved paths. // The INSIDE decision is untouched (M11 internal drag is byte-identical). The M11 boundary // (left the client rect -> OsDrag) is REFINED, not replaced: leaving the rect now asks // "single-capture and over REAPER's UI -> InstrumentDrop, else -> OsDrag" — so the M11 // OS-drag-out (multi payload, or pointer off REAPER) keeps its exact behavior. Position-only // + state-only (no hidden state), so re-entry back inside returns Internal. DragGesture decideGesture(int px, int py, const PanelClientRect& client, const DragState& state); // --- Path-list assembly ------------------------------------------------------- // One armed sample reduced to what path assembly needs: the resolved ABSOLUTE file path // the shell computed for it (empty when the shell could not resolve it — e.g. no project // dir / empty relative path). The shell resolves each via the SAME machinery the panel // already uses for audition/insert (resolveBankFile over the current project dir), so the // drag points at the real bank file — no temp copy. struct ResolvedSample { std::string absolutePath; // resolved absolute path, or "" when unresolvable bool fileExists = false; // shell stat() result — drives the skip-missing policy }; // The outcome of assembling the drag's path list: the de-duped, existing-only absolute // paths to hand to the OS, plus explicit tallies so the shell can decide whether to // initiate at all (an empty `paths` means nothing draggable — do NOT start a drag). struct PathList { std::vector paths; // de-duped, existing files, in first-seen order int skippedMissing = 0; // resolved but file did not exist (skip policy) int skippedUnresolved = 0; // shell could not resolve a path at all int skippedDuplicate = 0; // same absolute path seen more than once }; // Assembles the drag path list from the resolved samples (in selection order). // Policy (all explicit, all tested): // * SKIP-MISSING: a sample whose file does not exist on disk is skipped (counted in // skippedMissing) — a stale index entry must never put a dangling path on the OS // clipboard. This is the deliberate skip policy the brief asks be made explicit. // * SKIP-UNRESOLVED: an empty absolutePath (shell could not resolve) is skipped // (skippedUnresolved) — same reasoning, no empty entry reaches the OS. // * DEDUPE: the same absolute path appearing twice (two index entries, one file — the // cross-bank copy case) yields ONE CF_HDROP entry (skippedDuplicate counts the extras), // so the OS never sees a duplicate drop path. First occurrence wins; order preserved. // * EMPTY SELECTION: an empty input yields an empty PathList (all tallies zero) — the // shell reads paths.empty() and does not start a drag. // Comparison is exact-string (the shell normalizes slashes/case upstream if it wants // case-insensitive dedup on Windows — the pure layer does not guess a platform rule). PathList assemblePathList(const std::vector& resolved); } // namespace reasampler