Skip to main content

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}