S10: capture-first ReaSampler 9000 editor — browser, single-capture setup, zones toggle

Reverse S4 auto-select (empty=silence+empty state), add peak-thumbnail capture
browser + bank filter + keyboard-strip drag machine, v3 component state (selection
+ zones), and the S-NAME-1 filename/display rename (UID locked). MSVC min/max
macro collisions fixed post-implementation; 26/26 tests green.
This commit is contained in:
2026-07-26 20:36:36 -04:00
parent 991c190bb8
commit 7151e19432
16 changed files with 1906 additions and 325 deletions
+87 -42
View File
@@ -1,90 +1,135 @@
// reasampler_editor.h — the VST3 IPlugView LICE editor (Phase S1 bridge spike). THIN
// shell: hosts a LICE-drawn child window inside the host's IPlugView seat and routes
// host paint/click into the pure editor_geometry hit-test. Windows-only (D5).
// reasampler_editor.h — the VST3 IPlugView LICE editor for the ReaSampler 9000
// capture-first UI (Phase S10). THIN shell: hosts a LICE-drawn child window inside the
// host's IPlugView seat and routes host paint/mouse into the pure geometry modules
// (capture_browser, keyboard_strip) + the pure mapping (sample_map). Windows-only (D5).
//
// This is the phase's ONE genuine unknown (per CONTEXT.md §Phase S / S1): wiring LICE
// into an IPlugView. It reuses the bank_panel LICE/SWELL competence — a LICE_SysBitmap
// blitted in WM_PAINT, GET_X/Y_LPARAM hit-testing in WM_LBUTTONDOWN — but hangs it off a
// child HWND created in IPlugView::attached() rather than a docked SWELL dialog.
// The default face is the CAPTURE BROWSER: a bank-filter tab strip over a grid of
// scannable capture cards (peak thumbnail + name + root/key badge). A fresh instance with
// no pick shows a "pick a capture" EMPTY STATE and plays silence (the S10 policy reversal
// of the S4 first-sample auto-play). Picking a card loads that one capture and reveals a
// guided SINGLE-CAPTURE SETUP surface (a keyboard strip with the capture's root marker +
// a level readout). Multi-zone keymap editing is a demoted, opt-in ZONES panel (S10-Z),
// reached by a toggle and driven by the same keyboard_strip drag machine.
//
// Subclasses CPluginView (public.sdk/source/common/pluginview.h) for the IPlugView
// refcount + attached/removed/getSize/onSize boilerplate; we override the attach/remove
// hooks to create/destroy the child window and onSize to resize it.
// All layout/hit-test/drag math lives in the pure modules; this shell only draws + routes
// (a LICE_SysBitmap blitted in WM_PAINT, a WM_LBUTTONDOWN/WM_MOUSEMOVE/WM_LBUTTONUP
// drag-state machine hit-testing via the pure resolvers). Peak thumbnails are computed
// shell-side from the decoded WAV (bank_model's Sample carries no envelope) and cached —
// the mirror of bank_panel::thumbnailFor. Every edit commits OFF the audio thread via the
// processor's reloadFromBank (RT path untouched).
//
// Subclasses CPluginView for the IPlugView boilerplate; overrides the attach/remove hooks
// to create/destroy the child window and onSize to resize it.
#pragma once
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "public.sdk/source/common/pluginview.h"
#include "sample_map.h" // SampleChoice (the list the editor draws)
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
#include "peaks.h" // Envelope (the cached peak thumbnail)
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
#ifdef _WIN32
#include <windows.h>
#endif
class LICE_IBitmap; // fwd: the paint helpers take one; lice.h is included only in the .cpp
namespace reasampler::vst {
class ReaSamplerProcessor;
class ReaSamplerEditor : public Steinberg::CPluginView {
public:
// `processor` owns this editor's lifetime domain and outlives it; the editor reads
// the live bank through it (the sample list) and drives selection + reload when the
// user clicks a row. May be null (defensive — a real host always supplies one).
// `processor` owns this editor's lifetime domain and outlives it; the editor reads the
// live bank through it and drives selection/zone edits + reload on user input. May be
// null (defensive — a real host always supplies one).
explicit ReaSamplerEditor(ReaSamplerProcessor* processor);
~ReaSamplerEditor() override;
// Accept only the Windows HWND platform type (D5: Windows-only).
Steinberg::tresult PLUGIN_API isPlatformTypeSupported(
Steinberg::FIDString type) override;
// The view is user-resizable in the spike so we exercise the onSize path.
Steinberg::tresult PLUGIN_API canResize() override;
protected:
// CPluginView hooks: systemWindow is set by the time attachedToParent() fires.
void attachedToParent() override;
void removedFromParent() override;
Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override;
private:
// Which face the editor shows. The browser is the default; the Zones panel is the
// demoted opt-in view reached by the toggle. Both draw over the same snapshotted bank.
enum class View { kBrowser, kZones };
// What a mouse drag is currently editing (the drag-state machine). kNone = no drag in
// flight. The zone-edit grabs mirror keyboard_strip::ZoneGrab; kRootMarker is the
// single-capture root drag on the setup strip.
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody };
#ifdef _WIN32
// Draw the current surface into the child window's DC via a LICE bitmap.
void paint(HDC hdc);
// Route a client-space click: bank-sample pick (left list), zone edit (right panel),
// or the "Add Zone" button.
void onClick(int x, int y);
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
void paintSetup(LICE_IBitmap* bmp, const Rect& area);
void paintZones(LICE_IBitmap* bmp, int w, int h);
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
void onMouseDown(int x, int y);
void onMouseMove(int x, int y);
void onMouseUp(int x, int y);
static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
void invalidate();
HWND childHwnd_ = nullptr;
#endif
// Re-read the bank's sample list from the live bridge into `samples_`, and snapshot the
// instrument's performance map. Main/UI thread only (reads ext-state); called on attach
// and after any edit that reloads the instrument.
void refreshSampleList();
// Re-read the bank (samples + banks) from the live bridge and snapshot the instrument's
// selection + performance map. Main/UI thread only. Called on attach and after any edit.
void refreshFromBank();
// Push the edited performance map to the processor and rebuild the instrument OFF the
// audio thread. UI thread only. Centralizes the "commit an edit" path so every zone
// mutation reloads identically.
void commitMapAndReload();
// Publish the edited zones/selection to the processor, then rebuild the instrument OFF
// the audio thread. UI thread only. One place so every edit commits identically.
void commitAndReload();
// Recompute the capture cards visible under the current bank filter (samples_ narrowed by
// activeFilterBankId_; "" = All) into visible_. Called on refresh + filter change.
void rebuildVisible();
// The peak thumbnail for a bank sample id at `binCount` bins, computed once from the
// decoded WAV (mirror of bank_panel::thumbnailFor) and cached by (id, binCount). Returns
// an empty envelope when the WAV can't be resolved/decoded. UI thread only (file I/O).
const Envelope& thumbnailFor(const std::string& sampleId, int binCount);
ReaSamplerProcessor* processor_ = nullptr;
// The bank's samples, snapshotted for the current paint. Refreshed off the audio
// thread; the paint just draws it.
std::vector<SampleChoice> samples_;
// The currently-selected sample id (Tier-0 fallback + the "sample to add" for a new
// zone), mirrored for the paint's highlight.
std::string selectedId_;
// The instrument's performance map, snapshotted for edit + paint. Edits mutate this
// copy then commit it to the processor via commitMapAndReload.
PerformanceMap map_;
// The zone row currently highlighted (for the paint); -1 = none.
int selectedZone_ = -1;
// --- Snapshot of the live bank (drawn each paint; refreshed off the audio thread) ---
std::vector<SampleChoice> samples_; // every bank sample, bank order
std::vector<BankChoice> banks_; // the named banks, for the filter tab strip
std::vector<SampleChoice> visible_; // samples_ narrowed by the active bank filter
std::string selectedId_; // the single-capture pick ("" = empty state)
PerformanceMap map_; // the opt-in zones (empty = no zones)
// --- Transient UI state (not persisted; component state carries selection + zones) ---
View view_ = View::kBrowser; // default face is the browser
std::string activeFilterBankId_; // "" = All; else a bank id from banks_
int selectedZone_ = -1; // highlighted zone in the Zones panel; -1 = none
// --- Drag-state machine ------------------------------------------------------
DragKind drag_ = DragKind::kNone;
int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver
int dragStartLow_ = 0; // the grabbed field's note at grab time
int dragStartHigh_ = 0;
int dragStartRoot_ = 60;
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.
std::unordered_map<std::string, Envelope> thumbCache_;
};
} // namespace reasampler::vst