truce_slint/lib.rs
1//! Slint GUI backend for truce audio plugins.
2//!
3//! Provides `SlintEditor`, which implements `truce_core::Editor` using
4//! Slint's software renderer + baseview + wgpu. Developers write their UI
5//! in `.slint` markup (compiled at build time) and wire parameters through
6//! `PluginContext<P>`.
7//!
8//! # Usage
9//!
10//! ```ignore
11//! use truce_slint::SlintEditor;
12//! use truce_core::editor::PluginContext;
13//!
14//! SlintEditor::new(params, (400, 300), |state: PluginContext<MyParams>| {
15//! let ui = MyPluginUi::new().unwrap();
16//! truce_slint::bind! { state, ui,
17//! P::Gain => gain,
18//! P::Pan => pan,
19//! P::Bypass => bypass: bool,
20//! }
21//! })
22//! ```
23
24// baseview + wgpu live behind `blit` + `editor` on non-iOS hosts.
25// iOS uses `editor_ios.rs` which runs the same Slint
26// `MinimalSoftwareWindow` CPU renderer + blits via `CGImage`
27// (skipping baseview / wgpu entirely). `platform.rs` carries the
28// Slint platform-registration glue - needed on every target;
29// inside the file, the baseview / wgpu re-exports are themselves
30// cfg-gated.
31#[cfg(not(target_os = "ios"))]
32pub mod blit;
33#[cfg(not(target_os = "ios"))]
34pub mod editor;
35pub mod platform;
36#[cfg(not(target_os = "ios"))]
37mod screenshot;
38
39#[cfg(target_os = "ios")]
40mod editor_ios;
41
42#[cfg(not(target_os = "ios"))]
43pub use editor::{SlintEditor, SyncFn};
44
45#[cfg(target_os = "ios")]
46pub use editor_ios::{SlintEditor, SyncFn};
47
48// Re-export `PluginContext` so plugin authors using the `bind!` macro
49// don't need a direct truce-core dependency.
50pub use truce_core::editor::PluginContext;
51
52// Re-export slint so plugin authors can use it without a direct dependency.
53pub use slint;
54
55// Re-export paste (used by the bind! macro).
56#[doc(hidden)]
57pub use paste::paste;
58
59// Re-export truce_core (used by the bind! macro for cast helpers).
60#[doc(hidden)]
61pub use truce_core;
62
63/// Bind Slint properties to truce parameters.
64///
65/// Generates both the `on_<name>_changed` callback wiring (UI → host) and
66/// returns a sync closure (host → UI) called each frame.
67///
68/// # Syntax
69///
70/// ```ignore
71/// truce_slint::bind! { state, ui,
72/// PARAM_ID => property_name, // float (default)
73/// PARAM_ID => property_name: bool, // boolean
74/// }
75/// ```
76///
77/// `property_name` must match the Slint property name. The macro calls
78/// `ui.on_<name>_changed(...)` and `ui.set_<name>(...)` via identifier
79/// concatenation.
80///
81/// # Example
82///
83/// ```ignore
84/// let ui = MyPluginUi::new().unwrap();
85/// truce_slint::bind! { state, ui,
86/// P::Gain => gain,
87/// P::Pan => pan,
88/// P::Bypass => bypass: bool,
89/// }
90/// ```
91#[macro_export]
92macro_rules! bind {
93 ($state:expr, $ui:expr, $( $id:expr => $name:ident $( : $ty:ident $(($arg:expr))? )? ),* $(,)?) => {{
94 $(
95 $crate::bind!(@wire $state, $ui, $id, $name $( : $ty $(($arg))? )?);
96 )*
97 let ui = $ui;
98 // Return type is inferred from the surrounding `SetupFn` -
99 // typically `SyncFn<P>` aka `Box<dyn Fn(&PluginContext<P>)>`.
100 Box::new(move |state: &$crate::PluginContext<_>| {
101 $(
102 $crate::bind!(@sync state, ui, $id, $name $( : $ty $(($arg))? )?);
103 )*
104 })
105 }};
106
107 // -- float (default) --
108 (@wire $state:expr, $ui:expr, $id:expr, $name:ident) => {
109 {
110 let s = $state.clone();
111 let id: u32 = $id.into();
112 $crate::paste! {
113 $ui.[<on_ $name _changed>](move |v| s.automate(id, v as f64));
114 }
115 }
116 };
117 (@sync $state:expr, $ui:expr, $id:expr, $name:ident) => {
118 $crate::paste! {
119 // `state.get_param` resolves through the user's
120 // prelude's `PluginContextReadF{32,64}` trait - could
121 // be either precision. `.to_f32()` narrows uniformly,
122 // matching slint's `f32`-typed property setter.
123 $ui.[<set_ $name>]($state.get_param($id.into()).to_f32());
124 }
125 };
126
127 // -- bool --
128 (@wire $state:expr, $ui:expr, $id:expr, $name:ident : bool) => {
129 {
130 let s = $state.clone();
131 let id: u32 = $id.into();
132 $crate::paste! {
133 $ui.[<on_ $name _changed>](move |v: bool| {
134 s.automate(id, if v { 1.0 } else { 0.0 });
135 });
136 }
137 }
138 };
139 (@sync $state:expr, $ui:expr, $id:expr, $name:ident : bool) => {
140 $crate::paste! {
141 $ui.[<set_ $name>]($state.get_param($id.into()) > 0.5);
142 }
143 };
144
145 // -- choice (integer index for ComboBox / enum params) --
146 //
147 // Binds an integer property (e.g. ComboBox `current-index`) to an enum
148 // param. `count` is the number of options.
149 //
150 // ```ignore
151 // truce_slint::bind! { state, ui,
152 // P::Mode => mode: choice(3),
153 // }
154 // ```
155 (@wire $state:expr, $ui:expr, $id:expr, $name:ident : choice($count:expr)) => {
156 {
157 let s = $state.clone();
158 let id: u32 = $id.into();
159 let count: u32 = $count;
160 $crate::paste! {
161 $ui.[<on_ $name _changed>](move |v: i32| {
162 let norm = $crate::truce_core::cast::discrete_norm(v.max(0) as usize, count as usize);
163 s.automate(id, norm);
164 });
165 }
166 }
167 };
168 (@sync $state:expr, $ui:expr, $id:expr, $name:ident : choice($count:expr)) => {
169 {
170 let count: u32 = $count;
171 // `discrete_index` takes `f64`; `.to_f64()` widens
172 // uniformly regardless of which prelude routed
173 // `get_param`.
174 let norm = $state.get_param($id.into()).to_f64();
175 let idx = $crate::truce_core::cast::discrete_index(norm, count as usize) as i32;
176 $crate::paste! {
177 $ui.[<set_ $name>](idx);
178 }
179 }
180 };
181}