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_image(sink: &mut impl WidgetSink, label: &str, data_uri: &str, width: Option<u32>, height: Option<u32>) {
400    let id = sink.make_id(label);
401    sink.record_child(id.clone());
402    sink.declare(ElementDecl {
403        id,
404        kind: ElementKind::Image,
405        label: label.to_string(),
406        value: Value::ImageValue {
407            data: data_uri.to_string(),
408            width,
409            height,
410        },
411        meta: ElementMeta::default(),
412        window: sink.window_name(),
413    });
414}
415
416fn widget_image_button(sink: &mut impl WidgetSink, label: &str, data_uri: &str, selected: bool) -> Response {
417    let id = sink.make_id(label);
418    let clicked = matches!(sink.consume_edit(&id), Some(Value::Button(true)));
419    sink.record_child(id.clone());
420    sink.declare(ElementDecl {
421        id,
422        kind: ElementKind::ImageButton,
423        label: label.to_string(),
424        value: Value::ImageValue {
425            data: data_uri.to_string(),
426            width: None,
427            height: None,
428        },
429        meta: ElementMeta {
430            accent: if selected { Some(AccentColor::Teal) } else { None },
431            ..Default::default()
432        },
433        window: sink.window_name(),
434    });
435    Response { clicked, changed: clicked }
436}
437
438fn widget_plot(
439    sink: &mut impl WidgetSink,
440    label: &str,
441    series: &[(&str, &[f32], AccentColor, bool)],
442    x_label: Option<&str>,
443    y_label: Option<&str>,
444) {
445    let id = sink.make_id(label);
446    let plot_series: Vec<PlotSeries> = series
447        .iter()
448        .map(|(name, values, color, autoscale)| PlotSeries {
449            name: name.to_string(),
450            values: values.to_vec(),
451            color: color.as_str().to_string(),
452            autoscale: *autoscale,
453        })
454        .collect();
455    sink.record_child(id.clone());
456    sink.declare(ElementDecl {
457        id,
458        kind: ElementKind::Plot,
459        label: label.to_string(),
460        value: Value::PlotValue {
461            series: plot_series,
462            x_label: x_label.map(|s| s.to_string()),
463            y_label: y_label.map(|s| s.to_string()),
464        },
465        meta: ElementMeta::default(),
466        window: sink.window_name(),
467    });
468}
469
470// ── Label-based ID generation ────────────────────────────────────────
471
472fn make_label_id(prefix: &str, label: &str, label_counts: &mut HashMap<String, usize>) -> String {
473    let count = label_counts.entry(label.to_string()).or_insert(0);
474    let id = if *count == 0 {
475        format!("{prefix}::{label}")
476    } else {
477        format!("{prefix}::{label}#{count}")
478    };
479    *count += 1;
480    id
481}
482
483// ── Window ─────────────────────────────────────────────────────────--
484
485/// A named window containing UI elements. Created via `Context::window()`.
486pub struct Window<'a> {
487    name: Arc<str>,
488    ctx: &'a mut Context,
489    label_counts: HashMap<String, usize>,
490}
491
492impl<'a> WidgetSink for Window<'a> {
493    fn make_id(&mut self, label: &str) -> String {
494        make_label_id(&self.name, label, &mut self.label_counts)
495    }
496
497    fn declare(&mut self, decl: ElementDecl) {
498        self.ctx.declare(decl);
499    }
500
501    fn consume_edit(&mut self, id: &str) -> Option<Value> {
502        self.ctx.consume_edit(id)
503    }
504
505    fn window_name(&self) -> Arc<str> {
506        self.name.clone()
507    }
508
509    fn record_child(&mut self, _id: String) {
510        // Window is top-level — no parent to record into
511    }
512}
513
514impl<'a> Window<'a> {
515    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
516        Self {
517            name: Arc::from(name.as_str()),
518            ctx,
519            label_counts: HashMap::new(),
520        }
521    }
522
523    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
524        widget_slider(self, label, value, &range)
525    }
526
527    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
528        widget_slider_f64(self, label, value, &range)
529    }
530
531    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
532        widget_slider_int(self, label, value, &range)
533    }
534
535    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
536        widget_slider_uint(self, label, value, &range)
537    }
538
539    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
540        widget_checkbox(self, label, value)
541    }
542
543    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
544        widget_color3(self, label, value)
545    }
546
547    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
548        widget_color4(self, label, value)
549    }
550
551    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
552        widget_text_input(self, label, value)
553    }
554
555    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
556        widget_dropdown(self, label, selected, options)
557    }
558
559    pub fn button(&mut self, label: &str) -> Response {
560        widget_button(self, label)
561    }
562
563    pub fn label(&mut self, text: &str) {
564        let id = self.make_id("__label");
565        self.record_child(id.clone());
566        self.declare(ElementDecl {
567            id,
568            kind: ElementKind::LabelInline,
569            label: String::new(),
570            value: Value::String(text.to_string()),
571            meta: ElementMeta::default(),
572            window: self.window_name(),
573        });
574    }
575
576    pub fn kv(&mut self, label: &str, value: &str) {
577        widget_kv(self, label, value);
578    }
579
580    pub fn kv_value(&mut self, label: &str, value: &mut String) -> Response {
581        let id = self.make_id(label);
582        let (clicked, changed) = if let Some(Value::String(v)) = self.ctx.consume_edit(&id) {
583            let changed = *value != v;
584            *value = v;
585            (true, changed)
586        } else {
587            (false, false)
588        };
589        self.ctx.declare(ElementDecl {
590            id,
591            kind: ElementKind::KeyValue,
592            label: label.to_string(),
593            value: Value::String(value.clone()),
594            meta: ElementMeta::default(),
595            window: self.name.clone(),
596        });
597        Response { clicked, changed }
598    }
599
600    pub fn button_compact(&mut self, label: &str) -> Response {
601        widget_button_compact(self, label, None)
602    }
603
604    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
605        widget_button_compact(self, label, Some(accent))
606    }
607
608    pub fn horizontal<F>(&mut self, f: F)
609    where
610        F: FnOnce(&mut Horizontal<'_, 'a>),
611    {
612        let h_id = self.make_id("__horiz");
613        let mut horiz = Horizontal::new(h_id, self);
614        f(&mut horiz);
615        horiz.finish();
616    }
617
618    pub fn separator(&mut self) {
619        let id = self.make_id("__sep");
620        self.ctx.declare(ElementDecl {
621            id,
622            kind: ElementKind::Separator,
623            label: String::new(),
624            value: Value::Bool(false),
625            meta: ElementMeta::default(),
626            window: self.name.clone(),
627        });
628    }
629
630    pub fn section(&mut self, title: &str) {
631        let id = self.make_id(title);
632        self.ctx.declare(ElementDecl {
633            id,
634            kind: ElementKind::Section,
635            label: title.to_string(),
636            value: Value::String(title.to_string()),
637            meta: ElementMeta::default(),
638            window: self.name.clone(),
639        });
640    }
641
642    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
643        widget_progress_bar(self, label, value, accent, None);
644    }
645
646    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
647        widget_progress_bar(self, label, value, accent, Some(subtitle));
648    }
649
650    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
651        widget_stat(self, label, value, subvalue, accent);
652    }
653
654    pub fn status(
655        &mut self,
656        label: &str,
657        active: bool,
658        active_text: Option<&str>,
659        inactive_text: Option<&str>,
660        active_color: AccentColor,
661        inactive_color: AccentColor,
662    ) {
663        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
664    }
665
666    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
667        widget_mini_chart(self, label, values, unit, accent);
668    }
669
670    pub fn set_accent(&mut self, accent: AccentColor) {
671        let id = self.make_id(&format!("__accent_{}", accent.as_str()));
672        self.ctx.declare(ElementDecl {
673            id,
674            kind: ElementKind::Label,
675            label: String::new(),
676            value: Value::String(String::new()),
677            meta: ElementMeta {
678                accent: Some(accent),
679                ..Default::default()
680            },
681            window: self.name.clone(),
682        });
683    }
684
685    pub fn grid<F>(&mut self, cols: usize, f: F)
686    where
687        F: FnOnce(&mut Grid<'_, 'a>),
688    {
689        let grid_id = self.make_id("__grid");
690        let mut grid = Grid::new(grid_id, self, cols);
691        f(&mut grid);
692        grid.finish();
693    }
694
695    pub fn plot(
696        &mut self,
697        label: &str,
698        series: &[(&str, &[f32], AccentColor)],
699        x_label: Option<&str>,
700        y_label: Option<&str>,
701    ) {
702        // Default to autoscale=true for backward compatibility
703        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
704            .iter()
705            .map(|(name, values, color)| (*name, *values, *color, true))
706            .collect();
707        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
708    }
709
710    /// Plot with explicit autoscale control per series
711    /// series: (name, values, color, autoscale)
712    pub fn plot_with_autoscale(
713        &mut self,
714        label: &str,
715        series: &[(&str, &[f32], AccentColor, bool)],
716        x_label: Option<&str>,
717        y_label: Option<&str>,
718    ) {
719        widget_plot(self, label, series, x_label, y_label);
720    }
721
722    /// Display an image from a base64 data URI (e.g. `"data:image/png;base64,…"`).
723    pub fn image(&mut self, label: &str, data_uri: &str) {
724        widget_image(self, label, data_uri, None, None);
725    }
726
727    /// Display an image with an explicit size hint.
728    pub fn image_with_size(&mut self, label: &str, data_uri: &str, width: u32, height: u32) {
729        widget_image(self, label, data_uri, Some(width), Some(height));
730    }
731
732    /// Display a clickable image (thumbnail button). `selected` draws a highlight
733    /// border. Returns a `Response` whose `clicked()` is true the frame it is pressed.
734    pub fn image_button(&mut self, label: &str, data_uri: &str, selected: bool) -> Response {
735        widget_image_button(self, label, data_uri, selected)
736    }
737}
738
739// ── Horizontal ───────────────────────────────────────────────────────
740
741/// A horizontal layout container for arranging widgets side by side.
742pub struct Horizontal<'a, 'ctx> {
743    id: String,
744    window: &'a mut Window<'ctx>,
745    children: Vec<String>,
746    label_counts: HashMap<String, usize>,
747}
748
749impl<'a, 'ctx> WidgetSink for Horizontal<'a, 'ctx> {
750    fn make_id(&mut self, label: &str) -> String {
751        make_label_id(&self.id, label, &mut self.label_counts)
752    }
753
754    fn declare(&mut self, decl: ElementDecl) {
755        self.window.ctx.declare(decl);
756    }
757
758    fn consume_edit(&mut self, id: &str) -> Option<Value> {
759        self.window.ctx.consume_edit(id)
760    }
761
762    fn window_name(&self) -> Arc<str> {
763        self.window.name.clone()
764    }
765
766    fn record_child(&mut self, id: String) {
767        self.children.push(id);
768    }
769}
770
771impl<'a, 'ctx> Horizontal<'a, 'ctx> {
772    fn new(id: String, window: &'a mut Window<'ctx>) -> Self {
773        Self {
774            id,
775            window,
776            children: Vec::new(),
777            label_counts: HashMap::new(),
778        }
779    }
780
781    fn finish(self) {
782        self.window.ctx.declare(ElementDecl {
783            id: self.id,
784            kind: ElementKind::Horizontal,
785            label: String::new(),
786            value: Value::GridValue {
787                cols: self.children.len(),
788                children: self.children,
789            },
790            meta: ElementMeta::default(),
791            window: self.window.name.clone(),
792        });
793    }
794
795    pub fn button(&mut self, label: &str) -> Response {
796        self.button_accent_inner(label, None)
797    }
798
799    pub fn button_accent(&mut self, label: &str, accent: AccentColor) -> Response {
800        self.button_accent_inner(label, Some(accent))
801    }
802
803    fn button_accent_inner(&mut self, label: &str, accent: Option<AccentColor>) -> Response {
804        let id = self.make_id(label);
805        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
806        self.children.push(id.clone());
807        self.window.ctx.declare(ElementDecl {
808            id,
809            kind: ElementKind::ButtonInline,
810            label: label.to_string(),
811            value: Value::Button(false),
812            meta: ElementMeta {
813                accent,
814                ..Default::default()
815            },
816            window: self.window.name.clone(),
817        });
818        Response { clicked, changed: clicked }
819    }
820
821    pub fn label(&mut self, text: &str) {
822        let id = self.make_id("__label");
823        self.children.push(id.clone());
824        self.window.ctx.declare(ElementDecl {
825            id,
826            kind: ElementKind::LabelInline,
827            label: String::new(),
828            value: Value::String(text.to_string()),
829            meta: ElementMeta::default(),
830            window: self.window.name.clone(),
831        });
832    }
833
834    pub fn kv(&mut self, label: &str, value: &str) {
835        widget_kv(self, label, value);
836    }
837
838    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
839        widget_text_input(self, label, value)
840    }
841
842    pub fn text_input_inline(&mut self, placeholder: &str, value: &mut String) -> Response {
843        let id = self.make_id(placeholder);
844        let (clicked, changed) = if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
845            let changed = *value != s;
846            *value = s;
847            (true, changed)
848        } else {
849            (false, false)
850        };
851        self.children.push(id.clone());
852        self.window.ctx.declare(ElementDecl {
853            id,
854            kind: ElementKind::TextInputInline,
855            label: placeholder.to_string(),
856            value: Value::String(value.clone()),
857            meta: ElementMeta::default(),
858            window: self.window.name.clone(),
859        });
860        Response { clicked, changed }
861    }
862
863    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
864        widget_slider(self, label, value, &range)
865    }
866
867    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
868        widget_slider_f64(self, label, value, &range)
869    }
870
871    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
872        widget_slider_int(self, label, value, &range)
873    }
874
875    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
876        widget_slider_uint(self, label, value, &range)
877    }
878
879    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
880        widget_checkbox(self, label, value)
881    }
882
883    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
884        widget_color3(self, label, value)
885    }
886
887    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
888        widget_color4(self, label, value)
889    }
890
891    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
892        widget_dropdown(self, label, selected, options)
893    }
894
895    pub fn image(&mut self, label: &str, data_uri: &str) {
896        widget_image(self, label, data_uri, None, None);
897    }
898}
899
900// ── Grid ─────────────────────────────────────────────────────────────
901
902/// A grid container for arranging elements in columns.
903pub struct Grid<'a, 'ctx> {
904    id: String,
905    window: &'a mut Window<'ctx>,
906    cols: usize,
907    children: Vec<String>,
908    label_counts: HashMap<String, usize>,
909}
910
911impl<'a, 'ctx> WidgetSink for Grid<'a, 'ctx> {
912    fn make_id(&mut self, label: &str) -> String {
913        make_label_id(&self.id, label, &mut self.label_counts)
914    }
915
916    fn declare(&mut self, decl: ElementDecl) {
917        self.window.ctx.declare(decl);
918    }
919
920    fn consume_edit(&mut self, id: &str) -> Option<Value> {
921        self.window.ctx.consume_edit(id)
922    }
923
924    fn window_name(&self) -> Arc<str> {
925        self.window.name.clone()
926    }
927
928    fn record_child(&mut self, id: String) {
929        self.children.push(id);
930    }
931}
932
933impl<'a, 'ctx> Grid<'a, 'ctx> {
934    fn new(id: String, window: &'a mut Window<'ctx>, cols: usize) -> Self {
935        Self {
936            id,
937            window,
938            cols,
939            children: Vec::new(),
940            label_counts: HashMap::new(),
941        }
942    }
943
944    fn finish(self) {
945        self.window.ctx.declare(ElementDecl {
946            id: self.id,
947            kind: ElementKind::Grid,
948            label: String::new(),
949            value: Value::GridValue {
950                cols: self.cols,
951                children: self.children,
952            },
953            meta: ElementMeta::default(),
954            window: self.window.name.clone(),
955        });
956    }
957
958    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
959        widget_slider(self, label, value, &range)
960    }
961
962    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
963        widget_slider_f64(self, label, value, &range)
964    }
965
966    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
967        widget_slider_int(self, label, value, &range)
968    }
969
970    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
971        widget_slider_uint(self, label, value, &range)
972    }
973
974    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
975        widget_checkbox(self, label, value)
976    }
977
978    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
979        widget_color3(self, label, value)
980    }
981
982    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
983        widget_color4(self, label, value)
984    }
985
986    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
987        widget_text_input(self, label, value)
988    }
989
990    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
991        widget_dropdown(self, label, selected, options)
992    }
993
994    pub fn button(&mut self, label: &str) -> Response {
995        widget_button(self, label)
996    }
997
998    pub fn label(&mut self, text: &str) {
999        widget_label(self, text);
1000    }
1001
1002    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
1003        widget_progress_bar(self, label, value, accent, None);
1004    }
1005
1006    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
1007        widget_progress_bar(self, label, value, accent, Some(subtitle));
1008    }
1009
1010    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
1011        widget_stat(self, label, value, subvalue, accent);
1012    }
1013
1014    pub fn status(
1015        &mut self,
1016        label: &str,
1017        active: bool,
1018        active_text: Option<&str>,
1019        inactive_text: Option<&str>,
1020        active_color: AccentColor,
1021        inactive_color: AccentColor,
1022    ) {
1023        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
1024    }
1025
1026    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
1027        widget_mini_chart(self, label, values, unit, accent);
1028    }
1029
1030    pub fn plot(
1031        &mut self,
1032        label: &str,
1033        series: &[(&str, &[f32], AccentColor)],
1034        x_label: Option<&str>,
1035        y_label: Option<&str>,
1036    ) {
1037        // Default to autoscale=true for backward compatibility
1038        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
1039            .iter()
1040            .map(|(name, values, color)| (*name, *values, *color, true))
1041            .collect();
1042        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
1043    }
1044
1045    /// Plot with explicit autoscale control per series
1046    /// series: (name, values, color, autoscale)
1047    pub fn plot_with_autoscale(
1048        &mut self,
1049        label: &str,
1050        series: &[(&str, &[f32], AccentColor, bool)],
1051        x_label: Option<&str>,
1052        y_label: Option<&str>,
1053    ) {
1054        widget_plot(self, label, series, x_label, y_label);
1055    }
1056
1057    pub fn image(&mut self, label: &str, data_uri: &str) {
1058        widget_image(self, label, data_uri, None, None);
1059    }
1060
1061    /// Clickable image (thumbnail button); `selected` draws a highlight border.
1062    pub fn image_button(&mut self, label: &str, data_uri: &str, selected: bool) -> Response {
1063        widget_image_button(self, label, data_uri, selected)
1064    }
1065
1066    pub fn button_compact(&mut self, label: &str) -> Response {
1067        widget_button_compact(self, label, None)
1068    }
1069
1070    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
1071        widget_button_compact(self, label, Some(accent))
1072    }
1073
1074    pub fn kv(&mut self, label: &str, value: &str) {
1075        widget_kv(self, label, value);
1076    }
1077
1078    pub fn separator(&mut self) {
1079        let id = self.make_id("__sep");
1080        self.children.push(id.clone());
1081        self.window.ctx.declare(ElementDecl {
1082            id,
1083            kind: ElementKind::Separator,
1084            label: String::new(),
1085            value: Value::Bool(false),
1086            meta: ElementMeta::default(),
1087            window: self.window.name.clone(),
1088        });
1089    }
1090
1091    pub fn grid<F>(&mut self, cols: usize, f: F)
1092    where
1093        F: FnOnce(&mut Grid<'_, 'ctx>),
1094    {
1095        let grid_id = make_label_id(&self.id, "__grid", &mut self.label_counts);
1096        let mut child_grid = Grid::new(grid_id.clone(), self.window, cols);
1097        f(&mut child_grid);
1098        child_grid.finish();
1099        self.children.push(grid_id);
1100    }
1101}