Raw Window Handle (Bring Your Own Renderer)

If none of the built-in backends fit, you can implement the Editor trait directly. The host gives you a window handle — what you draw inside it is entirely up to you.

#When to reach for this

This is the lowest level of GUI integration. You probably want it if:

  • You have an existing rendering pipeline (Metal, OpenGL, Skia)
  • You want a web view (CEF, wry)
  • You need a framework truce doesn't wrap yet
  • You want absolute pixel-level control

For most plugins, start with the built-in GUI or one of the framework integrations (egui, iced, slint).

#Implementing the Editor trait

Here's the minimum implementation. The host calls open() with a parent window handle, and you create your UI as a child of that window:

use truce_core::editor::{Editor, PluginContext, RawWindowHandle};

pub struct MyEditor {
    size: (u32, u32),
    context: Option<PluginContext>,
    // your renderer state goes here
}

impl Editor for MyEditor {
    fn size(&self) -> (u32, u32) {
        self.size
    }

    fn open(&mut self, parent: RawWindowHandle, context: PluginContext) {
        self.context = Some(context);

        // parent tells you what kind of window to create:
        match parent {
            RawWindowHandle::AppKit(ns_view) => {
                // ns_view is an NSView* — create a child NSView or CAMetalLayer
            }
            RawWindowHandle::Win32(hwnd) => {
                // hwnd is an HWND — create a child window
            }
            RawWindowHandle::X11(window_id) => {
                // window_id is an X11 Window — create a child window
            }
        }
    }

    fn close(&mut self) {
        // tear down your renderer and child window
        self.context = None;
    }

    fn idle(&mut self) {
        // called ~60fps by the host — repaint here
    }

    fn set_size(&mut self, width: u32, height: u32) -> bool {
        self.size = (width, height);
        true
    }

    fn can_resize(&self) -> bool { false }
    fn set_scale_factor(&mut self, _factor: f64) {
        // Hosts deliver content scale here on Windows VST3
        // (`IPlugViewContentScaleSupport`) and CLAP
        // (`clap_plugin_gui::set_scale`). macOS hosts rarely call
        // it — AppKit handles Retina backing automatically. Resize
        // your off-screen buffers to physical pixels here if you
        // need them.
    }
}

unsafe impl Send for MyEditor {}

#Reading and writing parameters

PluginContext exposes its host bridge as methods. IDs use #[derive(Params)]'s generated *ParamId enum and convert to u32 through impl Into<u32>, so you can pass either the enum variant or a literal u32:

fn idle(&mut self) {
    let ctx = self.context.as_ref().unwrap();

    // read current values
    let gain = ctx.get_param(P::Gain);            // normalized 0.0-1.0
    let gain_plain = ctx.get_param_plain(P::Gain); // e.g., -60.0 to 6.0
    let gain_text = ctx.format_param(P::Gain);     // "0.0 dB"
    let meter_l = ctx.get_meter(P::MeterLeft);     // 0.0-1.0

    // render your UI with these values...

    // when the user drags a control:
    ctx.begin_edit(P::Gain);
    ctx.set_param(P::Gain, 0.75);   // normalized value
    ctx.end_edit(P::Gain);

    // ...or, for click-to-toggle / single-shot edits, the
    // `begin_edit + set_param + end_edit` triple collapses to:
    ctx.automate(P::Bypass, 1.0);
}

Always wrap drag gestures in begin_edit / end_edit so the host records automation correctly — automate(id, val) is the convenience wrapper for one-shot edits where the gesture and value arrive together.

#Connecting to your plugin

Same as every other backend:

impl PluginLogic for MyPlugin {
    fn custom_editor(&self) -> Option<Box<dyn Editor>> {
        Some(Box::new(MyEditor {
            size: (800, 600),
            context: None,
        }))
    }
}

Works with all formats (CLAP, VST3, VST2, LV2, AU, AAX) without any format-specific code.

#Useful helpers

#Scale factor

Query the display scale on macOS (2.0 on Retina, 1.0 otherwise):

let scale = truce_gui::backing_scale();

#baseview for windowing

If you want cross-platform window management without building it yourself, use baseview directly. It handles macOS/Windows/Linux child window creation, event dispatch, and DPI scaling:

use baseview::{Window, WindowOpenOptions, Size, WindowScalePolicy};

fn open(&mut self, parent: RawWindowHandle, context: PluginContext) {
    let options = WindowOpenOptions {
        title: String::from("My Plugin"),
        size: Size::new(800.0, 600.0),
        scale: WindowScalePolicy::SystemScaleFactor,
    };
    // open a child window with your custom WindowHandler
}

See the truce-egui and truce-slint source code for complete baseview integration examples.

#Reference implementations

The existing backends are good examples of real Editor implementations:

Backend Source Approach
Built-in crates/truce-gui/src/editor.rs baseview + wgpu + CPU pixel blit
GPU crates/truce-gpu/src/editor.rs baseview + wgpu + GPU rendering
egui crates/truce-egui/src/editor.rs baseview + egui-wgpu
Iced crates/truce-iced/src/editor.rs baseview + iced-wgpu
Slint crates/truce-slint/src/editor.rs baseview + software renderer + wgpu blit