feat(vst): stand up VST3 instrument spike (S1) — skeleton, IPlugView↔LICE editor, REAPER bridge read

Vendor Steinberg VST3 SDK (v3.7.9_build_61); add reasampler_vst.vst3 as a second, additive build artifact with pure editor-geometry + bridge-marshal helpers under CTest.
This commit is contained in:
2026-07-26 15:29:54 -04:00
parent 5595ba42f9
commit 3c2a7c45b2
17 changed files with 1256 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
// reasampler_editor.cpp — see reasampler_editor.h. The IPlugView<->LICE bridge.
// Windows-only (D5); the whole file is guarded so a non-Windows build (not a target,
// but keeps the TU honest) degrades to the CPluginView defaults.
#include "reasampler_editor.h"
#include <string>
#include "editor_geometry.h"
#include "reaper_bridge.h"
#ifdef _WIN32
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
// LICE — the same drawing stack bank_panel uses. On Windows LICE routes through native
// GDI; no SWELL needed for the child window itself.
#include "wdltypes.h"
#include "lice/lice.h"
#endif
using namespace Steinberg;
namespace reasampler::vst {
namespace {
#ifdef _WIN32
constexpr const wchar_t* kChildClassName = L"ReaSamplerVstEditor";
// Palette — house style, mirrored from bank_panel's dark theme so the instrument reads
// as the same tool.
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
const LICE_pixel kColTitleBg = LICE_RGBA(20, 20, 22, 255);
const LICE_pixel kColBtnBg = LICE_RGBA(44, 44, 48, 255);
const LICE_pixel kColBtnHitBg = LICE_RGBA(58, 96, 84, 255);
const LICE_pixel kColBtnBorder = LICE_RGBA(120, 200, 160, 255);
const COLORREF kRgbText = RGB(210, 230, 220);
void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
// LICE has no built-in font handle here; use GDI text into the bitmap DC, matching
// bank_panel's drawCenteredText approach (SetTextColor + DrawText on getDC()).
HDC dc = bmp->getDC();
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, col);
RECT gr{r.left, r.top, r.right, r.bottom};
DrawTextA(dc, s, -1, &gr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
}
#endif
} // namespace
ReaSamplerEditor::ReaSamplerEditor(ReaperBridge* bridge)
: CPluginView(nullptr), bridge_(bridge) {
// Default view size; the host may resize (canResize() == true).
ViewRect r(0, 0, 420, 260);
setRect(r);
}
ReaSamplerEditor::~ReaSamplerEditor() {
#ifdef _WIN32
// Defensive teardown: the host normally calls removed() (which destroys the child)
// before releasing us, but if we're destroyed while still attached, don't leak the
// window — mirror the create in attachedToParent().
if (childHwnd_) {
DestroyWindow(childHwnd_);
childHwnd_ = nullptr;
}
#endif
}
tresult PLUGIN_API ReaSamplerEditor::isPlatformTypeSupported(FIDString type) {
#ifdef _WIN32
if (type && std::string(type) == kPlatformTypeHWND) return kResultTrue;
#endif
return kResultFalse;
}
tresult PLUGIN_API ReaSamplerEditor::canResize() {
return kResultTrue;
}
#ifdef _WIN32
void ReaSamplerEditor::attachedToParent() {
// systemWindow is the parent HWND the host created (CPluginView::attached set it
// from the void* parent when the type is HWND).
HWND parent = static_cast<HWND>(systemWindow);
if (!parent) return;
HINSTANCE hInst = reinterpret_cast<HINSTANCE>(
GetWindowLongPtr(parent, GWLP_HINSTANCE));
if (!hInst) hInst = GetModuleHandle(nullptr);
// Register the child window class once per module.
static bool classRegistered = false;
if (!classRegistered) {
WNDCLASSW wc{};
wc.lpfnWndProc = &ReaSamplerEditor::wndProc;
wc.hInstance = hInst;
wc.lpszClassName = kChildClassName;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.style = CS_HREDRAW | CS_VREDRAW;
RegisterClassW(&wc);
classRegistered = true;
}
const ViewRect& r = getRect();
childHwnd_ = CreateWindowExW(
0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0, r.getWidth(),
r.getHeight(), parent, nullptr, hInst, nullptr);
if (childHwnd_) {
// Stash `this` so wndProc can route messages back to the instance.
SetWindowLongPtr(childHwnd_, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(this));
}
}
void ReaSamplerEditor::removedFromParent() {
if (childHwnd_) {
DestroyWindow(childHwnd_);
childHwnd_ = nullptr;
}
}
tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
// Let CPluginView latch the new rect, then resize the child window to match so the
// LICE surface fills the host-provided seat.
tresult res = CPluginView::onSize(newSize);
if (childHwnd_ && newSize) {
MoveWindow(childHwnd_, 0, 0, newSize->getWidth(), newSize->getHeight(), TRUE);
}
return res;
}
void ReaSamplerEditor::paint(HDC hdc) {
RECT cr{};
GetClientRect(childHwnd_, &cr);
const int w = cr.right - cr.left;
const int h = cr.bottom - cr.top;
if (w <= 0 || h <= 0) return;
// Same double-buffered LICE pattern as bank_panel::paintPanel: draw into a
// LICE_SysBitmap, then BitBlt to the window DC.
LICE_SysBitmap bmp(w, h);
LICE_Clear(&bmp, kColBackground);
const EditorLayout layout = layoutEditor(w, h);
// Title band.
LICE_FillRect(&bmp, layout.titleBar.left, layout.titleBar.top,
layout.titleBar.width(), layout.titleBar.height(), kColTitleBg, 1.0f,
0);
// Read a known live-state value over the bridge to prove the read spike. Show the
// raw ext-state presence (never the full blob) so the title reflects live project
// state without dumping JSON into the UI.
std::string title = "ReaSampler Instrument";
if (bridge_ && bridge_->isConnected()) {
auto banks = bridge_->readReasamplerExtState("banks");
title += banks ? " [bank: linked]" : " [bank: none]";
} else {
title += " [host: no bridge]";
}
Rect titleText{layout.titleBar.left + 8, layout.titleBar.top,
layout.titleBar.right - 8, layout.titleBar.bottom};
drawText(&bmp, titleText, title.c_str(), kRgbText);
// The clickable button — fill reflects the last hit-test (the routing proof).
LICE_FillRect(&bmp, layout.button.left, layout.button.top, layout.button.width(),
layout.button.height(), buttonHit_ ? kColBtnHitBg : kColBtnBg, 1.0f,
0);
LICE_DrawRect(&bmp, layout.button.left, layout.button.top, layout.button.width(),
layout.button.height(), kColBtnBorder, 1.0f, 0);
Rect btnText{layout.button.left + 8, layout.button.top, layout.button.right,
layout.button.bottom};
drawText(&bmp, btnText, buttonHit_ ? "clicked" : "click me", kRgbText);
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
}
void ReaSamplerEditor::onClick(int x, int y) {
RECT cr{};
GetClientRect(childHwnd_, &cr);
const EditorLayout layout = layoutEditor(cr.right - cr.left, cr.bottom - cr.top);
if (hitTest(layout, x, y) == HitTarget::kButton) {
buttonHit_ = !buttonHit_;
InvalidateRect(childHwnd_, nullptr, FALSE);
}
}
LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam) {
auto* self = reinterpret_cast<ReaSamplerEditor*>(
GetWindowLongPtr(hwnd, GWLP_USERDATA));
switch (msg) {
case WM_PAINT: {
PAINTSTRUCT ps{};
HDC hdc = BeginPaint(hwnd, &ps);
if (self) self->paint(hdc);
EndPaint(hwnd, &ps);
return 0;
}
case WM_LBUTTONDOWN:
if (self) self->onClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_ERASEBKGND:
return 1; // we fully repaint in WM_PAINT; skip the flicker-inducing erase
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
#else // non-Windows: not a build target (D5), but keep the TU compilable.
void ReaSamplerEditor::attachedToParent() {}
void ReaSamplerEditor::removedFromParent() {}
tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
return CPluginView::onSize(newSize);
}
#endif // _WIN32
} // namespace reasampler::vst