Vizia Integration
Vizia is a declarative GUI
framework with reactive data binding and CSS styling. truce-vizia
embeds it via baseview and bridges parameter reads/writes through a
ParamLens<P> whose per-param signals keep every widget bound to
the same id in sync.
Desktop only. macOS (x86_64 + aarch64), Windows (x86_64), and Linux
(x86_64 + aarch64) all work. iOS and Windows on ARM64 are
unsupported — see Platform support below.
Use the built-in GUI, truce-egui, or truce-slint on those.
Editor resize is currently a no-op on vizia.
ViziaEditor::set_sizerecords the new logical size but the running view doesn't repaint at it - vizia'sWindowHandlehas no resize entry point callable from outside its event loop. Ship vizia plugins fixed-size; don't call.resizable(true). Pending avizia_baseviewupstream patch.
#Setup
truce-vizia is not published on crates.io — vizia upstream
hasn't tagged a release that ships the baseview feature flag
(it's main-branch only), and crates.io rejects git-only deps
without a version shadow. Pull the crate from git instead:
[dependencies]
truce-vizia = { git = "https://github.com/truce-audio/truce.git", tag = "v0.54.0" }
Bump the tag in lockstep with your other truce-* deps on each
release; the sub-workspace inside the truce repo guarantees the
git-pinned vizia rev tracks the rest of the framework's version.
Vizia itself is a transitive dep — no direct vizia = … line is
required. truce-vizia re-exports it as truce_vizia::vizia so
use truce_vizia::vizia::prelude::*; brings the full API into scope.
Linux: Vizia renders with Skia, whose build links
-lEGLand-lwayland-egl, so on top of the shared GUI dev libraries you also need the EGL + Wayland-EGL dev packages (sudo apt install libegl1-mesa-dev libwayland-dev). Without them the link fails withrust-lld: error: unable to find library -lwayland-egl. The other GUI backends don't link Skia and don't need these.
Return a ViziaEditor from editor():
use truce::prelude::*;
use truce_font::JETBRAINS_MONO;
use truce_vizia::vizia::prelude::*;
use truce_vizia::widgets::{self, param_knob};
use truce_vizia::{ParamLens, ViziaEditor};
use MyParamsParamId as P;
impl PluginLogic for MyPlugin {
fn editor(&self) -> Box<dyn Editor> {
ViziaEditor::new(
self.params.clone(),
(240, 160),
|cx: &mut Context, lens: ParamLens<MyParams>| {
HStack::new(cx, |cx| {
param_knob(cx, lens.clone(), P::Gain, "Gain");
param_knob(cx, lens.clone(), P::Pan, "Pan");
})
.padding(Pixels(10.0))
.horizontal_gap(Pixels(10.0));
},
)
.with_stylesheet(widgets::BASE_CSS)
.with_font(JETBRAINS_MONO)
.into_editor()
}
}
The setup closure is bounded Fn (not FnOnce) — hosts may close
and re-open the editor, and each open call invokes the closure once
to build a fresh widget tree. (240, 160) is the window size in
logical points.
with_stylesheet(widgets::BASE_CSS) is recommended when you use
the built-in widgets — it carries vizia compatibility shims (the
collapsing knob head, dropdown popup overflow) without prescribing
any palette. with_font(JETBRAINS_MONO) matches the look of every
other built-in editor; omit it to inherit vizia's default font
stack.
#ParamLens
ParamLens<P> is the bridge between truce's atomic param store and
vizia's reactive runtime. Every widget bound to the same id
shares a Signal<f32>, so a knob and an XY pad pointing at the
same param update each other on the next frame without any extra
wiring.
// Read
lens.get(P::Gain) // normalized 0.0-1.0
lens.get_plain(P::Gain) // plain value
lens.format(P::Gain) // formatted string
lens.meter(P::MeterLeft) // meter level
// Write (one-shot, for clicks/toggles)
lens.automate(P::Bypass, 1.0);
// Write (continuous drag — records smooth automation)
lens.begin_edit(P::Gain);
lens.set(P::Gain, new_value); // call each frame during drag
lens.end_edit(P::Gain);
ParamLens<P> is cheap to clone — hand out clones freely to widgets,
they all share the same underlying store and the same per-param
signals.
#Widgets
truce-vizia ships param-binding widgets that handle the gesture
protocol internally:
use truce_vizia::widgets::{
param_knob, // rotary knob with label + formatted value
param_slider, // horizontal slider
param_toggle, // on/off switch
param_dropdown, // click-to-open list for enums / int ranges
param_xy_pad, // 2D pad for two params
level_meter, // vertical bar meter
};
param_selector(click-to-cycle) is deprecated since 0.56.0; useparam_dropdowninstead.
Typical layout:
fn my_view(cx: &mut Context, lens: ParamLens<MyParams>) {
HStack::new(cx, move |cx| {
VStack::new(cx, move |cx| {
HStack::new(cx, |cx| {
param_knob(cx, lens.clone(), P::Gain, "Gain");
param_knob(cx, lens.clone(), P::Pan, "Pan");
});
param_xy_pad(cx, lens.clone(), P::Pan, P::Gain,
"Pan / Gain", 130.0, 130.0);
});
level_meter(cx, lens.clone(),
&[P::MeterLeft, P::MeterRight], 240.0);
})
.padding(Pixels(10.0))
.horizontal_gap(Pixels(10.0));
}
Discrete params (IntParam, EnumParam<T>) snap to grid steps on
each gesture so the host sees integer / enum positions, not the raw
continuous value baseview hands the widget.
#Using raw vizia widgets
Any vizia widget works — just bind it to the shared signal and echo writes back through the lens:
let value_signal = lens.value_signal(P::Gain);
let lens_for_change = lens.clone();
Knob::new(cx, 0.0, value_signal, false)
.width(Pixels(48.0))
.height(Pixels(48.0))
.on_change(move |_cx, val| {
lens_for_change.automate(P::Gain, f64::from(val));
value_signal.set(val);
});
value_signal is shared across every widget for the same id;
echoing the new value into it keeps the rest of the UI in sync.
#Meters
Meter widgets use a separate reactive surface. level_meter reads
lens.meter_signal(id) — a per-meter Signal<f32> driven by a root
polling timer (~30 Hz) inside ViziaEditor::open that fans the latest
store values into every registered meter signal. The reactive graph
then re-evaluates the Memos driving the bar heights.
Declaration matches every other backend (see parameters § Meters):
#[derive(Params)]
pub struct MyParams {
// ...
#[meter] pub meter_l: MeterSlot,
#[meter] pub meter_r: MeterSlot,
}
Push from process() with context.set_meter(P::MeterL, peak),
draw with level_meter(cx, lens, &[P::MeterL, P::MeterR], 240.0).
#Theming
Widgets render against vizia's default theme and prescribe no colors
themselves. The BASE_CSS constant carries only the minimum CSS
needed for establishing a working baseline. Plugins that want a
particular palette layer their own stylesheet via with_stylesheet:
ViziaEditor::new(self.params.clone(), (240, 160), my_view)
.with_stylesheet(widgets::BASE_CSS)
.with_stylesheet(include_str!("../assets/dark.css"))
.with_font(JETBRAINS_MONO)
Stylesheets are applied in the order they were added, after vizia's
defaults. Each widget tags its outer container with a truce-*
class (truce-knob, truce-slider, truce-toggle, …) for targeted
styling.
#Screenshot testing
Vizia screenshots render against a CPU-backed Skia raster surface — no active OS window or GL context required:
#[test]
fn gui_screenshot() {
truce_test::screenshot!(Plugin, "screenshots/default.png").run();
}
See screenshot testing for the full flow.
#Platform support
| Platform | Supported | Notes |
|---|---|---|
| macOS (x86_64 + aarch64) | Yes | Universal cargo truce package slices |
| Windows (x86_64) | Yes | |
| Linux (x86_64 + aarch64) | Yes | |
| iOS | No | Vizia hard-pins vizia_baseview and baseview has no target_os = "ios" platform impl, so the chain truce-vizia → vizia → vizia_baseview → baseview doesn't link on iOS. The truce-vizia crate is gated with #![cfg(not(target_os = "ios"))]. Use truce-egui or truce-slint (both ship UIKit-backed editor stubs) or the built-in editor instead. |
| Windows on ARM64 | No | skia-bindings (pulled in by vizia → skia-safe) doesn't build for aarch64-pc-windows-msvc on the standard VS 2022 toolchain — its bundled clang 19 ships an incomplete arm_neon.h, and the older libclang versions that parse the header trip MSVC STL's static_assert "expected Clang 19.0.0 or newer". Universal Windows installers produced by cargo truce package need --host-only to skip the ARM64 slice on vizia plugins. |
#Example
crates/truce-vizia/examples/truce-example-gain-vizia
— complete plugin with knobs, XY pad, meter, JetBrains Mono font,
and per-OS screenshot tests.
crates/truce-vizia/examples/truce-example-gui-zoo-vizia
— passthrough plugin exercising every widget kind, every
ParamUnit variant, and the discrete-snap path.
Both live in the truce-vizia Cargo sub-workspace (the parent
workspace can't include them because vizia's hard-pinned baseview
rev is incompatible with iOS).