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:
@@ -0,0 +1,274 @@
|
||||
#include "core/namespaces.h"
|
||||
// reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell.
|
||||
// Windows-only (D5); guarded so a non-Windows build degrades to a stub that reports
|
||||
// "not supported" and draws nothing.
|
||||
|
||||
#include "shell/instrument/reasampler_embed.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/version/app_version.h" // vstPluginName (channel-derived embed label, S18)
|
||||
#include "core/instrument/map/bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh)
|
||||
#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
|
||||
#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text (L3)
|
||||
#include "core/instrument/ui/editor_geometry.h" // Rect (shared with embed_strip)
|
||||
#include "core/instrument/ui/embed_strip.h" // the pure strip layout + hit-test
|
||||
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "reasampler_processor.h"
|
||||
#include "core/ui/theme.h" // Role / InteractionState / spectralColor (L3)
|
||||
|
||||
// wdltypes.h first: it defines INT_PTR portably (and pulls <windows.h> on Windows), which
|
||||
// reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type.
|
||||
#include "wdltypes.h"
|
||||
|
||||
// REAPER's embed message/bitmap contract (vendored). REAPER_FXEMBED_IBitmap is an alias of
|
||||
// LICE_IBitmap, and the WM_* / DrawInfo / SizeHints definitions live here.
|
||||
#include "reaper_plugin_fx_embed.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// LICE — the same drawing stack the IPlugView editor and bank_panel use. REAPER hands us a
|
||||
// LICE bitmap; we draw into it with the same calls, then return (REAPER blits it).
|
||||
#include "lice/lice.h"
|
||||
#endif
|
||||
|
||||
using namespace Steinberg;
|
||||
|
||||
// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperUIEmbedInterface::iid; some
|
||||
// TU must DEFINE it. This is the only place that answers queryInterface for it, so the
|
||||
// definition lives with its sole use (mirrors reaper_bridge.cpp doing this for
|
||||
// IReaperHostApplication).
|
||||
DEF_CLASS_IID(Steinberg::IReaperUIEmbedInterface)
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
namespace {
|
||||
#ifdef _WIN32
|
||||
// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox
|
||||
// (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring
|
||||
// the local pre-L1 forest-green palette + raw GDI DrawTextA.
|
||||
KitBox toKitBox(const Rect& r) {
|
||||
return KitBox{r.x, r.y, r.width, r.height};
|
||||
}
|
||||
|
||||
// A short display name for a bank sample id, from the snapshotted list (the editor's helper,
|
||||
// duplicated small rather than shared across the shell/pure boundary).
|
||||
std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::string& id) {
|
||||
for (const SampleChoice& c : samples) {
|
||||
if (c.id == id) return c.displayName.empty() ? c.id : c.displayName;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
#endif
|
||||
|
||||
// Project the instrument's performance map into the strip's minimal zone shape (key ranges
|
||||
// only). Pure projection — kept here (shell side) because it reads PerformanceMap, a shell
|
||||
// type; embed_strip stays free of it.
|
||||
std::vector<EmbedZone> toEmbedZones(const PerformanceMap& map) {
|
||||
std::vector<EmbedZone> out;
|
||||
out.reserve(map.zones.size());
|
||||
for (const PerformanceZone& z : map.zones) out.push_back(EmbedZone{z.lowNote, z.highNote});
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
tresult PLUGIN_API ReaSamplerEmbed::queryInterface(const TUID iid, void** obj) {
|
||||
QUERY_INTERFACE(iid, obj, FUnknown::iid, IReaperUIEmbedInterface)
|
||||
QUERY_INTERFACE(iid, obj, IReaperUIEmbedInterface::iid, IReaperUIEmbedInterface)
|
||||
*obj = nullptr;
|
||||
return kNoInterface;
|
||||
}
|
||||
|
||||
void ReaSamplerEmbed::refresh() {
|
||||
if (!processor_) {
|
||||
samples_.clear();
|
||||
map_.zones.clear();
|
||||
selectedZone_ = -1;
|
||||
return;
|
||||
}
|
||||
auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
|
||||
map_ = processor_->performanceMap();
|
||||
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
|
||||
}
|
||||
|
||||
void ReaSamplerEmbed::maybeRefresh() {
|
||||
if (!processor_) { refresh(); return; } // clears state; cheap
|
||||
|
||||
// The performance map is a cheap in-process accessor (mutex + copy), and the editor may
|
||||
// have edited zones with NO bank-content change — always re-snapshot it so a zone edit
|
||||
// reflects immediately.
|
||||
map_ = processor_->performanceMap();
|
||||
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
|
||||
|
||||
// The EXPENSIVE part is the bank-blob bridge read (samples_). Gate it on the S9 bank-
|
||||
// generation stamp (a small ext-state read): only re-read the bank when the generation
|
||||
// changed since the last paint (a recapture / ingest / remove), or on the first paint
|
||||
// (lastSeenBankGeneration_ == -1). A pre-S9 project reads generation 0; the first paint
|
||||
// folds it and subsequent idle paints skip the bank read entirely.
|
||||
std::int64_t currentGen = lastSeenBankGeneration_;
|
||||
if (auto rawGen =
|
||||
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBankGenKey)) {
|
||||
currentGen = parseBankGeneration(*rawGen);
|
||||
} else if (lastSeenBankGeneration_ < 0) {
|
||||
currentGen = 0; // unprimed + no stamp (pre-S9): treat as generation 0 for the first read
|
||||
}
|
||||
// Intentional asymmetry: a TRANSIENT bridge failure (readReasamplerExtState returned
|
||||
// nullopt after we were already primed) leaves currentGen == lastSeenBankGeneration_,
|
||||
// so the bank-blob read is skipped and the editor keeps its last-known sample list.
|
||||
// A stale-but-intact list is better than clearing samples_ on every transient hiccup.
|
||||
|
||||
if (lastSeenBankGeneration_ < 0 || currentGen != lastSeenBankGeneration_) {
|
||||
auto banks =
|
||||
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
|
||||
lastSeenBankGeneration_ = currentGen;
|
||||
}
|
||||
}
|
||||
|
||||
TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) {
|
||||
switch (msg) {
|
||||
case REAPER_FXEMBED_WM_IS_SUPPORTED:
|
||||
#ifdef _WIN32
|
||||
return 1; // supported and available
|
||||
#else
|
||||
return 0; // not a build target off Windows
|
||||
#endif
|
||||
case REAPER_FXEMBED_WM_CREATE:
|
||||
#ifdef _WIN32
|
||||
// Create the kit's cached AA fonts before the first paint (Phase L, L3).
|
||||
// Idempotent + process-global (shared with the editor in this binary); NOT torn
|
||||
// down per-view — the OS reclaims the tiny static HFONT set at module unload.
|
||||
kitFontsInit();
|
||||
#endif
|
||||
refresh(); // prime the first paint's snapshot
|
||||
return 0;
|
||||
case REAPER_FXEMBED_WM_DESTROY:
|
||||
return 0;
|
||||
case REAPER_FXEMBED_WM_GETMINMAXINFO: {
|
||||
auto* hints = reinterpret_cast<REAPER_FXEMBED_SizeHints*>(parm3);
|
||||
if (!hints) return 0;
|
||||
// Minimum usable strip height: the keymap must not collapse below its floor
|
||||
// (kEmbedKeymapMinHeight) plus the level band.
|
||||
hints->min_width = 64;
|
||||
hints->max_width = 0; // 0 = unconstrained
|
||||
hints->min_height = kEmbedKeymapMinHeight + kEmbedLevelBandHeight;
|
||||
hints->max_height = 0; // 0 = unconstrained
|
||||
// Preferred aspect: wide strip, roughly 8:1 (w:h). 16.16 fixed point.
|
||||
hints->preferred_aspect = (8 << 16) / 1;
|
||||
hints->minimum_aspect = (4 << 16) / 1;
|
||||
return 1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
case REAPER_FXEMBED_WM_PAINT:
|
||||
return paint(parm2, parm3) ? 1 : 0;
|
||||
case REAPER_FXEMBED_WM_LBUTTONDOWN:
|
||||
// Selection at most (S6): map the click to a zone; force a redraw if it changed.
|
||||
return onMouseDown(parm3) ? REAPER_FXEMBED_RETNOTIFY_INVALIDATE : 0;
|
||||
#endif
|
||||
default:
|
||||
return 0; // unhandled messages (cursor, wheel, hittest) fall through
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) {
|
||||
auto* bmp = reinterpret_cast<LICE_IBitmap*>(bitmap);
|
||||
auto* di = reinterpret_cast<const REAPER_FXEMBED_DrawInfo*>(drawInfo);
|
||||
if (!bmp || !di) return false;
|
||||
const int w = di->width;
|
||||
const int h = di->height;
|
||||
if (w <= 0 || h <= 0) return false;
|
||||
|
||||
// Re-read live state each paint (UI thread) so the strip reflects keymap edits + bank
|
||||
// changes without its own timer — REAPER repaints the embed surface on its cadence. S9
|
||||
// dirty-guard: maybeRefresh does the EXPENSIVE bank-blob read only when the bank generation
|
||||
// changed (the flagged S6 follow-up), always refreshing the cheap performance map.
|
||||
maybeRefresh();
|
||||
|
||||
// REAPER hands us its own bitmap sized to the embed area; draw directly into it (unlike
|
||||
// the editor, which owns a LICE_SysBitmap and BitBlt's). Origin is the bitmap's (0,0).
|
||||
// Base canvas through the kit (bg/base + micro-gradient), Phase L L3.
|
||||
fillSurface(bmp, KitBox{0, 0, w, h}, Role::BgBase, InteractionState::Rest);
|
||||
|
||||
const EmbedLayout layout = layoutEmbed(w, h);
|
||||
|
||||
if (map_.zones.empty()) {
|
||||
// No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip
|
||||
// reads as "present, no zones" — the default single-capture face lives in the editor.
|
||||
LICE_FillRect(bmp, layout.keymap.x, layout.keymap.y, layout.keymap.width,
|
||||
layout.keymap.height, toLice(roleColor(Role::BgCell)), 0.5f, 0);
|
||||
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
|
||||
(samples_.empty() ? " (bank empty)" : " (no zones)");
|
||||
const Rect labelR = Rect::ltrb(layout.keymap.x + 4, layout.keymap.y, layout.keymap.right(),
|
||||
layout.keymap.bottom());
|
||||
text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left);
|
||||
} else {
|
||||
// Draw each zone as a segment across the keymap span, first-match order (so the painted
|
||||
// order matches selection + playback). Each segment takes its PASTEL SPECTRAL hue from
|
||||
// the center of its key span (spectralColor — §4), so the strip reads as the same
|
||||
// spectrum as the editor's keyboard strip. The SELECTED zone lifts to accent-primary
|
||||
// + a static glow ("which zone is live", never a pulse — §3.5).
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
const PerformanceZone& z = map_.zones[i];
|
||||
const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote);
|
||||
if (r.width <= 0) continue;
|
||||
const bool sel = (i == selectedZone_);
|
||||
if (sel) {
|
||||
// Static glow halo, then the crisp accent-primary fill.
|
||||
LICE_FillRect(bmp, r.x - 2, r.y, r.width + 4, r.height,
|
||||
toLice(roleColor(Role::AccentHot)), 0.30f, 0);
|
||||
LICE_FillRect(bmp, r.x, r.y, r.width, r.height,
|
||||
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
|
||||
} else {
|
||||
const double t = ((z.lowNote + z.highNote) * 0.5) / 127.0;
|
||||
LICE_FillRect(bmp, r.x, r.y, r.width, r.height,
|
||||
toLice(spectralColor(t)), 0.65f, 0);
|
||||
}
|
||||
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||
// Label the segment with the sample name when it is wide enough to read. The
|
||||
// selected (accent-fill) segment draws its label in bg/base for contrast (the
|
||||
// tight text-on-pastel pair, §4); the rest in text/primary.
|
||||
if (r.width >= 24) {
|
||||
const Rect lr = Rect::ltrb(r.x + 3, r.y, r.right() - 2, r.bottom());
|
||||
text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(),
|
||||
Font::Label, sel ? Role::BgBase : Role::TextPrimary, Align::Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The level band: a recessed bg/cell channel with an accent-primary fill following the
|
||||
// live activity level (a direct level follow — the one permitted "motion", §3.5).
|
||||
if (layout.levelBand.height > 0) {
|
||||
fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed);
|
||||
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
|
||||
const Rect fill = levelFillRect(layout, level);
|
||||
if (fill.width > 0) {
|
||||
LICE_FillRect(bmp, fill.x, fill.y, fill.width, fill.height,
|
||||
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReaSamplerEmbed::onMouseDown(TPtrInt drawInfo) {
|
||||
auto* di = reinterpret_cast<const REAPER_FXEMBED_DrawInfo*>(drawInfo);
|
||||
if (!di || di->width <= 0 || di->height <= 0) return false;
|
||||
refresh();
|
||||
const EmbedLayout layout = layoutEmbed(di->width, di->height);
|
||||
const std::vector<EmbedZone> zones = toEmbedZones(map_);
|
||||
const int hit = zoneAtPoint(layout, zones.data(), static_cast<int>(zones.size()),
|
||||
di->mouse_x, di->mouse_y);
|
||||
if (hit == selectedZone_) return false; // no change -> no redraw
|
||||
selectedZone_ = hit;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
} // namespace reasampler::vst
|
||||
Reference in New Issue
Block a user