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}