Skip to main content

embedded_gui/context/
mutators.rs

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