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    next_idx: usize,
293}
294
295impl<'a> Window<'a> {
296    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
297        Self {
298            name: Arc::from(name.as_str()),
299            ctx,
300            next_idx: 0,
301        }
302    }
303
304    fn make_id(&mut self, _label: &str) -> String {
305        let id = format!("{}::_{}", self.name, self.next_idx);
306        self.next_idx += 1;
307        id
308    }
309
310    /// A floating-point slider with a range.
311    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
312        let id = self.make_id(label);
313        let (clicked, changed) = if let Some(Value::Float(v)) = self.ctx.consume_edit(&id) {
314            let new = v as f32;
315            let changed = *value != new;
316            *value = new;
317            (true, changed)
318        } else {
319            (false, false)
320        };
321        self.ctx
322            .declare(build_slider(id, self.name.clone(), label, *value, &range));
323        Response { clicked, changed }
324    }
325
326    /// An integer slider with a range.
327    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
328        let id = self.make_id(label);
329        let (clicked, changed) = if let Some(Value::Int(v)) = self.ctx.consume_edit(&id) {
330            let new = v as i32;
331            let changed = *value != new;
332            *value = new;
333            (true, changed)
334        } else {
335            (false, false)
336        };
337        self.ctx
338            .declare(build_slider_int(id, self.name.clone(), label, *value, &range));
339        Response { clicked, changed }
340    }
341
342    /// A checkbox (boolean toggle).
343    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
344        let id = self.make_id(label);
345        let (clicked, changed) = if let Some(Value::Bool(v)) = self.ctx.consume_edit(&id) {
346            let changed = *value != v;
347            *value = v;
348            (true, changed)
349        } else {
350            (false, false)
351        };
352        self.ctx
353            .declare(build_checkbox(id, self.name.clone(), label, *value));
354        Response { clicked, changed }
355    }
356
357    /// An RGB color picker (3-component).
358    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
359        let id = self.make_id(label);
360        let (clicked, changed) = if let Some(Value::Color3(c)) = self.ctx.consume_edit(&id) {
361            let changed = *value != c;
362            *value = c;
363            (true, changed)
364        } else {
365            (false, false)
366        };
367        self.ctx
368            .declare(build_color3(id, self.name.clone(), label, *value));
369        Response { clicked, changed }
370    }
371
372    /// An RGBA color picker (4-component).
373    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
374        let id = self.make_id(label);
375        let (clicked, changed) = if let Some(Value::Color4(c)) = self.ctx.consume_edit(&id) {
376            let changed = *value != c;
377            *value = c;
378            (true, changed)
379        } else {
380            (false, false)
381        };
382        self.ctx
383            .declare(build_color4(id, self.name.clone(), label, *value));
384        Response { clicked, changed }
385    }
386
387    /// A text input field.
388    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
389        let id = self.make_id(label);
390        let (clicked, changed) = if let Some(Value::String(s)) = self.ctx.consume_edit(&id) {
391            let changed = *value != s;
392            *value = s;
393            (true, changed)
394        } else {
395            (false, false)
396        };
397        self.ctx
398            .declare(build_text_input(id, self.name.clone(), label, value));
399        Response { clicked, changed }
400    }
401
402    /// A dropdown selector.
403    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
404        let id = self.make_id(label);
405        let (clicked, changed) = if let Some(Value::Enum { selected: s, .. }) = self.ctx.consume_edit(&id) {
406            let changed = *selected != s;
407            *selected = s;
408            (true, changed)
409        } else {
410            (false, false)
411        };
412        self.ctx
413            .declare(build_dropdown(id, self.name.clone(), label, *selected, options));
414        Response { clicked, changed }
415    }
416
417    /// A button. Returns a `Response` — use `.clicked()` to check if it was pressed.
418    pub fn button(&mut self, label: &str) -> Response {
419        let id = self.make_id(label);
420        let clicked = matches!(self.ctx.consume_edit(&id), Some(Value::Button(true)));
421        self.ctx
422            .declare(build_button(id, self.name.clone(), label));
423        Response { clicked, changed: clicked }
424    }
425
426    /// A read-only text label.
427    pub fn label(&mut self, text: &str) {
428        let id = self.make_id(text);
429        self.ctx
430            .declare(build_label(id, self.name.clone(), text));
431    }
432
433    /// A visual separator line.
434    pub fn separator(&mut self) {
435        let id = format!("{}::__sep_{}", self.name, self.ctx.current_frame_len());
436        self.ctx.declare(ElementDecl {
437            id,
438            kind: ElementKind::Separator,
439            label: String::new(),
440            value: Value::Bool(false),
441            meta: ElementMeta::default(),
442            window: self.name.clone(),
443        });
444    }
445
446    /// A section header for grouping widgets.
447    pub fn section(&mut self, title: &str) {
448        let id = format!("{}::__sec_{}", self.name, self.ctx.current_frame_len());
449        self.ctx.declare(ElementDecl {
450            id,
451            kind: ElementKind::Section,
452            label: title.to_string(),
453            value: Value::String(title.to_string()),
454            meta: ElementMeta::default(),
455            window: self.name.clone(),
456        });
457    }
458
459    /// A progress bar (value from 0.0 to 1.0).
460    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
461        let id = self.make_id(label);
462        self.ctx
463            .declare(build_progress_bar(id, self.name.clone(), label, value, accent, None));
464    }
465
466    /// A progress bar with subtitle text.
467    pub fn progress_bar_with_subtitle(
468        &mut self,
469        label: &str,
470        value: f64,
471        accent: AccentColor,
472        subtitle: &str,
473    ) {
474        let id = self.make_id(label);
475        self.ctx.declare(build_progress_bar(
476            id,
477            self.name.clone(),
478            label,
479            value,
480            accent,
481            Some(subtitle),
482        ));
483    }
484
485    /// A stat card displaying a value with optional subvalue.
486    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
487        let id = self.make_id(label);
488        self.ctx
489            .declare(build_stat(id, self.name.clone(), label, value, subvalue, accent));
490    }
491
492    /// A status indicator with colored dot.
493    pub fn status(
494        &mut self,
495        label: &str,
496        active: bool,
497        active_text: Option<&str>,
498        inactive_text: Option<&str>,
499        active_color: AccentColor,
500        inactive_color: AccentColor,
501    ) {
502        let id = self.make_id(label);
503        self.ctx.declare(build_status(
504            id,
505            self.name.clone(),
506            label,
507            active,
508            active_text,
509            inactive_text,
510            active_color,
511            inactive_color,
512        ));
513    }
514
515    /// A mini sparkline chart.
516    pub fn mini_chart(
517        &mut self,
518        label: &str,
519        values: &[f32],
520        unit: Option<&str>,
521        accent: AccentColor,
522    ) {
523        let id = self.make_id(label);
524        self.ctx
525            .declare(build_mini_chart(id, self.name.clone(), label, values, unit, accent));
526    }
527
528    /// Set the accent color for this window.
529    /// Call this first before other widgets in the window.
530    pub fn set_accent(&mut self, accent: AccentColor) {
531        let id = format!("{}::__accent_{}", self.name, accent.as_str());
532        self.ctx.declare(ElementDecl {
533            id,
534            kind: ElementKind::Label,
535            label: String::new(),
536            value: Value::String(String::new()),
537            meta: ElementMeta {
538                accent: Some(accent.as_str().to_string()),
539                ..Default::default()
540            },
541            window: self.name.clone(),
542        });
543    }
544
545    /// Create a grid layout container. Elements added within the closure will be
546    /// arranged in a grid with the specified number of columns.
547    pub fn grid<F>(&mut self, cols: usize, f: F)
548    where
549        F: FnOnce(&mut Grid<'_, 'a>),
550    {
551        let grid_id = format!("{}::__grid_{}", self.name, self.ctx.current_frame_len());
552        let mut grid = Grid::new(&grid_id, self, cols);
553        f(&mut grid);
554        grid.finish();
555    }
556
557    /// Plot a data series as a larger chart.
558    pub fn plot(
559        &mut self,
560        label: &str,
561        series: &[(&str, &[f32], AccentColor)],
562        x_label: Option<&str>,
563        y_label: Option<&str>,
564    ) {
565        let id = self.make_id(label);
566        self.ctx
567            .declare(build_plot(id, self.name.clone(), label, series, x_label, y_label));
568    }
569}
570
571// ── Grid ─────────────────────────────────────────────────────────────
572
573/// A grid container for arranging elements in columns.
574pub struct Grid<'a, 'ctx> {
575    id: String,
576    window: &'a mut Window<'ctx>,
577    cols: usize,
578    children: Vec<String>,
579}
580
581impl<'a, 'ctx> Grid<'a, 'ctx> {
582    fn new(id: &str, window: &'a mut Window<'ctx>, cols: usize) -> Self {
583        Self {
584            id: id.to_string(),
585            window,
586            cols,
587            children: Vec::new(),
588        }
589    }
590
591    fn record_child(&mut self, id: String) {
592        self.children.push(id);
593    }
594
595    fn finish(self) {
596        self.window.ctx.declare(ElementDecl {
597            id: self.id,
598            kind: ElementKind::Grid,
599            label: String::new(),
600            value: Value::GridValue {
601                cols: self.cols,
602                children: self.children,
603            },
604            meta: ElementMeta::default(),
605            window: self.window.name.clone(),
606        });
607    }
608
609    fn make_id(&mut self, _label: &str) -> String {
610        let id = format!("{}::_{}", self.id, self.window.next_idx);
611        self.window.next_idx += 1;
612        id
613    }
614
615    // ── Interactive widgets ──────────────────────────────────────────
616
617    /// A floating-point slider.
618    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
619        let id = self.make_id(label);
620        let (clicked, changed) = if let Some(Value::Float(v)) = self.window.ctx.consume_edit(&id) {
621            let new = v as f32;
622            let changed = *value != new;
623            *value = new;
624            (true, changed)
625        } else {
626            (false, false)
627        };
628        self.record_child(id.clone());
629        self.window
630            .ctx
631            .declare(build_slider(id, self.window.name.clone(), label, *value, &range));
632        Response { clicked, changed }
633    }
634
635    /// An integer slider.
636    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
637        let id = self.make_id(label);
638        let (clicked, changed) = if let Some(Value::Int(v)) = self.window.ctx.consume_edit(&id) {
639            let new = v as i32;
640            let changed = *value != new;
641            *value = new;
642            (true, changed)
643        } else {
644            (false, false)
645        };
646        self.record_child(id.clone());
647        self.window.ctx.declare(build_slider_int(
648            id,
649            self.window.name.clone(),
650            label,
651            *value,
652            &range,
653        ));
654        Response { clicked, changed }
655    }
656
657    /// A checkbox.
658    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
659        let id = self.make_id(label);
660        let (clicked, changed) = if let Some(Value::Bool(v)) = self.window.ctx.consume_edit(&id) {
661            let changed = *value != v;
662            *value = v;
663            (true, changed)
664        } else {
665            (false, false)
666        };
667        self.record_child(id.clone());
668        self.window
669            .ctx
670            .declare(build_checkbox(id, self.window.name.clone(), label, *value));
671        Response { clicked, changed }
672    }
673
674    /// An RGB color picker.
675    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
676        let id = self.make_id(label);
677        let (clicked, changed) = if let Some(Value::Color3(c)) = self.window.ctx.consume_edit(&id) {
678            let changed = *value != c;
679            *value = c;
680            (true, changed)
681        } else {
682            (false, false)
683        };
684        self.record_child(id.clone());
685        self.window
686            .ctx
687            .declare(build_color3(id, self.window.name.clone(), label, *value));
688        Response { clicked, changed }
689    }
690
691    /// An RGBA color picker.
692    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
693        let id = self.make_id(label);
694        let (clicked, changed) = if let Some(Value::Color4(c)) = self.window.ctx.consume_edit(&id) {
695            let changed = *value != c;
696            *value = c;
697            (true, changed)
698        } else {
699            (false, false)
700        };
701        self.record_child(id.clone());
702        self.window
703            .ctx
704            .declare(build_color4(id, self.window.name.clone(), label, *value));
705        Response { clicked, changed }
706    }
707
708    /// A text input field.
709    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
710        let id = self.make_id(label);
711        let (clicked, changed) = if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
712            let changed = *value != s;
713            *value = s;
714            (true, changed)
715        } else {
716            (false, false)
717        };
718        self.record_child(id.clone());
719        self.window
720            .ctx
721            .declare(build_text_input(id, self.window.name.clone(), label, value));
722        Response { clicked, changed }
723    }
724
725    /// A dropdown selector.
726    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
727        let id = self.make_id(label);
728        let (clicked, changed) = if let Some(Value::Enum { selected: s, .. }) = self.window.ctx.consume_edit(&id) {
729            let changed = *selected != s;
730            *selected = s;
731            (true, changed)
732        } else {
733            (false, false)
734        };
735        self.record_child(id.clone());
736        self.window.ctx.declare(build_dropdown(
737            id,
738            self.window.name.clone(),
739            label,
740            *selected,
741            options,
742        ));
743        Response { clicked, changed }
744    }
745
746    /// A button. Returns a `Response` — use `.clicked()` to check if it was pressed.
747    pub fn button(&mut self, label: &str) -> Response {
748        let id = self.make_id(label);
749        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
750        self.record_child(id.clone());
751        self.window
752            .ctx
753            .declare(build_button(id, self.window.name.clone(), label));
754        Response { clicked, changed: clicked }
755    }
756
757    // ── Display widgets ──────────────────────────────────────────────
758
759    /// A read-only text label.
760    pub fn label(&mut self, text: &str) {
761        let id = self.make_id(text);
762        self.record_child(id.clone());
763        self.window
764            .ctx
765            .declare(build_label(id, self.window.name.clone(), text));
766    }
767
768    /// A progress bar (value from 0.0 to 1.0).
769    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
770        let id = self.make_id(label);
771        self.record_child(id.clone());
772        self.window
773            .ctx
774            .declare(build_progress_bar(id, self.window.name.clone(), label, value, accent, None));
775    }
776
777    /// A progress bar with subtitle text.
778    pub fn progress_bar_with_subtitle(
779        &mut self,
780        label: &str,
781        value: f64,
782        accent: AccentColor,
783        subtitle: &str,
784    ) {
785        let id = self.make_id(label);
786        self.record_child(id.clone());
787        self.window.ctx.declare(build_progress_bar(
788            id,
789            self.window.name.clone(),
790            label,
791            value,
792            accent,
793            Some(subtitle),
794        ));
795    }
796
797    /// A stat card.
798    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
799        let id = self.make_id(label);
800        self.record_child(id.clone());
801        self.window
802            .ctx
803            .declare(build_stat(id, self.window.name.clone(), label, value, subvalue, accent));
804    }
805
806    /// A status indicator.
807    pub fn status(
808        &mut self,
809        label: &str,
810        active: bool,
811        active_text: Option<&str>,
812        inactive_text: Option<&str>,
813        active_color: AccentColor,
814        inactive_color: AccentColor,
815    ) {
816        let id = self.make_id(label);
817        self.record_child(id.clone());
818        self.window.ctx.declare(build_status(
819            id,
820            self.window.name.clone(),
821            label,
822            active,
823            active_text,
824            inactive_text,
825            active_color,
826            inactive_color,
827        ));
828    }
829
830    /// A mini sparkline chart.
831    pub fn mini_chart(
832        &mut self,
833        label: &str,
834        values: &[f32],
835        unit: Option<&str>,
836        accent: AccentColor,
837    ) {
838        let id = self.make_id(label);
839        self.record_child(id.clone());
840        self.window.ctx.declare(build_mini_chart(
841            id,
842            self.window.name.clone(),
843            label,
844            values,
845            unit,
846            accent,
847        ));
848    }
849
850    /// A larger chart.
851    pub fn plot(
852        &mut self,
853        label: &str,
854        series: &[(&str, &[f32], AccentColor)],
855        x_label: Option<&str>,
856        y_label: Option<&str>,
857    ) {
858        let id = self.make_id(label);
859        self.record_child(id.clone());
860        self.window.ctx.declare(build_plot(
861            id,
862            self.window.name.clone(),
863            label,
864            series,
865            x_label,
866            y_label,
867        ));
868    }
869
870    /// A visual separator line.
871    pub fn separator(&mut self) {
872        let id = format!("{}::__sep_{}", self.id, self.window.ctx.current_frame_len());
873        self.record_child(id.clone());
874        self.window.ctx.declare(ElementDecl {
875            id,
876            kind: ElementKind::Separator,
877            label: String::new(),
878            value: Value::Bool(false),
879            meta: ElementMeta::default(),
880            window: self.window.name.clone(),
881        });
882    }
883
884    /// Create a nested grid layout container.
885    pub fn grid<F>(&mut self, cols: usize, f: F)
886    where
887        F: FnOnce(&mut Grid<'_, 'ctx>),
888    {
889        let grid_id = format!("{}::__grid_{}", self.id, self.window.ctx.current_frame_len());
890        let mut child_grid = Grid::new(&grid_id, self.window, cols);
891        f(&mut child_grid);
892        let child_id = child_grid.id.clone();
893        child_grid.finish();
894        self.children.push(child_id);
895    }
896}