Skip to main content

truce_gui_types/
macros.rs

1/// Declarative layout DSL for plugin GUIs.
2///
3/// # Example
4/// ```ignore
5/// use truce_gui_types::layout;
6///
7/// fn gui_layout() -> truce_gui_types::layout::PluginLayout {
8///     layout!("MY PLUGIN", "V1.0", 50.0, {
9///         row {
10///             knob(ID_GAIN, "Gain")
11///             slider(ID_PAN, "Pan")
12///             toggle(ID_BYPASS, "Bypass")
13///             meter(&[METER_L, METER_R], "Level")
14///         }
15///         section("FILTER") {
16///             knob(ID_CUTOFF, "Cutoff")
17///             knob(ID_RESO, "Reso")
18///         }
19///     })
20/// }
21/// ```
22#[macro_export]
23macro_rules! layout {
24    ($title:expr, $subtitle:expr, $knob_size:expr, { $($body:tt)* }) => {{
25        let rows = $crate::__layout_rows!( [] $($body)* );
26        $crate::layout::PluginLayout::build(
27            $crate::layout::HeaderTitles::pair($title, $subtitle),
28            rows,
29            $knob_size,
30        )
31    }};
32}
33
34#[macro_export]
35#[doc(hidden)]
36macro_rules! __layout_rows {
37    ( [ $($rows:expr),* ] ) => {
38        vec![ $($rows),* ]
39    };
40
41    ( [ $($rows:expr),* ] row { $($widgets:tt)* } $($rest:tt)* ) => {
42        $crate::__layout_rows!(
43            [ $($rows,)* $crate::layout::KnobRow {
44                label: None,
45                knobs: $crate::__layout_widgets!( [] $($widgets)* ),
46            } ]
47            $($rest)*
48        )
49    };
50
51    ( [ $($rows:expr),* ] section($label:expr) { $($widgets:tt)* } $($rest:tt)* ) => {
52        $crate::__layout_rows!(
53            [ $($rows,)* $crate::layout::KnobRow {
54                label: Some($label),
55                knobs: $crate::__layout_widgets!( [] $($widgets)* ),
56            } ]
57            $($rest)*
58        )
59    };
60}
61
62#[macro_export]
63#[doc(hidden)]
64macro_rules! __layout_widgets {
65    // Done
66    ( [ $($w:expr),* ] ) => {
67        vec![ $($w),* ]
68    };
69
70    // --- .span(N) variants MUST come before plain variants ---
71
72    ( [ $($w:expr),* ] knob($id:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
73        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::knob($id, $label).with_span($n) ] $($rest)* )
74    };
75    ( [ $($w:expr),* ] slider($id:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
76        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::slider($id, $label).with_span($n) ] $($rest)* )
77    };
78    ( [ $($w:expr),* ] toggle($id:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
79        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::toggle($id, $label).with_span($n) ] $($rest)* )
80    };
81    ( [ $($w:expr),* ] selector($id:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
82        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::selector($id, $label).with_span($n) ] $($rest)* )
83    };
84    ( [ $($w:expr),* ] meter($ids:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
85        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::meter($ids, $label).with_span($n) ] $($rest)* )
86    };
87    ( [ $($w:expr),* ] xy_pad($x:expr, $y:expr, $label:expr) .span($n:expr) $($rest:tt)* ) => {
88        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::xy_pad($x, $y, $label).with_span($n) ] $($rest)* )
89    };
90
91    // --- Plain variants (no .span) ---
92
93    ( [ $($w:expr),* ] knob($id:expr, $label:expr) $($rest:tt)* ) => {
94        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::knob($id, $label) ] $($rest)* )
95    };
96    ( [ $($w:expr),* ] slider($id:expr, $label:expr) $($rest:tt)* ) => {
97        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::slider($id, $label) ] $($rest)* )
98    };
99    ( [ $($w:expr),* ] toggle($id:expr, $label:expr) $($rest:tt)* ) => {
100        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::toggle($id, $label) ] $($rest)* )
101    };
102    ( [ $($w:expr),* ] selector($id:expr, $label:expr) $($rest:tt)* ) => {
103        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::selector($id, $label) ] $($rest)* )
104    };
105    ( [ $($w:expr),* ] meter($ids:expr, $label:expr) $($rest:tt)* ) => {
106        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::meter($ids, $label) ] $($rest)* )
107    };
108    ( [ $($w:expr),* ] xy_pad($x:expr, $y:expr, $label:expr) $($rest:tt)* ) => {
109        $crate::__layout_widgets!( [ $($w,)* $crate::layout::KnobDef::xy_pad($x, $y, $label) ] $($rest)* )
110    };
111}
112
113// ---------------------------------------------------------------------------
114// Grid layout DSL
115// ---------------------------------------------------------------------------
116
117/// Declarative grid layout DSL for plugin GUIs.
118///
119/// Defaults: no header, `cols` = max widgets per section,
120/// `cell_size` = `GRID_DEFAULT_CELL_SIZE`. Override any of those
121/// via the `cols:` / `cell:` keyword args, or set the header band
122/// with `title: "..."` and / or `subtitle: "..."` (each is
123/// independently optional).
124///
125/// # Example
126/// ```ignore
127/// use truce_gui_types::grid;
128///
129/// // Minimal - auto-cols, default cell size, no header.
130/// fn gui_layout() -> truce_gui_types::layout::GridLayout {
131///     grid!({
132///         knob(ID_GAIN, "Gain")
133///         slider(ID_PAN, "Pan")
134///     })
135/// }
136///
137/// // Force wrapping: 4 widgets on a 2-col grid.
138/// fn wrapped() -> truce_gui_types::layout::GridLayout {
139///     grid!(cols: 2, {
140///         knob(ID_GAIN, "Gain")
141///         slider(ID_PAN, "Pan")
142///         toggle(ID_BYPASS, "Bypass")
143///         meter(&[METER_L, METER_R], "Level")
144///     })
145/// }
146///
147/// // Header - title + subtitle, or either one alone.
148/// fn with_header() -> truce_gui_types::layout::GridLayout {
149///     grid!(title: "MY PLUGIN", subtitle: "V1.0", cols: 4, cell: 50.0, {
150///         knob(ID_GAIN, "Gain")
151///     })
152/// }
153///
154/// fn title_only() -> truce_gui_types::layout::GridLayout {
155///     grid!(title: "MY PLUGIN", { knob(ID_GAIN, "Gain") })
156/// }
157/// ```
158#[macro_export]
159macro_rules! grid {
160    // Minimal: just the body.
161    ({ $($body:tt)* }) => {{
162        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
163    }};
164    // cols only.
165    (cols: $cols:expr, { $($body:tt)* }) => {{
166        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
167            .with_cols($cols)
168    }};
169    // cell only.
170    (cell: $cell:expr, { $($body:tt)* }) => {{
171        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
172            .with_cell_size($cell)
173    }};
174    // cols + cell.
175    (cols: $cols:expr, cell: $cell:expr, { $($body:tt)* }) => {{
176        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
177            .with_grid($cols, $cell)
178    }};
179
180    // --- Header arms - title and subtitle both optional ---
181
182    // title + subtitle + body.
183    (title: $title:expr, subtitle: $subtitle:expr, { $($body:tt)* }) => {{
184        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
185            .with_titles($crate::layout::HeaderTitles::pair($title, $subtitle))
186    }};
187    // title + subtitle + cols.
188    (title: $title:expr, subtitle: $subtitle:expr, cols: $cols:expr, { $($body:tt)* }) => {{
189        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
190            .with_cols($cols)
191            .with_titles($crate::layout::HeaderTitles::pair($title, $subtitle))
192    }};
193    // title + subtitle + cell.
194    (title: $title:expr, subtitle: $subtitle:expr, cell: $cell:expr, { $($body:tt)* }) => {{
195        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
196            .with_cell_size($cell)
197            .with_titles($crate::layout::HeaderTitles::pair($title, $subtitle))
198    }};
199    // title + subtitle + cols + cell - full form.
200    (title: $title:expr, subtitle: $subtitle:expr, cols: $cols:expr, cell: $cell:expr, { $($body:tt)* }) => {{
201        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
202            .with_grid($cols, $cell)
203            .with_titles($crate::layout::HeaderTitles::pair($title, $subtitle))
204    }};
205
206    // title only - any combination of cols / cell, body required.
207    (title: $title:expr, { $($body:tt)* }) => {{
208        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
209            .with_title($title)
210    }};
211    (title: $title:expr, cols: $cols:expr, { $($body:tt)* }) => {{
212        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
213            .with_cols($cols)
214            .with_title($title)
215    }};
216    (title: $title:expr, cell: $cell:expr, { $($body:tt)* }) => {{
217        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
218            .with_cell_size($cell)
219            .with_title($title)
220    }};
221    (title: $title:expr, cols: $cols:expr, cell: $cell:expr, { $($body:tt)* }) => {{
222        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
223            .with_grid($cols, $cell)
224            .with_title($title)
225    }};
226
227    // subtitle only - same shape.
228    (subtitle: $subtitle:expr, { $($body:tt)* }) => {{
229        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
230            .with_subtitle($subtitle)
231    }};
232    (subtitle: $subtitle:expr, cols: $cols:expr, { $($body:tt)* }) => {{
233        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
234            .with_cols($cols)
235            .with_subtitle($subtitle)
236    }};
237    (subtitle: $subtitle:expr, cell: $cell:expr, { $($body:tt)* }) => {{
238        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
239            .with_cell_size($cell)
240            .with_subtitle($subtitle)
241    }};
242    (subtitle: $subtitle:expr, cols: $cols:expr, cell: $cell:expr, { $($body:tt)* }) => {{
243        $crate::layout::GridLayout::build($crate::__grid_sections!($($body)*))
244            .with_grid($cols, $cell)
245            .with_subtitle($subtitle)
246    }};
247}
248
249#[macro_export]
250#[doc(hidden)]
251macro_rules! __grid_sections {
252    ($($body:tt)*) => {{
253        let mut _widgets: Vec<$crate::layout::GridWidget> = Vec::new();
254        let mut _breaks: Vec<(usize, &'static str)> = Vec::new();
255        $crate::__grid_items!(_widgets, _breaks, $($body)*);
256        // Convert flat widgets + breaks into Section vec for build()
257        let mut _sections: Vec<$crate::layout::Section> = Vec::new();
258        let mut _cur_widgets: Vec<$crate::layout::GridWidget> = Vec::new();
259        let mut _cur_label: Option<&'static str> = None;
260        for (i, w) in _widgets.into_iter().enumerate() {
261            if let Some(&(_, label)) = _breaks.iter().find(|(idx, _)| *idx == i) {
262                if !_cur_widgets.is_empty() || _cur_label.is_some() {
263                    _sections.push($crate::layout::Section {
264                        label: _cur_label,
265                        widgets: std::mem::take(&mut _cur_widgets),
266                    });
267                }
268                _cur_label = Some(label);
269            }
270            _cur_widgets.push(w);
271        }
272        if !_cur_widgets.is_empty() || _cur_label.is_some() {
273            _sections.push($crate::layout::Section {
274                label: _cur_label,
275                widgets: _cur_widgets,
276            });
277        }
278        _sections
279    }};
280}
281
282#[macro_export]
283#[doc(hidden)]
284macro_rules! __grid_items {
285    // Base cases
286    ($w:ident, $b:ident) => {};
287    ($w:ident, $b:ident,) => {};
288
289    // Section break
290    ($w:ident, $b:ident, section($label:expr) $($rest:tt)*) => {
291        $b.push(($w.len(), $label));
292        $crate::__grid_items!($w, $b, $($rest)*);
293    };
294
295    // Widget types - dispatch to modifier parser
296    ($w:ident, $b:ident, knob($id:expr, $label:expr) $($rest:tt)*) => {
297        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::knob($id, $label), $($rest)*);
298    };
299    ($w:ident, $b:ident, slider($id:expr, $label:expr) $($rest:tt)*) => {
300        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::slider($id, $label), $($rest)*);
301    };
302    ($w:ident, $b:ident, toggle($id:expr, $label:expr) $($rest:tt)*) => {
303        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::toggle($id, $label), $($rest)*);
304    };
305    ($w:ident, $b:ident, selector($id:expr, $label:expr) $($rest:tt)*) => {
306        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::selector($id, $label), $($rest)*);
307    };
308    ($w:ident, $b:ident, meter($ids:expr, $label:expr) $($rest:tt)*) => {
309        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::meter($ids, $label), $($rest)*);
310    };
311    ($w:ident, $b:ident, xy_pad($x:expr, $y:expr, $label:expr) $($rest:tt)*) => {
312        $crate::__grid_mods!($w, $b, $crate::layout::GridWidget::xy_pad($x, $y, $label), $($rest)*);
313    };
314}
315
316#[macro_export]
317#[doc(hidden)]
318macro_rules! __grid_mods {
319    // .cols(N).rows(M)
320    ($w:ident, $b:ident, $widget:expr, .cols($c:expr) .rows($r:expr) $($rest:tt)*) => {
321        $w.push($widget.cols($c).rows($r));
322        $crate::__grid_items!($w, $b, $($rest)*);
323    };
324    // .rows(M).cols(N)
325    ($w:ident, $b:ident, $widget:expr, .rows($r:expr) .cols($c:expr) $($rest:tt)*) => {
326        $w.push($widget.cols($c).rows($r));
327        $crate::__grid_items!($w, $b, $($rest)*);
328    };
329    // .cols(N) only
330    ($w:ident, $b:ident, $widget:expr, .cols($c:expr) $($rest:tt)*) => {
331        $w.push($widget.cols($c));
332        $crate::__grid_items!($w, $b, $($rest)*);
333    };
334    // .rows(M) only
335    ($w:ident, $b:ident, $widget:expr, .rows($r:expr) $($rest:tt)*) => {
336        $w.push($widget.rows($r));
337        $crate::__grid_items!($w, $b, $($rest)*);
338    };
339    // No modifiers
340    ($w:ident, $b:ident, $widget:expr, $($rest:tt)*) => {
341        $w.push($widget);
342        $crate::__grid_items!($w, $b, $($rest)*);
343    };
344}