Skip to main content

embedded_gui/context/
builders.rs

1use crate::{
2    geometry::Rect,
3    haptics::HapticPattern,
4    image::{ImageFit, ImageRef, ReelPlayer},
5    render::TextAlign,
6    style::{Style, WidgetStyle},
7    widget::{FocusGroupId, StyleClassId, Widget, WidgetId},
8    widgets::{ChartMode, KeyboardLayout, NotificationLevel, ScaleMode, SurfaceState, WidgetKind},
9};
10use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
11
12use super::*;
13
14pub struct WidgetBuilder<'a, 'ctx, W, const NODES: usize, const EVENTS: usize, const DIRTY: usize> {
15    ctx: &'ctx mut GuiContext<'a, NODES, EVENTS, DIRTY>,
16    rect: Rect,
17    _widget: W,
18    parent: Option<WidgetId>,
19    style_class: Option<StyleClassId>,
20    focus_group: FocusGroupId,
21    style: Option<WidgetStyle>,
22}
23
24impl<'a, 'ctx, W, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
25    WidgetBuilder<'a, 'ctx, W, NODES, EVENTS, DIRTY>
26where
27    W: Widget + 'a,
28{
29    pub fn new(
30        ctx: &'ctx mut GuiContext<'a, NODES, EVENTS, DIRTY>,
31        rect: impl Into<Rect>,
32        widget: W,
33    ) -> Self {
34        Self {
35            ctx,
36            rect: rect.into(),
37            _widget: widget,
38            parent: None,
39            style_class: None,
40            focus_group: FocusGroupId::ROOT,
41            style: None,
42        }
43    }
44
45    pub fn with_parent(mut self, parent: WidgetId) -> Self {
46        self.parent = Some(parent);
47        self
48    }
49
50    pub fn with_style_class(mut self, style_class: StyleClassId) -> Self {
51        self.style_class = Some(style_class);
52        self
53    }
54
55    pub fn with_focus_group(mut self, focus_group: FocusGroupId) -> Self {
56        self.focus_group = focus_group;
57        self
58    }
59
60    pub fn with_style(mut self, style: impl Into<WidgetStyle>) -> Self {
61        self.style = Some(style.into());
62        self
63    }
64
65    pub fn build(self) -> Result<WidgetId, GuiError> {
66        let style = self
67            .style
68            .unwrap_or_else(|| WidgetStyle::from(Style::default()));
69        let id = self.ctx.add_widget(self.rect, WidgetKind::Spacer, style)?;
70        if let Some(parent) = self.parent {
71            self.ctx.add_child(parent, id)?;
72        }
73        if let Some(class_id) = self.style_class {
74            if let Some(node) = self.ctx.node_mut(id) {
75                node.style_class = Some(class_id);
76            }
77        }
78        if self.focus_group != FocusGroupId::ROOT {
79            self.ctx.set_focus_group(id, self.focus_group)?;
80        }
81        Ok(id)
82    }
83}
84
85impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
86    GuiContext<'a, NODES, EVENTS, DIRTY>
87{
88    pub fn spawn<'ctx, W>(
89        &'ctx mut self,
90        rect: impl Into<Rect>,
91        widget: W,
92    ) -> WidgetBuilder<'a, 'ctx, W, NODES, EVENTS, DIRTY>
93    where
94        W: Widget + 'a,
95    {
96        WidgetBuilder::new(self, rect, widget)
97    }
98    pub fn add_panel<S>(&mut self, rect: impl Into<Rect>, style: S) -> Result<WidgetId, GuiError>
99    where
100        S: Into<WidgetStyle>,
101    {
102        self.add_widget(rect, WidgetKind::Panel, style)
103    }
104
105    pub fn add_themed_panel(&mut self, rect: impl Into<Rect>) -> Result<WidgetId, GuiError> {
106        self.add_panel(rect, self.theme.panel)
107    }
108
109    pub fn add_label<S>(
110        &mut self,
111        rect: impl Into<Rect>,
112        text: &'a str,
113        style: S,
114    ) -> Result<WidgetId, GuiError>
115    where
116        S: Into<WidgetStyle>,
117    {
118        self.add_widget(rect, WidgetKind::Label(text), style)
119    }
120
121    pub fn add_themed_label(
122        &mut self,
123        rect: impl Into<Rect>,
124        text: &'a str,
125    ) -> Result<WidgetId, GuiError> {
126        self.add_label(rect, text, self.theme.label)
127    }
128
129    pub fn add_button<S>(
130        &mut self,
131        rect: impl Into<Rect>,
132        text: &'a str,
133        style: S,
134    ) -> Result<WidgetId, GuiError>
135    where
136        S: Into<WidgetStyle>,
137    {
138        let id = self.add_widget(rect, WidgetKind::Button(text), style)?;
139        self.ensure_focus();
140        Ok(id)
141    }
142
143    pub fn add_themed_button(
144        &mut self,
145        rect: impl Into<Rect>,
146        text: &'a str,
147    ) -> Result<WidgetId, GuiError> {
148        self.add_button(rect, text, self.theme.button)
149    }
150
151    #[cfg(feature = "rich-widgets")]
152    pub fn add_progress_bar<S>(
153        &mut self,
154        rect: impl Into<Rect>,
155        value: f32,
156        style: S,
157    ) -> Result<WidgetId, GuiError>
158    where
159        S: Into<WidgetStyle>,
160    {
161        self.add_widget(
162            rect,
163            WidgetKind::ProgressBar {
164                value: value.clamp(0.0, 1.0),
165            },
166            style,
167        )
168    }
169
170    #[cfg(feature = "rich-widgets")]
171    pub fn add_themed_progress_bar(
172        &mut self,
173        rect: impl Into<Rect>,
174        value: f32,
175    ) -> Result<WidgetId, GuiError> {
176        self.add_progress_bar(rect, value, self.theme.progress)
177    }
178
179    #[cfg(feature = "rich-widgets")]
180    pub fn add_toggle<S>(
181        &mut self,
182        rect: impl Into<Rect>,
183        label: &'a str,
184        on: bool,
185        style: S,
186    ) -> Result<WidgetId, GuiError>
187    where
188        S: Into<WidgetStyle>,
189    {
190        let id = self.add_widget(rect, WidgetKind::Toggle { label, on }, style)?;
191        self.ensure_focus();
192        Ok(id)
193    }
194
195    #[cfg(feature = "rich-widgets")]
196    pub fn add_themed_toggle(
197        &mut self,
198        rect: impl Into<Rect>,
199        label: &'a str,
200        on: bool,
201    ) -> Result<WidgetId, GuiError> {
202        self.add_toggle(rect, label, on, self.theme.toggle)
203    }
204
205    #[cfg(feature = "rich-widgets")]
206    pub fn add_checkbox<S>(
207        &mut self,
208        rect: impl Into<Rect>,
209        label: &'a str,
210        checked: bool,
211        style: S,
212    ) -> Result<WidgetId, GuiError>
213    where
214        S: Into<WidgetStyle>,
215    {
216        let id = self.add_widget(rect, WidgetKind::Checkbox { label, checked }, style)?;
217        self.ensure_focus();
218        Ok(id)
219    }
220
221    #[cfg(feature = "rich-widgets")]
222    pub fn add_themed_checkbox(
223        &mut self,
224        rect: impl Into<Rect>,
225        label: &'a str,
226        checked: bool,
227    ) -> Result<WidgetId, GuiError> {
228        self.add_checkbox(rect, label, checked, self.theme.checkbox)
229    }
230
231    #[cfg(feature = "rich-widgets")]
232    pub fn add_slider<S>(
233        &mut self,
234        rect: Rect,
235        value: f32,
236        min: f32,
237        max: f32,
238        style: S,
239    ) -> Result<WidgetId, GuiError>
240    where
241        S: Into<WidgetStyle>,
242    {
243        let value = value.clamp(min.min(max), min.max(max));
244        let id = self.add_widget(rect, WidgetKind::Slider { value, min, max }, style)?;
245        self.ensure_focus();
246        Ok(id)
247    }
248
249    #[cfg(feature = "rich-widgets")]
250    pub fn add_themed_slider(
251        &mut self,
252        rect: Rect,
253        value: f32,
254        min: f32,
255        max: f32,
256    ) -> Result<WidgetId, GuiError> {
257        self.add_slider(rect, value, min, max, self.theme.slider)
258    }
259
260    #[cfg(feature = "rich-widgets")]
261    pub fn add_value_label<S>(
262        &mut self,
263        rect: Rect,
264        label: &'a str,
265        value: i32,
266        style: S,
267    ) -> Result<WidgetId, GuiError>
268    where
269        S: Into<WidgetStyle>,
270    {
271        self.add_widget(rect, WidgetKind::ValueLabel { label, value }, style)
272    }
273
274    #[cfg(feature = "rich-widgets")]
275    pub fn add_themed_value_label(
276        &mut self,
277        rect: Rect,
278        label: &'a str,
279        value: i32,
280    ) -> Result<WidgetId, GuiError> {
281        self.add_value_label(rect, label, value, self.theme.value_label)
282    }
283
284    #[cfg(feature = "rich-widgets")]
285    pub fn add_icon_button<S>(
286        &mut self,
287        rect: Rect,
288        icon: char,
289        label: &'a str,
290        style: S,
291    ) -> Result<WidgetId, GuiError>
292    where
293        S: Into<WidgetStyle>,
294    {
295        let id = self.add_widget(rect, WidgetKind::IconButton { icon, label }, style)?;
296        self.ensure_focus();
297        Ok(id)
298    }
299
300    #[cfg(feature = "rich-widgets")]
301    pub fn add_themed_icon_button(
302        &mut self,
303        rect: Rect,
304        icon: char,
305        label: &'a str,
306    ) -> Result<WidgetId, GuiError> {
307        self.add_icon_button(rect, icon, label, self.theme.icon_button)
308    }
309
310    #[cfg(feature = "rich-widgets")]
311    pub fn add_list<S>(
312        &mut self,
313        rect: Rect,
314        items: &'a [&'a str],
315        selected: usize,
316        visible_rows: usize,
317        style: S,
318    ) -> Result<WidgetId, GuiError>
319    where
320        S: Into<WidgetStyle>,
321    {
322        let selected = selected.min(items.len().saturating_sub(1));
323        let id = self.add_widget(
324            rect,
325            WidgetKind::List {
326                items,
327                selected,
328                offset: selected,
329                visible_rows: visible_rows.max(1),
330            },
331            style,
332        )?;
333        self.ensure_focus();
334        Ok(id)
335    }
336
337    #[cfg(feature = "rich-widgets")]
338    pub fn add_feed_timeline<S>(
339        &mut self,
340        rect: Rect,
341        items: &'a [&'a str],
342        selected: usize,
343        visible_rows: usize,
344        expanded: bool,
345        style: S,
346    ) -> Result<WidgetId, GuiError>
347    where
348        S: Into<WidgetStyle>,
349    {
350        let selected = selected.min(items.len().saturating_sub(1));
351        let id = self.add_widget(
352            rect,
353            WidgetKind::FeedTimeline {
354                items,
355                selected,
356                offset: selected,
357                visible_rows: visible_rows.max(1),
358                expanded,
359            },
360            style,
361        )?;
362        self.ensure_focus();
363        Ok(id)
364    }
365
366    #[cfg(feature = "rich-widgets")]
367    pub fn add_themed_list(
368        &mut self,
369        rect: Rect,
370        items: &'a [&'a str],
371        selected: usize,
372        visible_rows: usize,
373    ) -> Result<WidgetId, GuiError> {
374        self.add_list(rect, items, selected, visible_rows, self.theme.list)
375    }
376
377    pub fn add_circular_list<S>(
378        &mut self,
379        rect: Rect,
380        items: &'a [&'a str],
381        selected: usize,
382        visible_rows: usize,
383        style: S,
384    ) -> Result<WidgetId, GuiError>
385    where
386        S: Into<WidgetStyle>,
387    {
388        let selected = selected.min(items.len().saturating_sub(1));
389        let id = self.add_widget(
390            rect,
391            WidgetKind::CircularList {
392                items,
393                selected,
394                offset: selected,
395                visible_rows: visible_rows.max(1),
396            },
397            style,
398        )?;
399        self.ensure_focus();
400        Ok(id)
401    }
402
403    pub fn add_themed_circular_list(
404        &mut self,
405        rect: Rect,
406        items: &'a [&'a str],
407        selected: usize,
408        visible_rows: usize,
409    ) -> Result<WidgetId, GuiError> {
410        self.add_circular_list(rect, items, selected, visible_rows, self.theme.list)
411    }
412
413    #[cfg(feature = "rich-widgets")]
414    pub fn add_scroll_view<S>(
415        &mut self,
416        rect: Rect,
417        offset_y: i32,
418        content_h: u32,
419        style: S,
420    ) -> Result<WidgetId, GuiError>
421    where
422        S: Into<WidgetStyle>,
423    {
424        let id = self.add_widget(
425            rect,
426            WidgetKind::ScrollView {
427                offset_y,
428                content_h,
429            },
430            style,
431        )?;
432        self.ensure_focus();
433        Ok(id)
434    }
435
436    #[cfg(feature = "rich-widgets")]
437    pub fn add_themed_scroll_view(
438        &mut self,
439        rect: Rect,
440        offset_y: i32,
441        content_h: u32,
442    ) -> Result<WidgetId, GuiError> {
443        self.add_scroll_view(rect, offset_y, content_h, self.theme.list)
444    }
445
446    #[cfg(feature = "rich-widgets")]
447    pub fn add_tabs<S>(
448        &mut self,
449        rect: Rect,
450        labels: &'a [&'a str],
451        selected: usize,
452        style: S,
453    ) -> Result<WidgetId, GuiError>
454    where
455        S: Into<WidgetStyle>,
456    {
457        let selected = selected.min(labels.len().saturating_sub(1));
458        let id = self.add_widget(rect, WidgetKind::Tabs { labels, selected }, style)?;
459        self.ensure_focus();
460        Ok(id)
461    }
462
463    #[cfg(feature = "rich-widgets")]
464    pub fn add_themed_tabs(
465        &mut self,
466        rect: Rect,
467        labels: &'a [&'a str],
468        selected: usize,
469    ) -> Result<WidgetId, GuiError> {
470        self.add_tabs(rect, labels, selected, self.theme.tabs)
471    }
472
473    #[cfg(feature = "rich-widgets")]
474    pub fn add_dialog<S>(
475        &mut self,
476        rect: Rect,
477        title: &'a str,
478        body: &'a str,
479        style: S,
480    ) -> Result<WidgetId, GuiError>
481    where
482        S: Into<WidgetStyle>,
483    {
484        let id = self.add_widget(rect, WidgetKind::Dialog { title, body }, style)?;
485        self.play_haptic(HapticPattern::Alert);
486        Ok(id)
487    }
488
489    #[cfg(feature = "rich-widgets")]
490    pub fn add_themed_dialog(
491        &mut self,
492        rect: Rect,
493        title: &'a str,
494        body: &'a str,
495    ) -> Result<WidgetId, GuiError> {
496        self.add_dialog(rect, title, body, self.theme.dialog)
497    }
498
499    #[cfg(feature = "rich-widgets")]
500    pub fn add_toast<S>(
501        &mut self,
502        rect: Rect,
503        text: &'a str,
504        ttl_ms: u32,
505        style: S,
506    ) -> Result<WidgetId, GuiError>
507    where
508        S: Into<WidgetStyle>,
509    {
510        let id = self.add_widget(rect, WidgetKind::Toast { text, ttl_ms }, style)?;
511        self.play_haptic(HapticPattern::Success);
512        Ok(id)
513    }
514
515    #[cfg(feature = "rich-widgets")]
516    pub fn add_themed_toast(
517        &mut self,
518        rect: Rect,
519        text: &'a str,
520        ttl_ms: u32,
521    ) -> Result<WidgetId, GuiError> {
522        self.add_toast(rect, text, ttl_ms, self.theme.toast)
523    }
524
525    #[cfg(feature = "rich-widgets")]
526    pub fn add_meter<S>(
527        &mut self,
528        rect: Rect,
529        value: f32,
530        min: f32,
531        max: f32,
532        style: S,
533    ) -> Result<WidgetId, GuiError>
534    where
535        S: Into<WidgetStyle>,
536    {
537        self.add_widget(rect, WidgetKind::Meter { value, min, max }, style)
538    }
539
540    #[cfg(feature = "rich-widgets")]
541    pub fn add_themed_meter(
542        &mut self,
543        rect: Rect,
544        value: f32,
545        min: f32,
546        max: f32,
547    ) -> Result<WidgetId, GuiError> {
548        self.add_meter(rect, value, min, max, self.theme.meter)
549    }
550
551    #[allow(clippy::too_many_arguments)]
552    #[cfg(feature = "rich-widgets")]
553    pub fn add_arc_gauge<S>(
554        &mut self,
555        rect: Rect,
556        value: f32,
557        min: f32,
558        max: f32,
559        start_deg: i32,
560        end_deg: i32,
561        thickness: u8,
562        antialias: bool,
563        style: S,
564    ) -> Result<WidgetId, GuiError>
565    where
566        S: Into<WidgetStyle>,
567    {
568        self.add_widget(
569            rect,
570            WidgetKind::ArcGauge {
571                value,
572                min,
573                max,
574                start_deg,
575                end_deg,
576                thickness: thickness.max(1),
577                antialias,
578                major_ticks: 6,
579                minor_ticks: 2,
580                show_value: false,
581            },
582            style,
583        )
584    }
585
586    #[cfg(feature = "rich-widgets")]
587    pub fn add_gauge<S>(
588        &mut self,
589        rect: Rect,
590        value: f32,
591        min: f32,
592        max: f32,
593        style: S,
594    ) -> Result<WidgetId, GuiError>
595    where
596        S: Into<WidgetStyle>,
597    {
598        self.add_widget(
599            rect,
600            WidgetKind::Gauge {
601                value,
602                min,
603                max,
604                major_ticks: 6,
605                minor_ticks: 2,
606                show_value: false,
607            },
608            style,
609        )
610    }
611
612    #[allow(clippy::too_many_arguments)]
613    pub fn add_sweeping_arc<S>(
614        &mut self,
615        rect: Rect,
616        progress: f32,
617        clockwise: bool,
618        arc_radius: u32,
619        frame_inset: u16,
620        corner_radius: u8,
621        bg_color: Rgb565,
622        arc_color: Rgb565,
623        frame_color: Rgb565,
624        style: S,
625    ) -> Result<WidgetId, GuiError>
626    where
627        S: Into<WidgetStyle>,
628    {
629        self.add_widget(
630            rect,
631            WidgetKind::SweepingArc {
632                progress: progress.clamp(0.0, 1.0),
633                clockwise,
634                arc_radius,
635                frame_inset,
636                corner_radius,
637                bg_color,
638                arc_color,
639                frame_color,
640            },
641            style,
642        )
643    }
644
645    #[allow(clippy::too_many_arguments)]
646    #[cfg(feature = "rich-widgets")]
647    pub fn add_gauge_needle<S>(
648        &mut self,
649        rect: Rect,
650        value: f32,
651        min: f32,
652        max: f32,
653        start_deg: i32,
654        end_deg: i32,
655        style: S,
656    ) -> Result<WidgetId, GuiError>
657    where
658        S: Into<WidgetStyle>,
659    {
660        self.add_widget(
661            rect,
662            WidgetKind::GaugeNeedle {
663                value,
664                min,
665                max,
666                start_deg,
667                end_deg,
668            },
669            style,
670        )
671    }
672
673    #[cfg(feature = "rich-widgets")]
674    pub fn add_chart<S>(
675        &mut self,
676        rect: Rect,
677        values: &'a [f32],
678        min: f32,
679        max: f32,
680        style: S,
681    ) -> Result<WidgetId, GuiError>
682    where
683        S: Into<WidgetStyle>,
684    {
685        self.add_widget(
686            rect,
687            WidgetKind::Chart {
688                values,
689                min,
690                max,
691                thickness: 1,
692                fill_under: false,
693                markers: false,
694                mode: ChartMode::Line,
695                show_grid: false,
696                show_axes: false,
697                show_labels: false,
698            },
699            style,
700        )
701    }
702
703    pub fn add_plotter<S>(
704        &mut self,
705        rect: Rect,
706        values: &'a [f32],
707        head: usize,
708        min: f32,
709        max: f32,
710        style: S,
711    ) -> Result<WidgetId, GuiError>
712    where
713        S: Into<WidgetStyle>,
714    {
715        self.add_widget(
716            rect,
717            WidgetKind::Plotter {
718                values,
719                head,
720                min,
721                max,
722                thickness: 1,
723                show_grid: false,
724                show_axes: false,
725            },
726            style,
727        )
728    }
729
730    pub fn add_themed_plotter(
731        &mut self,
732        rect: Rect,
733        values: &'a [f32],
734        head: usize,
735        min: f32,
736        max: f32,
737    ) -> Result<WidgetId, GuiError> {
738        self.add_plotter(rect, values, head, min, max, self.theme.panel)
739    }
740
741    #[cfg(feature = "rich-widgets")]
742    pub fn set_plotter_style(&mut self, id: WidgetId, thickness: u8) -> Result<(), GuiError> {
743        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
744        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
745        match node.kind {
746            WidgetKind::Plotter {
747                thickness: ref mut t,
748                ..
749            } => {
750                *t = thickness.max(1);
751                self.dirty.add(rect)?;
752                Ok(())
753            }
754            _ => Err(GuiError::NotFound),
755        }
756    }
757
758    #[cfg(feature = "rich-widgets")]
759    pub fn set_plotter_decoration(
760        &mut self,
761        id: WidgetId,
762        show_grid: bool,
763        show_axes: bool,
764    ) -> Result<(), GuiError> {
765        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
766        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
767        match node.kind {
768            WidgetKind::Plotter {
769                show_grid: ref mut grid,
770                show_axes: ref mut axes,
771                ..
772            } => {
773                *grid = show_grid;
774                *axes = show_axes;
775                self.dirty.add(rect)?;
776                Ok(())
777            }
778            _ => Err(GuiError::NotFound),
779        }
780    }
781
782    #[cfg(feature = "rich-widgets")]
783    pub fn set_chart_style(
784        &mut self,
785        id: WidgetId,
786        thickness: u8,
787        fill_under: bool,
788        markers: bool,
789    ) -> Result<(), GuiError> {
790        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
791        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
792        match node.kind {
793            WidgetKind::Chart {
794                thickness: ref mut t,
795                fill_under: ref mut fill,
796                markers: ref mut mark,
797                ..
798            } => {
799                *t = thickness.max(1);
800                *fill = fill_under;
801                *mark = markers;
802                self.dirty.add(rect)?;
803                Ok(())
804            }
805            _ => Err(GuiError::NotFound),
806        }
807    }
808
809    #[cfg(feature = "rich-widgets")]
810    pub fn set_chart_decoration(
811        &mut self,
812        id: WidgetId,
813        mode: ChartMode,
814        show_grid: bool,
815        show_axes: bool,
816        show_labels: bool,
817    ) -> Result<(), GuiError> {
818        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
819        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
820        match node.kind {
821            WidgetKind::Chart {
822                mode: ref mut chart_mode,
823                show_grid: ref mut grid,
824                show_axes: ref mut axes,
825                show_labels: ref mut labels,
826                ..
827            } => {
828                *chart_mode = mode;
829                *grid = show_grid;
830                *axes = show_axes;
831                *labels = show_labels;
832                self.dirty.add(rect)?;
833                Ok(())
834            }
835            _ => Err(GuiError::NotFound),
836        }
837    }
838
839    #[cfg(feature = "rich-widgets")]
840    pub fn add_spinner<S>(&mut self, rect: Rect, phase: f32, style: S) -> Result<WidgetId, GuiError>
841    where
842        S: Into<WidgetStyle>,
843    {
844        self.add_widget(rect, WidgetKind::Spinner { phase }, style)
845    }
846
847    #[cfg(feature = "rich-widgets")]
848    pub fn add_dropdown<S>(
849        &mut self,
850        rect: Rect,
851        items: &'a [&'a str],
852        selected: usize,
853        style: S,
854    ) -> Result<WidgetId, GuiError>
855    where
856        S: Into<WidgetStyle>,
857    {
858        let selected = selected.min(items.len().saturating_sub(1));
859        let id = self.add_widget(
860            rect,
861            WidgetKind::Dropdown {
862                items,
863                selected,
864                open: false,
865            },
866            style,
867        )?;
868        self.ensure_focus();
869        Ok(id)
870    }
871
872    #[cfg(feature = "rich-widgets")]
873    pub fn add_roller<S>(
874        &mut self,
875        rect: Rect,
876        items: &'a [&'a str],
877        selected: usize,
878        style: S,
879    ) -> Result<WidgetId, GuiError>
880    where
881        S: Into<WidgetStyle>,
882    {
883        let selected = selected.min(items.len().saturating_sub(1));
884        let id = self.add_widget(rect, WidgetKind::Roller { items, selected }, style)?;
885        self.ensure_focus();
886        Ok(id)
887    }
888
889    #[cfg(feature = "rich-widgets")]
890    pub fn add_table<S>(
891        &mut self,
892        rect: Rect,
893        rows: &'a [&'a [&'a str]],
894        style: S,
895    ) -> Result<WidgetId, GuiError>
896    where
897        S: Into<WidgetStyle>,
898    {
899        self.add_widget(
900            rect,
901            WidgetKind::Table {
902                rows,
903                separators: true,
904                cell_padding: 1,
905                align: TextAlign::Left,
906            },
907            style,
908        )
909    }
910
911    #[cfg(feature = "rich-widgets")]
912    pub fn set_table_style(
913        &mut self,
914        id: WidgetId,
915        separators: bool,
916        cell_padding: u8,
917        align: TextAlign,
918    ) -> Result<(), GuiError> {
919        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
920        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
921        match node.kind {
922            WidgetKind::Table {
923                separators: ref mut cell_sep,
924                cell_padding: ref mut pad,
925                align: ref mut table_align,
926                ..
927            } => {
928                *cell_sep = separators;
929                *pad = cell_padding.min(6);
930                *table_align = align;
931                self.dirty.add(rect)?;
932                Ok(())
933            }
934            _ => Err(GuiError::NotFound),
935        }
936    }
937
938    #[cfg(feature = "rich-widgets")]
939    pub fn add_scale<S>(
940        &mut self,
941        rect: Rect,
942        mode: ScaleMode,
943        min: f32,
944        max: f32,
945        value: f32,
946        style: S,
947    ) -> Result<WidgetId, GuiError>
948    where
949        S: Into<WidgetStyle>,
950    {
951        self.add_widget(
952            rect,
953            WidgetKind::Scale {
954                mode,
955                value: value.clamp(min, max),
956                min,
957                max,
958                major_ticks: 5,
959                minor_ticks: 3,
960                start_angle: 135,
961                end_angle: 45,
962                show_labels: true,
963                show_needle: true,
964                tick_color: Rgb565::CSS_GRAY,
965                needle_color: Rgb565::CSS_RED,
966            },
967            style,
968        )
969    }
970
971    #[cfg(feature = "rich-widgets")]
972    pub fn add_radial_scale<S>(
973        &mut self,
974        rect: Rect,
975        min: f32,
976        max: f32,
977        value: f32,
978        style: S,
979    ) -> Result<WidgetId, GuiError>
980    where
981        S: Into<WidgetStyle>,
982    {
983        self.add_scale(rect, ScaleMode::Radial, min, max, value, style)
984    }
985
986    #[cfg(feature = "rich-widgets")]
987    pub fn add_linear_scale<S>(
988        &mut self,
989        rect: Rect,
990        min: f32,
991        max: f32,
992        value: f32,
993        style: S,
994    ) -> Result<WidgetId, GuiError>
995    where
996        S: Into<WidgetStyle>,
997    {
998        self.add_scale(rect, ScaleMode::LinearHorizontal, min, max, value, style)
999    }
1000
1001    #[cfg(feature = "rich-widgets")]
1002    pub fn add_spinbox<S>(
1003        &mut self,
1004        rect: Rect,
1005        min: i32,
1006        max: i32,
1007        value: i32,
1008        style: S,
1009    ) -> Result<WidgetId, GuiError>
1010    where
1011        S: Into<WidgetStyle>,
1012    {
1013        self.add_widget(
1014            rect,
1015            WidgetKind::Spinbox {
1016                value: value.clamp(min, max),
1017                min,
1018                max,
1019                step: 1,
1020                digits: 4,
1021                decimals: 0,
1022                focused_digit: 0,
1023            },
1024            style,
1025        )
1026    }
1027
1028    #[cfg(feature = "rich-widgets")]
1029    pub fn add_textarea<S>(
1030        &mut self,
1031        rect: Rect,
1032        text: &'a str,
1033        placeholder: &'a str,
1034        style: S,
1035    ) -> Result<WidgetId, GuiError>
1036    where
1037        S: Into<WidgetStyle>,
1038    {
1039        let cursor = text.chars().count();
1040        let (text_buf, text_len) = textarea_storage_from_str(text);
1041        let id = self.add_widget(
1042            rect,
1043            WidgetKind::TextArea {
1044                text_buf,
1045                text_len,
1046                cursor,
1047                placeholder,
1048                selection: None,
1049                cursor_visible: true,
1050                read_only: false,
1051                single_line: false,
1052                accept_newline: true,
1053            },
1054            style,
1055        )?;
1056        self.ensure_focus();
1057        Ok(id)
1058    }
1059
1060    #[cfg(feature = "rich-widgets")]
1061    pub fn add_keyboard<S>(
1062        &mut self,
1063        rect: Rect,
1064        keys: &'a [char],
1065        cols: u8,
1066        target: Option<WidgetId>,
1067        style: S,
1068    ) -> Result<WidgetId, GuiError>
1069    where
1070        S: Into<WidgetStyle>,
1071    {
1072        self.add_keyboard_with_alt(rect, keys, None, cols, target, style)
1073    }
1074
1075    #[cfg(feature = "rich-widgets")]
1076    pub fn add_keyboard_with_alt<S>(
1077        &mut self,
1078        rect: Rect,
1079        keys: &'a [char],
1080        alt_keys: Option<&'a [char]>,
1081        cols: u8,
1082        target: Option<WidgetId>,
1083        style: S,
1084    ) -> Result<WidgetId, GuiError>
1085    where
1086        S: Into<WidgetStyle>,
1087    {
1088        let id = self.add_widget(
1089            rect,
1090            WidgetKind::Keyboard {
1091                keys,
1092                selected: 0,
1093                cols: cols.max(1),
1094                alt_keys,
1095                layout: KeyboardLayout::Normal,
1096                target,
1097            },
1098            style,
1099        )?;
1100        self.ensure_focus();
1101        Ok(id)
1102    }
1103
1104    pub fn add_image<S>(
1105        &mut self,
1106        rect: Rect,
1107        image: ImageRef<'a>,
1108        fit: ImageFit,
1109        style: S,
1110    ) -> Result<WidgetId, GuiError>
1111    where
1112        S: Into<WidgetStyle>,
1113    {
1114        self.add_widget(rect, WidgetKind::Image { image, fit }, style)
1115    }
1116
1117    #[cfg(feature = "rich-widgets")]
1118    pub fn add_peek_reveal<S>(
1119        &mut self,
1120        rect: Rect,
1121        icon: ImageRef<'a>,
1122        title: &'a str,
1123        subtitle: &'a str,
1124        style: S,
1125    ) -> Result<WidgetId, GuiError>
1126    where
1127        S: Into<WidgetStyle>,
1128    {
1129        self.add_widget(
1130            rect,
1131            WidgetKind::PeekReveal {
1132                icon,
1133                title,
1134                subtitle,
1135                progress: 0.0,
1136            },
1137            style,
1138        )
1139    }
1140
1141    #[cfg(feature = "rich-widgets")]
1142    pub fn add_glance_tile<S>(
1143        &mut self,
1144        rect: Rect,
1145        icon: char,
1146        title: &'a str,
1147        subtitle: &'a str,
1148        style: S,
1149    ) -> Result<WidgetId, GuiError>
1150    where
1151        S: Into<WidgetStyle>,
1152    {
1153        let id = self.add_widget(
1154            rect,
1155            WidgetKind::GlanceTile {
1156                icon,
1157                title,
1158                subtitle,
1159                highlighted: false,
1160            },
1161            style,
1162        )?;
1163        self.ensure_focus();
1164        Ok(id)
1165    }
1166
1167    #[cfg(feature = "rich-widgets")]
1168    pub fn add_card_deck<S>(
1169        &mut self,
1170        rect: Rect,
1171        titles: &'a [&'a str],
1172        selected: usize,
1173        style: S,
1174    ) -> Result<WidgetId, GuiError>
1175    where
1176        S: Into<WidgetStyle>,
1177    {
1178        self.add_widget(
1179            rect,
1180            WidgetKind::CardDeck {
1181                titles,
1182                selected: selected.min(titles.len().saturating_sub(1)),
1183            },
1184            style,
1185        )
1186    }
1187
1188    #[cfg(feature = "rich-widgets")]
1189    pub fn add_reel<S>(
1190        &mut self,
1191        rect: Rect,
1192        player: ReelPlayer<'a>,
1193        fit: ImageFit,
1194        style: S,
1195    ) -> Result<WidgetId, GuiError>
1196    where
1197        S: Into<WidgetStyle>,
1198    {
1199        self.add_widget(rect, WidgetKind::Reel { player, fit }, style)
1200    }
1201
1202    #[cfg(feature = "rich-widgets")]
1203    pub fn add_state_surface<S>(
1204        &mut self,
1205        rect: Rect,
1206        state: SurfaceState,
1207        title: &'a str,
1208        message: &'a str,
1209        action: Option<&'a str>,
1210        style: S,
1211    ) -> Result<WidgetId, GuiError>
1212    where
1213        S: Into<WidgetStyle>,
1214    {
1215        self.add_widget(
1216            rect,
1217            WidgetKind::StateSurface {
1218                state,
1219                title,
1220                message,
1221                action,
1222                busy_phase: 0.0,
1223            },
1224            style,
1225        )
1226    }
1227
1228    #[cfg(feature = "rich-widgets")]
1229    pub fn add_heads_up_banner<S>(
1230        &mut self,
1231        rect: Rect,
1232        level: NotificationLevel,
1233        text: &'a str,
1234        ttl_ms: u32,
1235        style: S,
1236    ) -> Result<WidgetId, GuiError>
1237    where
1238        S: Into<WidgetStyle>,
1239    {
1240        self.add_widget(
1241            rect,
1242            WidgetKind::HeadsUpBanner {
1243                level,
1244                text,
1245                ttl_ms,
1246            },
1247            style,
1248        )
1249    }
1250
1251    #[allow(clippy::too_many_arguments)]
1252    #[cfg(feature = "rich-widgets")]
1253    pub fn add_notification_action_sheet<S>(
1254        &mut self,
1255        rect: Rect,
1256        level: NotificationLevel,
1257        title: &'a str,
1258        body: &'a str,
1259        actions: &'a [&'a str],
1260        selected: usize,
1261        open: bool,
1262        style: S,
1263    ) -> Result<WidgetId, GuiError>
1264    where
1265        S: Into<WidgetStyle>,
1266    {
1267        self.add_widget(
1268            rect,
1269            WidgetKind::NotificationActionSheet {
1270                level,
1271                title,
1272                body,
1273                actions,
1274                selected: selected.min(actions.len().saturating_sub(1)),
1275                open,
1276            },
1277            style,
1278        )
1279    }
1280
1281    pub fn add_border<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
1282    where
1283        S: Into<WidgetStyle>,
1284    {
1285        self.add_widget(rect, WidgetKind::Border, style)
1286    }
1287
1288    pub fn add_spacer(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
1289        self.add_widget(rect, WidgetKind::Spacer, Style::default())
1290    }
1291
1292    #[cfg(feature = "rich-widgets")]
1293    pub fn add_menu<S>(
1294        &mut self,
1295        rect: Rect,
1296        items: &'a [&'a str],
1297        selected: usize,
1298        style: S,
1299    ) -> Result<WidgetId, GuiError>
1300    where
1301        S: Into<WidgetStyle>,
1302    {
1303        let selected = selected.min(items.len().saturating_sub(1));
1304        let id = self.add_widget(rect, WidgetKind::Menu { items, selected }, style)?;
1305        self.ensure_focus();
1306        Ok(id)
1307    }
1308
1309    pub fn add_dial<S>(
1310        &mut self,
1311        rect: Rect,
1312        value: f32,
1313        min: f32,
1314        max: f32,
1315        style: S,
1316    ) -> Result<WidgetId, GuiError>
1317    where
1318        S: Into<WidgetStyle>,
1319    {
1320        let value = value.clamp(min, max);
1321        let id = self.add_widget(rect, WidgetKind::Dial { value, min, max }, style)?;
1322        self.ensure_focus();
1323        Ok(id)
1324    }
1325
1326    pub fn add_themed_dial(
1327        &mut self,
1328        rect: Rect,
1329        value: f32,
1330        min: f32,
1331        max: f32,
1332    ) -> Result<WidgetId, GuiError> {
1333        self.add_dial(rect, value, min, max, self.theme.button)
1334    }
1335
1336    #[allow(clippy::too_many_arguments)]
1337    pub fn add_rle_player<S>(
1338        &mut self,
1339        rect: impl Into<Rect>,
1340        rle_data: &'static [u8],
1341        frame_width: u16,
1342        frame_height: u16,
1343        total_frames: usize,
1344        frame_duration_ms: u32,
1345        style: S,
1346    ) -> Result<WidgetId, GuiError>
1347    where
1348        S: Into<WidgetStyle>,
1349    {
1350        self.add_widget(
1351            rect,
1352            WidgetKind::RlePlayer {
1353                rle_data,
1354                frame_width,
1355                frame_height,
1356                total_frames,
1357                current_frame: 0,
1358                elapsed_ms: 0,
1359                frame_duration_ms,
1360            },
1361            style,
1362        )
1363    }
1364
1365    pub fn add_themed_rle_player(
1366        &mut self,
1367        rect: Rect,
1368        rle_data: &'static [u8],
1369        frame_width: u16,
1370        frame_height: u16,
1371        total_frames: usize,
1372        frame_duration_ms: u32,
1373    ) -> Result<WidgetId, GuiError> {
1374        self.add_rle_player(
1375            rect,
1376            rle_data,
1377            frame_width,
1378            frame_height,
1379            total_frames,
1380            frame_duration_ms,
1381            self.theme.panel,
1382        )
1383    }
1384
1385    pub fn add_autocomplete_widget<S>(
1386        &mut self,
1387        rect: Rect,
1388        suggestions: &'a [&'a str],
1389        style: S,
1390    ) -> Result<WidgetId, GuiError>
1391    where
1392        S: Into<WidgetStyle>,
1393    {
1394        let id = self.add_widget(
1395            rect,
1396            WidgetKind::AutoComplete {
1397                text_buf: [0; 32],
1398                text_len: 0,
1399                suggestions,
1400                filtered: [None; 8],
1401                filter_count: 0,
1402                selected: None,
1403                expanded: false,
1404            },
1405            style,
1406        )?;
1407        self.ensure_focus();
1408        Ok(id)
1409    }
1410
1411    pub fn add_themed_autocomplete(
1412        &mut self,
1413        rect: Rect,
1414        suggestions: &'a [&'a str],
1415    ) -> Result<WidgetId, GuiError> {
1416        self.add_autocomplete_widget(rect, suggestions, self.theme.panel)
1417    }
1418}