Skip to main content

w_gui/
window.rs

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