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, SurfaceState, WidgetKind},
9};
10use embedded_graphics_core::pixelcolor::Rgb565;
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_textarea<S>(
940        &mut self,
941        rect: Rect,
942        text: &'a str,
943        placeholder: &'a str,
944        style: S,
945    ) -> Result<WidgetId, GuiError>
946    where
947        S: Into<WidgetStyle>,
948    {
949        let cursor = text.chars().count();
950        let (text_buf, text_len) = textarea_storage_from_str(text);
951        let id = self.add_widget(
952            rect,
953            WidgetKind::TextArea {
954                text_buf,
955                text_len,
956                cursor,
957                placeholder,
958                selection: None,
959                cursor_visible: true,
960                read_only: false,
961                single_line: false,
962                accept_newline: true,
963            },
964            style,
965        )?;
966        self.ensure_focus();
967        Ok(id)
968    }
969
970    #[cfg(feature = "rich-widgets")]
971    pub fn add_keyboard<S>(
972        &mut self,
973        rect: Rect,
974        keys: &'a [char],
975        cols: u8,
976        target: Option<WidgetId>,
977        style: S,
978    ) -> Result<WidgetId, GuiError>
979    where
980        S: Into<WidgetStyle>,
981    {
982        self.add_keyboard_with_alt(rect, keys, None, cols, target, style)
983    }
984
985    #[cfg(feature = "rich-widgets")]
986    pub fn add_keyboard_with_alt<S>(
987        &mut self,
988        rect: Rect,
989        keys: &'a [char],
990        alt_keys: Option<&'a [char]>,
991        cols: u8,
992        target: Option<WidgetId>,
993        style: S,
994    ) -> Result<WidgetId, GuiError>
995    where
996        S: Into<WidgetStyle>,
997    {
998        let id = self.add_widget(
999            rect,
1000            WidgetKind::Keyboard {
1001                keys,
1002                selected: 0,
1003                cols: cols.max(1),
1004                alt_keys,
1005                layout: KeyboardLayout::Normal,
1006                target,
1007            },
1008            style,
1009        )?;
1010        self.ensure_focus();
1011        Ok(id)
1012    }
1013
1014    pub fn add_image<S>(
1015        &mut self,
1016        rect: Rect,
1017        image: ImageRef<'a>,
1018        fit: ImageFit,
1019        style: S,
1020    ) -> Result<WidgetId, GuiError>
1021    where
1022        S: Into<WidgetStyle>,
1023    {
1024        self.add_widget(rect, WidgetKind::Image { image, fit }, style)
1025    }
1026
1027    #[cfg(feature = "rich-widgets")]
1028    pub fn add_peek_reveal<S>(
1029        &mut self,
1030        rect: Rect,
1031        icon: ImageRef<'a>,
1032        title: &'a str,
1033        subtitle: &'a str,
1034        style: S,
1035    ) -> Result<WidgetId, GuiError>
1036    where
1037        S: Into<WidgetStyle>,
1038    {
1039        self.add_widget(
1040            rect,
1041            WidgetKind::PeekReveal {
1042                icon,
1043                title,
1044                subtitle,
1045                progress: 0.0,
1046            },
1047            style,
1048        )
1049    }
1050
1051    #[cfg(feature = "rich-widgets")]
1052    pub fn add_glance_tile<S>(
1053        &mut self,
1054        rect: Rect,
1055        icon: char,
1056        title: &'a str,
1057        subtitle: &'a str,
1058        style: S,
1059    ) -> Result<WidgetId, GuiError>
1060    where
1061        S: Into<WidgetStyle>,
1062    {
1063        let id = self.add_widget(
1064            rect,
1065            WidgetKind::GlanceTile {
1066                icon,
1067                title,
1068                subtitle,
1069                highlighted: false,
1070            },
1071            style,
1072        )?;
1073        self.ensure_focus();
1074        Ok(id)
1075    }
1076
1077    #[cfg(feature = "rich-widgets")]
1078    pub fn add_card_deck<S>(
1079        &mut self,
1080        rect: Rect,
1081        titles: &'a [&'a str],
1082        selected: usize,
1083        style: S,
1084    ) -> Result<WidgetId, GuiError>
1085    where
1086        S: Into<WidgetStyle>,
1087    {
1088        self.add_widget(
1089            rect,
1090            WidgetKind::CardDeck {
1091                titles,
1092                selected: selected.min(titles.len().saturating_sub(1)),
1093            },
1094            style,
1095        )
1096    }
1097
1098    #[cfg(feature = "rich-widgets")]
1099    pub fn add_reel<S>(
1100        &mut self,
1101        rect: Rect,
1102        player: ReelPlayer<'a>,
1103        fit: ImageFit,
1104        style: S,
1105    ) -> Result<WidgetId, GuiError>
1106    where
1107        S: Into<WidgetStyle>,
1108    {
1109        self.add_widget(rect, WidgetKind::Reel { player, fit }, style)
1110    }
1111
1112    #[cfg(feature = "rich-widgets")]
1113    pub fn add_state_surface<S>(
1114        &mut self,
1115        rect: Rect,
1116        state: SurfaceState,
1117        title: &'a str,
1118        message: &'a str,
1119        action: Option<&'a str>,
1120        style: S,
1121    ) -> Result<WidgetId, GuiError>
1122    where
1123        S: Into<WidgetStyle>,
1124    {
1125        self.add_widget(
1126            rect,
1127            WidgetKind::StateSurface {
1128                state,
1129                title,
1130                message,
1131                action,
1132                busy_phase: 0.0,
1133            },
1134            style,
1135        )
1136    }
1137
1138    #[cfg(feature = "rich-widgets")]
1139    pub fn add_heads_up_banner<S>(
1140        &mut self,
1141        rect: Rect,
1142        level: NotificationLevel,
1143        text: &'a str,
1144        ttl_ms: u32,
1145        style: S,
1146    ) -> Result<WidgetId, GuiError>
1147    where
1148        S: Into<WidgetStyle>,
1149    {
1150        self.add_widget(
1151            rect,
1152            WidgetKind::HeadsUpBanner {
1153                level,
1154                text,
1155                ttl_ms,
1156            },
1157            style,
1158        )
1159    }
1160
1161    #[allow(clippy::too_many_arguments)]
1162    #[cfg(feature = "rich-widgets")]
1163    pub fn add_notification_action_sheet<S>(
1164        &mut self,
1165        rect: Rect,
1166        level: NotificationLevel,
1167        title: &'a str,
1168        body: &'a str,
1169        actions: &'a [&'a str],
1170        selected: usize,
1171        open: bool,
1172        style: S,
1173    ) -> Result<WidgetId, GuiError>
1174    where
1175        S: Into<WidgetStyle>,
1176    {
1177        self.add_widget(
1178            rect,
1179            WidgetKind::NotificationActionSheet {
1180                level,
1181                title,
1182                body,
1183                actions,
1184                selected: selected.min(actions.len().saturating_sub(1)),
1185                open,
1186            },
1187            style,
1188        )
1189    }
1190
1191    pub fn add_border<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
1192    where
1193        S: Into<WidgetStyle>,
1194    {
1195        self.add_widget(rect, WidgetKind::Border, style)
1196    }
1197
1198    pub fn add_spacer(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
1199        self.add_widget(rect, WidgetKind::Spacer, Style::default())
1200    }
1201
1202    #[cfg(feature = "rich-widgets")]
1203    pub fn add_menu<S>(
1204        &mut self,
1205        rect: Rect,
1206        items: &'a [&'a str],
1207        selected: usize,
1208        style: S,
1209    ) -> Result<WidgetId, GuiError>
1210    where
1211        S: Into<WidgetStyle>,
1212    {
1213        let selected = selected.min(items.len().saturating_sub(1));
1214        let id = self.add_widget(rect, WidgetKind::Menu { items, selected }, style)?;
1215        self.ensure_focus();
1216        Ok(id)
1217    }
1218
1219    pub fn add_dial<S>(
1220        &mut self,
1221        rect: Rect,
1222        value: f32,
1223        min: f32,
1224        max: f32,
1225        style: S,
1226    ) -> Result<WidgetId, GuiError>
1227    where
1228        S: Into<WidgetStyle>,
1229    {
1230        let value = value.clamp(min, max);
1231        let id = self.add_widget(rect, WidgetKind::Dial { value, min, max }, style)?;
1232        self.ensure_focus();
1233        Ok(id)
1234    }
1235
1236    pub fn add_themed_dial(
1237        &mut self,
1238        rect: Rect,
1239        value: f32,
1240        min: f32,
1241        max: f32,
1242    ) -> Result<WidgetId, GuiError> {
1243        self.add_dial(rect, value, min, max, self.theme.button)
1244    }
1245
1246    #[allow(clippy::too_many_arguments)]
1247    pub fn add_rle_player<S>(
1248        &mut self,
1249        rect: impl Into<Rect>,
1250        rle_data: &'static [u8],
1251        frame_width: u16,
1252        frame_height: u16,
1253        total_frames: usize,
1254        frame_duration_ms: u32,
1255        style: S,
1256    ) -> Result<WidgetId, GuiError>
1257    where
1258        S: Into<WidgetStyle>,
1259    {
1260        self.add_widget(
1261            rect,
1262            WidgetKind::RlePlayer {
1263                rle_data,
1264                frame_width,
1265                frame_height,
1266                total_frames,
1267                current_frame: 0,
1268                elapsed_ms: 0,
1269                frame_duration_ms,
1270            },
1271            style,
1272        )
1273    }
1274
1275    pub fn add_themed_rle_player(
1276        &mut self,
1277        rect: Rect,
1278        rle_data: &'static [u8],
1279        frame_width: u16,
1280        frame_height: u16,
1281        total_frames: usize,
1282        frame_duration_ms: u32,
1283    ) -> Result<WidgetId, GuiError> {
1284        self.add_rle_player(
1285            rect,
1286            rle_data,
1287            frame_width,
1288            frame_height,
1289            total_frames,
1290            frame_duration_ms,
1291            self.theme.panel,
1292        )
1293    }
1294
1295    pub fn add_autocomplete_widget<S>(
1296        &mut self,
1297        rect: Rect,
1298        suggestions: &'a [&'a str],
1299        style: S,
1300    ) -> Result<WidgetId, GuiError>
1301    where
1302        S: Into<WidgetStyle>,
1303    {
1304        let id = self.add_widget(
1305            rect,
1306            WidgetKind::AutoComplete {
1307                text_buf: [0; 32],
1308                text_len: 0,
1309                suggestions,
1310                filtered: [None; 8],
1311                filter_count: 0,
1312                selected: None,
1313                expanded: false,
1314            },
1315            style,
1316        )?;
1317        self.ensure_focus();
1318        Ok(id)
1319    }
1320
1321    pub fn add_themed_autocomplete(
1322        &mut self,
1323        rect: Rect,
1324        suggestions: &'a [&'a str],
1325    ) -> Result<WidgetId, GuiError> {
1326        self.add_autocomplete_widget(rect, suggestions, self.theme.panel)
1327    }
1328}