Skip to main content

w_gui/
window.rs

1use std::ops::RangeInclusive;
2
3use crate::context::Context;
4use crate::element::{AccentColor, ElementDecl, ElementKind, ElementMeta, PlotSeries, Value};
5
6/// A named window containing UI elements. Created via `Context::window()`.
7pub struct Window<'a> {
8    name: String,
9    ctx: &'a mut Context,
10}
11
12impl<'a> Window<'a> {
13    pub(crate) fn new(name: String, ctx: &'a mut Context) -> Self {
14        Self { name, ctx }
15    }
16
17    fn make_id(&self, label: &str) -> String {
18        format!("{}::{}", self.name, label)
19    }
20
21    /// A floating-point slider with a range.
22    pub fn slider(&mut self, label: &str, value: &mut f32, range: RangeInclusive<f32>) {
23        let id = self.make_id(label);
24
25        if let Some(Value::Float(v)) = self.ctx.consume_edit(&id) {
26            *value = v as f32;
27        }
28
29        self.ctx.declare(ElementDecl {
30            id,
31            kind: ElementKind::Slider,
32            label: label.to_string(),
33            value: Value::Float(*value as f64),
34            meta: ElementMeta {
35                min: Some(*range.start() as f64),
36                max: Some(*range.end() as f64),
37                step: Some(0.01),
38                ..Default::default()
39            },
40            window: self.name.clone(),
41        });
42    }
43
44    /// An integer slider with a range.
45    pub fn slider_int(&mut self, label: &str, value: &mut i32, range: RangeInclusive<i32>) {
46        let id = self.make_id(label);
47
48        if let Some(Value::Int(v)) = self.ctx.consume_edit(&id) {
49            *value = v as i32;
50        }
51
52        self.ctx.declare(ElementDecl {
53            id,
54            kind: ElementKind::Slider,
55            label: label.to_string(),
56            value: Value::Int(*value as i64),
57            meta: ElementMeta {
58                min: Some(*range.start() as f64),
59                max: Some(*range.end() as f64),
60                step: Some(1.0),
61                ..Default::default()
62            },
63            window: self.name.clone(),
64        });
65    }
66
67    /// A checkbox (boolean toggle).
68    pub fn checkbox(&mut self, label: &str, value: &mut bool) {
69        let id = self.make_id(label);
70
71        if let Some(Value::Bool(v)) = self.ctx.consume_edit(&id) {
72            *value = v;
73        }
74
75        self.ctx.declare(ElementDecl {
76            id,
77            kind: ElementKind::Checkbox,
78            label: label.to_string(),
79            value: Value::Bool(*value),
80            meta: ElementMeta::default(),
81            window: self.name.clone(),
82        });
83    }
84
85    /// An RGB color picker (3-component).
86    pub fn color_picker(&mut self, label: &str, value: &mut [f32; 3]) {
87        let id = self.make_id(label);
88
89        if let Some(Value::Color3(c)) = self.ctx.consume_edit(&id) {
90            *value = c;
91        }
92
93        self.ctx.declare(ElementDecl {
94            id,
95            kind: ElementKind::ColorPicker3,
96            label: label.to_string(),
97            value: Value::Color3(*value),
98            meta: ElementMeta::default(),
99            window: self.name.clone(),
100        });
101    }
102
103    /// An RGBA color picker (4-component).
104    pub fn color_picker4(&mut self, label: &str, value: &mut [f32; 4]) {
105        let id = self.make_id(label);
106
107        if let Some(Value::Color4(c)) = self.ctx.consume_edit(&id) {
108            *value = c;
109        }
110
111        self.ctx.declare(ElementDecl {
112            id,
113            kind: ElementKind::ColorPicker4,
114            label: label.to_string(),
115            value: Value::Color4(*value),
116            meta: ElementMeta::default(),
117            window: self.name.clone(),
118        });
119    }
120
121    /// A text input field.
122    pub fn text_input(&mut self, label: &str, value: &mut String) {
123        let id = self.make_id(label);
124
125        if let Some(Value::String(s)) = self.ctx.consume_edit(&id) {
126            *value = s;
127        }
128
129        self.ctx.declare(ElementDecl {
130            id,
131            kind: ElementKind::TextInput,
132            label: label.to_string(),
133            value: Value::String(value.clone()),
134            meta: ElementMeta::default(),
135            window: self.name.clone(),
136        });
137    }
138
139    /// A dropdown selector. Returns the selected index.
140    pub fn dropdown(&mut self, label: &str, selected: &mut usize, options: &[&str]) {
141        let id = self.make_id(label);
142
143        if let Some(Value::Enum {
144            selected: s,
145            options: _,
146        }) = self.ctx.consume_edit(&id)
147        {
148            *selected = s;
149        }
150
151        self.ctx.declare(ElementDecl {
152            id,
153            kind: ElementKind::Dropdown,
154            label: label.to_string(),
155            value: Value::Enum {
156                selected: *selected,
157                options: options.iter().map(|s| s.to_string()).collect(),
158            },
159            meta: ElementMeta::default(),
160            window: self.name.clone(),
161        });
162    }
163
164    /// A button. Returns `true` for one frame when clicked.
165    pub fn button(&mut self, label: &str) -> bool {
166        let id = self.make_id(label);
167
168        let clicked = matches!(self.ctx.consume_edit(&id), Some(Value::Button(true)));
169
170        self.ctx.declare(ElementDecl {
171            id,
172            kind: ElementKind::Button,
173            label: label.to_string(),
174            value: Value::Button(false),
175            meta: ElementMeta::default(),
176            window: self.name.clone(),
177        });
178
179        clicked
180    }
181
182    /// A read-only text label.
183    pub fn label(&mut self, text: &str) {
184        let id = self.make_id(text);
185
186        self.ctx.declare(ElementDecl {
187            id,
188            kind: ElementKind::Label,
189            label: text.to_string(),
190            value: Value::String(text.to_string()),
191            meta: ElementMeta::default(),
192            window: self.name.clone(),
193        });
194    }
195
196    /// A visual separator line.
197    pub fn separator(&mut self) {
198        let id = format!("{}::__sep_{}", self.name, self.ctx.current_frame_len());
199
200        self.ctx.declare(ElementDecl {
201            id,
202            kind: ElementKind::Separator,
203            label: String::new(),
204            value: Value::Bool(false),
205            meta: ElementMeta::default(),
206            window: self.name.clone(),
207        });
208    }
209
210    /// A section header for grouping widgets.
211    pub fn section(&mut self, title: &str) {
212        let id = format!("{}::__sec_{}", self.name, self.ctx.current_frame_len());
213
214        self.ctx.declare(ElementDecl {
215            id,
216            kind: ElementKind::Section,
217            label: title.to_string(),
218            value: Value::String(title.to_string()),
219            meta: ElementMeta::default(),
220            window: self.name.clone(),
221        });
222    }
223
224    /// A progress bar (0.0 to 1.0 or 0 to 100).
225    /// 
226    /// # Example
227    /// ```
228    /// let mut progress = 0.75;
229    /// win.progress_bar("Loading", progress, AccentColor::Blue);
230    /// ```
231    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
232        let id = self.make_id(label);
233        let clamped = value.clamp(0.0, 1.0);
234
235        self.ctx.declare(ElementDecl {
236            id,
237            kind: ElementKind::ProgressBar,
238            label: label.to_string(),
239            value: Value::Progress(clamped),
240            meta: ElementMeta {
241                accent: Some(accent.as_str().to_string()),
242                ..Default::default()
243            },
244            window: self.name.clone(),
245        });
246    }
247
248    /// A progress bar with subtitle text.
249    pub fn progress_bar_with_subtitle(
250        &mut self,
251        label: &str,
252        value: f64,
253        accent: AccentColor,
254        subtitle: &str,
255    ) {
256        let id = self.make_id(label);
257        let clamped = value.clamp(0.0, 1.0);
258
259        self.ctx.declare(ElementDecl {
260            id,
261            kind: ElementKind::ProgressBar,
262            label: label.to_string(),
263            value: Value::Progress(clamped),
264            meta: ElementMeta {
265                accent: Some(accent.as_str().to_string()),
266                subtitle: Some(subtitle.to_string()),
267                ..Default::default()
268            },
269            window: self.name.clone(),
270        });
271    }
272
273    /// A stat card displaying a value with optional subvalue.
274    ///
275    /// # Example
276    /// ```
277    /// win.stat("FPS", "60", Some("avg 58"), AccentColor::Green);
278    /// ```
279    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
280        let id = self.make_id(label);
281
282        self.ctx.declare(ElementDecl {
283            id,
284            kind: ElementKind::Stat,
285            label: label.to_string(),
286            value: Value::StatValue {
287                value: value.to_string(),
288                subvalue: subvalue.map(|s| s.to_string()),
289            },
290            meta: ElementMeta {
291                accent: Some(accent.as_str().to_string()),
292                ..Default::default()
293            },
294            window: self.name.clone(),
295        });
296    }
297
298    /// A status indicator with colored dot.
299    ///
300    /// # Example
301    /// ```
302    /// win.status("AI State", true, Some("Enabled"), Some("Disabled"), AccentColor::Green, AccentColor::Red);
303    /// ```
304    pub fn status(
305        &mut self,
306        label: &str,
307        active: bool,
308        active_text: Option<&str>,
309        inactive_text: Option<&str>,
310        active_color: AccentColor,
311        inactive_color: AccentColor,
312    ) {
313        let id = self.make_id(label);
314
315        self.ctx.declare(ElementDecl {
316            id,
317            kind: ElementKind::Status,
318            label: label.to_string(),
319            value: Value::StatusValue {
320                active,
321                active_text: active_text.map(|s| s.to_string()),
322                inactive_text: inactive_text.map(|s| s.to_string()),
323                active_color: Some(active_color.as_str().to_string()),
324                inactive_color: Some(inactive_color.as_str().to_string()),
325            },
326            meta: ElementMeta::default(),
327            window: self.name.clone(),
328        });
329    }
330
331    /// A mini sparkline chart.
332    ///
333    /// # Example
334    /// ```
335    /// let values = vec![10.0, 15.0, 12.0, 18.0, 20.0];
336    /// win.mini_chart("Velocity", &values, Some("m/s"), AccentColor::Coral);
337    /// ```
338    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
339        let id = self.make_id(label);
340        let current = values.last().copied();
341
342        self.ctx.declare(ElementDecl {
343            id,
344            kind: ElementKind::MiniChart,
345            label: label.to_string(),
346            value: Value::ChartValue {
347                values: values.to_vec(),
348                current,
349                unit: unit.map(|s| s.to_string()),
350            },
351            meta: ElementMeta {
352                accent: Some(accent.as_str().to_string()),
353                ..Default::default()
354            },
355            window: self.name.clone(),
356        });
357    }
358
359    /// Set the accent color for this window (affects all cards in the window).
360    /// Call this first before other widgets in the window.
361    pub fn set_accent(&mut self, accent: AccentColor) {
362        // This is a marker element that sets the window's accent color
363        // The frontend uses the accent of the first element
364        let id = format!("{}::__accent_{}", self.name, accent.as_str());
365        self.ctx.declare(ElementDecl {
366            id,
367            kind: ElementKind::Label,
368            label: String::new(),
369            value: Value::String(String::new()),
370            meta: ElementMeta {
371                accent: Some(accent.as_str().to_string()),
372                ..Default::default()
373            },
374            window: self.name.clone(),
375        });
376    }
377
378    /// Create a grid layout container. Elements added within the closure will be
379    /// arranged in a grid with the specified number of columns.
380    ///
381    /// # Example
382    /// ```
383    /// win.grid(2, |grid| {
384    ///     grid.stat("FPS", "60", None, AccentColor::Green);
385    ///     grid.stat("MS", "16.7", Some("avg"), AccentColor::Blue);
386    ///     grid.stat("Entities", "1024", None, AccentColor::Coral);
387    ///     grid.stat("Memory", "256", Some("MB"), AccentColor::Purple);
388    /// });
389    /// ```
390    pub fn grid<F>(&mut self, cols: usize, f: F)
391    where
392        F: FnOnce(&mut Grid<'_, 'a>),
393    {
394        let grid_id = format!("{}::__grid_{}", self.name, self.ctx.current_frame_len());
395        let mut grid = Grid::new(&grid_id, self, cols);
396        f(&mut grid);
397        grid.finish();
398    }
399
400    /// Plot a data series as a larger chart.
401    ///
402    /// # Example
403    /// ```
404    /// let values = vec![10.0, 15.0, 12.0, 18.0, 20.0, 22.0, 19.0];
405    /// win.plot("Performance", &[("FPS", &values, AccentColor::Green)], Some("Time"), Some("FPS"));
406    /// ```
407    pub fn plot(
408        &mut self,
409        label: &str,
410        series: &[(&str, &[f32], AccentColor)],
411        x_label: Option<&str>,
412        y_label: Option<&str>,
413    ) {
414        let id = self.make_id(label);
415
416        let plot_series: Vec<PlotSeries> = series
417            .iter()
418            .map(|(name, values, color)| PlotSeries {
419                name: name.to_string(),
420                values: values.to_vec(),
421                color: color.as_str().to_string(),
422            })
423            .collect();
424
425        self.ctx.declare(ElementDecl {
426            id,
427            kind: ElementKind::Plot,
428            label: label.to_string(),
429            value: Value::PlotValue {
430                series: plot_series,
431                x_label: x_label.map(|s| s.to_string()),
432                y_label: y_label.map(|s| s.to_string()),
433            },
434            meta: ElementMeta::default(),
435            window: self.name.clone(),
436        });
437    }
438}
439
440/// A grid container for grouping elements.
441pub struct Grid<'a, 'ctx> {
442    id: String,
443    window: &'a mut Window<'ctx>,
444    cols: usize,
445    children: Vec<String>,
446}
447
448impl<'a, 'ctx> Grid<'a, 'ctx> {
449    fn new(id: &str, window: &'a mut Window<'ctx>, cols: usize) -> Self {
450        Self {
451            id: id.to_string(),
452            window,
453            cols,
454            children: Vec::new(),
455        }
456    }
457
458    fn record_child(&mut self, id: String) {
459        self.children.push(id);
460    }
461
462    fn finish(self) {
463        // Declare the grid container with references to all children
464        self.window.ctx.declare(ElementDecl {
465            id: self.id,
466            kind: ElementKind::Grid,
467            label: String::new(),
468            value: Value::GridValue {
469                cols: self.cols,
470                children: self.children,
471            },
472            meta: ElementMeta::default(),
473            window: self.window.name.clone(),
474        });
475    }
476
477    fn make_id(&self, label: &str) -> String {
478        format!("{}::{}", self.id, label)
479    }
480
481    /// Add a stat card to the grid.
482    pub fn stat(&mut self, label: &str, value: &str, subvalue: Option<&str>, accent: AccentColor) {
483        let id = self.make_id(label);
484
485        self.record_child(id.clone());
486        self.window.ctx.declare(ElementDecl {
487            id,
488            kind: ElementKind::Stat,
489            label: label.to_string(),
490            value: Value::StatValue {
491                value: value.to_string(),
492                subvalue: subvalue.map(|s| s.to_string()),
493            },
494            meta: ElementMeta {
495                accent: Some(accent.as_str().to_string()),
496                ..Default::default()
497            },
498            window: self.window.name.clone(),
499        });
500    }
501
502    /// Add a mini chart to the grid.
503    pub fn mini_chart(&mut self, label: &str, values: &[f32], unit: Option<&str>, accent: AccentColor) {
504        let id = self.make_id(label);
505        let current = values.last().copied();
506
507        self.record_child(id.clone());
508        self.window.ctx.declare(ElementDecl {
509            id,
510            kind: ElementKind::MiniChart,
511            label: label.to_string(),
512            value: Value::ChartValue {
513                values: values.to_vec(),
514                current,
515                unit: unit.map(|s| s.to_string()),
516            },
517            meta: ElementMeta {
518                accent: Some(accent.as_str().to_string()),
519                ..Default::default()
520            },
521            window: self.window.name.clone(),
522        });
523    }
524
525    /// Add a progress bar to the grid.
526    pub fn progress_bar(&mut self, label: &str, value: f64, accent: AccentColor) {
527        let id = self.make_id(label);
528        let clamped = value.clamp(0.0, 1.0);
529
530        self.record_child(id.clone());
531        self.window.ctx.declare(ElementDecl {
532            id,
533            kind: ElementKind::ProgressBar,
534            label: label.to_string(),
535            value: Value::Progress(clamped),
536            meta: ElementMeta {
537                accent: Some(accent.as_str().to_string()),
538                ..Default::default()
539            },
540            window: self.window.name.clone(),
541        });
542    }
543
544    /// Add a status indicator to the grid.
545    pub fn status(
546        &mut self,
547        label: &str,
548        active: bool,
549        active_text: Option<&str>,
550        inactive_text: Option<&str>,
551        active_color: AccentColor,
552        inactive_color: AccentColor,
553    ) {
554        let id = self.make_id(label);
555
556        self.record_child(id.clone());
557        self.window.ctx.declare(ElementDecl {
558            id,
559            kind: ElementKind::Status,
560            label: label.to_string(),
561            value: Value::StatusValue {
562                active,
563                active_text: active_text.map(|s| s.to_string()),
564                inactive_text: inactive_text.map(|s| s.to_string()),
565                active_color: Some(active_color.as_str().to_string()),
566                inactive_color: Some(inactive_color.as_str().to_string()),
567            },
568            meta: ElementMeta::default(),
569            window: self.window.name.clone(),
570        });
571    }
572}