Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+257
View File
@@ -0,0 +1,257 @@
#include "core/namespaces.h"
// drag_out_win — OS/COM initiation of native OS drag-out (M11). See drag_out_win.h.
//
// Windows path (primary): a hand-rolled minimal IDataObject exposing exactly one format,
// CF_HDROP, plus a minimal IDropSource, handed to OLE DoDragDrop with a COPY-ONLY effect
// mask. We roll our own rather than pull in a helper because the object is tiny (one
// format, one medium) and the copy-only guarantee must be structural and auditable in one
// place. mac/linux route to SWELL's file-list drag behind the same seam.
//
// Compiled into the reaper_reasampler MODULE. No REAPER API is used here (pure OS/COM); it
// is a leaf the bank_panel calls.
#include "shell/actions/drag_out_win.h"
#ifdef _WIN32
#include <windows.h>
#include <ole2.h> // DoDragDrop, IDataObject, IDropSource, ReleaseStgMedium
#include <shlobj.h> // DROPFILES, CF_HDROP
#include <cstring>
namespace reasampler {
namespace {
// Builds the CF_HDROP HGLOBAL: a DROPFILES header followed by a double-null-terminated
// list of wide (UTF-16) absolute paths. Windows CF_HDROP requires backslash separators and
// a trailing extra NUL after the final path's NUL. Returns nullptr on allocation failure or
// empty input. Ownership transfers to the STGMEDIUM (freed by ReleaseStgMedium / the OS).
HGLOBAL buildHDrop(const std::vector<std::string>& paths) {
if (paths.empty()) return nullptr;
// 1) Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths
// slash-normalized for its own resolution; CF_HDROP wants native backslashes).
std::vector<std::wstring> wide;
wide.reserve(paths.size());
std::size_t totalChars = 0; // characters incl. each path's terminating NUL
for (const std::string& p : paths) {
if (p.empty()) continue;
const int need = MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, nullptr, 0);
if (need <= 0) continue; // unconvertible path — skip rather than emit garbage
std::wstring w(static_cast<std::size_t>(need), L'\0');
MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, &w[0], need);
// `need` includes the NUL; drop it from the string length, we re-add it in the buffer.
if (!w.empty() && w.back() == L'\0') w.pop_back();
for (wchar_t& c : w) if (c == L'/') c = L'\\';
totalChars += w.size() + 1; // + the per-path NUL
wide.push_back(std::move(w));
}
if (wide.empty()) return nullptr;
totalChars += 1; // the extra double-NUL terminator after the last path
const SIZE_T bytes = sizeof(DROPFILES) + totalChars * sizeof(wchar_t);
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes);
if (!h) return nullptr;
auto* df = static_cast<DROPFILES*>(GlobalLock(h));
if (!df) { GlobalFree(h); return nullptr; }
df->pFiles = sizeof(DROPFILES); // offset to the file list
df->fWide = TRUE; // wide (UTF-16) path list
auto* dst = reinterpret_cast<wchar_t*>(reinterpret_cast<char*>(df) + sizeof(DROPFILES));
for (const std::wstring& w : wide) {
std::memcpy(dst, w.c_str(), (w.size() + 1) * sizeof(wchar_t)); // incl. NUL
dst += w.size() + 1;
}
*dst = L'\0'; // double-NUL terminates the list (ZEROINIT already did, explicit for clarity)
GlobalUnlock(h);
return h;
}
// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
class DropSource final : public IDropSource {
public:
// IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
if (riid == IID_IUnknown || riid == IID_IDropSource) {
*ppv = static_cast<IDropSource*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
ULONG STDMETHODCALLTYPE Release() override {
const ULONG r = --refs_;
if (r == 0) delete this;
return r;
}
// IDropSource
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
if (escapePressed) return DRAGDROP_S_CANCEL;
if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // released -> drop
return S_OK; // keep dragging
}
HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
}
private:
ULONG refs_ = 1;
};
// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
// built once at construction and cloned on each GetData call (OLE owns the returned medium).
class HDropDataObject final : public IDataObject {
public:
explicit HDropDataObject(HGLOBAL hdrop) : hdrop_(hdrop) {}
~HDropDataObject() { if (hdrop_) GlobalFree(hdrop_); }
// IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
if (riid == IID_IUnknown || riid == IID_IDataObject) {
*ppv = static_cast<IDataObject*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
ULONG STDMETHODCALLTYPE Release() override {
const ULONG r = --refs_;
if (r == 0) delete this;
return r;
}
// IDataObject — the two that matter for a drag source.
HRESULT STDMETHODCALLTYPE GetData(FORMATETC* fmt, STGMEDIUM* med) override {
if (!fmt || !med) return E_INVALIDARG;
if (!isHDrop(*fmt)) return DV_E_FORMATETC;
if (!hdrop_) return E_UNEXPECTED;
// Clone the HGLOBAL so the caller (OLE / target) owns an independent copy; our
// hdrop_ stays valid for repeat GetData calls and is freed in the dtor.
const SIZE_T sz = GlobalSize(hdrop_);
HGLOBAL copy = GlobalAlloc(GMEM_MOVEABLE, sz);
if (!copy) return E_OUTOFMEMORY;
void* src = GlobalLock(hdrop_);
if (!src) { GlobalFree(copy); return E_OUTOFMEMORY; }
void* dst = GlobalLock(copy);
if (!dst) { GlobalUnlock(hdrop_); GlobalFree(copy); return E_OUTOFMEMORY; }
std::memcpy(dst, src, sz);
GlobalUnlock(hdrop_);
GlobalUnlock(copy);
med->tymed = TYMED_HGLOBAL;
med->hGlobal = copy;
med->pUnkForRelease = nullptr; // caller releases via ReleaseStgMedium
return S_OK;
}
HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC* fmt) override {
return (fmt && isHDrop(*fmt)) ? S_OK : DV_E_FORMATETC;
}
// The remainder are the standard "not supported for a simple source" stubs.
HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC*, STGMEDIUM*) override { return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC*, FORMATETC* out) override {
if (out) out->ptd = nullptr;
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE SetData(FORMATETC*, STGMEDIUM*, BOOL) override { return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dir, IEnumFORMATETC** out) override {
if (dir == DATADIR_GET && out) {
FORMATETC fe = hdropFormat();
return SHCreateStdEnumFmtEtc(1, &fe, out);
}
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC*, DWORD, IAdviseSink*, DWORD*) override {
return OLE_E_ADVISENOTSUPPORTED;
}
HRESULT STDMETHODCALLTYPE DUnadvise(DWORD) override { return OLE_E_ADVISENOTSUPPORTED; }
HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA**) override {
return OLE_E_ADVISENOTSUPPORTED;
}
private:
static FORMATETC hdropFormat() {
FORMATETC fe{};
fe.cfFormat = CF_HDROP;
fe.ptd = nullptr;
fe.dwAspect = DVASPECT_CONTENT;
fe.lindex = -1;
fe.tymed = TYMED_HGLOBAL;
return fe;
}
static bool isHDrop(const FORMATETC& fe) {
return fe.cfFormat == CF_HDROP &&
(fe.tymed & TYMED_HGLOBAL) &&
fe.dwAspect == DVASPECT_CONTENT;
}
ULONG refs_ = 1;
HGLOBAL hdrop_ = nullptr;
};
} // namespace
bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
if (absolutePaths.empty()) return false;
// REAPER's main thread is already OLE-initialized (it hosts OLE drag targets), so we do
// NOT call OleInitialize here — a nested OleInitialize on an already-initialized STA is
// harmless-but-unnecessary, and OleUninitialize pairing across a REAPER-owned apartment
// is the kind of thing that bites. DoDragDrop works on the already-initialized STA.
HGLOBAL hdrop = buildHDrop(absolutePaths);
if (!hdrop) return false;
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
auto* source = new DropSource();
DWORD effect = 0;
// COPY-ONLY (invariant #1): the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is
// NEVER offered, so no drop target can relocate (delete) the bank file — only prune
// deletes bank bytes (Phase R boundary).
const HRESULT hr = DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
source->Release();
data->Release(); // frees the source HGLOBAL via HDropDataObject's dtor
return hr == DRAGDROP_S_DROP && effect == DROPEFFECT_COPY;
}
} // namespace reasampler
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
#include "wdltypes.h"
#include "swell/swell.h"
#include <vector>
namespace reasampler {
// SWELL provides a file-list drag surface (SWELL_InitiateDragDropOfFileList, verified in
// vendor/WDL/WDL/swell/swell-functions.h). It takes a C-string array + count and initiates
// a copy-style file drag from the given window. Unlike OLE it exposes no per-source effect
// mask, so the copy-only guarantee rests on SWELL's copy semantics rather than an explicit
// DROPEFFECT_COPY mask — an honest platform difference, not a faked equivalence. Windows is
// the exact-control path (D5: Windows is the shipping target).
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
if (absolutePaths.empty() || !panelHwnd) return false;
std::vector<const char*> ptrs;
ptrs.reserve(absolutePaths.size());
for (const std::string& p : absolutePaths) ptrs.push_back(p.c_str());
// srcrect null: SWELL positions the drag image at the current event. No custom icon.
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
return true; // fire-and-forget; SWELL owns the drag from here (no accept/cancel return)
}
} // namespace reasampler
#endif
+44
View File
@@ -0,0 +1,44 @@
#include "core/namespaces.h"
#pragma once
// drag_out_win — the OS/COM initiation half of native OS drag-out (Milestone 11). The pure
// gesture-boundary decision and path-list assembly live in drag_out.*; THIS is the platform
// shell that hands a resolved, existing-file path list to the operating system's drag-drop
// machinery so the user can drop bank samples into Explorer / another app / another DAW.
//
// ONE seam, platform-forked inside the .cpp:
// * Windows (primary — Daniel's target): OLE DoDragDrop with a minimal IDataObject
// carrying CF_HDROP (absolute paths, double-null-terminated wide list) and a minimal
// IDropSource. COPY-ONLY is STRUCTURAL: the IDataObject offers DROPEFFECT_COPY and the
// effect mask passed to DoDragDrop is DROPEFFECT_COPY alone — MOVE is never offered, so
// no target can pull the bank file out of the bank folder (invariant #1: a move would
// delete bank bytes, and per the Phase R boundary ONLY prune deletes files).
// * macOS/Linux (SWELL): SWELL_InitiateDragDropOfFileList (verified present in
// vendor/WDL/WDL/swell/swell-functions.h) behind the same seam. SWELL's file-list drag
// is a copy-style file drag; it exposes no per-source effect mask the way OLE does, so
// the copy-only guarantee there rests on SWELL's copy semantics rather than an explicit
// mask — noted honestly, not faked. Windows is where the mask control is exact.
//
// NON-DESTRUCTIVE (invariant #2): initiating a drag reads nothing but the path list and
// mutates no sample / index / selection. A cancelled or failed drag changes nothing — the
// OS layer here neither writes ext-state nor touches the book.
#include <string>
#include <vector>
struct HWND__; // avoid dragging windows.h into every includer; the shell casts as needed.
namespace reasampler {
// Initiates a native OS drag-out of `absolutePaths` (already resolved, existing, de-duped —
// the pure drag_out::assemblePathList output) from the panel window `panelHwnd`. COPY-ONLY;
// see the header note. A no-op when the path list is empty (nothing draggable — the caller
// checks this too, but the guard is repeated here so a direct call is safe).
//
// BLOCKING on Windows: OLE DoDragDrop runs its own modal message loop until the drop or
// cancel, then returns — the caller's gesture state should be reset AFTER this returns.
// Returns true if a drop was accepted (DROPEFFECT_COPY), false on cancel / failure /
// empty input. The return is advisory (a failed drag is visible by nothing happening —
// the caller does not surface an error, per the brief's no-console-output constraint).
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths);
} // namespace reasampler
+158
View File
@@ -0,0 +1,158 @@
#include "core/namespaces.h"
// instrument_drop_win — the REAPER shell for S17 drop-and-load. See instrument_drop_win.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT
// REAPERAPI_IMPLEMENT (main.cpp owns the pointers; here they are extern via the WANT list).
#include "shell/actions/instrument_drop_win.h"
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>
#include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
#include "core/wire/instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
#include "reaper_plugin.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_GetThingFromPoint
#define REAPERAPI_WANT_TrackFX_AddByName
#define REAPERAPI_WANT_TrackFX_Delete
#define REAPERAPI_WANT_TrackFX_SetPreset
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
// Write `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir and return its path;
// returns an empty path on any failure. The .vstpreset extension is load-bearing —
// TrackFX_SetPreset's full-path form is documented for .vstpreset files (VST3). The file is
// transient: the caller deletes it right after the SetPreset call.
//
// The temp filename embeds the process ID so two concurrent REAPER instances (e.g. stable +
// beta) cannot collide in the shared OS temp dir, and one instance's cleanup cannot
// accidentally delete another's in-flight file.
//
// Non-throwing: every std::filesystem call uses the error_code overload. The whole body is
// wrapped in try/catch to guarantee no exception crosses the REAPER C callback boundary
// (the same discipline persist.cpp uses — see its non-throwing scanPruneOrphans comment).
//
// Returns the path object (not a narrow string) so the caller can:
// (a) pass path.u8string() to TrackFX_SetPreset — UTF-8 on MSVC, not ACP-converted,
// so a temp dir with accented or CJK user-name bytes is handled correctly;
// (b) delete via the retained path object — not via re-parsing the narrow string —
// so the cleanup cannot leak if the conversion above were to round-trip incorrectly.
std::filesystem::path writeTempPreset(const std::vector<std::uint8_t>& bytes) {
try {
static std::atomic<unsigned> counter{0};
std::error_code ec;
const std::filesystem::path dir = std::filesystem::temp_directory_path(ec);
if (ec) return {};
// PID in the name keeps files from distinct REAPER instances distinct in the shared
// temp dir — prevents cross-instance collisions and spurious post-apply deletions.
const std::string name =
"reasampler_drop_" + std::to_string(GetCurrentProcessId()) +
"_" + std::to_string(counter.fetch_add(1)) + ".vstpreset";
const std::filesystem::path path = dir / name;
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) return {};
out.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
out.close();
if (!out) { // short write / flush failure -> don't hand REAPER a truncated preset
std::filesystem::remove(path, ec);
return {};
}
return path;
} catch (...) {
return {};
}
}
} // namespace
FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
FxDropTarget out;
char info[256] = {0};
// GetThingFromPoint returns the track under the point (may be null for a non-track thing)
// and fills `info` with what was hit. A non-empty info OR a non-null track means the point
// is over REAPER's own UI; a null track with an empty info means the pointer has left
// REAPER entirely (over another app / the desktop) — the OsDrag boundary.
MediaTrack* track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
out.track = track;
out.overReaperUi = (track != nullptr) || (info[0] != '\0');
// The hotspot is either the FX chain/floating window ("fx_*") OR the FX-button family of
// the track/mixer panel ("tcp.fx*"/"mcp.fx*"). The pure classifier owns the rule.
out.overFxHotspot = (track != nullptr) && infoNamesFxHotspot(info);
return out;
}
bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
if (!track || presetBytes.empty()) return false;
// Materialize the .vstpreset FIRST so an I/O failure leaves the track untouched (no FX
// added yet — nothing to roll back).
const std::filesystem::path presetPath = writeTempPreset(presetBytes);
if (presetPath.empty()) return false;
// The CHANNEL-correct FX name: "VST3:ReaSampler 9000" on stable, "VST3:ReaSampler 9000
// beta" on beta. Sourcing it from app_version::vstPluginName() (the same accessor the VST
// factory display name derives from) keeps the pairing invariant intact — a beta extension
// drops the beta VST, a stable extension the stable VST — with no literal to drift. (The
// preset's class ID forks by the same channel bit inside buildInstrumentDropPreset.)
const std::string fxName = "VST3:" + vstPluginName();
// Negative `instantiate` => always create a NEW instance (verified in the header). recFX
// = false: a normal track FX chain instance, not a record/monitoring FX.
const int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false,
/*instantiate=*/-1);
bool ok = fxIndex >= 0;
// Apply the dragged capture's component state through the DOCUMENTED channel: a full
// .vstpreset path handed to TrackFX_SetPreset (SDK: "Full paths to .vstpreset files are
// also supported for VST3 plug-ins"). REAPER parses the Steinberg container and feeds the
// 'Comp' chunk to the instance's setState — the same bytes the instrument's own
// serializer produced (instrument_drop::buildInstrumentDropPreset ->
// sample_map::serializeComponentState). Unlike the former "vst_chunk" named-config-parm
// write, a failure here is REPORTED (false), not silently ignored.
//
// u8string() gives UTF-8 bytes on MSVC (not ACP-converted), so a temp dir under an
// accented or CJK user-name is handled correctly by REAPER's path APIs.
if (ok) ok = TrackFX_SetPreset(track, fxIndex, presetPath.u8string().c_str());
// The preset file is transient regardless of outcome; delete via the retained path object
// (not a re-parsed narrow string) so cleanup cannot leak even if the UTF-8 conversion
// round-trip were incorrect.
std::error_code ec;
std::filesystem::remove(presetPath, ec);
if (!ok && fxIndex >= 0) {
// All-or-nothing: if the preset apply fails, remove the empty FX instance we just
// added so the track is left exactly as it was. TrackFX_Delete signature (verified
// in reaper_plugin_functions.h:7236): bool TrackFX_Delete(MediaTrack*, int fx).
TrackFX_Delete(track, fxIndex);
}
return ok;
}
bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
if (!track || presetBytes.empty()) return false;
// One undo point for the whole gesture (mirrors the bank-verb undo discipline). Both the
// FX add and the state apply are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
Undo_BeginBlock2(nullptr);
const bool ok = loadInstrumentOntoTrack(track, presetBytes);
// The undo label reflects the placement-of-the-player framing (not a capture, not an insert).
Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
return ok;
}
} // namespace reasampler
+70
View File
@@ -0,0 +1,70 @@
#include "core/namespaces.h"
#pragma once
// instrument_drop_win — the REAPER-facing shell half of S17 drop-and-load. The pure gesture
// decision lives in drag_out (DragGesture::InstrumentDrop) and the pure payload construction
// in instrument_drop; THIS is the platform shell that (a) resolves a screen point to a track
// + its FX-surface hotspot via REAPER's hit-test API, and (b) on release adds a ReaSampler
// 9000 instance to that track and applies the dragged capture as its component state via a
// temp .vstpreset + TrackFX_SetPreset (S-GA-DropFX: the earlier "vst_chunk" named-config-parm
// write was silently unappliable — see instrument_drop.h for the diagnosis).
//
// Compiled into the reaper_reasampler MODULE. REAPER-facing (GetThingFromPoint, TrackFX_*,
// Undo_*), so DAW-verified, not unit-tested; the pure decision + preset it drives are CTest'd.
//
// LOAD-BEARING (CONTEXT.md §Drop-and-load): this is an EXPLICIT user placement-of-the-player
// gesture — it adds a READER of the bank on a track and points it at one already-captured
// sample. It NEVER captures, NEVER writes the bank, and NEVER inserts a timeline item. The
// only writes are: a new FX instance on the target track + that instance's own component
// state — both REAPER-undoable, wrapped in one undo block so the whole gesture is one Ctrl-Z
// — plus a transient .vstpreset in the OS temp dir, deleted before returning.
#include <cstdint>
#include <vector>
// Opaque REAPER track handle at the boundary so includers don't need the SDK. The SDK
// declares it as a class (reaper_plugin.h) — match that spelling so the mangled name agrees.
class MediaTrack;
namespace reasampler {
// The result of hit-testing a screen point during a live InstrumentDrop drag.
struct FxDropTarget {
MediaTrack* track = nullptr; // the track under the pointer (null if none / not a track)
bool overReaperUi = false; // the point is over REAPER's own window/UI at all
bool overFxHotspot = false; // specifically over this track's FX button/chain surface
// A valid drop target: a resolved track whose FX hotspot is under the pointer.
bool valid() const { return track != nullptr && overFxHotspot; }
};
// Hit-test a screen point (REAPER screen coords) to an FX drop target. Wraps
// GetThingFromPoint, whose info string tells us what was hit ("tcp.fx*"/"mcp.fx*" for the
// TCP/MCP FX button family; "fx_chain"/"fx_N" for the FX-chain and floating-FX windows; bare
// "tcp"/"mcp" or other sub-element tokens for non-FX track-panel regions). `overReaperUi` is
// the shell-supplied predicate the pure drag_out::decideGesture consumes (true when the point
// is over REAPER's own UI — i.e. GetThingFromPoint returned a track OR a recognizable
// non-track thing, false when the pointer has left REAPER entirely). `overFxHotspot` is true
// only when the info string names a genuine FX-bearing surface — decided by the pure
// instrument_drop::infoNamesFxHotspot from the SDK's own hit-test string.
FxDropTarget resolveFxDropTarget(int screenX, int screenY);
// Perform the drop on `track`: add a fresh ReaSampler 9000 instance and apply `presetBytes`
// (the instrument_drop::buildInstrumentDropPreset output — a .vstpreset image) as its
// component state so it plays the dragged capture. Wraps the add + apply in one REAPER undo
// block (mirrors the bank-verb undo discipline). Returns true on success (the FX was added
// and the preset applied), false on any failure. All-or-nothing: if the preset apply fails
// after a successful add, the freshly-added FX instance is removed via TrackFX_Delete before
// returning false, leaving the track exactly as it was (no orphaned empty-state FX).
// NEVER inserts a timeline item; the ONLY persistent mutations are the FX instance + its
// state, both undoable.
bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes);
// Add a fresh ReaSampler 9000 instance to `track` and apply `presetBytes` as its component
// state. Same all-or-nothing add+apply contract as performInstrumentDrop (rolls the FX back
// via TrackFX_Delete on apply failure), but does NOT open its own undo block — the caller owns
// the undo grouping so the whole gesture (persist + FX-add + apply) collapses to
// one Ctrl-Z. This is the shared inner half performInstrumentDrop wraps in its own block.
// Returns true on success, false on any failure. NEVER inserts a timeline item.
bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes);
} // namespace reasampler