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_number(sink: &mut impl WidgetSink, label: &str, value: &mut f32) -> Response {
234    let id = sink.make_id(label);
235    let (clicked, changed) = if let Some(Value::Float(v)) = sink.consume_edit(&id) {
236        let new = v as f32;
237        let changed = *value != new;
238        *value = new;
239        (true, changed)
240    } else {
241        (false, false)
242    };
243    sink.record_child(id.clone());
244    sink.declare(ElementDecl {
245        id,
246        kind: ElementKind::NumberInput,
247        label: label.to_string(),
248        value: Value::Float(*value as f64),
249        meta: ElementMeta::default(),
250        window: sink.window_name(),
251    });
252    Response { clicked, changed }
253}
254
255fn widget_number_f64(sink: &mut impl WidgetSink, label: &str, value: &mut f64) -> Response {
256    let id = sink.make_id(label);
257    let (clicked, changed) = if let Some(Value::Float(v)) = sink.consume_edit(&id) {
258        let changed = *value != v;
259        *value = v;
260        (true, changed)
261    } else {
262        (false, false)
263    };
264    sink.record_child(id.clone());
265    sink.declare(ElementDecl {
266        id,
267        kind: ElementKind::NumberInput,
268        label: label.to_string(),
269        value: Value::Float(*value),
270        meta: ElementMeta::default(),
271        window: sink.window_name(),
272    });
273    Response { clicked, changed }
274}
275
276fn widget_number_int(sink: &mut impl WidgetSink, label: &str, value: &mut i32) -> Response {
277    let id = sink.make_id(label);
278    let (clicked, changed) = if let Some(Value::Int(v)) = sink.consume_edit(&id) {
279        let new = v as i32;
280        let changed = *value != new;
281        *value = new;
282        (true, changed)
283    } else {
284        (false, false)
285    };
286    sink.record_child(id.clone());
287    sink.declare(ElementDecl {
288        id,
289        kind: ElementKind::NumberInput,
290        label: label.to_string(),
291        value: Value::Int(*value as i64),
292        meta: ElementMeta::default(),
293        window: sink.window_name(),
294    });
295    Response { clicked, changed }
296}
297
298fn widget_number_uint(sink: &mut impl WidgetSink, label: &str, value: &mut u32) -> Response {
299    let id = sink.make_id(label);
300    let (clicked, changed) = if let Some(Value::Int(v)) = sink.consume_edit(&id) {
301        let new = v.max(0) as u32;
302        let changed = *value != new;
303        *value = new;
304        (true, changed)
305    } else {
306        (false, false)
307    };
308    sink.record_child(id.clone());
309    sink.declare(ElementDecl {
310        id,
311        kind: ElementKind::NumberInput,
312        label: label.to_string(),
313        value: Value::Int(*value as i64),
314        meta: ElementMeta::default(),
315        window: sink.window_name(),
316    });
317    Response { clicked, changed }
318}
319
320fn widget_dropdown(sink: &mut impl WidgetSink, label: &str, selected: &mut usize, options: &[&str]) -> Response {
321    let id = sink.make_id(label);
322    let (clicked, changed) = if let Some(Value::Enum { selected: s, .. }) = sink.consume_edit(&id) {
323        let changed = *selected != s;
324        *selected = s;
325        (true, changed)
326    } else {
327        (false, false)
328    };
329    sink.record_child(id.clone());
330    sink.declare(ElementDecl {
331        id,
332        kind: ElementKind::Dropdown,
333        label: label.to_string(),
334        value: Value::Enum {
335            selected: *selected,
336            options: options.iter().map(|s| s.to_string()).collect(),
337        },
338        meta: ElementMeta::default(),
339        window: sink.window_name(),
340    });
341    Response { clicked, changed }
342}
343
344fn widget_button(sink: &mut impl WidgetSink, label: &str) -> Response {
345    let id = sink.make_id(label);
346    let clicked = matches!(sink.consume_edit(&id), Some(Value::Button(true)));
347    sink.record_child(id.clone());
348    sink.declare(ElementDecl {
349        id,
350        kind: ElementKind::Button,
351        label: label.to_string(),
352        value: Value::Button(false),
353        meta: ElementMeta::default(),
354        window: sink.window_name(),
355    });
356    Response { clicked, changed: clicked }
357}
358
359fn widget_button_compact(sink: &mut impl WidgetSink, label: &str, accent: Option<AccentColor>) -> Response {
360    let id = sink.make_id(label);
361    let clicked = matches!(sink.consume_edit(&id), Some(Value::Button(true)));
362    sink.record_child(id.clone());
363    sink.declare(ElementDecl {
364        id,
365        kind: ElementKind::ButtonCompact,
366        label: label.to_string(),
367        value: Value::Button(false),
368        meta: ElementMeta {
369            accent,
370            ..Default::default()
371        },
372        window: sink.window_name(),
373    });
374    Response { clicked, changed: clicked }
375}
376
377fn widget_label(sink: &mut impl WidgetSink, text: &str) {
378    let id = sink.make_id("__label");
379    sink.record_child(id.clone());
380    sink.declare(ElementDecl {
381        id,
382        kind: ElementKind::Label,
383        label: String::new(),
384        value: Value::String(text.to_string()),
385        meta: ElementMeta::default(),
386        window: sink.window_name(),
387    });
388}
389
390fn widget_kv(sink: &mut impl WidgetSink, label: &str, value: &str) {
391    let id = sink.make_id(label);
392    sink.record_child(id.clone());
393    sink.declare(ElementDecl {
394        id,
395        kind: ElementKind::KeyValue,
396        label: label.to_string(),
397        value: Value::String(value.to_string()),
398        meta: ElementMeta::default(),
399        window: sink.window_name(),
400    });
401}
402
403fn widget_progress_bar(sink: &mut impl WidgetSink, label: &str, value: f64, accent: AccentColor, subtitle: Option<&str>) {
404    let id = sink.make_id(label);
405    sink.record_child(id.clone());
406    sink.declare(ElementDecl {
407        id,
408        kind: ElementKind::ProgressBar,
409        label: label.to_string(),
410        value: Value::Progress(value.clamp(0.0, 1.0)),
411        meta: ElementMeta {
412            accent: Some(accent),
413            subtitle: subtitle.map(|s| s.to_string()),
414            ..Default::default()
415        },
416        window: sink.window_name(),
417    });
418}
419
420fn widget_stat(sink: &mut impl WidgetSink, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
421    let id = sink.make_id(label);
422    sink.record_child(id.clone());
423    sink.declare(ElementDecl {
424        id,
425        kind: ElementKind::Stat,
426        label: label.to_string(),
427        value: Value::StatValue {
428            value: value.to_string(),
429            subvalue: subvalue.map(|s| s.to_string()),
430        },
431        meta: ElementMeta {
432            accent: Some(accent),
433            ..Default::default()
434        },
435        window: sink.window_name(),
436    });
437}
438
439fn widget_status(
440    sink: &mut impl WidgetSink,
441    label: &str,
442    active: bool,
443    active_text: Option<&str>,
444    inactive_text: Option<&str>,
445    active_color: AccentColor,
446    inactive_color: AccentColor,
447) {
448    let id = sink.make_id(label);
449    sink.record_child(id.clone());
450    sink.declare(ElementDecl {
451        id,
452        kind: ElementKind::Status,
453        label: label.to_string(),
454        value: Value::StatusValue {
455            active,
456            active_text: active_text.map(|s| s.to_string()),
457            inactive_text: inactive_text.map(|s| s.to_string()),
458            active_color: Some(active_color.as_str().to_string()),
459            inactive_color: Some(inactive_color.as_str().to_string()),
460        },
461        meta: ElementMeta::default(),
462        window: sink.window_name(),
463    });
464}
465
466fn widget_mini_chart(sink: &mut impl WidgetSink, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
467    let id = sink.make_id(label);
468    sink.record_child(id.clone());
469    sink.declare(ElementDecl {
470        id,
471        kind: ElementKind::MiniChart,
472        label: label.to_string(),
473        value: Value::ChartValue {
474            values: values.to_vec(),
475            current: values.last().copied(),
476            unit: unit.map(|s| s.to_string()),
477        },
478        meta: ElementMeta {
479            accent: Some(accent),
480            ..Default::default()
481        },
482        window: sink.window_name(),
483    });
484}
485
486fn widget_image(sink: &mut impl WidgetSink, label: &str, data_uri: &str, width: Option<u32>, height: Option<u32>) {
487    let id = sink.make_id(label);
488    sink.record_child(id.clone());
489    sink.declare(ElementDecl {
490        id,
491        kind: ElementKind::Image,
492        label: label.to_string(),
493        value: Value::ImageValue {
494            data: data_uri.to_string(),
495            width,
496            height,
497        },
498        meta: ElementMeta::default(),
499        window: sink.window_name(),
500    });
501}
502
503fn widget_image_button(sink: &mut impl WidgetSink, label: &str, data_uri: &str, selected: bool) -> Response {
504    let id = sink.make_id(label);
505    let clicked = matches!(sink.consume_edit(&id), Some(Value::Button(true)));
506    sink.record_child(id.clone());
507    sink.declare(ElementDecl {
508        id,
509        kind: ElementKind::ImageButton,
510        label: label.to_string(),
511        value: Value::ImageValue {
512            data: data_uri.to_string(),
513            width: None,
514            height: None,
515        },
516        meta: ElementMeta {
517            accent: if selected { Some(AccentColor::Teal) } else { None },
518            ..Default::default()
519        },
520        window: sink.window_name(),
521    });
522    Response { clicked, changed: clicked }
523}
524
525fn widget_plot(
526    sink: &mut impl WidgetSink,
527    label: &str,
528    series: &[(&str, &[f32], AccentColor, bool)],
529    x_label: Option<&str>,
530    y_label: Option<&str>,
531) {
532    let id = sink.make_id(label);
533    let plot_series: Vec<PlotSeries> = series
534        .iter()
535        .map(|(name, values, color, autoscale)| PlotSeries {
536            name: name.to_string(),
537            values: values.to_vec(),
538            color: color.as_str().to_string(),
539            autoscale: *autoscale,
540        })
541        .collect();
542    sink.record_child(id.clone());
543    sink.declare(ElementDecl {
544        id,
545        kind: ElementKind::Plot,
546        label: label.to_string(),
547        value: Value::PlotValue {
548            series: plot_series,
549            x_label: x_label.map(|s| s.to_string()),
550            y_label: y_label.map(|s| s.to_string()),
551        },
552        meta: ElementMeta::default(),
553        window: sink.window_name(),
554    });
555}
556
557// ── Label-based ID generation ────────────────────────────────────────
558
559fn make_label_id(prefix: &str, label: &str, label_counts: &mut HashMap<String, usize>) -> String {
560    let count = label_counts.entry(label.to_string()).or_insert(0);
561    let id = if *count == 0 {
562        format!("{prefix}::{label}")
563    } else {
564        format!("{prefix}::{label}#{count}")
565    };
566    *count += 1;
567    id
568}
569
570// ── Window ─────────────────────────────────────────────────────────--
571
572/// A named window containing UI elements. Created via `Context::window()`.
573pub struct Window<'a> {
574    name: Arc<str>,
575    ctx: &'a mut Context,
576    label_counts: HashMap<String, usize>,
577}
578
579impl<'a> WidgetSink for Window<'a> {
580    fn make_id(&mut self, label: &str) -> String {
581        make_label_id(&self.name, label, &mut self.label_counts)
582    }
583
584    fn declare(&mut self, decl: ElementDecl) {
585        self.ctx.declare(decl);
586    }
587
588    fn consume_edit(&mut self, id: &str) -> Option<Value> {
589        self.ctx.consume_edit(id)
590    }
591
592    fn window_name(&self) -> Arc<str> {
593        self.name.clone()
594    }
595
596    fn record_child(&mut self, _id: String) {
597        // Window is top-level — no parent to record into
598    }
599}
600
601impl<'a> Window<'a> {
602    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
603        Self {
604            name: Arc::from(name.as_str()),
605            ctx,
606            label_counts: HashMap::new(),
607        }
608    }
609
610    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
611        widget_slider(self, label, value, &range)
612    }
613
614    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
615        widget_slider_f64(self, label, value, &range)
616    }
617
618    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
619        widget_slider_int(self, label, value, &range)
620    }
621
622    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
623        widget_slider_uint(self, label, value, &range)
624    }
625
626    /// A free-typed numeric field with no slider or range clamp.
627    pub fn number_input(&mut self, label: &str, value: &mut f32) -> Response {
628        widget_number(self, label, value)
629    }
630
631    pub fn number_input_f64(&mut self, label: &str, value: &mut f64) -> Response {
632        widget_number_f64(self, label, value)
633    }
634
635    pub fn number_input_int(&mut self, label: &str, value: &mut i32) -> Response {
636        widget_number_int(self, label, value)
637    }
638
639    pub fn number_input_uint(&mut self, label: &str, value: &mut u32) -> Response {
640        widget_number_uint(self, label, value)
641    }
642
643    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
644        widget_checkbox(self, label, value)
645    }
646
647    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
648        widget_color3(self, label, value)
649    }
650
651    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
652        widget_color4(self, label, value)
653    }
654
655    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
656        widget_text_input(self, label, value)
657    }
658
659    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
660        widget_dropdown(self, label, selected, options)
661    }
662
663    pub fn button(&mut self, label: &str) -> Response {
664        widget_button(self, label)
665    }
666
667    pub fn label(&mut self, text: &str) {
668        let id = self.make_id("__label");
669        self.record_child(id.clone());
670        self.declare(ElementDecl {
671            id,
672            kind: ElementKind::LabelInline,
673            label: String::new(),
674            value: Value::String(text.to_string()),
675            meta: ElementMeta::default(),
676            window: self.window_name(),
677        });
678    }
679
680    pub fn kv(&mut self, label: &str, value: &str) {
681        widget_kv(self, label, value);
682    }
683
684    pub fn kv_value(&mut self, label: &str, value: &mut String) -> Response {
685        let id = self.make_id(label);
686        let (clicked, changed) = if let Some(Value::String(v)) = self.ctx.consume_edit(&id) {
687            let changed = *value != v;
688            *value = v;
689            (true, changed)
690        } else {
691            (false, false)
692        };
693        self.ctx.declare(ElementDecl {
694            id,
695            kind: ElementKind::KeyValue,
696            label: label.to_string(),
697            value: Value::String(value.clone()),
698            meta: ElementMeta::default(),
699            window: self.name.clone(),
700        });
701        Response { clicked, changed }
702    }
703
704    pub fn button_compact(&mut self, label: &str) -> Response {
705        widget_button_compact(self, label, None)
706    }
707
708    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
709        widget_button_compact(self, label, Some(accent))
710    }
711
712    pub fn horizontal<F>(&mut self, f: F)
713    where
714        F: FnOnce(&mut Horizontal<'_, 'a>),
715    {
716        let h_id = self.make_id("__horiz");
717        let mut horiz = Horizontal::new(h_id, self);
718        f(&mut horiz);
719        horiz.finish();
720    }
721
722    pub fn separator(&mut self) {
723        let id = self.make_id("__sep");
724        self.ctx.declare(ElementDecl {
725            id,
726            kind: ElementKind::Separator,
727            label: String::new(),
728            value: Value::Bool(false),
729            meta: ElementMeta::default(),
730            window: self.name.clone(),
731        });
732    }
733
734    pub fn section(&mut self, title: &str) {
735        let id = self.make_id(title);
736        self.ctx.declare(ElementDecl {
737            id,
738            kind: ElementKind::Section,
739            label: title.to_string(),
740            value: Value::String(title.to_string()),
741            meta: ElementMeta::default(),
742            window: self.name.clone(),
743        });
744    }
745
746    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
747        widget_progress_bar(self, label, value, accent, None);
748    }
749
750    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
751        widget_progress_bar(self, label, value, accent, Some(subtitle));
752    }
753
754    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
755        widget_stat(self, label, value, subvalue, accent);
756    }
757
758    pub fn status(
759        &mut self,
760        label: &str,
761        active: bool,
762        active_text: Option<&str>,
763        inactive_text: Option<&str>,
764        active_color: AccentColor,
765        inactive_color: AccentColor,
766    ) {
767        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
768    }
769
770    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
771        widget_mini_chart(self, label, values, unit, accent);
772    }
773
774    pub fn set_accent(&mut self, accent: AccentColor) {
775        let id = self.make_id(&format!("__accent_{}", accent.as_str()));
776        self.ctx.declare(ElementDecl {
777            id,
778            kind: ElementKind::Label,
779            label: String::new(),
780            value: Value::String(String::new()),
781            meta: ElementMeta {
782                accent: Some(accent),
783                ..Default::default()
784            },
785            window: self.name.clone(),
786        });
787    }
788
789    pub fn grid<F>(&mut self, cols: usize, f: F)
790    where
791        F: FnOnce(&mut Grid<'_, 'a>),
792    {
793        let grid_id = self.make_id("__grid");
794        let mut grid = Grid::new(grid_id, self, cols);
795        f(&mut grid);
796        grid.finish();
797    }
798
799    pub fn plot(
800        &mut self,
801        label: &str,
802        series: &[(&str, &[f32], AccentColor)],
803        x_label: Option<&str>,
804        y_label: Option<&str>,
805    ) {
806        // Default to autoscale=true for backward compatibility
807        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
808            .iter()
809            .map(|(name, values, color)| (*name, *values, *color, true))
810            .collect();
811        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
812    }
813
814    /// Plot with explicit autoscale control per series
815    /// series: (name, values, color, autoscale)
816    pub fn plot_with_autoscale(
817        &mut self,
818        label: &str,
819        series: &[(&str, &[f32], AccentColor, bool)],
820        x_label: Option<&str>,
821        y_label: Option<&str>,
822    ) {
823        widget_plot(self, label, series, x_label, y_label);
824    }
825
826    /// Display an image from a base64 data URI (e.g. `"data:image/png;base64,…"`).
827    pub fn image(&mut self, label: &str, data_uri: &str) {
828        widget_image(self, label, data_uri, None, None);
829    }
830
831    /// Display an image with an explicit size hint.
832    pub fn image_with_size(&mut self, label: &str, data_uri: &str, width: u32, height: u32) {
833        widget_image(self, label, data_uri, Some(width), Some(height));
834    }
835
836    /// Display a clickable image (thumbnail button). `selected` draws a highlight
837    /// border. Returns a `Response` whose `clicked()` is true the frame it is pressed.
838    pub fn image_button(&mut self, label: &str, data_uri: &str, selected: bool) -> Response {
839        widget_image_button(self, label, data_uri, selected)
840    }
841}
842
843// ── Horizontal ───────────────────────────────────────────────────────
844
845/// A horizontal layout container for arranging widgets side by side.
846pub struct Horizontal<'a, 'ctx> {
847    id: String,
848    window: &'a mut Window<'ctx>,
849    children: Vec<String>,
850    label_counts: HashMap<String, usize>,
851}
852
853impl<'a, 'ctx> WidgetSink for Horizontal<'a, 'ctx> {
854    fn make_id(&mut self, label: &str) -> String {
855        make_label_id(&self.id, label, &mut self.label_counts)
856    }
857
858    fn declare(&mut self, decl: ElementDecl) {
859        self.window.ctx.declare(decl);
860    }
861
862    fn consume_edit(&mut self, id: &str) -> Option<Value> {
863        self.window.ctx.consume_edit(id)
864    }
865
866    fn window_name(&self) -> Arc<str> {
867        self.window.name.clone()
868    }
869
870    fn record_child(&mut self, id: String) {
871        self.children.push(id);
872    }
873}
874
875impl<'a, 'ctx> Horizontal<'a, 'ctx> {
876    fn new(id: String, window: &'a mut Window<'ctx>) -> Self {
877        Self {
878            id,
879            window,
880            children: Vec::new(),
881            label_counts: HashMap::new(),
882        }
883    }
884
885    fn finish(self) {
886        self.window.ctx.declare(ElementDecl {
887            id: self.id,
888            kind: ElementKind::Horizontal,
889            label: String::new(),
890            value: Value::GridValue {
891                cols: self.children.len(),
892                children: self.children,
893            },
894            meta: ElementMeta::default(),
895            window: self.window.name.clone(),
896        });
897    }
898
899    pub fn button(&mut self, label: &str) -> Response {
900        self.button_accent_inner(label, None)
901    }
902
903    pub fn button_accent(&mut self, label: &str, accent: AccentColor) -> Response {
904        self.button_accent_inner(label, Some(accent))
905    }
906
907    fn button_accent_inner(&mut self, label: &str, accent: Option<AccentColor>) -> Response {
908        let id = self.make_id(label);
909        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
910        self.children.push(id.clone());
911        self.window.ctx.declare(ElementDecl {
912            id,
913            kind: ElementKind::ButtonInline,
914            label: label.to_string(),
915            value: Value::Button(false),
916            meta: ElementMeta {
917                accent,
918                ..Default::default()
919            },
920            window: self.window.name.clone(),
921        });
922        Response { clicked, changed: clicked }
923    }
924
925    pub fn label(&mut self, text: &str) {
926        let id = self.make_id("__label");
927        self.children.push(id.clone());
928        self.window.ctx.declare(ElementDecl {
929            id,
930            kind: ElementKind::LabelInline,
931            label: String::new(),
932            value: Value::String(text.to_string()),
933            meta: ElementMeta::default(),
934            window: self.window.name.clone(),
935        });
936    }
937
938    pub fn kv(&mut self, label: &str, value: &str) {
939        widget_kv(self, label, value);
940    }
941
942    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
943        widget_text_input(self, label, value)
944    }
945
946    pub fn text_input_inline(&mut self, placeholder: &str, value: &mut String) -> Response {
947        let id = self.make_id(placeholder);
948        let (clicked, changed) = if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
949            let changed = *value != s;
950            *value = s;
951            (true, changed)
952        } else {
953            (false, false)
954        };
955        self.children.push(id.clone());
956        self.window.ctx.declare(ElementDecl {
957            id,
958            kind: ElementKind::TextInputInline,
959            label: placeholder.to_string(),
960            value: Value::String(value.clone()),
961            meta: ElementMeta::default(),
962            window: self.window.name.clone(),
963        });
964        Response { clicked, changed }
965    }
966
967    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
968        widget_slider(self, label, value, &range)
969    }
970
971    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
972        widget_slider_f64(self, label, value, &range)
973    }
974
975    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
976        widget_slider_int(self, label, value, &range)
977    }
978
979    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
980        widget_slider_uint(self, label, value, &range)
981    }
982
983    /// A free-typed numeric field with no slider or range clamp.
984    pub fn number_input(&mut self, label: &str, value: &mut f32) -> Response {
985        widget_number(self, label, value)
986    }
987
988    pub fn number_input_f64(&mut self, label: &str, value: &mut f64) -> Response {
989        widget_number_f64(self, label, value)
990    }
991
992    pub fn number_input_int(&mut self, label: &str, value: &mut i32) -> Response {
993        widget_number_int(self, label, value)
994    }
995
996    pub fn number_input_uint(&mut self, label: &str, value: &mut u32) -> Response {
997        widget_number_uint(self, label, value)
998    }
999
1000    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
1001        widget_checkbox(self, label, value)
1002    }
1003
1004    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
1005        widget_color3(self, label, value)
1006    }
1007
1008    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
1009        widget_color4(self, label, value)
1010    }
1011
1012    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
1013        widget_dropdown(self, label, selected, options)
1014    }
1015
1016    pub fn image(&mut self, label: &str, data_uri: &str) {
1017        widget_image(self, label, data_uri, None, None);
1018    }
1019}
1020
1021// ── Grid ─────────────────────────────────────────────────────────────
1022
1023/// A grid container for arranging elements in columns.
1024pub struct Grid<'a, 'ctx> {
1025    id: String,
1026    window: &'a mut Window<'ctx>,
1027    cols: usize,
1028    children: Vec<String>,
1029    label_counts: HashMap<String, usize>,
1030}
1031
1032impl<'a, 'ctx> WidgetSink for Grid<'a, 'ctx> {
1033    fn make_id(&mut self, label: &str) -> String {
1034        make_label_id(&self.id, label, &mut self.label_counts)
1035    }
1036
1037    fn declare(&mut self, decl: ElementDecl) {
1038        self.window.ctx.declare(decl);
1039    }
1040
1041    fn consume_edit(&mut self, id: &str) -> Option<Value> {
1042        self.window.ctx.consume_edit(id)
1043    }
1044
1045    fn window_name(&self) -> Arc<str> {
1046        self.window.name.clone()
1047    }
1048
1049    fn record_child(&mut self, id: String) {
1050        self.children.push(id);
1051    }
1052}
1053
1054impl<'a, 'ctx> Grid<'a, 'ctx> {
1055    fn new(id: String, window: &'a mut Window<'ctx>, cols: usize) -> Self {
1056        Self {
1057            id,
1058            window,
1059            cols,
1060            children: Vec::new(),
1061            label_counts: HashMap::new(),
1062        }
1063    }
1064
1065    fn finish(self) {
1066        self.window.ctx.declare(ElementDecl {
1067            id: self.id,
1068            kind: ElementKind::Grid,
1069            label: String::new(),
1070            value: Value::GridValue {
1071                cols: self.cols,
1072                children: self.children,
1073            },
1074            meta: ElementMeta::default(),
1075            window: self.window.name.clone(),
1076        });
1077    }
1078
1079    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) -> Response {
1080        widget_slider(self, label, value, &range)
1081    }
1082
1083    pub fn slider_f64(&mut self, label: &str, value: &mut f64, range: RangeInclusive<f64>) -> Response {
1084        widget_slider_f64(self, label, value, &range)
1085    }
1086
1087    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) -> Response {
1088        widget_slider_int(self, label, value, &range)
1089    }
1090
1091    pub fn slider_uint(&mut self, label: &str, value: &mut u32, range: RangeInclusive<u32>) -> Response {
1092        widget_slider_uint(self, label, value, &range)
1093    }
1094
1095    /// A free-typed numeric field with no slider or range clamp.
1096    pub fn number_input(&mut self, label: &str, value: &mut f32) -> Response {
1097        widget_number(self, label, value)
1098    }
1099
1100    pub fn number_input_f64(&mut self, label: &str, value: &mut f64) -> Response {
1101        widget_number_f64(self, label, value)
1102    }
1103
1104    pub fn number_input_int(&mut self, label: &str, value: &mut i32) -> Response {
1105        widget_number_int(self, label, value)
1106    }
1107
1108    pub fn number_input_uint(&mut self, label: &str, value: &mut u32) -> Response {
1109        widget_number_uint(self, label, value)
1110    }
1111
1112    pub fn checkbox(&mut self, label: &str, value: &mut bool) -> Response {
1113        widget_checkbox(self, label, value)
1114    }
1115
1116    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) -> Response {
1117        widget_color3(self, label, value)
1118    }
1119
1120    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) -> Response {
1121        widget_color4(self, label, value)
1122    }
1123
1124    pub fn text_input(&mut self, label: &str, value: &mut String) -> Response {
1125        widget_text_input(self, label, value)
1126    }
1127
1128    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) -> Response {
1129        widget_dropdown(self, label, selected, options)
1130    }
1131
1132    pub fn button(&mut self, label: &str) -> Response {
1133        widget_button(self, label)
1134    }
1135
1136    pub fn label(&mut self, text: &str) {
1137        widget_label(self, text);
1138    }
1139
1140    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
1141        widget_progress_bar(self, label, value, accent, None);
1142    }
1143
1144    pub fn progress_bar_with_subtitle(&mut self, label: &str, value: f64, accent: AccentColor, subtitle: &str) {
1145        widget_progress_bar(self, label, value, accent, Some(subtitle));
1146    }
1147
1148    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
1149        widget_stat(self, label, value, subvalue, accent);
1150    }
1151
1152    pub fn status(
1153        &mut self,
1154        label: &str,
1155        active: bool,
1156        active_text: Option<&str>,
1157        inactive_text: Option<&str>,
1158        active_color: AccentColor,
1159        inactive_color: AccentColor,
1160    ) {
1161        widget_status(self, label, active, active_text, inactive_text, active_color, inactive_color);
1162    }
1163
1164    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
1165        widget_mini_chart(self, label, values, unit, accent);
1166    }
1167
1168    pub fn plot(
1169        &mut self,
1170        label: &str,
1171        series: &[(&str, &[f32], AccentColor)],
1172        x_label: Option<&str>,
1173        y_label: Option<&str>,
1174    ) {
1175        // Default to autoscale=true for backward compatibility
1176        let series_with_autoscale: Vec<(&str, &[f32], AccentColor, bool)> = series
1177            .iter()
1178            .map(|(name, values, color)| (*name, *values, *color, true))
1179            .collect();
1180        widget_plot(self, label, &series_with_autoscale, x_label, y_label);
1181    }
1182
1183    /// Plot with explicit autoscale control per series
1184    /// series: (name, values, color, autoscale)
1185    pub fn plot_with_autoscale(
1186        &mut self,
1187        label: &str,
1188        series: &[(&str, &[f32], AccentColor, bool)],
1189        x_label: Option<&str>,
1190        y_label: Option<&str>,
1191    ) {
1192        widget_plot(self, label, series, x_label, y_label);
1193    }
1194
1195    pub fn image(&mut self, label: &str, data_uri: &str) {
1196        widget_image(self, label, data_uri, None, None);
1197    }
1198
1199    /// Clickable image (thumbnail button); `selected` draws a highlight border.
1200    pub fn image_button(&mut self, label: &str, data_uri: &str, selected: bool) -> Response {
1201        widget_image_button(self, label, data_uri, selected)
1202    }
1203
1204    pub fn button_compact(&mut self, label: &str) -> Response {
1205        widget_button_compact(self, label, None)
1206    }
1207
1208    pub fn button_compact_accent(&mut self, label: &str, accent: AccentColor) -> Response {
1209        widget_button_compact(self, label, Some(accent))
1210    }
1211
1212    pub fn kv(&mut self, label: &str, value: &str) {
1213        widget_kv(self, label, value);
1214    }
1215
1216    pub fn separator(&mut self) {
1217        let id = self.make_id("__sep");
1218        self.children.push(id.clone());
1219        self.window.ctx.declare(ElementDecl {
1220            id,
1221            kind: ElementKind::Separator,
1222            label: String::new(),
1223            value: Value::Bool(false),
1224            meta: ElementMeta::default(),
1225            window: self.window.name.clone(),
1226        });
1227    }
1228
1229    pub fn grid<F>(&mut self, cols: usize, f: F)
1230    where
1231        F: FnOnce(&mut Grid<'_, 'ctx>),
1232    {
1233        let grid_id = make_label_id(&self.id, "__grid", &mut self.label_counts);
1234        let mut child_grid = Grid::new(grid_id.clone(), self.window, cols);
1235        f(&mut child_grid);
1236        child_grid.finish();
1237        self.children.push(grid_id);
1238    }
1239}