VSTDOCSv0.0.34

DOCS / AUDIO & NATIVE

Presets

Every shipping plugin rebuilds the same preset boilerplate: factory sounds, user saves, prev/next, a dirty asterisk. VSReacT ships it once — vsreact::PresetManager on the C++ side, usePresets() and <PresetBrowser> on the React side.

The C++ side

Construct a manager next to your ParameterBridge, chain its handleNativeCall, and attach() the RootView. Factory presets are parameter values in natural units — the same numbers you'd dial in by hand. User presets are full APVTS snapshots saved to userApplicationDataDirectory/<appName>/Presets.

PluginEditor.cpp
vsreact::PresetManager::Options options;
options.appName = "My Plugin";
options.factoryPresets = {
    { "Default",   { { "threshold", -18.0f }, { "ratio", 4.0f } } },
    { "Drum Smash",{ { "threshold", -30.0f }, { "ratio", 10.0f } } },
};
presets = std::make_unique<vsreact::PresetManager> (state, options);

rootOptions.onNativeCall = [this] (const juce::String& name, const juce::var& args) -> juce::var {
    if (auto handled = presets->handleNativeCall (name, args)) return *handled;
    if (auto handled = bridge.handleNativeCall (name, args))   return *handled;
    return {};
};
// after constructing the RootView:
presets->attach (*root);

Dirty tracking is automatic: the manager listens to every parameter, so the first tweak after a load or save pushes a preset event with dirty: true — the UI's asterisk appears without polling, and self-inflicted changes (the load itself) don't count as edits.

The React side

<PresetBrowser /> is the strip every plugin header has — prev/next arrows, the current name (asterisk when edited) opening a grouped menu (factory first, then user saves), and a save dialog. Drop it anywhere; multiple instances stay in sync because all state lives on the C++ side.

ui/src/main.tsx
import { PresetBrowser, usePresets } from "@vsreact/core";

<PresetBrowser width={200} />

// or headless, for a custom UI:
const { presets, current, dirty, load, save, next, prev } = usePresets();

The wire protocol

CALL / EVENTSHAPE
preset:list{current, dirty, presets: [{name, factory}]}
preset:load / save / delete{name} → {ok} — factory names are reserved against save/delete
preset:next / prevwalks factory-then-user order and wraps
event presetfull state after load/save/delete, and when a tweak first dirties the preset

Resizable editors

Related ship-readiness boilerplate, worth the four lines: because panels are flexbox, a resizable editor reflows instead of scaling. Read the saved size before installing the constrainer — its limits fire resized(), and persisting from that call would clobber the saved size with the construction-time bounds.

PluginEditor.cpp
const int savedW = (int) state.state.getProperty ("uiWidth", 720);
const int savedH = (int) state.state.getProperty ("uiHeight", 420);
setResizable (true, true);
setResizeLimits (600, 360, 1280, 800);
setSize (savedW, savedH);
constructed = true;

void resized() override
{
    root->setBounds (getLocalBounds());
    if (constructed)   // survives the session via get/setStateInformation
    {
        state.state.setProperty ("uiWidth", getWidth(), nullptr);
        state.state.setProperty ("uiHeight", getHeight(), nullptr);
    }
}

The compressor example wires all of this — presets, factory sounds, resize with persistence — in vsreact/examples/compressor.