Files
reasampler/src/drag_out.h
T
daniel cd64be9129 remove dead action_buttons module (superseded by action_bar in Phase L)
Deletes src/action_buttons.{h,cpp} and tests/test_action_buttons.cpp;
removes the static library, test target, and reaper_reasampler link
entry from CMakeLists.txt; scrubs all dangling comment references.
2026-07-27 22:04:27 -04:00

141 lines
8.9 KiB
C++

#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 <string>
#include <vector>
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<std::string> 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<ResolvedSample>& resolved);
} // namespace reasampler