Skip to main content

w_gui/
window.rs

1use std::ops::RangeInclusive;
2use std::sync::Arc;
3
4use crate::context::Context;
5use crate::element::{AccentColor, ElementDecl, ElementKind, ElementMeta, PlotSeries, Value};
6
7// ── Element declaration builders ─────────────────────────────────────
8// Shared by Window and Grid to avoid duplicating ElementDecl construction.
9
10fn build_slider(
11    id: String,
12    window: Arc<str>,
13    label: &str,
14    value: f32,
15    range: &RangeInclusive<f32>,
16) -> ElementDecl {
17    ElementDecl {
18        id,
19        kind: ElementKind::Slider,
20        label: label.to_string(),
21        value: Value::Float(value as f64),
22        meta: ElementMeta {
23            min: Some(*range.start() as f64),
24            max: Some(*range.end() as f64),
25            step: Some(0.01),
26            ..Default::default()
27        },
28        window,
29    }
30}
31
32fn build_slider_int(
33    id: String,
34    window: Arc<str>,
35    label: &str,
36    value: i32,
37    range: &RangeInclusive<i32>,
38) -> ElementDecl {
39    ElementDecl {
40        id,
41        kind: ElementKind::Slider,
42        label: label.to_string(),
43        value: Value::Int(value as i64),
44        meta: ElementMeta {
45            min: Some(*range.start() as f64),
46            max: Some(*range.end() as f64),
47            step: Some(1.0),
48            ..Default::default()
49        },
50        window,
51    }
52}
53
54fn build_checkbox(id: String, window: Arc<str>, label: &str, value: bool) -> ElementDecl {
55    ElementDecl {
56        id,
57        kind: ElementKind::Checkbox,
58        label: label.to_string(),
59        value: Value::Bool(value),
60        meta: ElementMeta::default(),
61        window,
62    }
63}
64
65fn build_color3(id: String, window: Arc<str>, label: &str, value: [f32; 3]) -> ElementDecl {
66    ElementDecl {
67        id,
68        kind: ElementKind::ColorPicker3,
69        label: label.to_string(),
70        value: Value::Color3(value),
71        meta: ElementMeta::default(),
72        window,
73    }
74}
75
76fn build_color4(id: String, window: Arc<str>, label: &str, value: [f32; 4]) -> ElementDecl {
77    ElementDecl {
78        id,
79        kind: ElementKind::ColorPicker4,
80        label: label.to_string(),
81        value: Value::Color4(value),
82        meta: ElementMeta::default(),
83        window,
84    }
85}
86
87fn build_text_input(id: String, window: Arc<str>, label: &str, value: &str) -> ElementDecl {
88    ElementDecl {
89        id,
90        kind: ElementKind::TextInput,
91        label: label.to_string(),
92        value: Value::String(value.to_string()),
93        meta: ElementMeta::default(),
94        window,
95    }
96}
97
98fn build_dropdown(
99    id: String,
100    window: Arc<str>,
101    label: &str,
102    selected: usize,
103    options: &[&str],
104) -> ElementDecl {
105    ElementDecl {
106        id,
107        kind: ElementKind::Dropdown,
108        label: label.to_string(),
109        value: Value::Enum {
110            selected,
111            options: options.iter().map(|s| s.to_string()).collect(),
112        },
113        meta: ElementMeta::default(),
114        window,
115    }
116}
117
118fn build_button(id: String, window: Arc<str>, label: &str) -> ElementDecl {
119    ElementDecl {
120        id,
121        kind: ElementKind::Button,
122        label: label.to_string(),
123        value: Value::Button(false),
124        meta: ElementMeta::default(),
125        window,
126    }
127}
128
129fn build_label(id: String, window: Arc<str>, text: &str) -> ElementDecl {
130    ElementDecl {
131        id,
132        kind: ElementKind::Label,
133        label: text.to_string(),
134        value: Value::String(text.to_string()),
135        meta: ElementMeta::default(),
136        window,
137    }
138}
139
140fn build_progress_bar(
141    id: String,
142    window: Arc<str>,
143    label: &str,
144    value: f64,
145    accent: AccentColor,
146    subtitle: Option<&str>,
147) -> ElementDecl {
148    ElementDecl {
149        id,
150        kind: ElementKind::ProgressBar,
151        label: label.to_string(),
152        value: Value::Progress(value.clamp(0.0, 1.0)),
153        meta: ElementMeta {
154            accent: Some(accent.as_str().to_string()),
155            subtitle: subtitle.map(|s| s.to_string()),
156            ..Default::default()
157        },
158        window,
159    }
160}
161
162fn build_stat(
163    id: String,
164    window: Arc<str>,
165    label: &str,
166    value: &str,
167    subvalue: Option<&str>,
168    accent: AccentColor,
169) -> ElementDecl {
170    ElementDecl {
171        id,
172        kind: ElementKind::Stat,
173        label: label.to_string(),
174        value: Value::StatValue {
175            value: value.to_string(),
176            subvalue: subvalue.map(|s| s.to_string()),
177        },
178        meta: ElementMeta {
179            accent: Some(accent.as_str().to_string()),
180            ..Default::default()
181        },
182        window,
183    }
184}
185
186fn build_status(
187    id: String,
188    window: Arc<str>,
189    label: &str,
190    active: bool,
191    active_text: Option<&str>,
192    inactive_text: Option<&str>,
193    active_color: AccentColor,
194    inactive_color: AccentColor,
195) -> ElementDecl {
196    ElementDecl {
197        id,
198        kind: ElementKind::Status,
199        label: label.to_string(),
200        value: Value::StatusValue {
201            active,
202            active_text: active_text.map(|s| s.to_string()),
203            inactive_text: inactive_text.map(|s| s.to_string()),
204            active_color: Some(active_color.as_str().to_string()),
205            inactive_color: Some(inactive_color.as_str().to_string()),
206        },
207        meta: ElementMeta::default(),
208        window,
209    }
210}
211
212fn build_mini_chart(
213    id: String,
214    window: Arc<str>,
215    label: &str,
216    values: &[f32],
217    unit: Option<&str>,
218    accent: AccentColor,
219) -> ElementDecl {
220    ElementDecl {
221        id,
222        kind: ElementKind::MiniChart,
223        label: label.to_string(),
224        value: Value::ChartValue {
225            values: values.to_vec(),
226            current: values.last().copied(),
227            unit: unit.map(|s| s.to_string()),
228        },
229        meta: ElementMeta {
230            accent: Some(accent.as_str().to_string()),
231            ..Default::default()
232        },
233        window,
234    }
235}
236
237fn build_plot(
238    id: String,
239    window: Arc<str>,
240    label: &str,
241    series: &[(&str, &[f32], AccentColor)],
242    x_label: Option<&str>,
243    y_label: Option<&str>,
244) -> ElementDecl {
245    let plot_series: Vec<PlotSeries> = series
246        .iter()
247        .map(|(name, values, color)| PlotSeries {
248            name: name.to_string(),
249            values: values.to_vec(),
250            color: color.as_str().to_string(),
251        })
252        .collect();
253
254    ElementDecl {
255        id,
256        kind: ElementKind::Plot,
257        label: label.to_string(),
258        value: Value::PlotValue {
259            series: plot_series,
260            x_label: x_label.map(|s| s.to_string()),
261            y_label: y_label.map(|s| s.to_string()),
262        },
263        meta: ElementMeta::default(),
264        window,
265    }
266}
267
268// ── Window ───────────────────────────────────────────────────────────
269
270/// A named window containing UI elements. Created via `Context::window()`.
271pub struct Window<'a> {
272    name: Arc<str>,
273    ctx: &'a mut Context,
274}
275
276impl<'a> Window<'a> {
277    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
278        Self {
279            name: Arc::from(name.as_str()),
280            ctx,
281        }
282    }
283
284    fn make_id(&self, label: &str) -> String {
285        format!("{}::{}", self.name, label)
286    }
287
288    /// A floating-point slider with a range.
289    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) {
290        let id = self.make_id(label);
291        if let Some(Value::Float(v)) = self.ctx.consume_edit(&id) {
292            *value = v as f32;
293        }
294        self.ctx
295            .declare(build_slider(id, self.name.clone(), label, *value, &range));
296    }
297
298    /// An integer slider with a range.
299    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) {
300        let id = self.make_id(label);
301        if let Some(Value::Int(v)) = self.ctx.consume_edit(&id) {
302            *value = v as i32;
303        }
304        self.ctx
305            .declare(build_slider_int(id, self.name.clone(), label, *value, &range));
306    }
307
308    /// A checkbox (boolean toggle).
309    pub fn checkbox(&mut self, label: &str, value: &mut bool) {
310        let id = self.make_id(label);
311        if let Some(Value::Bool(v)) = self.ctx.consume_edit(&id) {
312            *value = v;
313        }
314        self.ctx
315            .declare(build_checkbox(id, self.name.clone(), label, *value));
316    }
317
318    /// An RGB color picker (3-component).
319    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) {
320        let id = self.make_id(label);
321        if let Some(Value::Color3(c)) = self.ctx.consume_edit(&id) {
322            *value = c;
323        }
324        self.ctx
325            .declare(build_color3(id, self.name.clone(), label, *value));
326    }
327
328    /// An RGBA color picker (4-component).
329    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) {
330        let id = self.make_id(label);
331        if let Some(Value::Color4(c)) = self.ctx.consume_edit(&id) {
332            *value = c;
333        }
334        self.ctx
335            .declare(build_color4(id, self.name.clone(), label, *value));
336    }
337
338    /// A text input field.
339    pub fn text_input(&mut self, label: &str, value: &mut String) {
340        let id = self.make_id(label);
341        if let Some(Value::String(s)) = self.ctx.consume_edit(&id) {
342            *value = s;
343        }
344        self.ctx
345            .declare(build_text_input(id, self.name.clone(), label, value));
346    }
347
348    /// A dropdown selector.
349    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) {
350        let id = self.make_id(label);
351        if let Some(Value::Enum { selected: s, .. }) = self.ctx.consume_edit(&id) {
352            *selected = s;
353        }
354        self.ctx
355            .declare(build_dropdown(id, self.name.clone(), label, *selected, options));
356    }
357
358    /// A button. Returns `true` for one frame when clicked.
359    pub fn button(&mut self, label: &str) -> bool {
360        let id = self.make_id(label);
361        let clicked = matches!(self.ctx.consume_edit(&id), Some(Value::Button(true)));
362        self.ctx
363            .declare(build_button(id, self.name.clone(), label));
364        clicked
365    }
366
367    /// A read-only text label.
368    pub fn label(&mut self, text: &str) {
369        let id = self.make_id(text);
370        self.ctx
371            .declare(build_label(id, self.name.clone(), text));
372    }
373
374    /// A visual separator line.
375    pub fn separator(&mut self) {
376        let id = format!("{}::__sep_{}", self.name, self.ctx.current_frame_len());
377        self.ctx.declare(ElementDecl {
378            id,
379            kind: ElementKind::Separator,
380            label: String::new(),
381            value: Value::Bool(false),
382            meta: ElementMeta::default(),
383            window: self.name.clone(),
384        });
385    }
386
387    /// A section header for grouping widgets.
388    pub fn section(&mut self, title: &str) {
389        let id = format!("{}::__sec_{}", self.name, self.ctx.current_frame_len());
390        self.ctx.declare(ElementDecl {
391            id,
392            kind: ElementKind::Section,
393            label: title.to_string(),
394            value: Value::String(title.to_string()),
395            meta: ElementMeta::default(),
396            window: self.name.clone(),
397        });
398    }
399
400    /// A progress bar (value from 0.0 to 1.0).
401    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
402        let id = self.make_id(label);
403        self.ctx
404            .declare(build_progress_bar(id, self.name.clone(), label, value, accent, None));
405    }
406
407    /// A progress bar with subtitle text.
408    pub fn progress_bar_with_subtitle(
409        &mut self,
410        label: &str,
411        value: f64,
412        accent: AccentColor,
413        subtitle: &str,
414    ) {
415        let id = self.make_id(label);
416        self.ctx.declare(build_progress_bar(
417            id,
418            self.name.clone(),
419            label,
420            value,
421            accent,
422            Some(subtitle),
423        ));
424    }
425
426    /// A stat card displaying a value with optional subvalue.
427    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
428        let id = self.make_id(label);
429        self.ctx
430            .declare(build_stat(id, self.name.clone(), label, value, subvalue, accent));
431    }
432
433    /// A status indicator with colored dot.
434    pub fn status(
435        &mut self,
436        label: &str,
437        active: bool,
438        active_text: Option<&str>,
439        inactive_text: Option<&str>,
440        active_color: AccentColor,
441        inactive_color: AccentColor,
442    ) {
443        let id = self.make_id(label);
444        self.ctx.declare(build_status(
445            id,
446            self.name.clone(),
447            label,
448            active,
449            active_text,
450            inactive_text,
451            active_color,
452            inactive_color,
453        ));
454    }
455
456    /// A mini sparkline chart.
457    pub fn mini_chart(
458        &mut self,
459        label: &str,
460        values: &[f32],
461        unit: Option<&str>,
462        accent: AccentColor,
463    ) {
464        let id = self.make_id(label);
465        self.ctx
466            .declare(build_mini_chart(id, self.name.clone(), label, values, unit, accent));
467    }
468
469    /// Set the accent color for this window.
470    /// Call this first before other widgets in the window.
471    pub fn set_accent(&mut self, accent: AccentColor) {
472        let id = format!("{}::__accent_{}", self.name, accent.as_str());
473        self.ctx.declare(ElementDecl {
474            id,
475            kind: ElementKind::Label,
476            label: String::new(),
477            value: Value::String(String::new()),
478            meta: ElementMeta {
479                accent: Some(accent.as_str().to_string()),
480                ..Default::default()
481            },
482            window: self.name.clone(),
483        });
484    }
485
486    /// Create a grid layout container. Elements added within the closure will be
487    /// arranged in a grid with the specified number of columns.
488    pub fn grid<F>(&mut self, cols: usize, f: F)
489    where
490        F: FnOnce(&mut Grid<'_, 'a>),
491    {
492        let grid_id = format!("{}::__grid_{}", self.name, self.ctx.current_frame_len());
493        let mut grid = Grid::new(&grid_id, self, cols);
494        f(&mut grid);
495        grid.finish();
496    }
497
498    /// Plot a data series as a larger chart.
499    pub fn plot(
500        &mut self,
501        label: &str,
502        series: &[(&str, &[f32], AccentColor)],
503        x_label: Option<&str>,
504        y_label: Option<&str>,
505    ) {
506        let id = self.make_id(label);
507        self.ctx
508            .declare(build_plot(id, self.name.clone(), label, series, x_label, y_label));
509    }
510}
511
512// ── Grid ─────────────────────────────────────────────────────────────
513
514/// A grid container for arranging elements in columns.
515pub struct Grid<'a, 'ctx> {
516    id: String,
517    window: &'a mut Window<'ctx>,
518    cols: usize,
519    children: Vec<String>,
520}
521
522impl<'a, 'ctx> Grid<'a, 'ctx> {
523    fn new(id: &str, window: &'a mut Window<'ctx>, cols: usize) -> Self {
524        Self {
525            id: id.to_string(),
526            window,
527            cols,
528            children: Vec::new(),
529        }
530    }
531
532    fn record_child(&mut self, id: String) {
533        self.children.push(id);
534    }
535
536    fn finish(self) {
537        self.window.ctx.declare(ElementDecl {
538            id: self.id,
539            kind: ElementKind::Grid,
540            label: String::new(),
541            value: Value::GridValue {
542                cols: self.cols,
543                children: self.children,
544            },
545            meta: ElementMeta::default(),
546            window: self.window.name.clone(),
547        });
548    }
549
550    fn make_id(&self, label: &str) -> String {
551        format!("{}::{}", self.id, label)
552    }
553
554    // ── Interactive widgets ──────────────────────────────────────────
555
556    /// A floating-point slider.
557    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) {
558        let id = self.make_id(label);
559        if let Some(Value::Float(v)) = self.window.ctx.consume_edit(&id) {
560            *value = v as f32;
561        }
562        self.record_child(id.clone());
563        self.window
564            .ctx
565            .declare(build_slider(id, self.window.name.clone(), label, *value, &range));
566    }
567
568    /// An integer slider.
569    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) {
570        let id = self.make_id(label);
571        if let Some(Value::Int(v)) = self.window.ctx.consume_edit(&id) {
572            *value = v as i32;
573        }
574        self.record_child(id.clone());
575        self.window.ctx.declare(build_slider_int(
576            id,
577            self.window.name.clone(),
578            label,
579            *value,
580            &range,
581        ));
582    }
583
584    /// A checkbox.
585    pub fn checkbox(&mut self, label: &str, value: &mut bool) {
586        let id = self.make_id(label);
587        if let Some(Value::Bool(v)) = self.window.ctx.consume_edit(&id) {
588            *value = v;
589        }
590        self.record_child(id.clone());
591        self.window
592            .ctx
593            .declare(build_checkbox(id, self.window.name.clone(), label, *value));
594    }
595
596    /// An RGB color picker.
597    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) {
598        let id = self.make_id(label);
599        if let Some(Value::Color3(c)) = self.window.ctx.consume_edit(&id) {
600            *value = c;
601        }
602        self.record_child(id.clone());
603        self.window
604            .ctx
605            .declare(build_color3(id, self.window.name.clone(), label, *value));
606    }
607
608    /// An RGBA color picker.
609    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) {
610        let id = self.make_id(label);
611        if let Some(Value::Color4(c)) = self.window.ctx.consume_edit(&id) {
612            *value = c;
613        }
614        self.record_child(id.clone());
615        self.window
616            .ctx
617            .declare(build_color4(id, self.window.name.clone(), label, *value));
618    }
619
620    /// A text input field.
621    pub fn text_input(&mut self, label: &str, value: &mut String) {
622        let id = self.make_id(label);
623        if let Some(Value::String(s)) = self.window.ctx.consume_edit(&id) {
624            *value = s;
625        }
626        self.record_child(id.clone());
627        self.window
628            .ctx
629            .declare(build_text_input(id, self.window.name.clone(), label, value));
630    }
631
632    /// A dropdown selector.
633    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) {
634        let id = self.make_id(label);
635        if let Some(Value::Enum { selected: s, .. }) = self.window.ctx.consume_edit(&id) {
636            *selected = s;
637        }
638        self.record_child(id.clone());
639        self.window.ctx.declare(build_dropdown(
640            id,
641            self.window.name.clone(),
642            label,
643            *selected,
644            options,
645        ));
646    }
647
648    /// A button. Returns `true` for one frame when clicked.
649    pub fn button(&mut self, label: &str) -> bool {
650        let id = self.make_id(label);
651        let clicked = matches!(self.window.ctx.consume_edit(&id), Some(Value::Button(true)));
652        self.record_child(id.clone());
653        self.window
654            .ctx
655            .declare(build_button(id, self.window.name.clone(), label));
656        clicked
657    }
658
659    // ── Display widgets ──────────────────────────────────────────────
660
661    /// A read-only text label.
662    pub fn label(&mut self, text: &str) {
663        let id = self.make_id(text);
664        self.record_child(id.clone());
665        self.window
666            .ctx
667            .declare(build_label(id, self.window.name.clone(), text));
668    }
669
670    /// A progress bar (value from 0.0 to 1.0).
671    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
672        let id = self.make_id(label);
673        self.record_child(id.clone());
674        self.window
675            .ctx
676            .declare(build_progress_bar(id, self.window.name.clone(), label, value, accent, None));
677    }
678
679    /// A progress bar with subtitle text.
680    pub fn progress_bar_with_subtitle(
681        &mut self,
682        label: &str,
683        value: f64,
684        accent: AccentColor,
685        subtitle: &str,
686    ) {
687        let id = self.make_id(label);
688        self.record_child(id.clone());
689        self.window.ctx.declare(build_progress_bar(
690            id,
691            self.window.name.clone(),
692            label,
693            value,
694            accent,
695            Some(subtitle),
696        ));
697    }
698
699    /// A stat card.
700    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
701        let id = self.make_id(label);
702        self.record_child(id.clone());
703        self.window
704            .ctx
705            .declare(build_stat(id, self.window.name.clone(), label, value, subvalue, accent));
706    }
707
708    /// A status indicator.
709    pub fn status(
710        &mut self,
711        label: &str,
712        active: bool,
713        active_text: Option<&str>,
714        inactive_text: Option<&str>,
715        active_color: AccentColor,
716        inactive_color: AccentColor,
717    ) {
718        let id = self.make_id(label);
719        self.record_child(id.clone());
720        self.window.ctx.declare(build_status(
721            id,
722            self.window.name.clone(),
723            label,
724            active,
725            active_text,
726            inactive_text,
727            active_color,
728            inactive_color,
729        ));
730    }
731
732    /// A mini sparkline chart.
733    pub fn mini_chart(
734        &mut self,
735        label: &str,
736        values: &[f32],
737        unit: Option<&str>,
738        accent: AccentColor,
739    ) {
740        let id = self.make_id(label);
741        self.record_child(id.clone());
742        self.window.ctx.declare(build_mini_chart(
743            id,
744            self.window.name.clone(),
745            label,
746            values,
747            unit,
748            accent,
749        ));
750    }
751
752    /// A larger chart.
753    pub fn plot(
754        &mut self,
755        label: &str,
756        series: &[(&str, &[f32], AccentColor)],
757        x_label: Option<&str>,
758        y_label: Option<&str>,
759    ) {
760        let id = self.make_id(label);
761        self.record_child(id.clone());
762        self.window.ctx.declare(build_plot(
763            id,
764            self.window.name.clone(),
765            label,
766            series,
767            x_label,
768            y_label,
769        ));
770    }
771}