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