Skip to main content

truce_gui/
layout.rs

1//! Simple layout helpers for positioning widgets.
2
3/// A widget definition in the layout.
4#[derive(Clone, Debug)]
5pub enum WidgetDef {
6    /// Rotary knob (default for continuous params).
7    Knob { param_id: u32, label: &'static str },
8    /// Horizontal slider.
9    Slider { param_id: u32, label: &'static str },
10    /// Toggle button (on/off).
11    Toggle { param_id: u32, label: &'static str },
12}
13
14impl WidgetDef {
15    pub fn param_id(&self) -> u32 {
16        match self {
17            WidgetDef::Knob { param_id, .. } => *param_id,
18            WidgetDef::Slider { param_id, .. } => *param_id,
19            WidgetDef::Toggle { param_id, .. } => *param_id,
20        }
21    }
22
23    pub fn label(&self) -> &'static str {
24        match self {
25            WidgetDef::Knob { label, .. } => label,
26            WidgetDef::Slider { label, .. } => label,
27            WidgetDef::Toggle { label, .. } => label,
28        }
29    }
30}
31
32/// A widget definition for the layout — either explicit type or auto-detected.
33#[derive(Clone, Debug)]
34pub struct KnobDef {
35    pub param_id: u32,
36    pub label: &'static str,
37    /// Explicit widget type override. None = auto-detect from param range.
38    pub widget: Option<WidgetKind>,
39    /// How many grid columns this widget spans. Default = 1.
40    pub span: u32,
41    /// Second parameter ID for XY pad (Y axis). Ignored for other widgets.
42    pub param_id_y: Option<u32>,
43    /// Multiple meter IDs for multi-channel level meter. Ignored for other widgets.
44    pub meter_ids: Option<Vec<u32>>,
45}
46
47/// Explicit widget type for layout overrides.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum WidgetKind {
50    Knob,
51    Slider,
52    Toggle,
53    Selector,
54    /// Level meter. Shows one bar per meter ID. Supports mono, stereo, or multi-channel.
55    Meter,
56    /// XY pad. Controls two params — X param stored in `param_id`, Y param in `xy_param_y`.
57    XYPad,
58}
59
60impl KnobDef {
61    /// Knob (default for continuous params, auto-detected anyway).
62    pub fn knob(param_id: impl Into<u32>, label: &'static str) -> Self {
63        Self { param_id: param_id.into(), label, widget: Some(WidgetKind::Knob), span: 1, param_id_y: None, meter_ids: None }
64    }
65
66    /// Horizontal slider.
67    pub fn slider(param_id: impl Into<u32>, label: &'static str) -> Self {
68        Self { param_id: param_id.into(), label, widget: Some(WidgetKind::Slider), span: 1, param_id_y: None, meter_ids: None }
69    }
70
71    /// Toggle button.
72    pub fn toggle(param_id: impl Into<u32>, label: &'static str) -> Self {
73        Self { param_id: param_id.into(), label, widget: Some(WidgetKind::Toggle), span: 1, param_id_y: None, meter_ids: None }
74    }
75
76    /// Selector (click-to-cycle for enum params).
77    pub fn selector(param_id: impl Into<u32>, label: &'static str) -> Self {
78        Self { param_id: param_id.into(), label, widget: Some(WidgetKind::Selector), span: 1, param_id_y: None, meter_ids: None }
79    }
80
81    /// Level meter with one or more channels (display-only, reads from Plugin::get_meter()).
82    pub fn meter(ids: &[u32], label: &'static str) -> Self {
83        Self {
84            param_id: ids.first().copied().unwrap_or(0),
85            label,
86            widget: Some(WidgetKind::Meter),
87            span: 1,
88            param_id_y: None,
89            meter_ids: Some(ids.to_vec()),
90        }
91    }
92
93    /// XY pad controlling two parameters.
94    pub fn xy_pad(param_x: impl Into<u32>, param_y: impl Into<u32>, label: &'static str) -> Self {
95        Self { param_id: param_x.into(), label, widget: Some(WidgetKind::XYPad), span: 2, param_id_y: Some(param_y.into()), meter_ids: None }
96    }
97
98    /// Set the column span for this widget (default 1).
99    pub fn with_span(mut self, span: u32) -> Self {
100        self.span = span;
101        self
102    }
103}
104
105/// A row of widgets with an optional section label.
106#[derive(Clone, Debug)]
107pub struct KnobRow {
108    pub label: Option<&'static str>,
109    pub knobs: Vec<KnobDef>,
110}
111
112/// Layout configuration for a plugin UI.
113#[derive(Clone, Debug)]
114pub struct PluginLayout {
115    pub title: &'static str,
116    pub version: &'static str,
117    pub rows: Vec<KnobRow>,
118    pub width: u32,
119    pub height: u32,
120    pub knob_size: f32,
121}
122
123impl PluginLayout {
124    /// Calculate default window size based on the layout.
125    pub fn compute_size(rows: &[KnobRow], knob_size: f32) -> (u32, u32) {
126        let header_h = 30.0;
127        let row_h = knob_size + 30.0;
128        let section_label_h = 18.0;
129        let padding = 10.0;
130
131        let max_knobs = rows.iter()
132            .map(|r| r.knobs.iter().map(|k| k.span.max(1) as usize).sum::<usize>())
133            .max().unwrap_or(1);
134        let w = (max_knobs as f32 * (knob_size + 10.0) + 20.0).max(300.0);
135
136        let mut h = header_h + padding;
137        for row in rows {
138            if row.label.is_some() {
139                h += section_label_h;
140            }
141            h += row_h + padding;
142        }
143
144        (w as u32, h as u32)
145    }
146
147    /// Calculate default window size and return a PluginLayout.
148    pub fn build(
149        title: &'static str,
150        version: &'static str,
151        rows: Vec<KnobRow>,
152        knob_size: f32,
153    ) -> Self {
154        let (w, h) = Self::compute_size(&rows, knob_size);
155        Self {
156            title,
157            version,
158            rows,
159            width: w,
160            height: h,
161            knob_size,
162        }
163    }
164}
165
166// ---------------------------------------------------------------------------
167// Grid Layout
168// ---------------------------------------------------------------------------
169
170/// Sentinel value for auto-placed grid widgets.
171pub const AUTO: u32 = u32::MAX;
172
173// Grid spacing constants.
174pub const GRID_GAP: f32 = 30.0;
175pub const GRID_PADDING: f32 = 15.0;
176pub const GRID_HEADER_H: f32 = 30.0;
177pub const GRID_SECTION_H: f32 = 18.0;
178
179/// A widget placed in a grid layout.
180#[derive(Clone, Debug)]
181pub struct GridWidget {
182    /// Grid column (0-indexed, or AUTO for auto-flow).
183    pub col: u32,
184    /// Grid row (0-indexed, or AUTO for auto-flow).
185    pub row: u32,
186    /// Columns spanned (default 1).
187    pub col_span: u32,
188    /// Rows spanned (default 1).
189    pub row_span: u32,
190    /// Parameter ID (or first meter ID for meters).
191    pub param_id: u32,
192    /// Display label.
193    pub label: &'static str,
194    /// Widget type override. None = auto-detect from param range.
195    pub widget: Option<WidgetKind>,
196    /// Second param for XY pad (Y axis).
197    pub param_id_y: Option<u32>,
198    /// Multiple meter IDs for multi-channel level meter.
199    pub meter_ids: Option<Vec<u32>>,
200}
201
202impl GridWidget {
203    pub fn knob(param_id: impl Into<u32>, label: &'static str) -> Self {
204        Self {
205            col: AUTO, row: AUTO, col_span: 1, row_span: 1,
206            param_id: param_id.into(), label, widget: Some(WidgetKind::Knob),
207            param_id_y: None, meter_ids: None,
208        }
209    }
210
211    pub fn slider(param_id: impl Into<u32>, label: &'static str) -> Self {
212        Self {
213            col: AUTO, row: AUTO, col_span: 1, row_span: 1,
214            param_id: param_id.into(), label, widget: Some(WidgetKind::Slider),
215            param_id_y: None, meter_ids: None,
216        }
217    }
218
219    pub fn toggle(param_id: impl Into<u32>, label: &'static str) -> Self {
220        Self {
221            col: AUTO, row: AUTO, col_span: 1, row_span: 1,
222            param_id: param_id.into(), label, widget: Some(WidgetKind::Toggle),
223            param_id_y: None, meter_ids: None,
224        }
225    }
226
227    pub fn selector(param_id: impl Into<u32>, label: &'static str) -> Self {
228        Self {
229            col: AUTO, row: AUTO, col_span: 1, row_span: 1,
230            param_id: param_id.into(), label, widget: Some(WidgetKind::Selector),
231            param_id_y: None, meter_ids: None,
232        }
233    }
234
235    pub fn meter(ids: &[u32], label: &'static str) -> Self {
236        Self {
237            col: AUTO, row: AUTO, col_span: 1, row_span: 1,
238            param_id: ids.first().copied().unwrap_or(0), label,
239            widget: Some(WidgetKind::Meter),
240            param_id_y: None, meter_ids: Some(ids.to_vec()),
241        }
242    }
243
244    pub fn xy_pad(param_x: impl Into<u32>, param_y: impl Into<u32>, label: &'static str) -> Self {
245        Self {
246            col: AUTO, row: AUTO, col_span: 2, row_span: 2,
247            param_id: param_x.into(), label, widget: Some(WidgetKind::XYPad),
248            param_id_y: Some(param_y.into()), meter_ids: None,
249        }
250    }
251
252    /// Set the column span.
253    pub fn cols(mut self, n: u32) -> Self {
254        self.col_span = n;
255        self
256    }
257
258    /// Set the row span.
259    pub fn rows(mut self, n: u32) -> Self {
260        self.row_span = n;
261        self
262    }
263
264    /// Set explicit grid position (overrides auto-flow for this widget).
265    pub fn at(mut self, col: u32, row: u32) -> Self {
266        self.col = col;
267        self.row = row;
268        self
269    }
270}
271
272/// Grid-based layout for a plugin UI.
273#[derive(Clone, Debug)]
274pub struct GridLayout {
275    pub title: &'static str,
276    pub version: &'static str,
277    /// Number of columns in the grid.
278    pub cols: u32,
279    /// Section labels positioned above specific rows: (row_index, label).
280    pub sections: Vec<(u32, &'static str)>,
281    /// All widgets placed in the grid.
282    pub widgets: Vec<GridWidget>,
283    /// Cell size in pixels (width and height of one grid cell).
284    pub cell_size: f32,
285    /// Computed pixel width.
286    pub width: u32,
287    /// Computed pixel height.
288    pub height: u32,
289}
290
291impl GridLayout {
292    /// Build a grid layout from auto-flow widgets and section breaks.
293    ///
294    /// `breaks` maps widget indices to section labels: when auto-flow reaches
295    /// that index, it starts a new row and records the section label above it.
296    pub fn build(
297        title: &'static str,
298        version: &'static str,
299        cols: u32,
300        cell_size: f32,
301        widgets: Vec<GridWidget>,
302        breaks: Vec<(usize, &'static str)>,
303    ) -> Self {
304        let mut layout = Self {
305            title, version, cols,
306            sections: Vec::new(),
307            widgets, cell_size,
308            width: 0, height: 0,
309        };
310        layout.auto_flow_with_breaks(&breaks);
311        let (w, h) = layout.compute_size();
312        layout.width = w;
313        layout.height = h;
314        layout
315    }
316
317    /// Compute the window size from the grid.
318    pub fn compute_size(&self) -> (u32, u32) {
319        let max_row = self.widgets.iter()
320            .map(|w| w.row + w.row_span)
321            .max().unwrap_or(1);
322        let section_count = self.sections.len() as f32;
323
324        let w = GRID_PADDING * 2.0 + self.cols as f32 * (self.cell_size + GRID_GAP) - GRID_GAP;
325        let bottom_label_h = 28.0; // label + value text below the last row of widgets
326        let h = GRID_HEADER_H + GRID_PADDING
327            + max_row as f32 * (self.cell_size + GRID_GAP) - GRID_GAP
328            + section_count * GRID_SECTION_H
329            + bottom_label_h + GRID_PADDING;
330
331        (w.max(300.0) as u32, h as u32)
332    }
333
334    /// Auto-flow placement without section breaks.
335    pub fn auto_flow(&mut self) {
336        self.auto_flow_with_breaks(&[]);
337    }
338
339    /// Auto-flow placement with section breaks.
340    ///
341    /// Each break is `(widget_index, label)`: when the cursor reaches that
342    /// widget index, it advances to the next row and records a section label.
343    pub fn auto_flow_with_breaks(&mut self, breaks: &[(usize, &'static str)]) {
344        let mut occupied = std::collections::HashSet::new();
345        let mut cursor_col: u32 = 0;
346        let mut cursor_row: u32 = 0;
347        let mut any_emitted = false;
348
349        // First pass: mark cells occupied by explicitly-placed widgets.
350        for w in &self.widgets {
351            if w.col != AUTO && w.row != AUTO {
352                for c in w.col..w.col + w.col_span {
353                    for r in w.row..w.row + w.row_span {
354                        occupied.insert((c, r));
355                    }
356                }
357            }
358        }
359
360        // Second pass: auto-place widgets.
361        for (i, w) in self.widgets.iter_mut().enumerate() {
362            // Check for section breaks at this widget index.
363            for &(break_idx, label) in breaks {
364                if break_idx == i {
365                    if any_emitted || cursor_col > 0 {
366                        cursor_row += 1;
367                        cursor_col = 0;
368                    }
369                    self.sections.push((cursor_row, label));
370                    any_emitted = true;
371                }
372            }
373
374            if w.col != AUTO && w.row != AUTO {
375                // Explicitly placed — already marked in first pass.
376                any_emitted = true;
377                continue;
378            }
379
380            // Find next free position that fits this widget.
381            loop {
382                if cursor_col + w.col_span > self.cols {
383                    cursor_col = 0;
384                    cursor_row += 1;
385                }
386                let fits = (0..w.col_span).all(|dc|
387                    (0..w.row_span).all(|dr|
388                        !occupied.contains(&(cursor_col + dc, cursor_row + dr))
389                    )
390                );
391                if fits { break; }
392                cursor_col += 1;
393            }
394
395            w.col = cursor_col;
396            w.row = cursor_row;
397
398            for c in w.col..w.col + w.col_span {
399                for r in w.row..w.row + w.row_span {
400                    occupied.insert((c, r));
401                }
402            }
403
404            cursor_col += w.col_span;
405            any_emitted = true;
406        }
407    }
408}
409
410/// Compute cumulative section-label pixel offsets per row.
411///
412/// `offsets[r]` is the total vertical shift (from section labels) for row `r`.
413pub fn compute_section_offsets(layout: &GridLayout) -> Vec<f32> {
414    let max_row = layout.widgets.iter()
415        .map(|w| w.row + w.row_span)
416        .max().unwrap_or(1);
417    let mut offsets = vec![0.0f32; max_row as usize + 1];
418    let mut cumulative = 0.0;
419
420    for row in 0..=max_row {
421        if layout.sections.iter().any(|(r, _)| *r == row) {
422            cumulative += GRID_SECTION_H;
423        }
424        if (row as usize) < offsets.len() {
425            offsets[row as usize] = cumulative;
426        }
427    }
428    offsets
429}
430
431impl From<PluginLayout> for GridLayout {
432    fn from(pl: PluginLayout) -> Self {
433        let cols = pl.rows.iter()
434            .map(|r| r.knobs.iter().map(|k| k.span.max(1)).sum::<u32>())
435            .max().unwrap_or(1);
436
437        let mut widgets = Vec::new();
438        let mut sections = Vec::new();
439        let mut grid_row = 0u32;
440
441        for row in &pl.rows {
442            if let Some(label) = row.label {
443                sections.push((grid_row, label));
444            }
445            let mut col = 0u32;
446            for knob in &row.knobs {
447                widgets.push(GridWidget {
448                    col, row: grid_row,
449                    col_span: knob.span.max(1), row_span: 1,
450                    param_id: knob.param_id,
451                    label: knob.label,
452                    widget: knob.widget,
453                    param_id_y: knob.param_id_y,
454                    meter_ids: knob.meter_ids.clone(),
455                });
456                col += knob.span.max(1);
457            }
458            grid_row += 1;
459        }
460
461        let mut gl = GridLayout {
462            title: pl.title, version: pl.version,
463            cols, sections, widgets, cell_size: pl.knob_size,
464            width: 0, height: 0,
465        };
466        let (w, h) = gl.compute_size();
467        gl.width = w;
468        gl.height = h;
469        gl
470    }
471}
472
473/// Layout variant for editor dispatch.
474#[derive(Clone, Debug)]
475pub enum Layout {
476    Rows(PluginLayout),
477    Grid(GridLayout),
478}
479
480impl Layout {
481    pub fn width(&self) -> u32 {
482        match self { Layout::Rows(l) => l.width, Layout::Grid(g) => g.width }
483    }
484    pub fn height(&self) -> u32 {
485        match self { Layout::Rows(l) => l.height, Layout::Grid(g) => g.height }
486    }
487    pub fn title(&self) -> &str {
488        match self { Layout::Rows(l) => l.title, Layout::Grid(g) => g.title }
489    }
490    pub fn version(&self) -> &str {
491        match self { Layout::Rows(l) => l.version, Layout::Grid(g) => g.version }
492    }
493}