Built-in GUI

The built-in GUI renders widgets from a layout you define in code. No custom editor, no framework dependency — declare what you want and truce draws it. This is the default; override layout() on PluginLogic and you're done.

For a first walkthrough see the GUI chapter. This page is the reference for every option.

#GridLayout::build

use truce_gui_types::layout::{GridLayout, knob, slider, toggle, widgets};
use MyParamsParamId as P;

fn layout(&self) -> GridLayout {
    GridLayout::build(vec![widgets(vec![
        knob(P::Gain, "Gain"),
        slider(P::Pan, "Pan"),
        toggle(P::Bypass, "Bypass"),
    ])])
}

Signature:

GridLayout::build(sections: Vec<Section>) -> GridLayout

Defaults:

  • No header band. Add a title with .with_title("MY PLUGIN"), a right-aligned subtitle with .with_subtitle("v1.0"), or both with .with_titles(HeaderTitles::pair("MY PLUGIN", "v1.0")). Each slot is independently optional.
  • cols = the widest section's widget count (extended to fit any explicitly-positioned widget). Override with .with_cols(n) to force wrapping — e.g. .with_cols(2) on a 4-widget section produces a 2×2 grid.
  • cell_size = GRID_DEFAULT_CELL_SIZE (50.0 logical points). Override with .with_cell_size(s). Set both at once with .with_grid(cols, cell_size).
GridLayout::build(sections)                       // 95% case
GridLayout::build(sections).with_title("EQ")      // title only
GridLayout::build(sections).with_cols(2)          // force 2-col wrapping
GridLayout::build(sections).with_grid(4, 60.0)    // both at once

Widget constructors accept impl Into<u32>, so both typed enum IDs (recommended — P::Gain) and raw u32 values work.

#Widgets

Constructor Widget Default span Typical param type
knob(P::X, "Label") rotary knob 1×1 FloatParam, IntParam
slider(P::X, "Label") horizontal slider 1×1 FloatParam
toggle(P::X, "Label") pill on/off 1×1 BoolParam
selector(P::X, "Label") click-to-cycle 1×1 EnumParam<T>, IntParam
dropdown(P::X, "Label") click-to-open list 1×1 EnumParam<T>, IntParam
meter(&[P::L, P::R], "Label") level meter (one bar per ID) 1×1 MeterSlot
xy_pad(P::X, P::Y, "Label") 2D control pad 2×2 two FloatParams

If you don't specify a widget type, the default is inferred from the parameter type: BoolParam → toggle, EnumParam → selector, everything else → knob.

#Sections

Group widgets under labelled headers with section(). Use widgets() for the ungrouped rows.

use truce_gui_types::layout::{GridLayout, knob, section, widgets};

GridLayout::build(vec![
    section("LOW", vec![
        knob(P::LowFreq, "Freq"),
        knob(P::LowGain, "Gain"),
        knob(P::LowQ, "Q"),
    ]),
    section("MID", vec![
        knob(P::MidFreq, "Freq"),
        knob(P::MidGain, "Gain"),
        knob(P::MidQ, "Q"),
    ]),
    widgets(vec![knob(P::Output, "Output")]),
])
.with_title("EQ")

cols auto-resolves to 3 here (the widest section has 3 widgets), so each section renders as one row.

Each section starts a new row with a header strip. Widgets inside a section flow left-to-right within that section's row.

#Spanning and positioning

Widgets default to 1×1 cells. Override with .cols(), .rows(), and .at():

dropdown(P::Wave, "Wave").cols(2),                     // 2 cells wide
meter(&[P::L, P::R], "Level").rows(3),                 // 3 cells tall
xy_pad(P::X, P::Y, "Pad").cols(2).rows(2),             // already 2×2 by default
meter(&[P::L, P::R], "Level").at(2, 0).rows(3),        // pinned to column 2, row 0

.at(col, row) is useful when you want a widget anchored (e.g. a tall meter in the corner) while the rest flow freely.

#Meters

Declare meter slots in your params struct with #[meter]:

#[derive(Params)]
pub struct MyParams {
    #[param(name = "Gain", range = "linear(-60, 6)", unit = "dB")]
    pub gain: FloatParam,

    #[meter] pub meter_left:  MeterSlot,
    #[meter] pub meter_right: MeterSlot,
}

Meter IDs auto-assign starting at 256 and appear in the generated MyParamsParamId enum alongside parameters.

Push from process() (realtime-safe atomic write):

context.set_meter(P::MeterLeft,  buffer.output_peak(0));
context.set_meter(P::MeterRight, buffer.output_peak(1));

Draw in the layout:

meter(&[P::MeterLeft, P::MeterRight], "Level").rows(3)

#Theming

Colours come from a named theme. Dark is the default. Switch themes or override individual colours:

use truce_gui_types::theme::{Theme, Color};

GridLayout::build(sections)
    .with_title("MY PLUGIN")
    .theme(Theme::light())
GridLayout::build(sections)
    .with_title("MY PLUGIN")
    .theme(Theme {
        primary: Color::rgb(0x00, 0xd2, 0xff),
        ..Theme::dark()
    })

Fonts: fontdue rasterisation with JetBrains Mono Regular embedded at compile time. No font file on disk, no runtime load.

Rendering: truce-gpu through wgpu (Metal on macOS, DX12 on Windows, Vulkan on Linux). Tiny-skia CPU rasterisation is the fallback.

#Interaction

The framework handles all of the following automatically — you don't wire any of it by hand:

  • Knob / slider: drag to adjust. Scroll-wheel to fine-tune. Double-click to reset to default.
  • Toggle: click to flip.
  • Selector: click to cycle forward.
  • Dropdown: click to open the popup list, click an option to select.
  • XY pad: drag anywhere on the pad to set both parameters.
  • Right-click: opens the host's context menu (automation, reset, enter value).

#A full example

use GainParamsParamId as P;
use truce_gui_types::layout::{GridLayout, knob, meter, widgets, xy_pad};

fn layout(&self) -> GridLayout {
    GridLayout::build(vec![widgets(vec![
        knob(P::Gain, "Gain"),
        knob(P::Pan,  "Pan"),
        xy_pad(P::Pan, P::Gain, "XY"),
        meter(&[P::MeterLeft, P::MeterRight], "Level").rows(2),
    ])])
    .with_title("GAIN")
}

#Moving beyond the built-in GUI

The built-in widget set covers the common audio-plugin shape — knobs / sliders / meters / dropdowns. When you need text input fields, lists, tables, analyzer curves, or a specific visual style the theme system can't reach, switch to a framework backend:

  • egui — immediate-mode, fast to prototype.
  • iced — retained-mode, Elm architecture, good for complex custom UIs.
  • slint — declarative .slint markup with data binding.
  • raw-window-handle — full control: Metal, OpenGL, Skia, anything.