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        widget_label(self, text);
526    }
527
528    pub fn kv(&mut self, label: &str, value: &str) {
529        widget_kv(self, label, value);
530    }
531
532    pub fn kv_value(&mut self, label: &str, value: &mut String) -> Response {
533        let id = self.make_id(label);
534        let (clicked, changed) = if let Some(Value::String(v)) = self.ctx.consume_edit(&id) {
535            let changed = *value != v;
536            *value = v;
537            (true, changed)
538        } else {
539            (false, false)
540        };
541        self.ctx.declare(ElementDecl {
542            id,
543            kind: ElementKind::KeyValue,
544            label: label.to_string(),
545            value: Value::String(value.clone()),
546            meta: ElementMeta::default(),
547            window: self.name.clone(),
548        });
549        Response { clicked, changed }
550    }
551
552    pub fn button_compact(&mut self, label: &str) -> Response {
553        widget_button_compact(self, label, None)
554    }
555
556    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
557        widget_button_compact(self, label, Some(accent))
558    }
559
560    pub fn horizontal<F>(&mut self, f: F)
561    where
562        F: FnOnce(&mut Horizontal<'_, 'a>),
563    {
564        let h_id = self.make_id("__horiz");
565        let mut horiz = Horizontal::new(h_id, self);
566        f(&mut horiz);
567        horiz.finish();
568    }
569
570    pub fn separator(&mut self) {
571        let id = self.make_id("__sep");
572        self.ctx.declare(ElementDecl {
573            id,
574            kind: ElementKind::Separator,
575            label: String::new(),
576            value: Value::Bool(false),
577            meta: ElementMeta::default(),
578            window: self.name.clone(),
579        });
580    }
581
582    pub fn section(&mut self, title: &str) {
583        let id = self.make_id(title);
584        self.ctx.declare(ElementDecl {
585            id,
586            kind: ElementKind::Section,
587            label: title.to_string(),
588            value: Value::String(title.to_string()),
589            meta: ElementMeta::default(),
590            window: self.name.clone(),
591        });
592    }
593
594    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
595        widget_progress_bar(self, label, value, accent, None);
596    }
597
598    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
599        widget_progress_bar(self, label, value, accent, Some(subtitle));
600    }
601
602    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
603        widget_stat(self, label, value, subvalue, accent);
604    }
605
606    pub fn status(
607        &mut self,
608        label: &str,
609        active: bool,
610        active_text: Option<&str>,
611        inactive_text: Option<&str>,
612        active_color: AccentColor,
613        inactive_color: AccentColor,
614    ) {
615        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
616    }
617
618    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
619        widget_mini_chart(self, label, values, unit, accent);
620    }
621
622    pub fn set_accent(&mut self, accent: AccentColor) {
623        let id = self.make_id(&format!("__accent_{}", accent.as_str()));
624        self.ctx.declare(ElementDecl {
625            id,
626            kind: ElementKind::Label,
627            label: String::new(),
628            value: Value::String(String::new()),
629            meta: ElementMeta {
630                accent: Some(accent),
631                ..Default::default()
632            },
633            window: self.name.clone(),
634        });
635    }
636
637    pub fn grid<F>(&mut self, cols: usize, f: F)
638    where
639        F: FnOnce(&mut Grid<'_, 'a>),
640    {
641        let grid_id = self.make_id("__grid");
642        let mut grid = Grid::new(grid_id, self, cols);
643        f(&mut grid);
644        grid.finish();
645    }
646
647    pub fn plot(
648        &mut self,
649        label: &str,
650        series: &[(&str, &[f32], AccentColor)],
651        x_label: Option<&str>,
652        y_label: Option<&str>,
653    ) {
654        // Default to autoscale=true for backward compatibility
655        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
656            .iter()
657            .map(|(name, values, color)| (*name, *values, *color, true))
658            .collect();
659        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
660    }
661
662    /// Plot with explicit autoscale control per series
663    /// series: (name, values, color, autoscale)
664    pub fn plot_with_autoscale(
665        &mut self,
666        label: &str,
667        series: &[(&str, &[f32], AccentColor, bool)],
668        x_label: Option<&str>,
669        y_label: Option<&str>,
670    ) {
671        widget_plot(self, label, series, x_label, y_label);
672    }
673}
674
675// ── Horizontal ───────────────────────────────────────────────────────
676
677/// A horizontal layout container for arranging widgets side by side.
678pub struct Horizontal<'a, 'ctx> {
679    id: String,
680    window: &'a mut Window<'ctx>,
681    children: Vec<String>,
682    label_counts: HashMap<String, usize>,
683}
684
685impl<'a, 'ctx> WidgetSink for Horizontal<'a, 'ctx> {
686    fn make_id(&mut self, label: &str) -> String {
687        make_label_id(&self.id, label, &mut self.label_counts)
688    }
689
690    fn declare(&mut self, decl: ElementDecl) {
691        self.window.ctx.declare(decl);
692    }
693
694    fn consume_edit(&mut self, id: &str) -> Option<Value> {
695        self.window.ctx.consume_edit(id)
696    }
697
698    fn window_name(&self) -> Arc<str> {
699        self.window.name.clone()
700    }
701
702    fn record_child(&mut self, id: String) {
703        self.children.push(id);
704    }
705}
706
707impl<'a, 'ctx> Horizontal<'a, 'ctx> {
708    fn new(id: String, window: &'a mut Window<'ctx>) -> Self {
709        Self {
710            id,
711            window,
712            children: Vec::new(),
713            label_counts: HashMap::new(),
714        }
715    }
716
717    fn finish(self) {
718        self.window.ctx.declare(ElementDecl {
719            id: self.id,
720            kind: ElementKind::Horizontal,
721            label: String::new(),
722            value: Value::GridValue {
723                cols: self.children.len(),
724                children: self.children,
725            },
726            meta: ElementMeta::default(),
727            window: self.window.name.clone(),
728        });
729    }
730
731    pub fn button(&mut self, label: &str) -> Response {
732        self.button_accent_inner(label, None)
733    }
734
735    pub fn button_accent(&mut self, label: &str, accent: AccentColor) -> Response {
736        self.button_accent_inner(label, Some(accent))
737    }
738
739    fn button_accent_inner(&mut self, label: &str, accent: Option<AccentColor>) -> Response {
740        let id = self.make_id(label);
741        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
742        self.children.push(id.clone());
743        self.window.ctx.declare(ElementDecl {
744            id,
745            kind: ElementKind::ButtonInline,
746            label: label.to_string(),
747            value: Value::Button(false),
748            meta: ElementMeta {
749                accent,
750                ..Default::default()
751            },
752            window: self.window.name.clone(),
753        });
754        Response { clicked, changed: clicked }
755    }
756
757    pub fn label(&mut self, text: &str) {
758        widget_label(self, text);
759    }
760
761    pub fn kv(&mut self, label: &str, value: &str) {
762        widget_kv(self, label, value);
763    }
764
765    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
766        widget_text_input(self, label, value)
767    }
768
769    pub fn text_input_inline(&mut self, placeholder: &str, value: &mut String) -> Response {
770        let id = self.make_id(placeholder);
771        let (clicked, changed) = if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
772            let changed = *value != s;
773            *value = s;
774            (true, changed)
775        } else {
776            (false, false)
777        };
778        self.children.push(id.clone());
779        self.window.ctx.declare(ElementDecl {
780            id,
781            kind: ElementKind::TextInputInline,
782            label: placeholder.to_string(),
783            value: Value::String(value.clone()),
784            meta: ElementMeta::default(),
785            window: self.window.name.clone(),
786        });
787        Response { clicked, changed }
788    }
789
790    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
791        widget_slider(self, label, value, &range)
792    }
793
794    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
795        widget_slider_f64(self, label, value, &range)
796    }
797
798    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
799        widget_slider_int(self, label, value, &range)
800    }
801
802    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
803        widget_slider_uint(self, label, value, &range)
804    }
805
806    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
807        widget_checkbox(self, label, value)
808    }
809
810    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
811        widget_color3(self, label, value)
812    }
813
814    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
815        widget_color4(self, label, value)
816    }
817
818    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
819        widget_dropdown(self, label, selected, options)
820    }
821}
822
823// ── Grid ─────────────────────────────────────────────────────────────
824
825/// A grid container for arranging elements in columns.
826pub struct Grid<'a, 'ctx> {
827    id: String,
828    window: &'a mut Window<'ctx>,
829    cols: usize,
830    children: Vec<String>,
831    label_counts: HashMap<String, usize>,
832}
833
834impl<'a, 'ctx> WidgetSink for Grid<'a, 'ctx> {
835    fn make_id(&mut self, label: &str) -> String {
836        make_label_id(&self.id, label, &mut self.label_counts)
837    }
838
839    fn declare(&mut self, decl: ElementDecl) {
840        self.window.ctx.declare(decl);
841    }
842
843    fn consume_edit(&mut self, id: &str) -> Option<Value> {
844        self.window.ctx.consume_edit(id)
845    }
846
847    fn window_name(&self) -> Arc<str> {
848        self.window.name.clone()
849    }
850
851    fn record_child(&mut self, id: String) {
852        self.children.push(id);
853    }
854}
855
856impl<'a, 'ctx> Grid<'a, 'ctx> {
857    fn new(id: String, window: &'a mut Window<'ctx>, cols: usize) -> Self {
858        Self {
859            id,
860            window,
861            cols,
862            children: Vec::new(),
863            label_counts: HashMap::new(),
864        }
865    }
866
867    fn finish(self) {
868        self.window.ctx.declare(ElementDecl {
869            id: self.id,
870            kind: ElementKind::Grid,
871            label: String::new(),
872            value: Value::GridValue {
873                cols: self.cols,
874                children: self.children,
875            },
876            meta: ElementMeta::default(),
877            window: self.window.name.clone(),
878        });
879    }
880
881    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
882        widget_slider(self, label, value, &range)
883    }
884
885    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
886        widget_slider_f64(self, label, value, &range)
887    }
888
889    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
890        widget_slider_int(self, label, value, &range)
891    }
892
893    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
894        widget_slider_uint(self, label, value, &range)
895    }
896
897    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
898        widget_checkbox(self, label, value)
899    }
900
901    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
902        widget_color3(self, label, value)
903    }
904
905    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
906        widget_color4(self, label, value)
907    }
908
909    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
910        widget_text_input(self, label, value)
911    }
912
913    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
914        widget_dropdown(self, label, selected, options)
915    }
916
917    pub fn button(&mut self, label: &str) -> Response {
918        widget_button(self, label)
919    }
920
921    pub fn label(&mut self, text: &str) {
922        widget_label(self, text);
923    }
924
925    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
926        widget_progress_bar(self, label, value, accent, None);
927    }
928
929    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
930        widget_progress_bar(self, label, value, accent, Some(subtitle));
931    }
932
933    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
934        widget_stat(self, label, value, subvalue, accent);
935    }
936
937    pub fn status(
938        &mut self,
939        label: &str,
940        active: bool,
941        active_text: Option<&str>,
942        inactive_text: Option<&str>,
943        active_color: AccentColor,
944        inactive_color: AccentColor,
945    ) {
946        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
947    }
948
949    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
950        widget_mini_chart(self, label, values, unit, accent);
951    }
952
953    pub fn plot(
954        &mut self,
955        label: &str,
956        series: &[(&str, &[f32], AccentColor)],
957        x_label: Option<&str>,
958        y_label: Option<&str>,
959    ) {
960        // Default to autoscale=true for backward compatibility
961        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
962            .iter()
963            .map(|(name, values, color)| (*name, *values, *color, true))
964            .collect();
965        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
966    }
967
968    /// Plot with explicit autoscale control per series
969    /// series: (name, values, color, autoscale)
970    pub fn plot_with_autoscale(
971        &mut self,
972        label: &str,
973        series: &[(&str, &[f32], AccentColor, bool)],
974        x_label: Option<&str>,
975        y_label: Option<&str>,
976    ) {
977        widget_plot(self, label, series, x_label, y_label);
978    }
979
980    pub fn button_compact(&mut self, label: &str) -> Response {
981        widget_button_compact(self, label, None)
982    }
983
984    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
985        widget_button_compact(self, label, Some(accent))
986    }
987
988    pub fn kv(&mut self, label: &str, value: &str) {
989        widget_kv(self, label, value);
990    }
991
992    pub fn separator(&mut self) {
993        let id = self.make_id("__sep");
994        self.children.push(id.clone());
995        self.window.ctx.declare(ElementDecl {
996            id,
997            kind: ElementKind::Separator,
998            label: String::new(),
999            value: Value::Bool(false),
1000            meta: ElementMeta::default(),
1001            window: self.window.name.clone(),
1002        });
1003    }
1004
1005    pub fn grid<F>(&mut self, cols: usize, f: F)
1006    where
1007        F: FnOnce(&mut Grid<'_, 'ctx>),
1008    {
1009        let grid_id = make_label_id(&self.id, "__grid", &mut self.label_counts);
1010        let mut child_grid = Grid::new(grid_id.clone(), self.window, cols);
1011        f(&mut child_grid);
1012        child_grid.finish();
1013        self.children.push(grid_id);
1014    }
1015}