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