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