Skip to main content

w_gui/
window.rs

1use std::ops::RangeInclusive;
2use std::sync::Arc;
3
4use crate::context::Context;
5use crate::element::{AccentColor, ElementDecl, ElementKind, ElementMeta, PlotSeries, Value};
6
7/// Response from a widget interaction.
8pub struct Response {
9    clicked: bool,
10    changed: bool,
11}
12
13impl Response {
14    /// Returns `true` whenever the widget was interacted with since the last update.
15    pub fn clicked(&self) -> bool {
16        self.clicked
17    }
18
19    /// Returns `true` only if the widget's value actually changed since the last update.
20    pub fn changed(&self) -> bool {
21        self.changed
22    }
23}
24
25// ── Element declaration builders ─────────────────────────────────────
26// Shared by Window and Grid to avoid duplicating ElementDecl construction.
27
28fn build_slider(
29    id: String,
30    window: Arc<str>,
31    label: &str,
32    value: f32,
33    range: &RangeInclusive<f32>,
34) -> ElementDecl {
35    ElementDecl {
36        id,
37        kind: ElementKind::Slider,
38        label: label.to_string(),
39        value: Value::Float(value as f64),
40        meta: ElementMeta {
41            min: Some(*range.start() as f64),
42            max: Some(*range.end() as f64),
43            step: Some(0.01),
44            ..Default::default()
45        },
46        window,
47    }
48}
49
50fn build_slider_int(
51    id: String,
52    window: Arc<str>,
53    label: &str,
54    value: i32,
55    range: &RangeInclusive<i32>,
56) -> ElementDecl {
57    ElementDecl {
58        id,
59        kind: ElementKind::Slider,
60        label: label.to_string(),
61        value: Value::Int(value as i64),
62        meta: ElementMeta {
63            min: Some(*range.start() as f64),
64            max: Some(*range.end() as f64),
65            step: Some(1.0),
66            ..Default::default()
67        },
68        window,
69    }
70}
71
72fn build_checkbox(id: String, window: Arc<str>, label: &str, value: bool) -> ElementDecl {
73    ElementDecl {
74        id,
75        kind: ElementKind::Checkbox,
76        label: label.to_string(),
77        value: Value::Bool(value),
78        meta: ElementMeta::default(),
79        window,
80    }
81}
82
83fn build_color3(id: String, window: Arc<str>, label: &str, value: [f32; 3]) -> ElementDecl {
84    ElementDecl {
85        id,
86        kind: ElementKind::ColorPicker3,
87        label: label.to_string(),
88        value: Value::Color3(value),
89        meta: ElementMeta::default(),
90        window,
91    }
92}
93
94fn build_color4(id: String, window: Arc<str>, label: &str, value: [f32; 4]) -> ElementDecl {
95    ElementDecl {
96        id,
97        kind: ElementKind::ColorPicker4,
98        label: label.to_string(),
99        value: Value::Color4(value),
100        meta: ElementMeta::default(),
101        window,
102    }
103}
104
105fn build_text_input(id: String, window: Arc<str>, label: &str, value: &str) -> ElementDecl {
106    ElementDecl {
107        id,
108        kind: ElementKind::TextInput,
109        label: label.to_string(),
110        value: Value::String(value.to_string()),
111        meta: ElementMeta::default(),
112        window,
113    }
114}
115
116fn build_dropdown(
117    id: String,
118    window: Arc<str>,
119    label: &str,
120    selected: usize,
121    options: &[&str],
122) -> ElementDecl {
123    ElementDecl {
124        id,
125        kind: ElementKind::Dropdown,
126        label: label.to_string(),
127        value: Value::Enum {
128            selected,
129            options: options.iter().map(|s| s.to_string()).collect(),
130        },
131        meta: ElementMeta::default(),
132        window,
133    }
134}
135
136fn build_button(id: String, window: Arc<str>, label: &str) -> ElementDecl {
137    ElementDecl {
138        id,
139        kind: ElementKind::Button,
140        label: label.to_string(),
141        value: Value::Button(false),
142        meta: ElementMeta::default(),
143        window,
144    }
145}
146
147fn build_label(id: String, window: Arc<str>, text: &str) -> ElementDecl {
148    ElementDecl {
149        id,
150        kind: ElementKind::Label,
151        label: text.to_string(),
152        value: Value::String(text.to_string()),
153        meta: ElementMeta::default(),
154        window,
155    }
156}
157
158fn build_progress_bar(
159    id: String,
160    window: Arc<str>,
161    label: &str,
162    value: f64,
163    accent: AccentColor,
164    subtitle: Option<&str>,
165) -> ElementDecl {
166    ElementDecl {
167        id,
168        kind: ElementKind::ProgressBar,
169        label: label.to_string(),
170        value: Value::Progress(value.clamp(0.0, 1.0)),
171        meta: ElementMeta {
172            accent: Some(accent.as_str().to_string()),
173            subtitle: subtitle.map(|s| s.to_string()),
174            ..Default::default()
175        },
176        window,
177    }
178}
179
180fn build_stat(
181    id: String,
182    window: Arc<str>,
183    label: &str,
184    value: &str,
185    subvalue: Option<&str>,
186    accent: AccentColor,
187) -> ElementDecl {
188    ElementDecl {
189        id,
190        kind: ElementKind::Stat,
191        label: label.to_string(),
192        value: Value::StatValue {
193            value: value.to_string(),
194            subvalue: subvalue.map(|s| s.to_string()),
195        },
196        meta: ElementMeta {
197            accent: Some(accent.as_str().to_string()),
198            ..Default::default()
199        },
200        window,
201    }
202}
203
204fn build_status(
205    id: String,
206    window: Arc<str>,
207    label: &str,
208    active: bool,
209    active_text: Option<&str>,
210    inactive_text: Option<&str>,
211    active_color: AccentColor,
212    inactive_color: AccentColor,
213) -> ElementDecl {
214    ElementDecl {
215        id,
216        kind: ElementKind::Status,
217        label: label.to_string(),
218        value: Value::StatusValue {
219            active,
220            active_text: active_text.map(|s| s.to_string()),
221            inactive_text: inactive_text.map(|s| s.to_string()),
222            active_color: Some(active_color.as_str().to_string()),
223            inactive_color: Some(inactive_color.as_str().to_string()),
224        },
225        meta: ElementMeta::default(),
226        window,
227    }
228}
229
230fn build_mini_chart(
231    id: String,
232    window: Arc<str>,
233    label: &str,
234    values: &[f32],
235    unit: Option<&str>,
236    accent: AccentColor,
237) -> ElementDecl {
238    ElementDecl {
239        id,
240        kind: ElementKind::MiniChart,
241        label: label.to_string(),
242        value: Value::ChartValue {
243            values: values.to_vec(),
244            current: values.last().copied(),
245            unit: unit.map(|s| s.to_string()),
246        },
247        meta: ElementMeta {
248            accent: Some(accent.as_str().to_string()),
249            ..Default::default()
250        },
251        window,
252    }
253}
254
255fn build_plot(
256    id: String,
257    window: Arc<str>,
258    label: &str,
259    series: &[(&str, &[f32], AccentColor)],
260    x_label: Option<&str>,
261    y_label: Option<&str>,
262) -> ElementDecl {
263    let plot_series: Vec<PlotSeries> = series
264        .iter()
265        .map(|(name, values, color)| PlotSeries {
266            name: name.to_string(),
267            values: values.to_vec(),
268            color: color.as_str().to_string(),
269        })
270        .collect();
271
272    ElementDecl {
273        id,
274        kind: ElementKind::Plot,
275        label: label.to_string(),
276        value: Value::PlotValue {
277            series: plot_series,
278            x_label: x_label.map(|s| s.to_string()),
279            y_label: y_label.map(|s| s.to_string()),
280        },
281        meta: ElementMeta::default(),
282        window,
283    }
284}
285
286// ── Window ───────────────────────────────────────────────────────────
287
288/// A named window containing UI elements. Created via `Context::window()`.
289pub struct Window<'a> {
290    name: Arc<str>,
291    ctx: &'a mut Context,
292}
293
294impl<'a> Window<'a> {
295    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
296        Self {
297            name: Arc::from(name.as_str()),
298            ctx,
299        }
300    }
301
302    fn make_id(&self, label: &str) -> String {
303        format!("{}::{}", self.name, label)
304    }
305
306    /// A floating-point slider with a range.
307    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
308        let id = self.make_id(label);
309        let (clicked, changed) = if let Some(Value::Float(v)) = self.ctx.consume_edit(&id) {
310            let new = v as f32;
311            let changed = *value != new;
312            *value = new;
313            (true, changed)
314        } else {
315            (false, false)
316        };
317        self.ctx
318            .declare(build_slider(id, self.name.clone(), label, *value, &range));
319        Response { clicked, changed }
320    }
321
322    /// An integer slider with a range.
323    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
324        let id = self.make_id(label);
325        let (clicked, changed) = if let Some(Value::Int(v)) = self.ctx.consume_edit(&id) {
326            let new = v as i32;
327            let changed = *value != new;
328            *value = new;
329            (true, changed)
330        } else {
331            (false, false)
332        };
333        self.ctx
334            .declare(build_slider_int(id, self.name.clone(), label, *value, &range));
335        Response { clicked, changed }
336    }
337
338    /// A checkbox (boolean toggle).
339    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
340        let id = self.make_id(label);
341        let (clicked, changed) = if let Some(Value::Bool(v)) = self.ctx.consume_edit(&id) {
342            let changed = *value != v;
343            *value = v;
344            (true, changed)
345        } else {
346            (false, false)
347        };
348        self.ctx
349            .declare(build_checkbox(id, self.name.clone(), label, *value));
350        Response { clicked, changed }
351    }
352
353    /// An RGB color picker (3-component).
354    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
355        let id = self.make_id(label);
356        let (clicked, changed) = if let Some(Value::Color3(c)) = self.ctx.consume_edit(&id) {
357            let changed = *value != c;
358            *value = c;
359            (true, changed)
360        } else {
361            (false, false)
362        };
363        self.ctx
364            .declare(build_color3(id, self.name.clone(), label, *value));
365        Response { clicked, changed }
366    }
367
368    /// An RGBA color picker (4-component).
369    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
370        let id = self.make_id(label);
371        let (clicked, changed) = if let Some(Value::Color4(c)) = self.ctx.consume_edit(&id) {
372            let changed = *value != c;
373            *value = c;
374            (true, changed)
375        } else {
376            (false, false)
377        };
378        self.ctx
379            .declare(build_color4(id, self.name.clone(), label, *value));
380        Response { clicked, changed }
381    }
382
383    /// A text input field.
384    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
385        let id = self.make_id(label);
386        let (clicked, changed) = if let Some(Value::String(s)) = self.ctx.consume_edit(&id) {
387            let changed = *value != s;
388            *value = s;
389            (true, changed)
390        } else {
391            (false, false)
392        };
393        self.ctx
394            .declare(build_text_input(id, self.name.clone(), label, value));
395        Response { clicked, changed }
396    }
397
398    /// A dropdown selector.
399    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
400        let id = self.make_id(label);
401        let (clicked, changed) = if let Some(Value::Enum { selected: s, .. }) = self.ctx.consume_edit(&id) {
402            let changed = *selected != s;
403            *selected = s;
404            (true, changed)
405        } else {
406            (false, false)
407        };
408        self.ctx
409            .declare(build_dropdown(id, self.name.clone(), label, *selected, options));
410        Response { clicked, changed }
411    }
412
413    /// A button. Returns a `Response` — use `.clicked()` to check if it was pressed.
414    pub fn button(&mut self, label: &str) -> Response {
415        let id = self.make_id(label);
416        let clicked = matches!(self.ctx.consume_edit(&id), Some(Value::Button(true)));
417        self.ctx
418            .declare(build_button(id, self.name.clone(), label));
419        Response { clicked, changed: clicked }
420    }
421
422    /// A read-only text label.
423    pub fn label(&mut self, text: &str) {
424        let id = self.make_id(text);
425        self.ctx
426            .declare(build_label(id, self.name.clone(), text));
427    }
428
429    /// A visual separator line.
430    pub fn separator(&mut self) {
431        let id = format!("{}::__sep_{}", self.name, self.ctx.current_frame_len());
432        self.ctx.declare(ElementDecl {
433            id,
434            kind: ElementKind::Separator,
435            label: String::new(),
436            value: Value::Bool(false),
437            meta: ElementMeta::default(),
438            window: self.name.clone(),
439        });
440    }
441
442    /// A section header for grouping widgets.
443    pub fn section(&mut self, title: &str) {
444        let id = format!("{}::__sec_{}", self.name, self.ctx.current_frame_len());
445        self.ctx.declare(ElementDecl {
446            id,
447            kind: ElementKind::Section,
448            label: title.to_string(),
449            value: Value::String(title.to_string()),
450            meta: ElementMeta::default(),
451            window: self.name.clone(),
452        });
453    }
454
455    /// A progress bar (value from 0.0 to 1.0).
456    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
457        let id = self.make_id(label);
458        self.ctx
459            .declare(build_progress_bar(id, self.name.clone(), label, value, accent, None));
460    }
461
462    /// A progress bar with subtitle text.
463    pub fn progress_bar_with_subtitle(
464        &mut self,
465        label: &str,
466        value: f64,
467        accent: AccentColor,
468        subtitle: &str,
469    ) {
470        let id = self.make_id(label);
471        self.ctx.declare(build_progress_bar(
472            id,
473            self.name.clone(),
474            label,
475            value,
476            accent,
477            Some(subtitle),
478        ));
479    }
480
481    /// A stat card displaying a value with optional subvalue.
482    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
483        let id = self.make_id(label);
484        self.ctx
485            .declare(build_stat(id, self.name.clone(), label, value, subvalue, accent));
486    }
487
488    /// A status indicator with colored dot.
489    pub fn status(
490        &mut self,
491        label: &str,
492        active: bool,
493        active_text: Option<&str>,
494        inactive_text: Option<&str>,
495        active_color: AccentColor,
496        inactive_color: AccentColor,
497    ) {
498        let id = self.make_id(label);
499        self.ctx.declare(build_status(
500            id,
501            self.name.clone(),
502            label,
503            active,
504            active_text,
505            inactive_text,
506            active_color,
507            inactive_color,
508        ));
509    }
510
511    /// A mini sparkline chart.
512    pub fn mini_chart(
513        &mut self,
514        label: &str,
515        values: &[f32],
516        unit: Option<&str>,
517        accent: AccentColor,
518    ) {
519        let id = self.make_id(label);
520        self.ctx
521            .declare(build_mini_chart(id, self.name.clone(), label, values, unit, accent));
522    }
523
524    /// Set the accent color for this window.
525    /// Call this first before other widgets in the window.
526    pub fn set_accent(&mut self, accent: AccentColor) {
527        let id = format!("{}::__accent_{}", self.name, accent.as_str());
528        self.ctx.declare(ElementDecl {
529            id,
530            kind: ElementKind::Label,
531            label: String::new(),
532            value: Value::String(String::new()),
533            meta: ElementMeta {
534                accent: Some(accent.as_str().to_string()),
535                ..Default::default()
536            },
537            window: self.name.clone(),
538        });
539    }
540
541    /// Create a grid layout container. Elements added within the closure will be
542    /// arranged in a grid with the specified number of columns.
543    pub fn grid<F>(&mut self, cols: usize, f: F)
544    where
545        F: FnOnce(&mut Grid<'_, 'a>),
546    {
547        let grid_id = format!("{}::__grid_{}", self.name, self.ctx.current_frame_len());
548        let mut grid = Grid::new(&grid_id, self, cols);
549        f(&mut grid);
550        grid.finish();
551    }
552
553    /// Plot a data series as a larger chart.
554    pub fn plot(
555        &mut self,
556        label: &str,
557        series: &[(&str, &[f32], AccentColor)],
558        x_label: Option<&str>,
559        y_label: Option<&str>,
560    ) {
561        let id = self.make_id(label);
562        self.ctx
563            .declare(build_plot(id, self.name.clone(), label, series, x_label, y_label));
564    }
565}
566
567// ── Grid ─────────────────────────────────────────────────────────────
568
569/// A grid container for arranging elements in columns.
570pub struct Grid<'a, 'ctx> {
571    id: String,
572    window: &'a mut Window<'ctx>,
573    cols: usize,
574    children: Vec<String>,
575}
576
577impl<'a, 'ctx> Grid<'a, 'ctx> {
578    fn new(id: &str, window: &'a mut Window<'ctx>, cols: usize) -> Self {
579        Self {
580            id: id.to_string(),
581            window,
582            cols,
583            children: Vec::new(),
584        }
585    }
586
587    fn record_child(&mut self, id: String) {
588        self.children.push(id);
589    }
590
591    fn finish(self) {
592        self.window.ctx.declare(ElementDecl {
593            id: self.id,
594            kind: ElementKind::Grid,
595            label: String::new(),
596            value: Value::GridValue {
597                cols: self.cols,
598                children: self.children,
599            },
600            meta: ElementMeta::default(),
601            window: self.window.name.clone(),
602        });
603    }
604
605    fn make_id(&self, label: &str) -> String {
606        format!("{}::{}", self.id, label)
607    }
608
609    // ── Interactive widgets ──────────────────────────────────────────
610
611    /// A floating-point slider.
612    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
613        let id = self.make_id(label);
614        let (clicked, changed) = if let Some(Value::Float(v)) = self.window.ctx.consume_edit(&id) {
615            let new = v as f32;
616            let changed = *value != new;
617            *value = new;
618            (true, changed)
619        } else {
620            (false, false)
621        };
622        self.record_child(id.clone());
623        self.window
624            .ctx
625            .declare(build_slider(id, self.window.name.clone(), label, *value, &range));
626        Response { clicked, changed }
627    }
628
629    /// An integer slider.
630    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
631        let id = self.make_id(label);
632        let (clicked, changed) = if let Some(Value::Int(v)) = self.window.ctx.consume_edit(&id) {
633            let new = v as i32;
634            let changed = *value != new;
635            *value = new;
636            (true, changed)
637        } else {
638            (false, false)
639        };
640        self.record_child(id.clone());
641        self.window.ctx.declare(build_slider_int(
642            id,
643            self.window.name.clone(),
644            label,
645            *value,
646            &range,
647        ));
648        Response { clicked, changed }
649    }
650
651    /// A checkbox.
652    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
653        let id = self.make_id(label);
654        let (clicked, changed) = if let Some(Value::Bool(v)) = self.window.ctx.consume_edit(&id) {
655            let changed = *value != v;
656            *value = v;
657            (true, changed)
658        } else {
659            (false, false)
660        };
661        self.record_child(id.clone());
662        self.window
663            .ctx
664            .declare(build_checkbox(id, self.window.name.clone(), label, *value));
665        Response { clicked, changed }
666    }
667
668    /// An RGB color picker.
669    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
670        let id = self.make_id(label);
671        let (clicked, changed) = if let Some(Value::Color3(c)) = self.window.ctx.consume_edit(&id) {
672            let changed = *value != c;
673            *value = c;
674            (true, changed)
675        } else {
676            (false, false)
677        };
678        self.record_child(id.clone());
679        self.window
680            .ctx
681            .declare(build_color3(id, self.window.name.clone(), label, *value));
682        Response { clicked, changed }
683    }
684
685    /// An RGBA color picker.
686    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
687        let id = self.make_id(label);
688        let (clicked, changed) = if let Some(Value::Color4(c)) = self.window.ctx.consume_edit(&id) {
689            let changed = *value != c;
690            *value = c;
691            (true, changed)
692        } else {
693            (false, false)
694        };
695        self.record_child(id.clone());
696        self.window
697            .ctx
698            .declare(build_color4(id, self.window.name.clone(), label, *value));
699        Response { clicked, changed }
700    }
701
702    /// A text input field.
703    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
704        let id = self.make_id(label);
705        let (clicked, changed) = if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
706            let changed = *value != s;
707            *value = s;
708            (true, changed)
709        } else {
710            (false, false)
711        };
712        self.record_child(id.clone());
713        self.window
714            .ctx
715            .declare(build_text_input(id, self.window.name.clone(), label, value));
716        Response { clicked, changed }
717    }
718
719    /// A dropdown selector.
720    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
721        let id = self.make_id(label);
722        let (clicked, changed) = if let Some(Value::Enum { selected: s, .. }) = self.window.ctx.consume_edit(&id) {
723            let changed = *selected != s;
724            *selected = s;
725            (true, changed)
726        } else {
727            (false, false)
728        };
729        self.record_child(id.clone());
730        self.window.ctx.declare(build_dropdown(
731            id,
732            self.window.name.clone(),
733            label,
734            *selected,
735            options,
736        ));
737        Response { clicked, changed }
738    }
739
740    /// A button. Returns a `Response` — use `.clicked()` to check if it was pressed.
741    pub fn button(&mut self, label: &str) -> Response {
742        let id = self.make_id(label);
743        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
744        self.record_child(id.clone());
745        self.window
746            .ctx
747            .declare(build_button(id, self.window.name.clone(), label));
748        Response { clicked, changed: clicked }
749    }
750
751    // ── Display widgets ──────────────────────────────────────────────
752
753    /// A read-only text label.
754    pub fn label(&mut self, text: &str) {
755        let id = self.make_id(text);
756        self.record_child(id.clone());
757        self.window
758            .ctx
759            .declare(build_label(id, self.window.name.clone(), text));
760    }
761
762    /// A progress bar (value from 0.0 to 1.0).
763    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
764        let id = self.make_id(label);
765        self.record_child(id.clone());
766        self.window
767            .ctx
768            .declare(build_progress_bar(id, self.window.name.clone(), label, value, accent, None));
769    }
770
771    /// A progress bar with subtitle text.
772    pub fn progress_bar_with_subtitle(
773        &mut self,
774        label: &str,
775        value: f64,
776        accent: AccentColor,
777        subtitle: &str,
778    ) {
779        let id = self.make_id(label);
780        self.record_child(id.clone());
781        self.window.ctx.declare(build_progress_bar(
782            id,
783            self.window.name.clone(),
784            label,
785            value,
786            accent,
787            Some(subtitle),
788        ));
789    }
790
791    /// A stat card.
792    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
793        let id = self.make_id(label);
794        self.record_child(id.clone());
795        self.window
796            .ctx
797            .declare(build_stat(id, self.window.name.clone(), label, value, subvalue, accent));
798    }
799
800    /// A status indicator.
801    pub fn status(
802        &mut self,
803        label: &str,
804        active: bool,
805        active_text: Option<&str>,
806        inactive_text: Option<&str>,
807        active_color: AccentColor,
808        inactive_color: AccentColor,
809    ) {
810        let id = self.make_id(label);
811        self.record_child(id.clone());
812        self.window.ctx.declare(build_status(
813            id,
814            self.window.name.clone(),
815            label,
816            active,
817            active_text,
818            inactive_text,
819            active_color,
820            inactive_color,
821        ));
822    }
823
824    /// A mini sparkline chart.
825    pub fn mini_chart(
826        &mut self,
827        label: &str,
828        values: &[f32],
829        unit: Option<&str>,
830        accent: AccentColor,
831    ) {
832        let id = self.make_id(label);
833        self.record_child(id.clone());
834        self.window.ctx.declare(build_mini_chart(
835            id,
836            self.window.name.clone(),
837            label,
838            values,
839            unit,
840            accent,
841        ));
842    }
843
844    /// A larger chart.
845    pub fn plot(
846        &mut self,
847        label: &str,
848        series: &[(&str, &[f32], AccentColor)],
849        x_label: Option<&str>,
850        y_label: Option<&str>,
851    ) {
852        let id = self.make_id(label);
853        self.record_child(id.clone());
854        self.window.ctx.declare(build_plot(
855            id,
856            self.window.name.clone(),
857            label,
858            series,
859            x_label,
860            y_label,
861        ));
862    }
863
864    /// A visual separator line.
865    pub fn separator(&mut self) {
866        let id = format!("{}::__sep_{}", self.id, self.window.ctx.current_frame_len());
867        self.record_child(id.clone());
868        self.window.ctx.declare(ElementDecl {
869            id,
870            kind: ElementKind::Separator,
871            label: String::new(),
872            value: Value::Bool(false),
873            meta: ElementMeta::default(),
874            window: self.window.name.clone(),
875        });
876    }
877
878    /// Create a nested grid layout container.
879    pub fn grid<F>(&mut self, cols: usize, f: F)
880    where
881        F: FnOnce(&mut Grid<'_, 'ctx>),
882    {
883        let grid_id = format!("{}::__grid_{}", self.id, self.window.ctx.current_frame_len());
884        let mut child_grid = Grid::new(&grid_id, self.window, cols);
885        f(&mut child_grid);
886        let child_id = child_grid.id.clone();
887        child_grid.finish();
888        self.children.push(child_id);
889    }
890}