truce_loader/traits.rs
1//! The PluginLogic trait — the single trait plugin developers implement.
2
3use truce_core::buffer::AudioBuffer;
4use truce_core::events::EventList;
5use truce_core::process::{ProcessContext, ProcessStatus};
6use truce_gui::interaction::WidgetRegion;
7use truce_gui::render::RenderBackend;
8use truce_gui::widgets::WidgetType;
9
10/// The trait for hot-reloadable plugin logic.
11///
12/// Implement this in your logic dylib. The shell loads it via
13/// `Box<dyn PluginLogic>` and delegates audio processing and GUI
14/// rendering to it.
15///
16/// All methods use safe Rust types. No `unsafe`, no `#[repr(C)]`,
17/// no raw pointers.
18pub trait PluginLogic: Send + 'static {
19 /// Create a new instance with default state.
20 fn new() -> Self where Self: Sized;
21
22 /// Reset for a new sample rate / block size.
23 fn reset(&mut self, sample_rate: f64, max_block_size: usize);
24
25 /// Return a mutable reference to the plugin's Params, if it owns one.
26 ///
27 /// If this returns Some, the shell automatically syncs parameter
28 /// values from host automation events and advances smoothers before
29 /// each process() call. The developer never calls sync manually.
30 ///
31 /// Return None if the plugin reads params via `context.param(id)` instead.
32 fn params_mut(&mut self) -> Option<&mut dyn truce_params::Params> { None }
33
34 /// Process one block of audio.
35 fn process(
36 &mut self,
37 buffer: &mut AudioBuffer,
38 events: &EventList,
39 context: &mut ProcessContext,
40 ) -> ProcessStatus;
41
42 /// Render the GUI into the backend.
43 ///
44 /// Default: no-op. The shell uses BuiltinEditor with the layout
45 /// from `layout()` to draw standard widgets automatically.
46 /// Override only for custom visuals.
47 fn render(&self, _backend: &mut dyn RenderBackend) {}
48
49 /// Whether this plugin uses a custom render() implementation.
50 /// If false (default), the shell uses BuiltinEditor with
51 /// standard widget drawing from layout().
52 fn uses_custom_render(&self) -> bool { false }
53
54 /// Return the widget layout.
55 ///
56 /// Use `GridLayout::build()` for the layout. Widgets auto-flow
57 /// left-to-right. Use `.cols(n)` and `.rows(n)` for spanning.
58 fn layout(&self) -> truce_gui::layout::GridLayout {
59 truce_gui::layout::GridLayout::build("", "", 1, 80.0, vec![], vec![])
60 }
61
62 /// Hit test: which widget (if any) is at (x, y)?
63 fn hit_test(&self, widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
64 default_hit_test(widgets, x, y)
65 }
66
67 /// Serialize plugin-specific state (DSP state, not params).
68 fn save_state(&self) -> Vec<u8> { Vec::new() }
69
70 /// Restore plugin-specific state.
71 fn load_state(&mut self, _data: &[u8]) {}
72
73 /// Report latency in samples.
74 fn latency(&self) -> u32 { 0 }
75
76 /// Report tail time in samples.
77 fn tail(&self) -> u32 { 0 }
78
79 /// Provide a custom editor instead of the built-in widget layout.
80 ///
81 /// Return `Some(editor)` to use a custom `Editor` implementation
82 /// (e.g., `truce_egui::EguiEditor`). The shell calls this first;
83 /// if it returns `None`, the shell falls back to creating a
84 /// `BuiltinEditor` from `layout()`.
85 fn custom_editor(&self) -> Option<Box<dyn truce_core::editor::Editor>> { None }
86}
87
88/// Default hit test: circular for knobs, rectangular for others,
89/// skip meters.
90pub fn default_hit_test(widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
91 for (i, w) in widgets.iter().enumerate() {
92 if w.widget_type == WidgetType::Meter { continue; }
93 if w.widget_type == WidgetType::Knob {
94 let dx = x - w.cx;
95 let dy = y - w.cy;
96 if dx * dx + dy * dy <= w.radius * w.radius {
97 return Some(i);
98 }
99 } else if x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h {
100 return Some(i);
101 }
102 }
103 None
104}