Skip to main content

embedded_gui/context/
mutators.rs

1use embedded_graphics_core::pixelcolor::Rgb565;
2
3#[cfg(not(feature = "std"))]
4use crate::math::F32Ext as _;
5use crate::{
6    geometry::Rect,
7    input::{UiEvent, WidgetEvent, WidgetEventKind},
8    layout::{Axis, LayoutItem, LinearLayout},
9    state::{FeedTimelineState, ListState, ScrollState, SliderState, TabsState},
10    widget::{EventContext, EventPhase, EventPolicy, FocusGroupId, WidgetFlags, WidgetId},
11    widgets::{KeyboardLayout, SurfaceState, TEXTAREA_CAPACITY, WidgetKind, WidgetNode},
12};
13
14use super::*;
15
16impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
17    GuiContext<'a, NODES, EVENTS, DIRTY>
18{
19    pub fn set_progress(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
20        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
21        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
22        match node.kind {
23            WidgetKind::ProgressBar { value: ref mut v } => {
24                *v = value.clamp(0.0, 1.0);
25                self.dirty.add(rect)?;
26                Ok(())
27            }
28            #[cfg(feature = "rich-widgets")]
29            WidgetKind::PeekReveal {
30                progress: ref mut v,
31                ..
32            } => {
33                *v = value.clamp(0.0, 1.0);
34                self.dirty.add(rect)?;
35                Ok(())
36            }
37            WidgetKind::SweepingArc {
38                progress: ref mut v,
39                ..
40            } => {
41                *v = value.clamp(0.0, 1.0);
42                self.dirty.add(rect)?;
43                Ok(())
44            }
45            _ => Err(GuiError::NotFound),
46        }
47    }
48
49    #[cfg(feature = "rich-widgets")]
50    pub fn set_glance_highlighted(
51        &mut self,
52        id: WidgetId,
53        highlighted: bool,
54    ) -> Result<(), GuiError> {
55        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
56        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
57        match node.kind {
58            WidgetKind::GlanceTile {
59                highlighted: ref mut h,
60                ..
61            } => {
62                *h = highlighted;
63                self.dirty.add(rect)?;
64                Ok(())
65            }
66            _ => Err(GuiError::NotFound),
67        }
68    }
69
70    #[cfg(feature = "rich-widgets")]
71    pub fn set_card_deck_selected(
72        &mut self,
73        id: WidgetId,
74        selected: usize,
75    ) -> Result<(), GuiError> {
76        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
77        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
78        match node.kind {
79            WidgetKind::CardDeck {
80                titles,
81                selected: ref mut current,
82            } => {
83                *current = selected.min(titles.len().saturating_sub(1));
84                self.dirty.add(rect)?;
85                Ok(())
86            }
87            _ => Err(GuiError::NotFound),
88        }
89    }
90
91    #[cfg(feature = "rich-widgets")]
92    pub fn tick_reel(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
93        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
94        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
95        match node.kind {
96            WidgetKind::Reel {
97                player: ref mut reel,
98                ..
99            } => {
100                reel.tick(dt_ms);
101                self.dirty.add(rect)?;
102                Ok(())
103            }
104            _ => Err(GuiError::NotFound),
105        }
106    }
107
108    #[cfg(feature = "rich-widgets")]
109    pub fn set_state_surface_state(
110        &mut self,
111        id: WidgetId,
112        state: SurfaceState,
113    ) -> Result<(), GuiError> {
114        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
115        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
116        match node.kind {
117            WidgetKind::StateSurface {
118                state: ref mut current,
119                ..
120            } => {
121                *current = state;
122                self.dirty.add(rect)?;
123                Ok(())
124            }
125            _ => Err(GuiError::NotFound),
126        }
127    }
128
129    #[cfg(feature = "rich-widgets")]
130    pub fn set_state_surface_message(
131        &mut self,
132        id: WidgetId,
133        message: &'a str,
134    ) -> Result<(), GuiError> {
135        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
136        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
137        match node.kind {
138            WidgetKind::StateSurface {
139                message: ref mut current,
140                ..
141            } => {
142                *current = message;
143                self.dirty.add(rect)?;
144                Ok(())
145            }
146            _ => Err(GuiError::NotFound),
147        }
148    }
149
150    #[cfg(feature = "rich-widgets")]
151    pub fn set_state_surface_action(
152        &mut self,
153        id: WidgetId,
154        action: Option<&'a str>,
155    ) -> Result<(), GuiError> {
156        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
157        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
158        match node.kind {
159            WidgetKind::StateSurface {
160                action: ref mut current,
161                ..
162            } => {
163                *current = action;
164                self.dirty.add(rect)?;
165                Ok(())
166            }
167            _ => Err(GuiError::NotFound),
168        }
169    }
170
171    #[cfg(feature = "rich-widgets")]
172    pub fn set_state_surface_busy_phase(
173        &mut self,
174        id: WidgetId,
175        phase: f32,
176    ) -> Result<(), GuiError> {
177        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
178        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
179        match node.kind {
180            WidgetKind::StateSurface {
181                busy_phase: ref mut current,
182                ..
183            } => {
184                *current = phase;
185                self.dirty.add(rect)?;
186                Ok(())
187            }
188            _ => Err(GuiError::NotFound),
189        }
190    }
191
192    #[cfg(feature = "rich-widgets")]
193    pub fn tick_state_surface(
194        &mut self,
195        id: WidgetId,
196        dt_ms: u32,
197        cycles_per_sec: f32,
198    ) -> Result<(), GuiError> {
199        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
200            WidgetKind::StateSurface { busy_phase, .. } => {
201                busy_phase + (dt_ms as f32 / 1000.0) * cycles_per_sec
202            }
203            _ => return Err(GuiError::NotFound),
204        };
205        self.set_state_surface_busy_phase(id, phase)
206    }
207
208    #[cfg(feature = "rich-widgets")]
209    pub fn set_heads_up_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
210        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
211        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
212        match node.kind {
213            WidgetKind::HeadsUpBanner {
214                ttl_ms: ref mut current,
215                ..
216            } => {
217                *current = ttl_ms;
218                self.dirty.add(rect)?;
219                Ok(())
220            }
221            _ => Err(GuiError::NotFound),
222        }
223    }
224
225    #[cfg(feature = "rich-widgets")]
226    pub fn tick_heads_up(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
227        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
228            WidgetKind::HeadsUpBanner { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
229            _ => return Err(GuiError::NotFound),
230        };
231        self.set_heads_up_ttl(id, ttl)
232    }
233
234    #[cfg(feature = "rich-widgets")]
235    pub fn set_notification_sheet_open(
236        &mut self,
237        id: WidgetId,
238        open: bool,
239    ) -> Result<(), GuiError> {
240        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
241        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
242        match node.kind {
243            WidgetKind::NotificationActionSheet {
244                open: ref mut current,
245                ..
246            } => {
247                *current = open;
248                self.dirty.add(rect)?;
249                Ok(())
250            }
251            _ => Err(GuiError::NotFound),
252        }
253    }
254
255    #[cfg(feature = "rich-widgets")]
256    pub fn set_notification_sheet_selected(
257        &mut self,
258        id: WidgetId,
259        selected: usize,
260    ) -> Result<(), GuiError> {
261        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
262        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
263        match node.kind {
264            WidgetKind::NotificationActionSheet {
265                actions,
266                selected: ref mut current,
267                ..
268            } => {
269                *current = selected.min(actions.len().saturating_sub(1));
270                self.dirty.add(rect)?;
271                Ok(())
272            }
273            _ => Err(GuiError::NotFound),
274        }
275    }
276
277    #[cfg(feature = "rich-widgets")]
278    pub fn set_menu_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
279        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
280        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
281        match node.kind {
282            WidgetKind::Menu {
283                items,
284                selected: ref mut current,
285            } => {
286                *current = selected.min(items.len().saturating_sub(1));
287                self.dirty.add(rect)?;
288                Ok(())
289            }
290            _ => Err(GuiError::NotFound),
291        }
292    }
293
294    #[cfg(feature = "rich-widgets")]
295    pub fn menu_selected(&self, id: WidgetId) -> Option<usize> {
296        match self.node(id)?.kind {
297            WidgetKind::Menu { selected, .. } => Some(selected),
298            _ => None,
299        }
300    }
301
302    #[cfg(feature = "rich-widgets")]
303    pub fn list_selected(&self, id: WidgetId) -> Option<usize> {
304        match self.node(id)?.kind {
305            WidgetKind::List { selected, .. } => Some(selected),
306            _ => None,
307        }
308    }
309
310    #[cfg(feature = "rich-widgets")]
311    pub fn set_list_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
312        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
313        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
314        match node.kind {
315            WidgetKind::List {
316                items,
317                selected: ref mut current,
318                ref mut offset,
319                visible_rows,
320            } => {
321                let mut state = ListState::new(*current, *offset, visible_rows);
322                state.set_selected(selected, items.len());
323                *current = state.selected;
324                *offset = state.offset;
325                self.dirty.add(rect)?;
326                Ok(())
327            }
328            _ => Err(GuiError::NotFound),
329        }
330    }
331
332    #[cfg(feature = "rich-widgets")]
333    pub fn feed_selected(&self, id: WidgetId) -> Option<usize> {
334        match self.node(id)?.kind {
335            WidgetKind::FeedTimeline { selected, .. } => Some(selected),
336            _ => None,
337        }
338    }
339
340    #[cfg(feature = "rich-widgets")]
341    pub fn set_feed_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
342        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
343        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
344        match node.kind {
345            WidgetKind::FeedTimeline {
346                items,
347                selected: ref mut current,
348                ref mut offset,
349                visible_rows,
350                ..
351            } => {
352                let mut state = FeedTimelineState::new(*current, *offset, visible_rows, false);
353                state.set_selected(selected, items.len());
354                *current = state.selected;
355                *offset = state.offset;
356                self.dirty.add(rect)?;
357                Ok(())
358            }
359            _ => Err(GuiError::NotFound),
360        }
361    }
362
363    #[cfg(feature = "rich-widgets")]
364    pub fn set_feed_expanded(&mut self, id: WidgetId, expanded: bool) -> Result<(), GuiError> {
365        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
366        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
367        match node.kind {
368            WidgetKind::FeedTimeline {
369                expanded: ref mut current,
370                ..
371            } => {
372                *current = expanded;
373                self.dirty.add(rect)?;
374                Ok(())
375            }
376            _ => Err(GuiError::NotFound),
377        }
378    }
379
380    #[cfg(feature = "rich-widgets")]
381    pub fn set_toggle(&mut self, id: WidgetId, on: bool) -> Result<(), GuiError> {
382        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
383        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
384        match node.kind {
385            WidgetKind::Toggle { on: ref mut v, .. } => {
386                *v = on;
387                self.dirty.add(rect)?;
388                Ok(())
389            }
390            _ => Err(GuiError::NotFound),
391        }
392    }
393
394    #[cfg(feature = "rich-widgets")]
395    pub fn toggle_value(&self, id: WidgetId) -> Option<bool> {
396        match self.node(id)?.kind {
397            WidgetKind::Toggle { on, .. } => Some(on),
398            _ => None,
399        }
400    }
401
402    #[cfg(feature = "rich-widgets")]
403    pub fn set_checked(&mut self, id: WidgetId, checked: bool) -> Result<(), GuiError> {
404        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
405        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
406        match node.kind {
407            WidgetKind::Checkbox {
408                checked: ref mut v, ..
409            } => {
410                *v = checked;
411                self.dirty.add(rect)?;
412                Ok(())
413            }
414            _ => Err(GuiError::NotFound),
415        }
416    }
417
418    #[cfg(feature = "rich-widgets")]
419    pub fn checked_value(&self, id: WidgetId) -> Option<bool> {
420        match self.node(id)?.kind {
421            WidgetKind::Checkbox { checked, .. } => Some(checked),
422            _ => None,
423        }
424    }
425
426    #[cfg(feature = "rich-widgets")]
427    pub fn set_slider_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
428        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
429        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
430        match node.kind {
431            WidgetKind::Slider {
432                value: ref mut v,
433                min,
434                max,
435            } => {
436                let mut state = SliderState::new(*v, min, max);
437                state.set_value(value);
438                *v = state.value;
439                self.dirty.add(rect)?;
440                Ok(())
441            }
442            _ => Err(GuiError::NotFound),
443        }
444    }
445
446    #[cfg(feature = "rich-widgets")]
447    pub fn slider_value(&self, id: WidgetId) -> Option<f32> {
448        match self.node(id)?.kind {
449            WidgetKind::Slider { value, .. } => Some(value),
450            _ => None,
451        }
452    }
453
454    #[cfg(feature = "rich-widgets")]
455    pub fn set_value_label(&mut self, id: WidgetId, value: i32) -> Result<(), GuiError> {
456        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
457        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
458        match node.kind {
459            WidgetKind::ValueLabel {
460                value: ref mut v, ..
461            } => {
462                *v = value;
463                self.dirty.add(rect)?;
464                Ok(())
465            }
466            _ => Err(GuiError::NotFound),
467        }
468    }
469
470    #[cfg(feature = "rich-widgets")]
471    pub fn set_scroll_offset(&mut self, id: WidgetId, offset_y: i32) -> Result<(), GuiError> {
472        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
473        match node.kind {
474            WidgetKind::ScrollView {
475                offset_y: ref mut v,
476                content_h,
477            } => {
478                let mut state = ScrollState::new(*v, content_h);
479                state.set_offset(offset_y);
480                *v = state.offset_y;
481                self.mark_subtree_dirty(id)?;
482                Ok(())
483            }
484            _ => Err(GuiError::NotFound),
485        }
486    }
487
488    #[cfg(feature = "rich-widgets")]
489    pub fn scroll_offset(&self, id: WidgetId) -> Option<i32> {
490        match self.node(id)?.kind {
491            WidgetKind::ScrollView { offset_y, .. } => Some(offset_y),
492            _ => None,
493        }
494    }
495
496    #[cfg(feature = "rich-widgets")]
497    pub fn set_tab_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
498        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
499        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
500        match node.kind {
501            WidgetKind::Tabs {
502                labels,
503                selected: ref mut v,
504            } => {
505                let mut state = TabsState::new(*v);
506                state.set_selected(selected, labels.len());
507                *v = state.selected;
508                self.dirty.add(rect)?;
509                Ok(())
510            }
511            _ => Err(GuiError::NotFound),
512        }
513    }
514
515    #[cfg(feature = "rich-widgets")]
516    pub fn tab_selected(&self, id: WidgetId) -> Option<usize> {
517        match self.node(id)?.kind {
518            WidgetKind::Tabs { selected, .. } => Some(selected),
519            _ => None,
520        }
521    }
522
523    #[cfg(feature = "rich-widgets")]
524    pub fn set_toast_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
525        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
526        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
527        match node.kind {
528            WidgetKind::Toast {
529                ttl_ms: ref mut v, ..
530            } => {
531                *v = ttl_ms;
532                self.dirty.add(rect)?;
533                Ok(())
534            }
535            _ => Err(GuiError::NotFound),
536        }
537    }
538
539    #[cfg(feature = "rich-widgets")]
540    pub fn tick_toast(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
541        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
542            WidgetKind::Toast { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
543            _ => return Err(GuiError::NotFound),
544        };
545        self.set_toast_ttl(id, ttl)
546    }
547
548    #[cfg(feature = "rich-widgets")]
549    pub fn set_meter_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
550        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
551        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
552        match node.kind {
553            WidgetKind::Meter {
554                value: ref mut v,
555                min,
556                max,
557            } => {
558                *v = value.clamp(min.min(max), min.max(max));
559                self.dirty.add(rect)?;
560                Ok(())
561            }
562            _ => Err(GuiError::NotFound),
563        }
564    }
565
566    #[cfg(feature = "rich-widgets")]
567    pub fn set_spinner_phase(&mut self, id: WidgetId, phase: f32) -> Result<(), GuiError> {
568        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
569        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
570        match node.kind {
571            WidgetKind::Spinner { phase: ref mut v } => {
572                *v = phase;
573                self.dirty.add(rect)?;
574                Ok(())
575            }
576            _ => Err(GuiError::NotFound),
577        }
578    }
579
580    #[cfg(feature = "rich-widgets")]
581    pub fn tick_spinner(
582        &mut self,
583        id: WidgetId,
584        dt_ms: u32,
585        cycles_per_sec: f32,
586    ) -> Result<(), GuiError> {
587        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
588            WidgetKind::Spinner { phase } => phase + (dt_ms as f32 / 1000.0) * cycles_per_sec,
589            _ => return Err(GuiError::NotFound),
590        };
591        self.set_spinner_phase(id, phase)
592    }
593
594    #[cfg(feature = "rich-widgets")]
595    pub fn set_dropdown_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
596        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
597        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
598        match node.kind {
599            WidgetKind::Dropdown {
600                items,
601                selected: ref mut current,
602                ..
603            } => {
604                *current = selected.min(items.len().saturating_sub(1));
605                self.dirty.add(rect)?;
606                Ok(())
607            }
608            _ => Err(GuiError::NotFound),
609        }
610    }
611
612    #[cfg(feature = "rich-widgets")]
613    pub fn dropdown_selected(&self, id: WidgetId) -> Option<usize> {
614        match self.node(id)?.kind {
615            WidgetKind::Dropdown { selected, .. } => Some(selected),
616            _ => None,
617        }
618    }
619
620    #[cfg(feature = "rich-widgets")]
621    pub fn set_dropdown_open(&mut self, id: WidgetId, open: bool) -> Result<(), GuiError> {
622        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
623        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
624        match node.kind {
625            WidgetKind::Dropdown {
626                open: ref mut is_open,
627                ..
628            } => {
629                if *is_open != open {
630                    *is_open = open;
631                    self.dirty.add(rect)?;
632                    self.push_event(if open {
633                        UiEvent::Opened(id)
634                    } else {
635                        UiEvent::Closed(id)
636                    })?;
637                }
638                Ok(())
639            }
640            _ => Err(GuiError::NotFound),
641        }
642    }
643
644    #[cfg(feature = "rich-widgets")]
645    pub fn dropdown_open(&self, id: WidgetId) -> Option<bool> {
646        match self.node(id)?.kind {
647            WidgetKind::Dropdown { open, .. } => Some(open),
648            _ => None,
649        }
650    }
651
652    #[cfg(feature = "rich-widgets")]
653    pub fn set_roller_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
654        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
655        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
656        match node.kind {
657            WidgetKind::Roller {
658                items,
659                selected: ref mut current,
660            } => {
661                *current = selected.min(items.len().saturating_sub(1));
662                self.dirty.add(rect)?;
663                Ok(())
664            }
665            _ => Err(GuiError::NotFound),
666        }
667    }
668
669    #[cfg(feature = "rich-widgets")]
670    pub fn roller_selected(&self, id: WidgetId) -> Option<usize> {
671        match self.node(id)?.kind {
672            WidgetKind::Roller { selected, .. } => Some(selected),
673            _ => None,
674        }
675    }
676
677    #[cfg(feature = "rich-widgets")]
678    pub fn set_textarea_text(&mut self, id: WidgetId, text: &'a str) -> Result<(), GuiError> {
679        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
680        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
681        match node.kind {
682            WidgetKind::TextArea {
683                text_buf: ref mut buf,
684                text_len: ref mut len,
685                cursor: ref mut c,
686                ..
687            } => {
688                let (next_buf, next_len) = textarea_storage_from_str(text);
689                *buf = next_buf;
690                *len = next_len;
691                *c = (*c).min(textarea_text(buf, *len).chars().count());
692                self.dirty.add(rect)?;
693                Ok(())
694            }
695            _ => Err(GuiError::NotFound),
696        }
697    }
698
699    #[cfg(feature = "rich-widgets")]
700    pub fn textarea_text(&self, id: WidgetId) -> Option<&str> {
701        match &self.node(id)?.kind {
702            WidgetKind::TextArea {
703                text_buf, text_len, ..
704            } => Some(textarea_text(text_buf, *text_len)),
705            _ => None,
706        }
707    }
708
709    #[cfg(feature = "rich-widgets")]
710    pub fn set_textarea_cursor(&mut self, id: WidgetId, cursor: usize) -> Result<(), GuiError> {
711        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
712        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
713        match node.kind {
714            WidgetKind::TextArea {
715                text_buf,
716                text_len,
717                cursor: ref mut current,
718                ..
719            } => {
720                let text = textarea_text(&text_buf, text_len);
721                *current = cursor.min(text.chars().count());
722                self.dirty.add(rect)?;
723                Ok(())
724            }
725            _ => Err(GuiError::NotFound),
726        }
727    }
728
729    #[cfg(feature = "rich-widgets")]
730    pub fn move_textarea_cursor(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
731        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
732        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, false)
733    }
734
735    #[cfg(feature = "rich-widgets")]
736    pub fn move_textarea_cursor_select(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
737        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
738        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, true)
739    }
740
741    #[cfg(feature = "rich-widgets")]
742    pub fn move_textarea_cursor_word(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
743        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
744            WidgetKind::TextArea {
745                text_buf,
746                text_len,
747                cursor,
748                ..
749            } => (textarea_text(text_buf, *text_len), *cursor),
750            _ => return Err(GuiError::NotFound),
751        };
752        let next = if delta >= 0 {
753            next_word_boundary(text, cursor)
754        } else {
755            prev_word_boundary(text, cursor)
756        };
757        self.set_textarea_cursor_with_extend(id, next, false)
758    }
759
760    #[cfg(feature = "rich-widgets")]
761    pub fn move_textarea_cursor_word_select(
762        &mut self,
763        id: WidgetId,
764        delta: i8,
765    ) -> Result<(), GuiError> {
766        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
767            WidgetKind::TextArea {
768                text_buf,
769                text_len,
770                cursor,
771                ..
772            } => (textarea_text(text_buf, *text_len), *cursor),
773            _ => return Err(GuiError::NotFound),
774        };
775        let next = if delta >= 0 {
776            next_word_boundary(text, cursor)
777        } else {
778            prev_word_boundary(text, cursor)
779        };
780        self.set_textarea_cursor_with_extend(id, next, true)
781    }
782
783    #[cfg(feature = "rich-widgets")]
784    pub fn set_textarea_cursor_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
785        self.set_textarea_cursor(id, 0)
786    }
787
788    #[cfg(feature = "rich-widgets")]
789    pub fn set_textarea_cursor_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
790        let len = self
791            .textarea_text(id)
792            .map(|text| text.chars().count())
793            .ok_or(GuiError::NotFound)?;
794        self.set_textarea_cursor(id, len)
795    }
796
797    #[cfg(feature = "rich-widgets")]
798    pub fn set_textarea_cursor_line_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
799        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
800        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
801        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
802        self.set_textarea_cursor_with_extend(id, next, false)
803    }
804
805    #[cfg(feature = "rich-widgets")]
806    pub fn set_textarea_cursor_line_home_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
807        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
808        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
809        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
810        self.set_textarea_cursor_with_extend(id, next, true)
811    }
812
813    #[cfg(feature = "rich-widgets")]
814    pub fn set_textarea_cursor_line_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
815        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
816        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
817        let row_end = textarea_row_end_col(text, row, wrap_cols);
818        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
819        self.set_textarea_cursor_with_extend(id, next, false)
820    }
821
822    #[cfg(feature = "rich-widgets")]
823    pub fn set_textarea_cursor_line_end_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
824        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
825        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
826        let row_end = textarea_row_end_col(text, row, wrap_cols);
827        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
828        self.set_textarea_cursor_with_extend(id, next, true)
829    }
830
831    #[cfg(feature = "rich-widgets")]
832    pub fn textarea_cursor(&self, id: WidgetId) -> Option<usize> {
833        match self.node(id)?.kind {
834            WidgetKind::TextArea { cursor, .. } => Some(cursor),
835            _ => None,
836        }
837    }
838
839    #[cfg(feature = "rich-widgets")]
840    pub fn set_textarea_selection(
841        &mut self,
842        id: WidgetId,
843        start: usize,
844        end: usize,
845    ) -> Result<(), GuiError> {
846        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
847        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
848        match node.kind {
849            WidgetKind::TextArea {
850                text_buf,
851                text_len,
852                selection: ref mut current,
853                ..
854            } => {
855                let text = textarea_text(&text_buf, text_len);
856                let len = text.chars().count();
857                let start = start.min(len);
858                let end = end.min(len);
859                *current = Some((start.min(end), start.max(end)));
860                self.dirty.add(rect)?;
861                Ok(())
862            }
863            _ => Err(GuiError::NotFound),
864        }
865    }
866
867    #[cfg(feature = "rich-widgets")]
868    pub fn clear_textarea_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
869        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
870        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
871        match node.kind {
872            WidgetKind::TextArea {
873                selection: ref mut current,
874                ..
875            } => {
876                *current = None;
877                self.dirty.add(rect)?;
878                Ok(())
879            }
880            _ => Err(GuiError::NotFound),
881        }
882    }
883
884    #[cfg(feature = "rich-widgets")]
885    pub fn textarea_selection(&self, id: WidgetId) -> Option<(usize, usize)> {
886        match self.node(id)?.kind {
887            WidgetKind::TextArea { selection, .. } => selection,
888            _ => None,
889        }
890    }
891
892    #[cfg(feature = "rich-widgets")]
893    pub fn textarea_cursor_visible(&self, id: WidgetId) -> Option<bool> {
894        match self.node(id)?.kind {
895            WidgetKind::TextArea { cursor_visible, .. } => Some(cursor_visible),
896            _ => None,
897        }
898    }
899
900    #[cfg(feature = "rich-widgets")]
901    pub fn set_textarea_capabilities(
902        &mut self,
903        id: WidgetId,
904        read_only: bool,
905        single_line: bool,
906        accept_newline: bool,
907    ) -> Result<(), GuiError> {
908        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
909        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
910        match node.kind {
911            WidgetKind::TextArea {
912                read_only: ref mut ro,
913                single_line: ref mut sl,
914                accept_newline: ref mut an,
915                ..
916            } => {
917                *ro = read_only;
918                *sl = single_line;
919                *an = accept_newline && !single_line;
920                self.dirty.add(rect)?;
921                Ok(())
922            }
923            _ => Err(GuiError::NotFound),
924        }
925    }
926
927    #[cfg(feature = "rich-widgets")]
928    pub fn textarea_insert_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
929        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
930        let before = self.capture_textarea_snapshot(id)?;
931        let mut emit = false;
932        if let Some(node) = self.node_mut(id) {
933            if let WidgetKind::TextArea {
934                text_buf,
935                text_len,
936                cursor,
937                selection,
938                read_only,
939                single_line,
940                accept_newline,
941                ..
942            } = &mut node.kind
943            {
944                if *read_only {
945                    return Ok(());
946                }
947                if ch == '\n' && (*single_line || !*accept_newline) {
948                    return Ok(());
949                }
950                let mut chars: heapless::Vec<char, TEXTAREA_CAPACITY> = heapless::Vec::new();
951                for c in textarea_text(text_buf, *text_len).chars() {
952                    let _ = chars.push(c);
953                }
954                let original_len = chars.len();
955                let original_cursor = *cursor;
956
957                if ch == '\u{8}' {
958                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
959                    if !removed_selection && *cursor > 0 && *cursor <= chars.len() {
960                        chars.remove(*cursor - 1);
961                        *cursor -= 1;
962                    }
963                    if removed_selection
964                        || *cursor != original_cursor
965                        || chars.len() != original_len
966                    {
967                        *selection = None;
968                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
969                        *text_buf = next_buf;
970                        *text_len = next_len;
971                        emit = true;
972                    }
973                } else if ch == '\u{7f}' {
974                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
975                    if !removed_selection && *cursor < chars.len() {
976                        chars.remove(*cursor);
977                    }
978                    if removed_selection || chars.len() != original_len {
979                        *selection = None;
980                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
981                        *text_buf = next_buf;
982                        *text_len = next_len;
983                        emit = true;
984                    }
985                } else if ch != '\n' || *cursor < TEXTAREA_CAPACITY {
986                    if delete_selection_if_any(&mut chars, cursor, selection) {
987                        *selection = None;
988                    }
989                    if chars.len() < TEXTAREA_CAPACITY && *cursor <= chars.len() {
990                        let _ = chars.insert(*cursor, ch);
991                        *cursor += 1;
992                        *selection = None;
993                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
994                        *text_buf = next_buf;
995                        *text_len = next_len;
996                        emit = true;
997                    }
998                }
999            } else {
1000                return Err(GuiError::NotFound);
1001            }
1002        }
1003        if emit {
1004            self.push_textarea_undo(id, before);
1005            self.clear_textarea_redo_for(id);
1006            self.dirty.add(rect)?;
1007            self.push_event(UiEvent::TextInput { id, ch })?;
1008            self.push_event(UiEvent::ValueChanged(id))?;
1009        }
1010        Ok(())
1011    }
1012
1013    #[cfg(feature = "rich-widgets")]
1014    pub(crate) fn textarea_line_context(
1015        &self,
1016        id: WidgetId,
1017    ) -> Result<(&str, usize, usize), GuiError> {
1018        let node = self.node(id).ok_or(GuiError::NotFound)?;
1019        match &node.kind {
1020            WidgetKind::TextArea {
1021                text_buf,
1022                text_len,
1023                cursor,
1024                ..
1025            } => {
1026                let font = node.style.normal.font;
1027                let inner_w = node.rect.w.saturating_sub(2);
1028                let cols = (inner_w / font.advance()).max(1) as usize;
1029                Ok((textarea_text(text_buf, *text_len), *cursor, cols))
1030            }
1031            _ => Err(GuiError::NotFound),
1032        }
1033    }
1034
1035    #[cfg(feature = "rich-widgets")]
1036    pub(crate) fn set_textarea_cursor_with_extend(
1037        &mut self,
1038        id: WidgetId,
1039        cursor: usize,
1040        extend_selection: bool,
1041    ) -> Result<(), GuiError> {
1042        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1043        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1044        match node.kind {
1045            WidgetKind::TextArea {
1046                text_buf,
1047                text_len,
1048                cursor: ref mut current_cursor,
1049                ref mut selection,
1050                ..
1051            } => {
1052                let len = textarea_text(&text_buf, text_len).chars().count();
1053                let next = cursor.min(len);
1054                if extend_selection {
1055                    let anchor = match *selection {
1056                        Some((start, end)) => {
1057                            if *current_cursor == start {
1058                                end
1059                            } else {
1060                                start
1061                            }
1062                        }
1063                        None => *current_cursor,
1064                    };
1065                    if anchor == next {
1066                        *selection = None;
1067                    } else {
1068                        *selection = Some((anchor.min(next), anchor.max(next)));
1069                    }
1070                } else {
1071                    *selection = None;
1072                }
1073                *current_cursor = next;
1074                self.dirty.add(rect)?;
1075                Ok(())
1076            }
1077            _ => Err(GuiError::NotFound),
1078        }
1079    }
1080
1081    #[cfg(feature = "rich-widgets")]
1082    pub(crate) fn capture_textarea_snapshot(
1083        &self,
1084        id: WidgetId,
1085    ) -> Result<TextareaSnapshot, GuiError> {
1086        match self.node(id).ok_or(GuiError::NotFound)?.kind {
1087            WidgetKind::TextArea {
1088                text_buf,
1089                text_len,
1090                cursor,
1091                selection,
1092                ..
1093            } => Ok(TextareaSnapshot {
1094                text_buf,
1095                text_len,
1096                cursor,
1097                selection,
1098            }),
1099            _ => Err(GuiError::NotFound),
1100        }
1101    }
1102
1103    #[cfg(feature = "rich-widgets")]
1104    pub(crate) fn apply_textarea_snapshot(
1105        &mut self,
1106        id: WidgetId,
1107        snap: TextareaSnapshot,
1108    ) -> Result<(), GuiError> {
1109        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1110        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1111        match node.kind {
1112            WidgetKind::TextArea {
1113                text_buf: ref mut buf,
1114                text_len: ref mut len,
1115                cursor: ref mut c,
1116                selection: ref mut sel,
1117                ..
1118            } => {
1119                *buf = snap.text_buf;
1120                *len = snap.text_len;
1121                *c = snap.cursor;
1122                *sel = snap.selection;
1123                self.dirty.add(rect)?;
1124                self.push_event(UiEvent::ValueChanged(id))
1125            }
1126            _ => Err(GuiError::NotFound),
1127        }
1128    }
1129
1130    #[cfg(feature = "rich-widgets")]
1131    pub(crate) fn push_textarea_undo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
1132        if self.textarea_undo.len() == self.textarea_undo.capacity() {
1133            self.textarea_undo.remove(0);
1134        }
1135        let _ = self
1136            .textarea_undo
1137            .push(TextareaHistoryEntry { id, snapshot });
1138    }
1139
1140    #[cfg(feature = "rich-widgets")]
1141    pub(crate) fn push_textarea_redo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
1142        if self.textarea_redo.len() == self.textarea_redo.capacity() {
1143            self.textarea_redo.remove(0);
1144        }
1145        let _ = self
1146            .textarea_redo
1147            .push(TextareaHistoryEntry { id, snapshot });
1148    }
1149
1150    #[cfg(feature = "rich-widgets")]
1151    pub(crate) fn clear_textarea_redo_for(&mut self, id: WidgetId) {
1152        let mut i = 0usize;
1153        while i < self.textarea_redo.len() {
1154            if self.textarea_redo[i].id == id {
1155                self.textarea_redo.remove(i);
1156            } else {
1157                i += 1;
1158            }
1159        }
1160    }
1161
1162    #[cfg(feature = "rich-widgets")]
1163    pub(crate) fn textarea_undo(&mut self, id: WidgetId) -> Result<(), GuiError> {
1164        let Some(pos) = self.textarea_undo.iter().rposition(|entry| entry.id == id) else {
1165            return Ok(());
1166        };
1167        let current = self.capture_textarea_snapshot(id)?;
1168        let prior = self.textarea_undo.remove(pos).snapshot;
1169        self.push_textarea_redo(id, current);
1170        self.apply_textarea_snapshot(id, prior)
1171    }
1172
1173    #[cfg(feature = "rich-widgets")]
1174    pub(crate) fn textarea_redo(&mut self, id: WidgetId) -> Result<(), GuiError> {
1175        let Some(pos) = self.textarea_redo.iter().rposition(|entry| entry.id == id) else {
1176            return Ok(());
1177        };
1178        let current = self.capture_textarea_snapshot(id)?;
1179        let next = self.textarea_redo.remove(pos).snapshot;
1180        self.push_textarea_undo(id, current);
1181        self.apply_textarea_snapshot(id, next)
1182    }
1183
1184    #[cfg(feature = "rich-widgets")]
1185    pub fn textarea_backspace(&mut self, id: WidgetId) -> Result<(), GuiError> {
1186        self.textarea_insert_char(id, '\u{8}')
1187    }
1188
1189    #[cfg(feature = "rich-widgets")]
1190    pub fn textarea_delete_forward(&mut self, id: WidgetId) -> Result<(), GuiError> {
1191        self.textarea_insert_char(id, '\u{7f}')
1192    }
1193
1194    #[cfg(feature = "rich-widgets")]
1195    pub fn keyboard_selected_key(&self, id: WidgetId) -> Option<char> {
1196        match self.node(id)?.kind {
1197            WidgetKind::Keyboard {
1198                keys,
1199                alt_keys,
1200                selected,
1201                layout,
1202                ..
1203            } => keyboard_char_for_layout(keys, alt_keys, selected, layout),
1204            _ => None,
1205        }
1206    }
1207
1208    #[cfg(feature = "rich-widgets")]
1209    pub fn keyboard_layout(&self, id: WidgetId) -> Option<KeyboardLayout> {
1210        match self.node(id)?.kind {
1211            WidgetKind::Keyboard { layout, .. } => Some(layout),
1212            _ => None,
1213        }
1214    }
1215
1216    #[cfg(feature = "rich-widgets")]
1217    pub fn set_keyboard_layout(
1218        &mut self,
1219        id: WidgetId,
1220        layout: KeyboardLayout,
1221    ) -> Result<(), GuiError> {
1222        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1223        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1224        match node.kind {
1225            WidgetKind::Keyboard {
1226                layout: ref mut current,
1227                ..
1228            } => {
1229                *current = layout;
1230                self.dirty.add(rect)?;
1231                Ok(())
1232            }
1233            _ => Err(GuiError::NotFound),
1234        }
1235    }
1236
1237    #[cfg(feature = "rich-widgets")]
1238    pub fn set_keyboard_target(
1239        &mut self,
1240        id: WidgetId,
1241        target: Option<WidgetId>,
1242    ) -> Result<(), GuiError> {
1243        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1244        match node.kind {
1245            WidgetKind::Keyboard {
1246                target: ref mut current,
1247                ..
1248            } => {
1249                *current = target;
1250                Ok(())
1251            }
1252            _ => Err(GuiError::NotFound),
1253        }
1254    }
1255
1256    #[cfg(feature = "rich-widgets")]
1257    pub fn set_gauge_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1258        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1259        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1260        match node.kind {
1261            WidgetKind::Gauge {
1262                value: ref mut v,
1263                min,
1264                max,
1265                ..
1266            }
1267            | WidgetKind::ArcGauge {
1268                value: ref mut v,
1269                min,
1270                max,
1271                ..
1272            }
1273            | WidgetKind::GaugeNeedle {
1274                value: ref mut v,
1275                min,
1276                max,
1277                ..
1278            } => {
1279                *v = value.clamp(min.min(max), min.max(max));
1280                self.dirty.add(rect)?;
1281                Ok(())
1282            }
1283            _ => Err(GuiError::NotFound),
1284        }
1285    }
1286
1287    #[cfg(feature = "rich-widgets")]
1288    pub fn set_gauge_ticks(
1289        &mut self,
1290        id: WidgetId,
1291        major_ticks: u8,
1292        minor_ticks: u8,
1293        show_value: bool,
1294    ) -> Result<(), GuiError> {
1295        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1296        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1297        match node.kind {
1298            WidgetKind::Gauge {
1299                major_ticks: ref mut major,
1300                minor_ticks: ref mut minor,
1301                show_value: ref mut show,
1302                ..
1303            }
1304            | WidgetKind::ArcGauge {
1305                major_ticks: ref mut major,
1306                minor_ticks: ref mut minor,
1307                show_value: ref mut show,
1308                ..
1309            } => {
1310                *major = major_ticks.max(1);
1311                *minor = minor_ticks.max(1);
1312                *show = show_value;
1313                self.dirty.add(rect)?;
1314                Ok(())
1315            }
1316            _ => Err(GuiError::NotFound),
1317        }
1318    }
1319
1320    pub fn set_widget_rect(&mut self, id: WidgetId, rect: Rect) -> Result<(), GuiError> {
1321        let old = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1322        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1323        node.rect = rect;
1324        self.dirty.add(old)?;
1325        self.mark_subtree_dirty(id)?;
1326        Ok(())
1327    }
1328
1329    pub fn set_widget_x(&mut self, id: WidgetId, x: i32) -> Result<(), GuiError> {
1330        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1331        rect.x = x;
1332        self.set_widget_rect(id, rect)
1333    }
1334
1335    pub fn set_widget_y(&mut self, id: WidgetId, y: i32) -> Result<(), GuiError> {
1336        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1337        rect.y = y;
1338        self.set_widget_rect(id, rect)
1339    }
1340
1341    pub fn set_widget_width(&mut self, id: WidgetId, w: u32) -> Result<(), GuiError> {
1342        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1343        rect.w = w.max(1);
1344        self.set_widget_rect(id, rect)
1345    }
1346
1347    pub fn set_widget_height(&mut self, id: WidgetId, h: u32) -> Result<(), GuiError> {
1348        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1349        rect.h = h.max(1);
1350        self.set_widget_rect(id, rect)
1351    }
1352
1353    pub fn set_widget_opacity(&mut self, id: WidgetId, opacity: u8) -> Result<(), GuiError> {
1354        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1355        node.style.normal.opacity = opacity;
1356        node.style.focused.opacity = opacity;
1357        node.style.pressed.opacity = opacity;
1358        node.style.disabled.opacity = opacity;
1359        self.mark_subtree_dirty(id)
1360    }
1361
1362    pub fn set_widget_corner_radius(&mut self, id: WidgetId, radius: u8) -> Result<(), GuiError> {
1363        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1364        node.style.normal.corner_radius = radius;
1365        node.style.focused.corner_radius = radius;
1366        node.style.pressed.corner_radius = radius;
1367        node.style.disabled.corner_radius = radius;
1368        self.mark_subtree_dirty(id)
1369    }
1370
1371    pub fn set_widget_accent(&mut self, id: WidgetId, accent: Rgb565) -> Result<(), GuiError> {
1372        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1373        node.style.normal.accent = accent;
1374        node.style.focused.accent = accent;
1375        node.style.pressed.accent = accent;
1376        node.style.disabled.accent = accent;
1377        self.mark_subtree_dirty(id)
1378    }
1379
1380    pub fn set_widget_parent(
1381        &mut self,
1382        id: WidgetId,
1383        parent: Option<WidgetId>,
1384    ) -> Result<(), GuiError> {
1385        if let Some(parent) = parent {
1386            self.node(parent).ok_or(GuiError::NotFound)?;
1387        }
1388        self.node_mut(id).ok_or(GuiError::NotFound)?.parent = parent;
1389        self.mark_subtree_dirty(id)?;
1390        Ok(())
1391    }
1392
1393    pub fn add_child(&mut self, parent: WidgetId, child: WidgetId) -> Result<(), GuiError> {
1394        self.set_widget_parent(child, Some(parent))
1395    }
1396
1397    pub fn children_of(&self, parent: WidgetId) -> impl Iterator<Item = &WidgetNode<'a>> + '_ {
1398        self.widgets
1399            .iter()
1400            .filter(move |node| node.parent == Some(parent))
1401    }
1402
1403    #[inline]
1404    pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
1405        let node = self.node(id)?;
1406        if node.parent.is_none() {
1407            return Some(node.rect);
1408        }
1409        let mut rect = node.rect;
1410        let mut parent = node.parent;
1411        let mut depth = 0;
1412        while let Some(parent_id) = parent {
1413            if depth >= NODES {
1414                return None;
1415            }
1416            let parent_node = self.node(parent_id)?;
1417            rect.x += parent_node.rect.x;
1418            rect.y += parent_node.rect.y;
1419            parent = parent_node.parent;
1420            depth += 1;
1421        }
1422        Some(rect)
1423    }
1424
1425    pub fn set_flag(
1426        &mut self,
1427        id: WidgetId,
1428        flag: WidgetFlags,
1429        enabled: bool,
1430    ) -> Result<(), GuiError> {
1431        let was_set = self.has_flag(id, flag)?;
1432        let before_state = self.current_visual_state(id);
1433        self.mark_subtree_dirty(id)?;
1434        self.node_mut(id)
1435            .ok_or(GuiError::NotFound)?
1436            .flags
1437            .set(flag, enabled);
1438        if flag == WidgetFlags::DISABLED
1439            && enabled
1440            && self.pressed.is_some_and(|pressed| pressed.id == id)
1441        {
1442            self.pressed = None;
1443        }
1444        self.mark_subtree_dirty(id)?;
1445        if self
1446            .focus
1447            .is_some_and(|focus| !self.effective_focusable(focus))
1448        {
1449            self.focus = None;
1450            self.ensure_focus();
1451        }
1452        if flag == WidgetFlags::DISABLED && was_set != enabled {
1453            let after_state = self.current_visual_state(id);
1454            self.start_state_transition(id, before_state, after_state);
1455        }
1456        Ok(())
1457    }
1458
1459    pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
1460        Ok(self
1461            .node(id)
1462            .ok_or(GuiError::NotFound)?
1463            .flags
1464            .contains(flag))
1465    }
1466
1467    pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
1468        self.set_flag(id, flag, true)
1469    }
1470
1471    pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
1472        self.set_flag(id, flag, false)
1473    }
1474
1475    pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
1476        self.set_flag(id, WidgetFlags::HIDDEN, hidden)
1477    }
1478
1479    pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
1480        self.set_flag(id, WidgetFlags::DISABLED, disabled)
1481    }
1482
1483    pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
1484        self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
1485    }
1486
1487    pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
1488        self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
1489    }
1490
1491    pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
1492        self.set_hidden(id, !visible)
1493    }
1494
1495    pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
1496        self.set_disabled(id, !enabled)
1497    }
1498
1499    pub fn event_path<const M: usize>(
1500        &self,
1501        target: WidgetId,
1502        out: &mut heapless::Vec<EventContext, M>,
1503    ) -> Result<usize, GuiError> {
1504        self.node(target).ok_or(GuiError::NotFound)?;
1505        out.clear();
1506
1507        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
1508        let mut current = Some(target);
1509        while let Some(id) = current {
1510            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
1511            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
1512        }
1513
1514        for id in chain.iter().rev().copied().filter(|&id| id != target) {
1515            out.push(EventContext {
1516                target,
1517                current: id,
1518                phase: EventPhase::Capture,
1519            })
1520            .map_err(|_| GuiError::EventsFull)?;
1521        }
1522
1523        out.push(EventContext {
1524            target,
1525            current: target,
1526            phase: EventPhase::Target,
1527        })
1528        .map_err(|_| GuiError::EventsFull)?;
1529
1530        for id in chain.iter().copied().skip(1) {
1531            out.push(EventContext {
1532                target,
1533                current: id,
1534                phase: EventPhase::Bubble,
1535            })
1536            .map_err(|_| GuiError::EventsFull)?;
1537        }
1538
1539        Ok(out.len())
1540    }
1541
1542    pub fn widget_event_path<const M: usize>(
1543        &self,
1544        target: WidgetId,
1545        kind: WidgetEventKind,
1546        out: &mut heapless::Vec<WidgetEvent, M>,
1547    ) -> Result<usize, GuiError> {
1548        self.node(target).ok_or(GuiError::NotFound)?;
1549        out.clear();
1550
1551        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
1552        let mut current = Some(target);
1553        while let Some(id) = current {
1554            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
1555            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
1556        }
1557
1558        for id in chain.iter().rev().copied().filter(|&id| id != target) {
1559            out.push(WidgetEvent {
1560                target,
1561                current: id,
1562                phase: EventPhase::Capture,
1563                kind,
1564            })
1565            .map_err(|_| GuiError::EventsFull)?;
1566        }
1567
1568        out.push(WidgetEvent {
1569            target,
1570            current: target,
1571            phase: EventPhase::Target,
1572            kind,
1573        })
1574        .map_err(|_| GuiError::EventsFull)?;
1575
1576        if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
1577            for id in chain.iter().copied().skip(1) {
1578                out.push(WidgetEvent {
1579                    target,
1580                    current: id,
1581                    phase: EventPhase::Bubble,
1582                    kind,
1583                })
1584                .map_err(|_| GuiError::EventsFull)?;
1585            }
1586        }
1587
1588        Ok(out.len())
1589    }
1590
1591    pub fn dispatch_widget_event<const M: usize, F>(
1592        &self,
1593        target: WidgetId,
1594        kind: WidgetEventKind,
1595        scratch: &mut heapless::Vec<WidgetEvent, M>,
1596        mut handler: F,
1597    ) -> Result<(), GuiError>
1598    where
1599        F: FnMut(WidgetEvent) -> EventPolicy,
1600    {
1601        self.widget_event_path(target, kind, scratch)?;
1602        for event in scratch.iter().copied() {
1603            let handler_policy = handler(event);
1604            if matches!(handler_policy, EventPolicy::Stop)
1605                || self.stop_due_to_builtin_widget_behavior(event)
1606                || self.stop_due_to_registered_policy(event)
1607            {
1608                break;
1609            }
1610        }
1611        Ok(())
1612    }
1613
1614    pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
1615        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1616        self.dirty.add(rect)?;
1617        let child_ids: heapless::Vec<WidgetId, NODES> = self
1618            .widgets
1619            .iter()
1620            .filter(|node| node.parent == Some(id))
1621            .map(|node| node.id)
1622            .collect();
1623        for child in child_ids {
1624            self.mark_subtree_dirty(child)?;
1625        }
1626        Ok(())
1627    }
1628
1629    pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
1630        self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
1631        Ok(())
1632    }
1633
1634    pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
1635        self.active_focus_group = group;
1636        if let Some(focus) = self.focus {
1637            let still_valid = self.node(focus).is_some_and(|node| {
1638                group.is_none_or(|active| node.focus_group == active)
1639                    && self.effective_focusable(focus)
1640            });
1641            if !still_valid {
1642                self.focus = None;
1643                self.ensure_focus();
1644            }
1645        }
1646    }
1647
1648    pub fn apply_layout(
1649        &mut self,
1650        layout: LinearLayout,
1651        area: Rect,
1652        ids: &[WidgetId],
1653    ) -> Result<usize, GuiError> {
1654        let mut rects = [Rect::empty(); 16];
1655        let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
1656        for (id, rect) in ids.iter().copied().zip(rects).take(count) {
1657            self.set_widget_rect(id, rect)?;
1658        }
1659        Ok(count)
1660    }
1661
1662    pub fn apply_layout_flex(
1663        &mut self,
1664        layout: LinearLayout,
1665        area: Rect,
1666        ids: &[WidgetId],
1667        items: &[LayoutItem],
1668        enable_grow: bool,
1669        enable_shrink: bool,
1670    ) -> Result<usize, GuiError> {
1671        let mut rects = [Rect::empty(); 16];
1672        let count = ids.len().min(items.len()).min(rects.len());
1673        let laid_out = layout.arrange_items_flex(
1674            area,
1675            &items[..count],
1676            &mut rects,
1677            enable_grow,
1678            enable_shrink,
1679        );
1680        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
1681            self.set_widget_rect(id, rect)?;
1682        }
1683        Ok(laid_out)
1684    }
1685
1686    pub fn apply_layout_intrinsic(
1687        &mut self,
1688        layout: LinearLayout,
1689        area: Rect,
1690        ids: &[WidgetId],
1691    ) -> Result<usize, GuiError> {
1692        self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
1693    }
1694
1695    pub fn apply_layout_intrinsic_with_cross(
1696        &mut self,
1697        layout: LinearLayout,
1698        area: Rect,
1699        ids: &[WidgetId],
1700        preserve_cross: bool,
1701    ) -> Result<usize, GuiError> {
1702        let mut specs = [LayoutItem::fill(); 16];
1703        let mut rects = [Rect::empty(); 16];
1704        let count = ids.len().min(specs.len()).min(rects.len());
1705
1706        for (idx, id) in ids.iter().copied().take(count).enumerate() {
1707            let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
1708            specs[idx] = match layout.axis {
1709                Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
1710                    crate::layout::Constraint::Length(h)
1711                } else {
1712                    crate::layout::Constraint::Fill(1)
1713                }),
1714                Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
1715                    crate::layout::Constraint::Length(w)
1716                } else {
1717                    crate::layout::Constraint::Fill(1)
1718                }),
1719            };
1720        }
1721
1722        let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
1723        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
1724            self.set_widget_rect(id, rect)?;
1725        }
1726        Ok(laid_out)
1727    }
1728}