Skip to main content

embedded_gui/
context.rs

1use embedded_graphics_core::pixelcolor::Rgb565;
2use heapless::Vec;
3
4#[cfg(not(feature = "std"))]
5use crate::math::F32Ext as _;
6use crate::{
7    geometry::{DirtyError, DirtyTracker, Rect},
8    image::{ImageFit, ImageRef, ReelPlayer},
9    input::{
10        InputEvent, PointerState, UiEvent, UiEventFilter, WidgetDispatchPolicy, WidgetEvent,
11        WidgetEventKind,
12    },
13    layout::{Axis, LayoutItem, LinearLayout},
14    present::PresentRegion,
15    render::{RenderCtx, RenderQuality, TextAlign},
16    state::{FeedTimelineState, ListState, ScrollState, SliderState, TabsState},
17    style::{Style, Theme, VisualState, WidgetStyle, lerp_style},
18    widget::{
19        EventContext, EventPhase, EventPolicy, FocusGroupId, MenuContract, StyleClassId,
20        WidgetFlags, WidgetId,
21    },
22    widgets::{
23        ChartMode, KeyboardLayout, NotificationLevel, SurfaceState, TEXTAREA_CAPACITY, WidgetKind,
24        WidgetNode,
25    },
26};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum GuiError {
30    WidgetsFull,
31    EventsFull,
32    DirtyFull,
33    NotFound,
34}
35
36impl From<DirtyError> for GuiError {
37    fn from(_: DirtyError) -> Self {
38        Self::DirtyFull
39    }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
43struct PressTracker {
44    id: WidgetId,
45    start_x: i32,
46    start_y: i32,
47    last_x: i32,
48    last_y: i32,
49    elapsed_ms: u32,
50    long_emitted: bool,
51    gesture_emitted: bool,
52    repeat_elapsed_ms: u32,
53    scroll_velocity: f32,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
57struct InertiaScroll {
58    id: WidgetId,
59    velocity: f32,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct ScrollPhysics {
64    pub velocity_threshold: f32,
65    pub velocity_decay: f32,
66    pub drag_velocity_blend: f32,
67}
68
69impl Default for ScrollPhysics {
70    fn default() -> Self {
71        Self {
72            velocity_threshold: 0.05,
73            velocity_decay: 0.86,
74            drag_velocity_blend: 0.4,
75        }
76    }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct PressTiming {
81    pub long_press_ms: u32,
82    pub repeat_delay_ms: u32,
83    pub repeat_interval_ms: u32,
84}
85
86impl PressTiming {
87    pub const fn new(long_press_ms: u32, repeat_delay_ms: u32, repeat_interval_ms: u32) -> Self {
88        Self {
89            long_press_ms,
90            repeat_delay_ms,
91            repeat_interval_ms,
92        }
93    }
94}
95
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
97pub struct WidgetKeyInputPolicy {
98    pub raw_select: bool,
99    pub raw_back: bool,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub enum KeyBindingAction {
104    Default,
105    Ignore,
106    Activate,
107    Back,
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub struct WidgetKeyBindings {
112    pub select: KeyBindingAction,
113    pub back: KeyBindingAction,
114}
115
116impl Default for WidgetKeyBindings {
117    fn default() -> Self {
118        Self {
119            select: KeyBindingAction::Default,
120            back: KeyBindingAction::Default,
121        }
122    }
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126struct TextareaSnapshot {
127    text_buf: [u8; TEXTAREA_CAPACITY],
128    text_len: u8,
129    cursor: usize,
130    selection: Option<(usize, usize)>,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134struct TextareaHistoryEntry {
135    id: WidgetId,
136    snapshot: TextareaSnapshot,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140struct StateTransition {
141    id: WidgetId,
142    from: VisualState,
143    to: VisualState,
144    elapsed_ms: u32,
145}
146
147pub struct GuiContext<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize> {
148    viewport: Rect,
149    widgets: Vec<WidgetNode<'a>, NODES>,
150    subscriptions: Vec<(WidgetId, UiEventFilter), NODES>,
151    dispatch_policies: Vec<(WidgetId, WidgetDispatchPolicy), NODES>,
152    class_styles: Vec<(StyleClassId, WidgetStyle), NODES>,
153    events: Vec<UiEvent, EVENTS>,
154    dirty: DirtyTracker<DIRTY>,
155    theme: Theme,
156    focus: Option<WidgetId>,
157    active_focus_group: Option<FocusGroupId>,
158    render_quality: RenderQuality,
159    long_press_ms: u32,
160    textarea_cursor_blink_ms: u32,
161    textarea_cursor_blink_elapsed_ms: u32,
162    press_repeat_delay_ms: u32,
163    press_repeat_interval_ms: u32,
164    select_double_window_ms: u32,
165    select_elapsed_ms: u32,
166    last_select_id: Option<WidgetId>,
167    pointer_double_window_ms: u32,
168    pointer_elapsed_ms: u32,
169    last_pointer_id: Option<WidgetId>,
170    pressed: Option<PressTracker>,
171    inertia_scroll: Option<InertiaScroll>,
172    scroll_physics: ScrollPhysics,
173    state_transition_ms: u32,
174    state_transitions: Vec<StateTransition, NODES>,
175    widget_press_timings: Vec<(WidgetId, PressTiming), NODES>,
176    widget_key_policies: Vec<(WidgetId, WidgetKeyInputPolicy), NODES>,
177    widget_key_bindings: Vec<(WidgetId, WidgetKeyBindings), NODES>,
178    menu_contract: MenuContract,
179    textarea_undo: Vec<TextareaHistoryEntry, NODES>,
180    textarea_redo: Vec<TextareaHistoryEntry, NODES>,
181    next_id: u16,
182}
183
184impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
185    GuiContext<'a, NODES, EVENTS, DIRTY>
186{
187    pub fn new(viewport: Rect) -> Self {
188        let mut dirty = DirtyTracker::new();
189        let _ = dirty.mark_all(viewport);
190        Self {
191            viewport,
192            widgets: Vec::new(),
193            subscriptions: Vec::new(),
194            dispatch_policies: Vec::new(),
195            class_styles: Vec::new(),
196            events: Vec::new(),
197            dirty,
198            theme: Theme::default(),
199            focus: None,
200            active_focus_group: None,
201            render_quality: RenderQuality::High,
202            long_press_ms: 500,
203            textarea_cursor_blink_ms: 500,
204            textarea_cursor_blink_elapsed_ms: 0,
205            press_repeat_delay_ms: 650,
206            press_repeat_interval_ms: 140,
207            select_double_window_ms: 300,
208            select_elapsed_ms: 0,
209            last_select_id: None,
210            pointer_double_window_ms: 300,
211            pointer_elapsed_ms: 0,
212            last_pointer_id: None,
213            pressed: None,
214            inertia_scroll: None,
215            scroll_physics: ScrollPhysics::default(),
216            state_transition_ms: 0,
217            state_transitions: Vec::new(),
218            widget_press_timings: Vec::new(),
219            widget_key_policies: Vec::new(),
220            widget_key_bindings: Vec::new(),
221            menu_contract: MenuContract::default(),
222            textarea_undo: Vec::new(),
223            textarea_redo: Vec::new(),
224            next_id: 1,
225        }
226    }
227
228    pub const fn viewport(&self) -> Rect {
229        self.viewport
230    }
231
232    pub fn set_viewport(&mut self, viewport: Rect) -> Result<(), GuiError> {
233        self.viewport = viewport;
234        self.dirty.mark_all(viewport)?;
235        Ok(())
236    }
237
238    pub fn clear_widgets(&mut self) -> Result<(), GuiError> {
239        self.widgets.clear();
240        self.subscriptions.clear();
241        self.dispatch_policies.clear();
242        self.class_styles.clear();
243        self.focus = None;
244        self.pressed = None;
245        self.inertia_scroll = None;
246        self.last_select_id = None;
247        self.select_elapsed_ms = 0;
248        self.last_pointer_id = None;
249        self.pointer_elapsed_ms = 0;
250        self.state_transitions.clear();
251        self.widget_press_timings.clear();
252        self.widget_key_policies.clear();
253        self.widget_key_bindings.clear();
254        self.textarea_undo.clear();
255        self.textarea_redo.clear();
256        self.dirty.mark_all(self.viewport)?;
257        Ok(())
258    }
259
260    pub const fn long_press_threshold_ms(&self) -> u32 {
261        self.long_press_ms
262    }
263
264    pub fn set_long_press_threshold_ms(&mut self, threshold_ms: u32) {
265        self.long_press_ms = threshold_ms.max(1);
266    }
267
268    pub fn set_press_repeat_timing(&mut self, delay_ms: u32, interval_ms: u32) {
269        self.press_repeat_delay_ms = delay_ms.max(1);
270        self.press_repeat_interval_ms = interval_ms.max(1);
271    }
272
273    pub fn set_double_select_window_ms(&mut self, window_ms: u32) {
274        self.select_double_window_ms = window_ms.max(1);
275    }
276
277    pub fn set_double_pointer_window_ms(&mut self, window_ms: u32) {
278        self.pointer_double_window_ms = window_ms.max(1);
279    }
280
281    pub fn menu_contract(&self) -> MenuContract {
282        self.menu_contract
283    }
284
285    pub fn set_menu_contract(&mut self, contract: MenuContract) {
286        self.menu_contract = contract;
287    }
288
289    pub fn set_widget_press_timing(
290        &mut self,
291        id: WidgetId,
292        timing: PressTiming,
293    ) -> Result<(), GuiError> {
294        self.node(id).ok_or(GuiError::NotFound)?;
295        let timing = PressTiming {
296            long_press_ms: timing.long_press_ms.max(1),
297            repeat_delay_ms: timing.repeat_delay_ms.max(1),
298            repeat_interval_ms: timing.repeat_interval_ms.max(1),
299        };
300        if let Some((_, current)) = self
301            .widget_press_timings
302            .iter_mut()
303            .find(|(timing_id, _)| *timing_id == id)
304        {
305            *current = timing;
306            return Ok(());
307        }
308        self.widget_press_timings
309            .push((id, timing))
310            .map_err(|_| GuiError::WidgetsFull)
311    }
312
313    pub fn clear_widget_press_timing(&mut self, id: WidgetId) -> Result<(), GuiError> {
314        self.node(id).ok_or(GuiError::NotFound)?;
315        if let Some(pos) = self
316            .widget_press_timings
317            .iter()
318            .position(|(timing_id, _)| *timing_id == id)
319        {
320            self.widget_press_timings.remove(pos);
321        }
322        Ok(())
323    }
324
325    pub fn widget_press_timing(&self, id: WidgetId) -> Result<Option<PressTiming>, GuiError> {
326        self.node(id).ok_or(GuiError::NotFound)?;
327        Ok(self
328            .widget_press_timings
329            .iter()
330            .find(|(timing_id, _)| *timing_id == id)
331            .map(|(_, timing)| *timing))
332    }
333
334    pub fn set_widget_key_input_policy(
335        &mut self,
336        id: WidgetId,
337        policy: WidgetKeyInputPolicy,
338    ) -> Result<(), GuiError> {
339        self.node(id).ok_or(GuiError::NotFound)?;
340        if let Some((_, current)) = self
341            .widget_key_policies
342            .iter_mut()
343            .find(|(policy_id, _)| *policy_id == id)
344        {
345            *current = policy;
346            return Ok(());
347        }
348        self.widget_key_policies
349            .push((id, policy))
350            .map_err(|_| GuiError::WidgetsFull)
351    }
352
353    pub fn clear_widget_key_input_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
354        self.node(id).ok_or(GuiError::NotFound)?;
355        if let Some(pos) = self
356            .widget_key_policies
357            .iter()
358            .position(|(policy_id, _)| *policy_id == id)
359        {
360            self.widget_key_policies.remove(pos);
361        }
362        Ok(())
363    }
364
365    pub fn widget_key_input_policy(
366        &self,
367        id: WidgetId,
368    ) -> Result<Option<WidgetKeyInputPolicy>, GuiError> {
369        self.node(id).ok_or(GuiError::NotFound)?;
370        Ok(self
371            .widget_key_policies
372            .iter()
373            .find(|(policy_id, _)| *policy_id == id)
374            .map(|(_, policy)| *policy))
375    }
376
377    pub fn set_widget_key_bindings(
378        &mut self,
379        id: WidgetId,
380        bindings: WidgetKeyBindings,
381    ) -> Result<(), GuiError> {
382        self.node(id).ok_or(GuiError::NotFound)?;
383        if let Some((_, current)) = self
384            .widget_key_bindings
385            .iter_mut()
386            .find(|(binding_id, _)| *binding_id == id)
387        {
388            *current = bindings;
389            return Ok(());
390        }
391        self.widget_key_bindings
392            .push((id, bindings))
393            .map_err(|_| GuiError::WidgetsFull)
394    }
395
396    pub fn clear_widget_key_bindings(&mut self, id: WidgetId) -> Result<(), GuiError> {
397        self.node(id).ok_or(GuiError::NotFound)?;
398        if let Some(pos) = self
399            .widget_key_bindings
400            .iter()
401            .position(|(binding_id, _)| *binding_id == id)
402        {
403            self.widget_key_bindings.remove(pos);
404        }
405        Ok(())
406    }
407
408    pub fn widget_key_bindings(&self, id: WidgetId) -> Result<Option<WidgetKeyBindings>, GuiError> {
409        self.node(id).ok_or(GuiError::NotFound)?;
410        Ok(self
411            .widget_key_bindings
412            .iter()
413            .find(|(binding_id, _)| *binding_id == id)
414            .map(|(_, bindings)| *bindings))
415    }
416
417    pub fn set_scroll_physics(
418        &mut self,
419        velocity_threshold: f32,
420        velocity_decay: f32,
421        drag_velocity_blend: f32,
422    ) {
423        self.scroll_physics.velocity_threshold = velocity_threshold.max(0.001);
424        self.scroll_physics.velocity_decay = velocity_decay.clamp(0.01, 0.999);
425        self.scroll_physics.drag_velocity_blend = drag_velocity_blend.clamp(0.01, 1.0);
426    }
427
428    pub fn set_state_transition_duration_ms(&mut self, duration_ms: u32) {
429        self.state_transition_ms = duration_ms;
430        if duration_ms == 0 {
431            self.state_transitions.clear();
432        }
433    }
434
435    pub fn active_state_transitions(&self) -> usize {
436        self.state_transitions.len()
437    }
438
439    pub fn set_textarea_cursor_blink_timing(&mut self, period_ms: u32) {
440        self.textarea_cursor_blink_ms = period_ms.max(1);
441    }
442
443    pub fn widgets(&self) -> &[WidgetNode<'a>] {
444        self.widgets.as_slice()
445    }
446
447    pub fn dirty_regions(&self) -> &[Rect] {
448        self.dirty.as_slice()
449    }
450
451    pub fn present_regions(&self) -> impl Iterator<Item = PresentRegion> + '_ {
452        self.dirty
453            .as_slice()
454            .iter()
455            .copied()
456            .map(PresentRegion::from)
457    }
458
459    pub fn bounding_present_region(&self) -> Option<PresentRegion> {
460        self.dirty.bounding_rect().map(PresentRegion::from)
461    }
462
463    pub fn clear_dirty(&mut self) {
464        self.dirty.clear();
465    }
466
467    pub const fn theme(&self) -> Theme {
468        self.theme
469    }
470
471    pub fn set_theme(&mut self, theme: Theme) -> Result<(), GuiError> {
472        self.theme = theme;
473        self.dirty.mark_all(self.viewport)?;
474        Ok(())
475    }
476
477    pub fn set_style_class<S>(&mut self, class: StyleClassId, style: S) -> Result<(), GuiError>
478    where
479        S: Into<WidgetStyle>,
480    {
481        if class == StyleClassId::NONE {
482            return Ok(());
483        }
484        if let Some((_, slot)) = self.class_styles.iter_mut().find(|(id, _)| *id == class) {
485            *slot = style.into();
486        } else {
487            self.class_styles
488                .push((class, style.into()))
489                .map_err(|_| GuiError::WidgetsFull)?;
490        }
491        self.dirty.mark_all(self.viewport)?;
492        Ok(())
493    }
494
495    pub fn clear_style_class(&mut self, class: StyleClassId) -> Result<(), GuiError> {
496        if let Some(pos) = self.class_styles.iter().position(|(id, _)| *id == class) {
497            self.class_styles.remove(pos);
498            self.dirty.mark_all(self.viewport)?;
499        }
500        Ok(())
501    }
502
503    pub fn set_style_class_state(
504        &mut self,
505        class: StyleClassId,
506        state: VisualState,
507        style: Style,
508    ) -> Result<(), GuiError> {
509        if class == StyleClassId::NONE {
510            return Ok(());
511        }
512        if let Some((_, slot)) = self.class_styles.iter_mut().find(|(id, _)| *id == class) {
513            *slot = slot.with_state_override(state, style);
514        } else {
515            let base = WidgetStyle::new(Style::new()).with_state_override(state, style);
516            self.class_styles
517                .push((class, base))
518                .map_err(|_| GuiError::WidgetsFull)?;
519        }
520        self.dirty.mark_all(self.viewport)?;
521        Ok(())
522    }
523
524    pub fn set_widget_style_class(
525        &mut self,
526        id: WidgetId,
527        class: Option<StyleClassId>,
528    ) -> Result<(), GuiError> {
529        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
530        node.style_class = class.filter(|c| *c != StyleClassId::NONE);
531        self.mark_subtree_dirty(id)
532    }
533
534    pub fn apply_widget_style_transition(
535        &mut self,
536        id: WidgetId,
537        from: VisualState,
538        to: VisualState,
539        t: f32,
540    ) -> Result<(), GuiError> {
541        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
542        let a = node.style.resolve(from);
543        let b = node.style.resolve(to);
544        let blended = lerp_style(a, b, t);
545        node.style = node.style.with_state_override(VisualState::Normal, blended);
546        self.mark_subtree_dirty(id)
547    }
548
549    pub const fn render_quality(&self) -> RenderQuality {
550        self.render_quality
551    }
552
553    pub fn set_render_quality(&mut self, quality: RenderQuality) -> Result<(), GuiError> {
554        if self.render_quality != quality {
555            self.render_quality = quality;
556            self.dirty.mark_all(self.viewport)?;
557        }
558        Ok(())
559    }
560
561    pub const fn focus(&self) -> Option<WidgetId> {
562        self.focus
563    }
564
565    pub fn set_focus(&mut self, focus: Option<WidgetId>) -> Result<(), GuiError> {
566        if let Some(id) = focus {
567            self.node(id).ok_or(GuiError::NotFound)?;
568            if !self.effective_focusable(id) {
569                return Err(GuiError::NotFound);
570            }
571        }
572
573        let old = self.focus;
574        self.focus = focus;
575        self.textarea_cursor_blink_elapsed_ms = 0;
576        self.set_textarea_cursor_visible(old, true);
577        self.set_textarea_cursor_visible(focus, true);
578        self.start_focus_transitions(old, focus);
579        self.mark_focus_pair(old, focus)?;
580        if let Some(id) = old {
581            self.push_event(UiEvent::Defocused(id))?;
582        }
583        if let Some(id) = focus {
584            self.push_event(UiEvent::Focused(id))?;
585        }
586        self.push_event(UiEvent::FocusChanged { old, new: focus })?;
587        Ok(())
588    }
589
590    pub fn add_panel<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
591    where
592        S: Into<WidgetStyle>,
593    {
594        self.add_widget(rect, WidgetKind::Panel, style)
595    }
596
597    pub fn add_themed_panel(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
598        self.add_panel(rect, self.theme.panel)
599    }
600
601    pub fn add_label<S>(
602        &mut self,
603        rect: Rect,
604        text: &'a str,
605        style: S,
606    ) -> Result<WidgetId, GuiError>
607    where
608        S: Into<WidgetStyle>,
609    {
610        self.add_widget(rect, WidgetKind::Label(text), style)
611    }
612
613    pub fn add_themed_label(&mut self, rect: Rect, text: &'a str) -> Result<WidgetId, GuiError> {
614        self.add_label(rect, text, self.theme.label)
615    }
616
617    pub fn add_button<S>(
618        &mut self,
619        rect: Rect,
620        text: &'a str,
621        style: S,
622    ) -> Result<WidgetId, GuiError>
623    where
624        S: Into<WidgetStyle>,
625    {
626        let id = self.add_widget(rect, WidgetKind::Button(text), style)?;
627        self.ensure_focus();
628        Ok(id)
629    }
630
631    pub fn add_themed_button(&mut self, rect: Rect, text: &'a str) -> Result<WidgetId, GuiError> {
632        self.add_button(rect, text, self.theme.button)
633    }
634
635    pub fn add_progress_bar<S>(
636        &mut self,
637        rect: Rect,
638        value: f32,
639        style: S,
640    ) -> Result<WidgetId, GuiError>
641    where
642        S: Into<WidgetStyle>,
643    {
644        self.add_widget(
645            rect,
646            WidgetKind::ProgressBar {
647                value: value.clamp(0.0, 1.0),
648            },
649            style,
650        )
651    }
652
653    pub fn add_themed_progress_bar(
654        &mut self,
655        rect: Rect,
656        value: f32,
657    ) -> Result<WidgetId, GuiError> {
658        self.add_progress_bar(rect, value, self.theme.progress)
659    }
660
661    pub fn add_toggle<S>(
662        &mut self,
663        rect: Rect,
664        label: &'a str,
665        on: bool,
666        style: S,
667    ) -> Result<WidgetId, GuiError>
668    where
669        S: Into<WidgetStyle>,
670    {
671        let id = self.add_widget(rect, WidgetKind::Toggle { label, on }, style)?;
672        self.ensure_focus();
673        Ok(id)
674    }
675
676    pub fn add_themed_toggle(
677        &mut self,
678        rect: Rect,
679        label: &'a str,
680        on: bool,
681    ) -> Result<WidgetId, GuiError> {
682        self.add_toggle(rect, label, on, self.theme.toggle)
683    }
684
685    pub fn add_checkbox<S>(
686        &mut self,
687        rect: Rect,
688        label: &'a str,
689        checked: bool,
690        style: S,
691    ) -> Result<WidgetId, GuiError>
692    where
693        S: Into<WidgetStyle>,
694    {
695        let id = self.add_widget(rect, WidgetKind::Checkbox { label, checked }, style)?;
696        self.ensure_focus();
697        Ok(id)
698    }
699
700    pub fn add_themed_checkbox(
701        &mut self,
702        rect: Rect,
703        label: &'a str,
704        checked: bool,
705    ) -> Result<WidgetId, GuiError> {
706        self.add_checkbox(rect, label, checked, self.theme.checkbox)
707    }
708
709    pub fn add_slider<S>(
710        &mut self,
711        rect: Rect,
712        value: f32,
713        min: f32,
714        max: f32,
715        style: S,
716    ) -> Result<WidgetId, GuiError>
717    where
718        S: Into<WidgetStyle>,
719    {
720        let value = value.clamp(min.min(max), min.max(max));
721        let id = self.add_widget(rect, WidgetKind::Slider { value, min, max }, style)?;
722        self.ensure_focus();
723        Ok(id)
724    }
725
726    pub fn add_themed_slider(
727        &mut self,
728        rect: Rect,
729        value: f32,
730        min: f32,
731        max: f32,
732    ) -> Result<WidgetId, GuiError> {
733        self.add_slider(rect, value, min, max, self.theme.slider)
734    }
735
736    pub fn add_value_label<S>(
737        &mut self,
738        rect: Rect,
739        label: &'a str,
740        value: i32,
741        style: S,
742    ) -> Result<WidgetId, GuiError>
743    where
744        S: Into<WidgetStyle>,
745    {
746        self.add_widget(rect, WidgetKind::ValueLabel { label, value }, style)
747    }
748
749    pub fn add_themed_value_label(
750        &mut self,
751        rect: Rect,
752        label: &'a str,
753        value: i32,
754    ) -> Result<WidgetId, GuiError> {
755        self.add_value_label(rect, label, value, self.theme.value_label)
756    }
757
758    pub fn add_icon_button<S>(
759        &mut self,
760        rect: Rect,
761        icon: char,
762        label: &'a str,
763        style: S,
764    ) -> Result<WidgetId, GuiError>
765    where
766        S: Into<WidgetStyle>,
767    {
768        let id = self.add_widget(rect, WidgetKind::IconButton { icon, label }, style)?;
769        self.ensure_focus();
770        Ok(id)
771    }
772
773    pub fn add_themed_icon_button(
774        &mut self,
775        rect: Rect,
776        icon: char,
777        label: &'a str,
778    ) -> Result<WidgetId, GuiError> {
779        self.add_icon_button(rect, icon, label, self.theme.icon_button)
780    }
781
782    pub fn add_list<S>(
783        &mut self,
784        rect: Rect,
785        items: &'a [&'a str],
786        selected: usize,
787        visible_rows: usize,
788        style: S,
789    ) -> Result<WidgetId, GuiError>
790    where
791        S: Into<WidgetStyle>,
792    {
793        let selected = selected.min(items.len().saturating_sub(1));
794        let id = self.add_widget(
795            rect,
796            WidgetKind::List {
797                items,
798                selected,
799                offset: selected,
800                visible_rows: visible_rows.max(1),
801            },
802            style,
803        )?;
804        self.ensure_focus();
805        Ok(id)
806    }
807
808    pub fn add_feed_timeline<S>(
809        &mut self,
810        rect: Rect,
811        items: &'a [&'a str],
812        selected: usize,
813        visible_rows: usize,
814        expanded: bool,
815        style: S,
816    ) -> Result<WidgetId, GuiError>
817    where
818        S: Into<WidgetStyle>,
819    {
820        let selected = selected.min(items.len().saturating_sub(1));
821        let id = self.add_widget(
822            rect,
823            WidgetKind::FeedTimeline {
824                items,
825                selected,
826                offset: selected,
827                visible_rows: visible_rows.max(1),
828                expanded,
829            },
830            style,
831        )?;
832        self.ensure_focus();
833        Ok(id)
834    }
835
836    pub fn add_themed_list(
837        &mut self,
838        rect: Rect,
839        items: &'a [&'a str],
840        selected: usize,
841        visible_rows: usize,
842    ) -> Result<WidgetId, GuiError> {
843        self.add_list(rect, items, selected, visible_rows, self.theme.list)
844    }
845
846    pub fn add_scroll_view<S>(
847        &mut self,
848        rect: Rect,
849        offset_y: i32,
850        content_h: u32,
851        style: S,
852    ) -> Result<WidgetId, GuiError>
853    where
854        S: Into<WidgetStyle>,
855    {
856        let id = self.add_widget(
857            rect,
858            WidgetKind::ScrollView {
859                offset_y,
860                content_h,
861            },
862            style,
863        )?;
864        self.ensure_focus();
865        Ok(id)
866    }
867
868    pub fn add_themed_scroll_view(
869        &mut self,
870        rect: Rect,
871        offset_y: i32,
872        content_h: u32,
873    ) -> Result<WidgetId, GuiError> {
874        self.add_scroll_view(rect, offset_y, content_h, self.theme.list)
875    }
876
877    pub fn add_tabs<S>(
878        &mut self,
879        rect: Rect,
880        labels: &'a [&'a str],
881        selected: usize,
882        style: S,
883    ) -> Result<WidgetId, GuiError>
884    where
885        S: Into<WidgetStyle>,
886    {
887        let selected = selected.min(labels.len().saturating_sub(1));
888        let id = self.add_widget(rect, WidgetKind::Tabs { labels, selected }, style)?;
889        self.ensure_focus();
890        Ok(id)
891    }
892
893    pub fn add_themed_tabs(
894        &mut self,
895        rect: Rect,
896        labels: &'a [&'a str],
897        selected: usize,
898    ) -> Result<WidgetId, GuiError> {
899        self.add_tabs(rect, labels, selected, self.theme.tabs)
900    }
901
902    pub fn add_dialog<S>(
903        &mut self,
904        rect: Rect,
905        title: &'a str,
906        body: &'a str,
907        style: S,
908    ) -> Result<WidgetId, GuiError>
909    where
910        S: Into<WidgetStyle>,
911    {
912        self.add_widget(rect, WidgetKind::Dialog { title, body }, style)
913    }
914
915    pub fn add_themed_dialog(
916        &mut self,
917        rect: Rect,
918        title: &'a str,
919        body: &'a str,
920    ) -> Result<WidgetId, GuiError> {
921        self.add_dialog(rect, title, body, self.theme.dialog)
922    }
923
924    pub fn add_toast<S>(
925        &mut self,
926        rect: Rect,
927        text: &'a str,
928        ttl_ms: u32,
929        style: S,
930    ) -> Result<WidgetId, GuiError>
931    where
932        S: Into<WidgetStyle>,
933    {
934        self.add_widget(rect, WidgetKind::Toast { text, ttl_ms }, style)
935    }
936
937    pub fn add_themed_toast(
938        &mut self,
939        rect: Rect,
940        text: &'a str,
941        ttl_ms: u32,
942    ) -> Result<WidgetId, GuiError> {
943        self.add_toast(rect, text, ttl_ms, self.theme.toast)
944    }
945
946    pub fn add_meter<S>(
947        &mut self,
948        rect: Rect,
949        value: f32,
950        min: f32,
951        max: f32,
952        style: S,
953    ) -> Result<WidgetId, GuiError>
954    where
955        S: Into<WidgetStyle>,
956    {
957        self.add_widget(rect, WidgetKind::Meter { value, min, max }, style)
958    }
959
960    pub fn add_themed_meter(
961        &mut self,
962        rect: Rect,
963        value: f32,
964        min: f32,
965        max: f32,
966    ) -> Result<WidgetId, GuiError> {
967        self.add_meter(rect, value, min, max, self.theme.meter)
968    }
969
970    #[allow(clippy::too_many_arguments)]
971    pub fn add_arc_gauge<S>(
972        &mut self,
973        rect: Rect,
974        value: f32,
975        min: f32,
976        max: f32,
977        start_deg: i32,
978        end_deg: i32,
979        thickness: u8,
980        antialias: bool,
981        style: S,
982    ) -> Result<WidgetId, GuiError>
983    where
984        S: Into<WidgetStyle>,
985    {
986        self.add_widget(
987            rect,
988            WidgetKind::ArcGauge {
989                value,
990                min,
991                max,
992                start_deg,
993                end_deg,
994                thickness: thickness.max(1),
995                antialias,
996                major_ticks: 6,
997                minor_ticks: 2,
998                show_value: false,
999            },
1000            style,
1001        )
1002    }
1003
1004    pub fn add_gauge<S>(
1005        &mut self,
1006        rect: Rect,
1007        value: f32,
1008        min: f32,
1009        max: f32,
1010        style: S,
1011    ) -> Result<WidgetId, GuiError>
1012    where
1013        S: Into<WidgetStyle>,
1014    {
1015        self.add_widget(
1016            rect,
1017            WidgetKind::Gauge {
1018                value,
1019                min,
1020                max,
1021                major_ticks: 6,
1022                minor_ticks: 2,
1023                show_value: false,
1024            },
1025            style,
1026        )
1027    }
1028
1029    #[allow(clippy::too_many_arguments)]
1030    pub fn add_gauge_needle<S>(
1031        &mut self,
1032        rect: Rect,
1033        value: f32,
1034        min: f32,
1035        max: f32,
1036        start_deg: i32,
1037        end_deg: i32,
1038        style: S,
1039    ) -> Result<WidgetId, GuiError>
1040    where
1041        S: Into<WidgetStyle>,
1042    {
1043        self.add_widget(
1044            rect,
1045            WidgetKind::GaugeNeedle {
1046                value,
1047                min,
1048                max,
1049                start_deg,
1050                end_deg,
1051            },
1052            style,
1053        )
1054    }
1055
1056    pub fn add_chart<S>(
1057        &mut self,
1058        rect: Rect,
1059        values: &'a [f32],
1060        min: f32,
1061        max: f32,
1062        style: S,
1063    ) -> Result<WidgetId, GuiError>
1064    where
1065        S: Into<WidgetStyle>,
1066    {
1067        self.add_widget(
1068            rect,
1069            WidgetKind::Chart {
1070                values,
1071                min,
1072                max,
1073                thickness: 1,
1074                fill_under: false,
1075                markers: false,
1076                mode: ChartMode::Line,
1077                show_grid: false,
1078                show_axes: false,
1079                show_labels: false,
1080            },
1081            style,
1082        )
1083    }
1084
1085    pub fn set_chart_style(
1086        &mut self,
1087        id: WidgetId,
1088        thickness: u8,
1089        fill_under: bool,
1090        markers: bool,
1091    ) -> Result<(), GuiError> {
1092        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1093        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1094        match node.kind {
1095            WidgetKind::Chart {
1096                thickness: ref mut t,
1097                fill_under: ref mut fill,
1098                markers: ref mut mark,
1099                ..
1100            } => {
1101                *t = thickness.max(1);
1102                *fill = fill_under;
1103                *mark = markers;
1104                self.dirty.add(rect)?;
1105                Ok(())
1106            }
1107            _ => Err(GuiError::NotFound),
1108        }
1109    }
1110
1111    pub fn set_chart_decoration(
1112        &mut self,
1113        id: WidgetId,
1114        mode: ChartMode,
1115        show_grid: bool,
1116        show_axes: bool,
1117        show_labels: bool,
1118    ) -> Result<(), GuiError> {
1119        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1120        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1121        match node.kind {
1122            WidgetKind::Chart {
1123                mode: ref mut chart_mode,
1124                show_grid: ref mut grid,
1125                show_axes: ref mut axes,
1126                show_labels: ref mut labels,
1127                ..
1128            } => {
1129                *chart_mode = mode;
1130                *grid = show_grid;
1131                *axes = show_axes;
1132                *labels = show_labels;
1133                self.dirty.add(rect)?;
1134                Ok(())
1135            }
1136            _ => Err(GuiError::NotFound),
1137        }
1138    }
1139
1140    pub fn add_spinner<S>(&mut self, rect: Rect, phase: f32, style: S) -> Result<WidgetId, GuiError>
1141    where
1142        S: Into<WidgetStyle>,
1143    {
1144        self.add_widget(rect, WidgetKind::Spinner { phase }, style)
1145    }
1146
1147    pub fn add_dropdown<S>(
1148        &mut self,
1149        rect: Rect,
1150        items: &'a [&'a str],
1151        selected: usize,
1152        style: S,
1153    ) -> Result<WidgetId, GuiError>
1154    where
1155        S: Into<WidgetStyle>,
1156    {
1157        let selected = selected.min(items.len().saturating_sub(1));
1158        let id = self.add_widget(
1159            rect,
1160            WidgetKind::Dropdown {
1161                items,
1162                selected,
1163                open: false,
1164            },
1165            style,
1166        )?;
1167        self.ensure_focus();
1168        Ok(id)
1169    }
1170
1171    pub fn add_roller<S>(
1172        &mut self,
1173        rect: Rect,
1174        items: &'a [&'a str],
1175        selected: usize,
1176        style: S,
1177    ) -> Result<WidgetId, GuiError>
1178    where
1179        S: Into<WidgetStyle>,
1180    {
1181        let selected = selected.min(items.len().saturating_sub(1));
1182        let id = self.add_widget(rect, WidgetKind::Roller { items, selected }, style)?;
1183        self.ensure_focus();
1184        Ok(id)
1185    }
1186
1187    pub fn add_table<S>(
1188        &mut self,
1189        rect: Rect,
1190        rows: &'a [&'a [&'a str]],
1191        style: S,
1192    ) -> Result<WidgetId, GuiError>
1193    where
1194        S: Into<WidgetStyle>,
1195    {
1196        self.add_widget(
1197            rect,
1198            WidgetKind::Table {
1199                rows,
1200                separators: true,
1201                cell_padding: 1,
1202                align: TextAlign::Left,
1203            },
1204            style,
1205        )
1206    }
1207
1208    pub fn set_table_style(
1209        &mut self,
1210        id: WidgetId,
1211        separators: bool,
1212        cell_padding: u8,
1213        align: TextAlign,
1214    ) -> Result<(), GuiError> {
1215        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1216        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1217        match node.kind {
1218            WidgetKind::Table {
1219                separators: ref mut cell_sep,
1220                cell_padding: ref mut pad,
1221                align: ref mut table_align,
1222                ..
1223            } => {
1224                *cell_sep = separators;
1225                *pad = cell_padding.min(6);
1226                *table_align = align;
1227                self.dirty.add(rect)?;
1228                Ok(())
1229            }
1230            _ => Err(GuiError::NotFound),
1231        }
1232    }
1233
1234    pub fn add_textarea<S>(
1235        &mut self,
1236        rect: Rect,
1237        text: &'a str,
1238        placeholder: &'a str,
1239        style: S,
1240    ) -> Result<WidgetId, GuiError>
1241    where
1242        S: Into<WidgetStyle>,
1243    {
1244        let cursor = text.chars().count();
1245        let (text_buf, text_len) = textarea_storage_from_str(text);
1246        let id = self.add_widget(
1247            rect,
1248            WidgetKind::TextArea {
1249                text_buf,
1250                text_len,
1251                cursor,
1252                placeholder,
1253                selection: None,
1254                cursor_visible: true,
1255                read_only: false,
1256                single_line: false,
1257                accept_newline: true,
1258            },
1259            style,
1260        )?;
1261        self.ensure_focus();
1262        Ok(id)
1263    }
1264
1265    pub fn add_keyboard<S>(
1266        &mut self,
1267        rect: Rect,
1268        keys: &'a [char],
1269        cols: u8,
1270        target: Option<WidgetId>,
1271        style: S,
1272    ) -> Result<WidgetId, GuiError>
1273    where
1274        S: Into<WidgetStyle>,
1275    {
1276        self.add_keyboard_with_alt(rect, keys, None, cols, target, style)
1277    }
1278
1279    pub fn add_keyboard_with_alt<S>(
1280        &mut self,
1281        rect: Rect,
1282        keys: &'a [char],
1283        alt_keys: Option<&'a [char]>,
1284        cols: u8,
1285        target: Option<WidgetId>,
1286        style: S,
1287    ) -> Result<WidgetId, GuiError>
1288    where
1289        S: Into<WidgetStyle>,
1290    {
1291        let id = self.add_widget(
1292            rect,
1293            WidgetKind::Keyboard {
1294                keys,
1295                selected: 0,
1296                cols: cols.max(1),
1297                alt_keys,
1298                layout: KeyboardLayout::Normal,
1299                target,
1300            },
1301            style,
1302        )?;
1303        self.ensure_focus();
1304        Ok(id)
1305    }
1306
1307    pub fn add_image<S>(
1308        &mut self,
1309        rect: Rect,
1310        image: ImageRef<'a>,
1311        fit: ImageFit,
1312        style: S,
1313    ) -> Result<WidgetId, GuiError>
1314    where
1315        S: Into<WidgetStyle>,
1316    {
1317        self.add_widget(rect, WidgetKind::Image { image, fit }, style)
1318    }
1319
1320    pub fn add_peek_reveal<S>(
1321        &mut self,
1322        rect: Rect,
1323        icon: ImageRef<'a>,
1324        title: &'a str,
1325        subtitle: &'a str,
1326        style: S,
1327    ) -> Result<WidgetId, GuiError>
1328    where
1329        S: Into<WidgetStyle>,
1330    {
1331        self.add_widget(
1332            rect,
1333            WidgetKind::PeekReveal {
1334                icon,
1335                title,
1336                subtitle,
1337                progress: 0.0,
1338            },
1339            style,
1340        )
1341    }
1342
1343    pub fn add_glance_tile<S>(
1344        &mut self,
1345        rect: Rect,
1346        icon: char,
1347        title: &'a str,
1348        subtitle: &'a str,
1349        style: S,
1350    ) -> Result<WidgetId, GuiError>
1351    where
1352        S: Into<WidgetStyle>,
1353    {
1354        let id = self.add_widget(
1355            rect,
1356            WidgetKind::GlanceTile {
1357                icon,
1358                title,
1359                subtitle,
1360                highlighted: false,
1361            },
1362            style,
1363        )?;
1364        self.ensure_focus();
1365        Ok(id)
1366    }
1367
1368    pub fn add_card_deck<S>(
1369        &mut self,
1370        rect: Rect,
1371        titles: &'a [&'a str],
1372        selected: usize,
1373        style: S,
1374    ) -> Result<WidgetId, GuiError>
1375    where
1376        S: Into<WidgetStyle>,
1377    {
1378        self.add_widget(
1379            rect,
1380            WidgetKind::CardDeck {
1381                titles,
1382                selected: selected.min(titles.len().saturating_sub(1)),
1383            },
1384            style,
1385        )
1386    }
1387
1388    pub fn add_reel<S>(
1389        &mut self,
1390        rect: Rect,
1391        player: ReelPlayer<'a>,
1392        fit: ImageFit,
1393        style: S,
1394    ) -> Result<WidgetId, GuiError>
1395    where
1396        S: Into<WidgetStyle>,
1397    {
1398        self.add_widget(rect, WidgetKind::Reel { player, fit }, style)
1399    }
1400
1401    pub fn add_state_surface<S>(
1402        &mut self,
1403        rect: Rect,
1404        state: SurfaceState,
1405        title: &'a str,
1406        message: &'a str,
1407        action: Option<&'a str>,
1408        style: S,
1409    ) -> Result<WidgetId, GuiError>
1410    where
1411        S: Into<WidgetStyle>,
1412    {
1413        self.add_widget(
1414            rect,
1415            WidgetKind::StateSurface {
1416                state,
1417                title,
1418                message,
1419                action,
1420                busy_phase: 0.0,
1421            },
1422            style,
1423        )
1424    }
1425
1426    pub fn add_heads_up_banner<S>(
1427        &mut self,
1428        rect: Rect,
1429        level: NotificationLevel,
1430        text: &'a str,
1431        ttl_ms: u32,
1432        style: S,
1433    ) -> Result<WidgetId, GuiError>
1434    where
1435        S: Into<WidgetStyle>,
1436    {
1437        self.add_widget(
1438            rect,
1439            WidgetKind::HeadsUpBanner {
1440                level,
1441                text,
1442                ttl_ms,
1443            },
1444            style,
1445        )
1446    }
1447
1448    #[allow(clippy::too_many_arguments)]
1449    pub fn add_notification_action_sheet<S>(
1450        &mut self,
1451        rect: Rect,
1452        level: NotificationLevel,
1453        title: &'a str,
1454        body: &'a str,
1455        actions: &'a [&'a str],
1456        selected: usize,
1457        open: bool,
1458        style: S,
1459    ) -> Result<WidgetId, GuiError>
1460    where
1461        S: Into<WidgetStyle>,
1462    {
1463        self.add_widget(
1464            rect,
1465            WidgetKind::NotificationActionSheet {
1466                level,
1467                title,
1468                body,
1469                actions,
1470                selected: selected.min(actions.len().saturating_sub(1)),
1471                open,
1472            },
1473            style,
1474        )
1475    }
1476
1477    pub fn add_border<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
1478    where
1479        S: Into<WidgetStyle>,
1480    {
1481        self.add_widget(rect, WidgetKind::Border, style)
1482    }
1483
1484    pub fn add_spacer(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
1485        self.add_widget(rect, WidgetKind::Spacer, Style::default())
1486    }
1487
1488    pub fn add_menu<S>(
1489        &mut self,
1490        rect: Rect,
1491        items: &'a [&'a str],
1492        selected: usize,
1493        style: S,
1494    ) -> Result<WidgetId, GuiError>
1495    where
1496        S: Into<WidgetStyle>,
1497    {
1498        let selected = selected.min(items.len().saturating_sub(1));
1499        let id = self.add_widget(rect, WidgetKind::Menu { items, selected }, style)?;
1500        self.ensure_focus();
1501        Ok(id)
1502    }
1503
1504    pub fn set_progress(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1505        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1506        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1507        match node.kind {
1508            WidgetKind::ProgressBar { value: ref mut v } => {
1509                *v = value.clamp(0.0, 1.0);
1510                self.dirty.add(rect)?;
1511                Ok(())
1512            }
1513            WidgetKind::PeekReveal {
1514                progress: ref mut v,
1515                ..
1516            } => {
1517                *v = value.clamp(0.0, 1.0);
1518                self.dirty.add(rect)?;
1519                Ok(())
1520            }
1521            _ => Err(GuiError::NotFound),
1522        }
1523    }
1524
1525    pub fn set_glance_highlighted(
1526        &mut self,
1527        id: WidgetId,
1528        highlighted: bool,
1529    ) -> Result<(), GuiError> {
1530        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1531        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1532        match node.kind {
1533            WidgetKind::GlanceTile {
1534                highlighted: ref mut h,
1535                ..
1536            } => {
1537                *h = highlighted;
1538                self.dirty.add(rect)?;
1539                Ok(())
1540            }
1541            _ => Err(GuiError::NotFound),
1542        }
1543    }
1544
1545    pub fn set_card_deck_selected(
1546        &mut self,
1547        id: WidgetId,
1548        selected: usize,
1549    ) -> Result<(), GuiError> {
1550        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1551        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1552        match node.kind {
1553            WidgetKind::CardDeck {
1554                titles,
1555                selected: ref mut current,
1556            } => {
1557                *current = selected.min(titles.len().saturating_sub(1));
1558                self.dirty.add(rect)?;
1559                Ok(())
1560            }
1561            _ => Err(GuiError::NotFound),
1562        }
1563    }
1564
1565    pub fn tick_reel(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1566        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1567        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1568        match node.kind {
1569            WidgetKind::Reel {
1570                player: ref mut reel,
1571                ..
1572            } => {
1573                reel.tick(dt_ms);
1574                self.dirty.add(rect)?;
1575                Ok(())
1576            }
1577            _ => Err(GuiError::NotFound),
1578        }
1579    }
1580
1581    pub fn set_state_surface_state(
1582        &mut self,
1583        id: WidgetId,
1584        state: SurfaceState,
1585    ) -> Result<(), GuiError> {
1586        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1587        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1588        match node.kind {
1589            WidgetKind::StateSurface {
1590                state: ref mut current,
1591                ..
1592            } => {
1593                *current = state;
1594                self.dirty.add(rect)?;
1595                Ok(())
1596            }
1597            _ => Err(GuiError::NotFound),
1598        }
1599    }
1600
1601    pub fn set_state_surface_message(
1602        &mut self,
1603        id: WidgetId,
1604        message: &'a str,
1605    ) -> Result<(), GuiError> {
1606        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1607        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1608        match node.kind {
1609            WidgetKind::StateSurface {
1610                message: ref mut current,
1611                ..
1612            } => {
1613                *current = message;
1614                self.dirty.add(rect)?;
1615                Ok(())
1616            }
1617            _ => Err(GuiError::NotFound),
1618        }
1619    }
1620
1621    pub fn set_state_surface_action(
1622        &mut self,
1623        id: WidgetId,
1624        action: Option<&'a str>,
1625    ) -> Result<(), GuiError> {
1626        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1627        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1628        match node.kind {
1629            WidgetKind::StateSurface {
1630                action: ref mut current,
1631                ..
1632            } => {
1633                *current = action;
1634                self.dirty.add(rect)?;
1635                Ok(())
1636            }
1637            _ => Err(GuiError::NotFound),
1638        }
1639    }
1640
1641    pub fn set_state_surface_busy_phase(
1642        &mut self,
1643        id: WidgetId,
1644        phase: f32,
1645    ) -> Result<(), GuiError> {
1646        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1647        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1648        match node.kind {
1649            WidgetKind::StateSurface {
1650                busy_phase: ref mut current,
1651                ..
1652            } => {
1653                *current = phase;
1654                self.dirty.add(rect)?;
1655                Ok(())
1656            }
1657            _ => Err(GuiError::NotFound),
1658        }
1659    }
1660
1661    pub fn tick_state_surface(
1662        &mut self,
1663        id: WidgetId,
1664        dt_ms: u32,
1665        cycles_per_sec: f32,
1666    ) -> Result<(), GuiError> {
1667        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1668            WidgetKind::StateSurface { busy_phase, .. } => {
1669                busy_phase + (dt_ms as f32 / 1000.0) * cycles_per_sec
1670            }
1671            _ => return Err(GuiError::NotFound),
1672        };
1673        self.set_state_surface_busy_phase(id, phase)
1674    }
1675
1676    pub fn set_heads_up_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
1677        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1678        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1679        match node.kind {
1680            WidgetKind::HeadsUpBanner {
1681                ttl_ms: ref mut current,
1682                ..
1683            } => {
1684                *current = ttl_ms;
1685                self.dirty.add(rect)?;
1686                Ok(())
1687            }
1688            _ => Err(GuiError::NotFound),
1689        }
1690    }
1691
1692    pub fn tick_heads_up(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1693        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1694            WidgetKind::HeadsUpBanner { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
1695            _ => return Err(GuiError::NotFound),
1696        };
1697        self.set_heads_up_ttl(id, ttl)
1698    }
1699
1700    pub fn set_notification_sheet_open(
1701        &mut self,
1702        id: WidgetId,
1703        open: bool,
1704    ) -> Result<(), GuiError> {
1705        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1706        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1707        match node.kind {
1708            WidgetKind::NotificationActionSheet {
1709                open: ref mut current,
1710                ..
1711            } => {
1712                *current = open;
1713                self.dirty.add(rect)?;
1714                Ok(())
1715            }
1716            _ => Err(GuiError::NotFound),
1717        }
1718    }
1719
1720    pub fn set_notification_sheet_selected(
1721        &mut self,
1722        id: WidgetId,
1723        selected: usize,
1724    ) -> Result<(), GuiError> {
1725        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1726        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1727        match node.kind {
1728            WidgetKind::NotificationActionSheet {
1729                actions,
1730                selected: ref mut current,
1731                ..
1732            } => {
1733                *current = selected.min(actions.len().saturating_sub(1));
1734                self.dirty.add(rect)?;
1735                Ok(())
1736            }
1737            _ => Err(GuiError::NotFound),
1738        }
1739    }
1740
1741    pub fn set_menu_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1742        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1743        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1744        match node.kind {
1745            WidgetKind::Menu {
1746                items,
1747                selected: ref mut current,
1748            } => {
1749                *current = selected.min(items.len().saturating_sub(1));
1750                self.dirty.add(rect)?;
1751                Ok(())
1752            }
1753            _ => Err(GuiError::NotFound),
1754        }
1755    }
1756
1757    pub fn menu_selected(&self, id: WidgetId) -> Option<usize> {
1758        match self.node(id)?.kind {
1759            WidgetKind::Menu { selected, .. } => Some(selected),
1760            _ => None,
1761        }
1762    }
1763
1764    pub fn list_selected(&self, id: WidgetId) -> Option<usize> {
1765        match self.node(id)?.kind {
1766            WidgetKind::List { selected, .. } => Some(selected),
1767            _ => None,
1768        }
1769    }
1770
1771    pub fn set_list_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1772        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1773        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1774        match node.kind {
1775            WidgetKind::List {
1776                items,
1777                selected: ref mut current,
1778                ref mut offset,
1779                visible_rows,
1780            } => {
1781                let mut state = ListState::new(*current, *offset, visible_rows);
1782                state.set_selected(selected, items.len());
1783                *current = state.selected;
1784                *offset = state.offset;
1785                self.dirty.add(rect)?;
1786                Ok(())
1787            }
1788            _ => Err(GuiError::NotFound),
1789        }
1790    }
1791
1792    pub fn feed_selected(&self, id: WidgetId) -> Option<usize> {
1793        match self.node(id)?.kind {
1794            WidgetKind::FeedTimeline { selected, .. } => Some(selected),
1795            _ => None,
1796        }
1797    }
1798
1799    pub fn set_feed_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1800        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1801        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1802        match node.kind {
1803            WidgetKind::FeedTimeline {
1804                items,
1805                selected: ref mut current,
1806                ref mut offset,
1807                visible_rows,
1808                ..
1809            } => {
1810                let mut state = FeedTimelineState::new(*current, *offset, visible_rows, false);
1811                state.set_selected(selected, items.len());
1812                *current = state.selected;
1813                *offset = state.offset;
1814                self.dirty.add(rect)?;
1815                Ok(())
1816            }
1817            _ => Err(GuiError::NotFound),
1818        }
1819    }
1820
1821    pub fn set_feed_expanded(&mut self, id: WidgetId, expanded: bool) -> Result<(), GuiError> {
1822        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1823        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1824        match node.kind {
1825            WidgetKind::FeedTimeline {
1826                expanded: ref mut current,
1827                ..
1828            } => {
1829                *current = expanded;
1830                self.dirty.add(rect)?;
1831                Ok(())
1832            }
1833            _ => Err(GuiError::NotFound),
1834        }
1835    }
1836
1837    pub fn set_toggle(&mut self, id: WidgetId, on: bool) -> Result<(), GuiError> {
1838        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1839        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1840        match node.kind {
1841            WidgetKind::Toggle { on: ref mut v, .. } => {
1842                *v = on;
1843                self.dirty.add(rect)?;
1844                Ok(())
1845            }
1846            _ => Err(GuiError::NotFound),
1847        }
1848    }
1849
1850    pub fn toggle_value(&self, id: WidgetId) -> Option<bool> {
1851        match self.node(id)?.kind {
1852            WidgetKind::Toggle { on, .. } => Some(on),
1853            _ => None,
1854        }
1855    }
1856
1857    pub fn set_checked(&mut self, id: WidgetId, checked: bool) -> Result<(), GuiError> {
1858        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1859        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1860        match node.kind {
1861            WidgetKind::Checkbox {
1862                checked: ref mut v, ..
1863            } => {
1864                *v = checked;
1865                self.dirty.add(rect)?;
1866                Ok(())
1867            }
1868            _ => Err(GuiError::NotFound),
1869        }
1870    }
1871
1872    pub fn checked_value(&self, id: WidgetId) -> Option<bool> {
1873        match self.node(id)?.kind {
1874            WidgetKind::Checkbox { checked, .. } => Some(checked),
1875            _ => None,
1876        }
1877    }
1878
1879    pub fn set_slider_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1880        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1881        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1882        match node.kind {
1883            WidgetKind::Slider {
1884                value: ref mut v,
1885                min,
1886                max,
1887            } => {
1888                let mut state = SliderState::new(*v, min, max);
1889                state.set_value(value);
1890                *v = state.value;
1891                self.dirty.add(rect)?;
1892                Ok(())
1893            }
1894            _ => Err(GuiError::NotFound),
1895        }
1896    }
1897
1898    pub fn slider_value(&self, id: WidgetId) -> Option<f32> {
1899        match self.node(id)?.kind {
1900            WidgetKind::Slider { value, .. } => Some(value),
1901            _ => None,
1902        }
1903    }
1904
1905    pub fn set_value_label(&mut self, id: WidgetId, value: i32) -> Result<(), GuiError> {
1906        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1907        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1908        match node.kind {
1909            WidgetKind::ValueLabel {
1910                value: ref mut v, ..
1911            } => {
1912                *v = value;
1913                self.dirty.add(rect)?;
1914                Ok(())
1915            }
1916            _ => Err(GuiError::NotFound),
1917        }
1918    }
1919
1920    pub fn set_scroll_offset(&mut self, id: WidgetId, offset_y: i32) -> Result<(), GuiError> {
1921        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1922        match node.kind {
1923            WidgetKind::ScrollView {
1924                offset_y: ref mut v,
1925                content_h,
1926            } => {
1927                let mut state = ScrollState::new(*v, content_h);
1928                state.set_offset(offset_y);
1929                *v = state.offset_y;
1930                self.mark_subtree_dirty(id)?;
1931                Ok(())
1932            }
1933            _ => Err(GuiError::NotFound),
1934        }
1935    }
1936
1937    pub fn scroll_offset(&self, id: WidgetId) -> Option<i32> {
1938        match self.node(id)?.kind {
1939            WidgetKind::ScrollView { offset_y, .. } => Some(offset_y),
1940            _ => None,
1941        }
1942    }
1943
1944    pub fn set_tab_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1945        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1946        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1947        match node.kind {
1948            WidgetKind::Tabs {
1949                labels,
1950                selected: ref mut v,
1951            } => {
1952                let mut state = TabsState::new(*v);
1953                state.set_selected(selected, labels.len());
1954                *v = state.selected;
1955                self.dirty.add(rect)?;
1956                Ok(())
1957            }
1958            _ => Err(GuiError::NotFound),
1959        }
1960    }
1961
1962    pub fn tab_selected(&self, id: WidgetId) -> Option<usize> {
1963        match self.node(id)?.kind {
1964            WidgetKind::Tabs { selected, .. } => Some(selected),
1965            _ => None,
1966        }
1967    }
1968
1969    pub fn set_toast_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
1970        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1971        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1972        match node.kind {
1973            WidgetKind::Toast {
1974                ttl_ms: ref mut v, ..
1975            } => {
1976                *v = ttl_ms;
1977                self.dirty.add(rect)?;
1978                Ok(())
1979            }
1980            _ => Err(GuiError::NotFound),
1981        }
1982    }
1983
1984    pub fn tick_toast(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1985        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1986            WidgetKind::Toast { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
1987            _ => return Err(GuiError::NotFound),
1988        };
1989        self.set_toast_ttl(id, ttl)
1990    }
1991
1992    pub fn set_meter_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1993        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1994        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1995        match node.kind {
1996            WidgetKind::Meter {
1997                value: ref mut v,
1998                min,
1999                max,
2000            } => {
2001                *v = value.clamp(min.min(max), min.max(max));
2002                self.dirty.add(rect)?;
2003                Ok(())
2004            }
2005            _ => Err(GuiError::NotFound),
2006        }
2007    }
2008
2009    pub fn set_spinner_phase(&mut self, id: WidgetId, phase: f32) -> Result<(), GuiError> {
2010        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2011        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2012        match node.kind {
2013            WidgetKind::Spinner { phase: ref mut v } => {
2014                *v = phase;
2015                self.dirty.add(rect)?;
2016                Ok(())
2017            }
2018            _ => Err(GuiError::NotFound),
2019        }
2020    }
2021
2022    pub fn tick_spinner(
2023        &mut self,
2024        id: WidgetId,
2025        dt_ms: u32,
2026        cycles_per_sec: f32,
2027    ) -> Result<(), GuiError> {
2028        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
2029            WidgetKind::Spinner { phase } => phase + (dt_ms as f32 / 1000.0) * cycles_per_sec,
2030            _ => return Err(GuiError::NotFound),
2031        };
2032        self.set_spinner_phase(id, phase)
2033    }
2034
2035    pub fn set_dropdown_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2036        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2037        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2038        match node.kind {
2039            WidgetKind::Dropdown {
2040                items,
2041                selected: ref mut current,
2042                ..
2043            } => {
2044                *current = selected.min(items.len().saturating_sub(1));
2045                self.dirty.add(rect)?;
2046                Ok(())
2047            }
2048            _ => Err(GuiError::NotFound),
2049        }
2050    }
2051
2052    pub fn dropdown_selected(&self, id: WidgetId) -> Option<usize> {
2053        match self.node(id)?.kind {
2054            WidgetKind::Dropdown { selected, .. } => Some(selected),
2055            _ => None,
2056        }
2057    }
2058
2059    pub fn set_dropdown_open(&mut self, id: WidgetId, open: bool) -> Result<(), GuiError> {
2060        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2061        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2062        match node.kind {
2063            WidgetKind::Dropdown {
2064                open: ref mut is_open,
2065                ..
2066            } => {
2067                if *is_open != open {
2068                    *is_open = open;
2069                    self.dirty.add(rect)?;
2070                    self.push_event(if open {
2071                        UiEvent::Opened(id)
2072                    } else {
2073                        UiEvent::Closed(id)
2074                    })?;
2075                }
2076                Ok(())
2077            }
2078            _ => Err(GuiError::NotFound),
2079        }
2080    }
2081
2082    pub fn dropdown_open(&self, id: WidgetId) -> Option<bool> {
2083        match self.node(id)?.kind {
2084            WidgetKind::Dropdown { open, .. } => Some(open),
2085            _ => None,
2086        }
2087    }
2088
2089    pub fn set_roller_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2090        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2091        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2092        match node.kind {
2093            WidgetKind::Roller {
2094                items,
2095                selected: ref mut current,
2096            } => {
2097                *current = selected.min(items.len().saturating_sub(1));
2098                self.dirty.add(rect)?;
2099                Ok(())
2100            }
2101            _ => Err(GuiError::NotFound),
2102        }
2103    }
2104
2105    pub fn roller_selected(&self, id: WidgetId) -> Option<usize> {
2106        match self.node(id)?.kind {
2107            WidgetKind::Roller { selected, .. } => Some(selected),
2108            _ => None,
2109        }
2110    }
2111
2112    pub fn set_textarea_text(&mut self, id: WidgetId, text: &'a str) -> Result<(), GuiError> {
2113        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2114        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2115        match node.kind {
2116            WidgetKind::TextArea {
2117                text_buf: ref mut buf,
2118                text_len: ref mut len,
2119                cursor: ref mut c,
2120                ..
2121            } => {
2122                let (next_buf, next_len) = textarea_storage_from_str(text);
2123                *buf = next_buf;
2124                *len = next_len;
2125                *c = (*c).min(textarea_text(buf, *len).chars().count());
2126                self.dirty.add(rect)?;
2127                Ok(())
2128            }
2129            _ => Err(GuiError::NotFound),
2130        }
2131    }
2132
2133    pub fn textarea_text(&self, id: WidgetId) -> Option<&str> {
2134        match &self.node(id)?.kind {
2135            WidgetKind::TextArea {
2136                text_buf, text_len, ..
2137            } => Some(textarea_text(text_buf, *text_len)),
2138            _ => None,
2139        }
2140    }
2141
2142    pub fn set_textarea_cursor(&mut self, id: WidgetId, cursor: usize) -> Result<(), GuiError> {
2143        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2144        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2145        match node.kind {
2146            WidgetKind::TextArea {
2147                text_buf,
2148                text_len,
2149                cursor: ref mut current,
2150                ..
2151            } => {
2152                let text = textarea_text(&text_buf, text_len);
2153                *current = cursor.min(text.chars().count());
2154                self.dirty.add(rect)?;
2155                Ok(())
2156            }
2157            _ => Err(GuiError::NotFound),
2158        }
2159    }
2160
2161    pub fn move_textarea_cursor(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2162        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2163        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, false)
2164    }
2165
2166    pub fn move_textarea_cursor_select(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2167        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2168        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, true)
2169    }
2170
2171    pub fn move_textarea_cursor_word(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2172        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2173            WidgetKind::TextArea {
2174                text_buf,
2175                text_len,
2176                cursor,
2177                ..
2178            } => (textarea_text(text_buf, *text_len), *cursor),
2179            _ => return Err(GuiError::NotFound),
2180        };
2181        let next = if delta >= 0 {
2182            next_word_boundary(text, cursor)
2183        } else {
2184            prev_word_boundary(text, cursor)
2185        };
2186        self.set_textarea_cursor_with_extend(id, next, false)
2187    }
2188
2189    pub fn move_textarea_cursor_word_select(
2190        &mut self,
2191        id: WidgetId,
2192        delta: i8,
2193    ) -> Result<(), GuiError> {
2194        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2195            WidgetKind::TextArea {
2196                text_buf,
2197                text_len,
2198                cursor,
2199                ..
2200            } => (textarea_text(text_buf, *text_len), *cursor),
2201            _ => return Err(GuiError::NotFound),
2202        };
2203        let next = if delta >= 0 {
2204            next_word_boundary(text, cursor)
2205        } else {
2206            prev_word_boundary(text, cursor)
2207        };
2208        self.set_textarea_cursor_with_extend(id, next, true)
2209    }
2210
2211    pub fn set_textarea_cursor_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2212        self.set_textarea_cursor(id, 0)
2213    }
2214
2215    pub fn set_textarea_cursor_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2216        let len = self
2217            .textarea_text(id)
2218            .map(|text| text.chars().count())
2219            .ok_or(GuiError::NotFound)?;
2220        self.set_textarea_cursor(id, len)
2221    }
2222
2223    pub fn set_textarea_cursor_line_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2224        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2225        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2226        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2227        self.set_textarea_cursor_with_extend(id, next, false)
2228    }
2229
2230    pub fn set_textarea_cursor_line_home_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2231        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2232        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2233        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2234        self.set_textarea_cursor_with_extend(id, next, true)
2235    }
2236
2237    pub fn set_textarea_cursor_line_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2238        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2239        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2240        let row_end = textarea_row_end_col(text, row, wrap_cols);
2241        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2242        self.set_textarea_cursor_with_extend(id, next, false)
2243    }
2244
2245    pub fn set_textarea_cursor_line_end_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2246        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2247        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2248        let row_end = textarea_row_end_col(text, row, wrap_cols);
2249        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2250        self.set_textarea_cursor_with_extend(id, next, true)
2251    }
2252
2253    pub fn textarea_cursor(&self, id: WidgetId) -> Option<usize> {
2254        match self.node(id)?.kind {
2255            WidgetKind::TextArea { cursor, .. } => Some(cursor),
2256            _ => None,
2257        }
2258    }
2259
2260    pub fn set_textarea_selection(
2261        &mut self,
2262        id: WidgetId,
2263        start: usize,
2264        end: usize,
2265    ) -> Result<(), GuiError> {
2266        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2267        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2268        match node.kind {
2269            WidgetKind::TextArea {
2270                text_buf,
2271                text_len,
2272                selection: ref mut current,
2273                ..
2274            } => {
2275                let text = textarea_text(&text_buf, text_len);
2276                let len = text.chars().count();
2277                let start = start.min(len);
2278                let end = end.min(len);
2279                *current = Some((start.min(end), start.max(end)));
2280                self.dirty.add(rect)?;
2281                Ok(())
2282            }
2283            _ => Err(GuiError::NotFound),
2284        }
2285    }
2286
2287    pub fn clear_textarea_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
2288        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2289        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2290        match node.kind {
2291            WidgetKind::TextArea {
2292                selection: ref mut current,
2293                ..
2294            } => {
2295                *current = None;
2296                self.dirty.add(rect)?;
2297                Ok(())
2298            }
2299            _ => Err(GuiError::NotFound),
2300        }
2301    }
2302
2303    pub fn textarea_selection(&self, id: WidgetId) -> Option<(usize, usize)> {
2304        match self.node(id)?.kind {
2305            WidgetKind::TextArea { selection, .. } => selection,
2306            _ => None,
2307        }
2308    }
2309
2310    pub fn textarea_cursor_visible(&self, id: WidgetId) -> Option<bool> {
2311        match self.node(id)?.kind {
2312            WidgetKind::TextArea { cursor_visible, .. } => Some(cursor_visible),
2313            _ => None,
2314        }
2315    }
2316
2317    pub fn set_textarea_capabilities(
2318        &mut self,
2319        id: WidgetId,
2320        read_only: bool,
2321        single_line: bool,
2322        accept_newline: bool,
2323    ) -> Result<(), GuiError> {
2324        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2325        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2326        match node.kind {
2327            WidgetKind::TextArea {
2328                read_only: ref mut ro,
2329                single_line: ref mut sl,
2330                accept_newline: ref mut an,
2331                ..
2332            } => {
2333                *ro = read_only;
2334                *sl = single_line;
2335                *an = accept_newline && !single_line;
2336                self.dirty.add(rect)?;
2337                Ok(())
2338            }
2339            _ => Err(GuiError::NotFound),
2340        }
2341    }
2342
2343    pub fn textarea_insert_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
2344        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2345        let before = self.capture_textarea_snapshot(id)?;
2346        let mut emit = false;
2347        if let Some(node) = self.node_mut(id) {
2348            if let WidgetKind::TextArea {
2349                text_buf,
2350                text_len,
2351                cursor,
2352                selection,
2353                read_only,
2354                single_line,
2355                accept_newline,
2356                ..
2357            } = &mut node.kind
2358            {
2359                if *read_only {
2360                    return Ok(());
2361                }
2362                if ch == '\n' && (*single_line || !*accept_newline) {
2363                    return Ok(());
2364                }
2365                let mut chars: heapless::Vec<char, TEXTAREA_CAPACITY> = heapless::Vec::new();
2366                for c in textarea_text(text_buf, *text_len).chars() {
2367                    let _ = chars.push(c);
2368                }
2369                let original_len = chars.len();
2370                let original_cursor = *cursor;
2371
2372                if ch == '\u{8}' {
2373                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2374                    if !removed_selection && *cursor > 0 && *cursor <= chars.len() {
2375                        chars.remove(*cursor - 1);
2376                        *cursor -= 1;
2377                    }
2378                    if removed_selection
2379                        || *cursor != original_cursor
2380                        || chars.len() != original_len
2381                    {
2382                        *selection = None;
2383                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2384                        *text_buf = next_buf;
2385                        *text_len = next_len;
2386                        emit = true;
2387                    }
2388                } else if ch == '\u{7f}' {
2389                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2390                    if !removed_selection && *cursor < chars.len() {
2391                        chars.remove(*cursor);
2392                    }
2393                    if removed_selection || chars.len() != original_len {
2394                        *selection = None;
2395                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2396                        *text_buf = next_buf;
2397                        *text_len = next_len;
2398                        emit = true;
2399                    }
2400                } else if ch != '\n' || *cursor < TEXTAREA_CAPACITY {
2401                    if delete_selection_if_any(&mut chars, cursor, selection) {
2402                        *selection = None;
2403                    }
2404                    if chars.len() < TEXTAREA_CAPACITY && *cursor <= chars.len() {
2405                        let _ = chars.insert(*cursor, ch);
2406                        *cursor += 1;
2407                        *selection = None;
2408                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2409                        *text_buf = next_buf;
2410                        *text_len = next_len;
2411                        emit = true;
2412                    }
2413                }
2414            } else {
2415                return Err(GuiError::NotFound);
2416            }
2417        }
2418        if emit {
2419            self.push_textarea_undo(id, before);
2420            self.clear_textarea_redo_for(id);
2421            self.dirty.add(rect)?;
2422            self.push_event(UiEvent::TextInput { id, ch })?;
2423            self.push_event(UiEvent::ValueChanged(id))?;
2424        }
2425        Ok(())
2426    }
2427
2428    fn textarea_line_context(&self, id: WidgetId) -> Result<(&str, usize, usize), GuiError> {
2429        let node = self.node(id).ok_or(GuiError::NotFound)?;
2430        match &node.kind {
2431            WidgetKind::TextArea {
2432                text_buf,
2433                text_len,
2434                cursor,
2435                ..
2436            } => {
2437                let font = node.style.normal.font;
2438                let inner_w = node.rect.w.saturating_sub(2);
2439                let cols = (inner_w / font.advance()).max(1) as usize;
2440                Ok((textarea_text(text_buf, *text_len), *cursor, cols))
2441            }
2442            _ => Err(GuiError::NotFound),
2443        }
2444    }
2445
2446    fn set_textarea_cursor_with_extend(
2447        &mut self,
2448        id: WidgetId,
2449        cursor: usize,
2450        extend_selection: bool,
2451    ) -> Result<(), GuiError> {
2452        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2453        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2454        match node.kind {
2455            WidgetKind::TextArea {
2456                text_buf,
2457                text_len,
2458                cursor: ref mut current_cursor,
2459                ref mut selection,
2460                ..
2461            } => {
2462                let len = textarea_text(&text_buf, text_len).chars().count();
2463                let next = cursor.min(len);
2464                if extend_selection {
2465                    let anchor = match *selection {
2466                        Some((start, end)) => {
2467                            if *current_cursor == start {
2468                                end
2469                            } else {
2470                                start
2471                            }
2472                        }
2473                        None => *current_cursor,
2474                    };
2475                    if anchor == next {
2476                        *selection = None;
2477                    } else {
2478                        *selection = Some((anchor.min(next), anchor.max(next)));
2479                    }
2480                } else {
2481                    *selection = None;
2482                }
2483                *current_cursor = next;
2484                self.dirty.add(rect)?;
2485                Ok(())
2486            }
2487            _ => Err(GuiError::NotFound),
2488        }
2489    }
2490
2491    fn capture_textarea_snapshot(&self, id: WidgetId) -> Result<TextareaSnapshot, GuiError> {
2492        match self.node(id).ok_or(GuiError::NotFound)?.kind {
2493            WidgetKind::TextArea {
2494                text_buf,
2495                text_len,
2496                cursor,
2497                selection,
2498                ..
2499            } => Ok(TextareaSnapshot {
2500                text_buf,
2501                text_len,
2502                cursor,
2503                selection,
2504            }),
2505            _ => Err(GuiError::NotFound),
2506        }
2507    }
2508
2509    fn apply_textarea_snapshot(
2510        &mut self,
2511        id: WidgetId,
2512        snap: TextareaSnapshot,
2513    ) -> Result<(), GuiError> {
2514        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2515        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2516        match node.kind {
2517            WidgetKind::TextArea {
2518                text_buf: ref mut buf,
2519                text_len: ref mut len,
2520                cursor: ref mut c,
2521                selection: ref mut sel,
2522                ..
2523            } => {
2524                *buf = snap.text_buf;
2525                *len = snap.text_len;
2526                *c = snap.cursor;
2527                *sel = snap.selection;
2528                self.dirty.add(rect)?;
2529                self.push_event(UiEvent::ValueChanged(id))
2530            }
2531            _ => Err(GuiError::NotFound),
2532        }
2533    }
2534
2535    fn push_textarea_undo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2536        if self.textarea_undo.len() == self.textarea_undo.capacity() {
2537            self.textarea_undo.remove(0);
2538        }
2539        let _ = self
2540            .textarea_undo
2541            .push(TextareaHistoryEntry { id, snapshot });
2542    }
2543
2544    fn push_textarea_redo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2545        if self.textarea_redo.len() == self.textarea_redo.capacity() {
2546            self.textarea_redo.remove(0);
2547        }
2548        let _ = self
2549            .textarea_redo
2550            .push(TextareaHistoryEntry { id, snapshot });
2551    }
2552
2553    fn clear_textarea_redo_for(&mut self, id: WidgetId) {
2554        let mut i = 0usize;
2555        while i < self.textarea_redo.len() {
2556            if self.textarea_redo[i].id == id {
2557                self.textarea_redo.remove(i);
2558            } else {
2559                i += 1;
2560            }
2561        }
2562    }
2563
2564    fn textarea_undo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2565        let Some(pos) = self.textarea_undo.iter().rposition(|entry| entry.id == id) else {
2566            return Ok(());
2567        };
2568        let current = self.capture_textarea_snapshot(id)?;
2569        let prior = self.textarea_undo.remove(pos).snapshot;
2570        self.push_textarea_redo(id, current);
2571        self.apply_textarea_snapshot(id, prior)
2572    }
2573
2574    fn textarea_redo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2575        let Some(pos) = self.textarea_redo.iter().rposition(|entry| entry.id == id) else {
2576            return Ok(());
2577        };
2578        let current = self.capture_textarea_snapshot(id)?;
2579        let next = self.textarea_redo.remove(pos).snapshot;
2580        self.push_textarea_undo(id, current);
2581        self.apply_textarea_snapshot(id, next)
2582    }
2583
2584    pub fn textarea_backspace(&mut self, id: WidgetId) -> Result<(), GuiError> {
2585        self.textarea_insert_char(id, '\u{8}')
2586    }
2587
2588    pub fn textarea_delete_forward(&mut self, id: WidgetId) -> Result<(), GuiError> {
2589        self.textarea_insert_char(id, '\u{7f}')
2590    }
2591
2592    pub fn keyboard_selected_key(&self, id: WidgetId) -> Option<char> {
2593        match self.node(id)?.kind {
2594            WidgetKind::Keyboard {
2595                keys,
2596                alt_keys,
2597                selected,
2598                layout,
2599                ..
2600            } => keyboard_char_for_layout(keys, alt_keys, selected, layout),
2601            _ => None,
2602        }
2603    }
2604
2605    pub fn keyboard_layout(&self, id: WidgetId) -> Option<KeyboardLayout> {
2606        match self.node(id)?.kind {
2607            WidgetKind::Keyboard { layout, .. } => Some(layout),
2608            _ => None,
2609        }
2610    }
2611
2612    pub fn set_keyboard_layout(
2613        &mut self,
2614        id: WidgetId,
2615        layout: KeyboardLayout,
2616    ) -> Result<(), GuiError> {
2617        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2618        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2619        match node.kind {
2620            WidgetKind::Keyboard {
2621                layout: ref mut current,
2622                ..
2623            } => {
2624                *current = layout;
2625                self.dirty.add(rect)?;
2626                Ok(())
2627            }
2628            _ => Err(GuiError::NotFound),
2629        }
2630    }
2631
2632    pub fn set_keyboard_target(
2633        &mut self,
2634        id: WidgetId,
2635        target: Option<WidgetId>,
2636    ) -> Result<(), GuiError> {
2637        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2638        match node.kind {
2639            WidgetKind::Keyboard {
2640                target: ref mut current,
2641                ..
2642            } => {
2643                *current = target;
2644                Ok(())
2645            }
2646            _ => Err(GuiError::NotFound),
2647        }
2648    }
2649
2650    pub fn set_gauge_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
2651        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2652        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2653        match node.kind {
2654            WidgetKind::Gauge {
2655                value: ref mut v,
2656                min,
2657                max,
2658                ..
2659            }
2660            | WidgetKind::ArcGauge {
2661                value: ref mut v,
2662                min,
2663                max,
2664                ..
2665            }
2666            | WidgetKind::GaugeNeedle {
2667                value: ref mut v,
2668                min,
2669                max,
2670                ..
2671            } => {
2672                *v = value.clamp(min.min(max), min.max(max));
2673                self.dirty.add(rect)?;
2674                Ok(())
2675            }
2676            _ => Err(GuiError::NotFound),
2677        }
2678    }
2679
2680    pub fn set_gauge_ticks(
2681        &mut self,
2682        id: WidgetId,
2683        major_ticks: u8,
2684        minor_ticks: u8,
2685        show_value: bool,
2686    ) -> Result<(), GuiError> {
2687        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2688        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2689        match node.kind {
2690            WidgetKind::Gauge {
2691                major_ticks: ref mut major,
2692                minor_ticks: ref mut minor,
2693                show_value: ref mut show,
2694                ..
2695            }
2696            | WidgetKind::ArcGauge {
2697                major_ticks: ref mut major,
2698                minor_ticks: ref mut minor,
2699                show_value: ref mut show,
2700                ..
2701            } => {
2702                *major = major_ticks.max(1);
2703                *minor = minor_ticks.max(1);
2704                *show = show_value;
2705                self.dirty.add(rect)?;
2706                Ok(())
2707            }
2708            _ => Err(GuiError::NotFound),
2709        }
2710    }
2711
2712    pub fn set_widget_rect(&mut self, id: WidgetId, rect: Rect) -> Result<(), GuiError> {
2713        let old = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2714        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2715        node.rect = rect;
2716        self.dirty.add(old)?;
2717        self.mark_subtree_dirty(id)?;
2718        Ok(())
2719    }
2720
2721    pub fn set_widget_x(&mut self, id: WidgetId, x: i32) -> Result<(), GuiError> {
2722        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2723        rect.x = x;
2724        self.set_widget_rect(id, rect)
2725    }
2726
2727    pub fn set_widget_y(&mut self, id: WidgetId, y: i32) -> Result<(), GuiError> {
2728        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2729        rect.y = y;
2730        self.set_widget_rect(id, rect)
2731    }
2732
2733    pub fn set_widget_width(&mut self, id: WidgetId, w: u32) -> Result<(), GuiError> {
2734        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2735        rect.w = w.max(1);
2736        self.set_widget_rect(id, rect)
2737    }
2738
2739    pub fn set_widget_height(&mut self, id: WidgetId, h: u32) -> Result<(), GuiError> {
2740        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2741        rect.h = h.max(1);
2742        self.set_widget_rect(id, rect)
2743    }
2744
2745    pub fn set_widget_opacity(&mut self, id: WidgetId, opacity: u8) -> Result<(), GuiError> {
2746        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2747        node.style.normal.opacity = opacity;
2748        node.style.focused.opacity = opacity;
2749        node.style.pressed.opacity = opacity;
2750        node.style.disabled.opacity = opacity;
2751        self.mark_subtree_dirty(id)
2752    }
2753
2754    pub fn set_widget_corner_radius(&mut self, id: WidgetId, radius: u8) -> Result<(), GuiError> {
2755        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2756        node.style.normal.corner_radius = radius;
2757        node.style.focused.corner_radius = radius;
2758        node.style.pressed.corner_radius = radius;
2759        node.style.disabled.corner_radius = radius;
2760        self.mark_subtree_dirty(id)
2761    }
2762
2763    pub fn set_widget_accent(&mut self, id: WidgetId, accent: Rgb565) -> Result<(), GuiError> {
2764        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2765        node.style.normal.accent = accent;
2766        node.style.focused.accent = accent;
2767        node.style.pressed.accent = accent;
2768        node.style.disabled.accent = accent;
2769        self.mark_subtree_dirty(id)
2770    }
2771
2772    pub fn set_widget_parent(
2773        &mut self,
2774        id: WidgetId,
2775        parent: Option<WidgetId>,
2776    ) -> Result<(), GuiError> {
2777        if let Some(parent) = parent {
2778            self.node(parent).ok_or(GuiError::NotFound)?;
2779        }
2780        self.node_mut(id).ok_or(GuiError::NotFound)?.parent = parent;
2781        self.mark_subtree_dirty(id)?;
2782        Ok(())
2783    }
2784
2785    pub fn add_child(&mut self, parent: WidgetId, child: WidgetId) -> Result<(), GuiError> {
2786        self.set_widget_parent(child, Some(parent))
2787    }
2788
2789    pub fn children_of(&self, parent: WidgetId) -> impl Iterator<Item = &WidgetNode<'a>> + '_ {
2790        self.widgets
2791            .iter()
2792            .filter(move |node| node.parent == Some(parent))
2793    }
2794
2795    pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
2796        let node = self.node(id)?;
2797        let mut rect = node.rect;
2798        let mut parent = node.parent;
2799        let mut depth = 0;
2800        while let Some(parent_id) = parent {
2801            if depth >= NODES {
2802                return None;
2803            }
2804            let parent_node = self.node(parent_id)?;
2805            rect.x += parent_node.rect.x;
2806            rect.y += parent_node.rect.y;
2807            parent = parent_node.parent;
2808            depth += 1;
2809        }
2810        Some(rect)
2811    }
2812
2813    pub fn set_flag(
2814        &mut self,
2815        id: WidgetId,
2816        flag: WidgetFlags,
2817        enabled: bool,
2818    ) -> Result<(), GuiError> {
2819        let was_set = self.has_flag(id, flag)?;
2820        let before_state = self.current_visual_state(id);
2821        self.mark_subtree_dirty(id)?;
2822        self.node_mut(id)
2823            .ok_or(GuiError::NotFound)?
2824            .flags
2825            .set(flag, enabled);
2826        if flag == WidgetFlags::DISABLED
2827            && enabled
2828            && self.pressed.is_some_and(|pressed| pressed.id == id)
2829        {
2830            self.pressed = None;
2831        }
2832        self.mark_subtree_dirty(id)?;
2833        if self
2834            .focus
2835            .is_some_and(|focus| !self.effective_focusable(focus))
2836        {
2837            self.focus = None;
2838            self.ensure_focus();
2839        }
2840        if flag == WidgetFlags::DISABLED && was_set != enabled {
2841            let after_state = self.current_visual_state(id);
2842            self.start_state_transition(id, before_state, after_state);
2843        }
2844        Ok(())
2845    }
2846
2847    pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
2848        Ok(self
2849            .node(id)
2850            .ok_or(GuiError::NotFound)?
2851            .flags
2852            .contains(flag))
2853    }
2854
2855    pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2856        self.set_flag(id, flag, true)
2857    }
2858
2859    pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2860        self.set_flag(id, flag, false)
2861    }
2862
2863    pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
2864        self.set_flag(id, WidgetFlags::HIDDEN, hidden)
2865    }
2866
2867    pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
2868        self.set_flag(id, WidgetFlags::DISABLED, disabled)
2869    }
2870
2871    pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
2872        self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
2873    }
2874
2875    pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
2876        self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
2877    }
2878
2879    pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
2880        self.set_hidden(id, !visible)
2881    }
2882
2883    pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
2884        self.set_disabled(id, !enabled)
2885    }
2886
2887    pub fn event_path<const M: usize>(
2888        &self,
2889        target: WidgetId,
2890        out: &mut heapless::Vec<EventContext, M>,
2891    ) -> Result<usize, GuiError> {
2892        self.node(target).ok_or(GuiError::NotFound)?;
2893        out.clear();
2894
2895        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2896        let mut current = Some(target);
2897        while let Some(id) = current {
2898            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2899            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2900        }
2901
2902        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2903            out.push(EventContext {
2904                target,
2905                current: id,
2906                phase: EventPhase::Capture,
2907            })
2908            .map_err(|_| GuiError::EventsFull)?;
2909        }
2910
2911        out.push(EventContext {
2912            target,
2913            current: target,
2914            phase: EventPhase::Target,
2915        })
2916        .map_err(|_| GuiError::EventsFull)?;
2917
2918        for id in chain.iter().copied().skip(1) {
2919            out.push(EventContext {
2920                target,
2921                current: id,
2922                phase: EventPhase::Bubble,
2923            })
2924            .map_err(|_| GuiError::EventsFull)?;
2925        }
2926
2927        Ok(out.len())
2928    }
2929
2930    pub fn widget_event_path<const M: usize>(
2931        &self,
2932        target: WidgetId,
2933        kind: WidgetEventKind,
2934        out: &mut heapless::Vec<WidgetEvent, M>,
2935    ) -> Result<usize, GuiError> {
2936        self.node(target).ok_or(GuiError::NotFound)?;
2937        out.clear();
2938
2939        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2940        let mut current = Some(target);
2941        while let Some(id) = current {
2942            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2943            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2944        }
2945
2946        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2947            out.push(WidgetEvent {
2948                target,
2949                current: id,
2950                phase: EventPhase::Capture,
2951                kind,
2952            })
2953            .map_err(|_| GuiError::EventsFull)?;
2954        }
2955
2956        out.push(WidgetEvent {
2957            target,
2958            current: target,
2959            phase: EventPhase::Target,
2960            kind,
2961        })
2962        .map_err(|_| GuiError::EventsFull)?;
2963
2964        if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
2965            for id in chain.iter().copied().skip(1) {
2966                out.push(WidgetEvent {
2967                    target,
2968                    current: id,
2969                    phase: EventPhase::Bubble,
2970                    kind,
2971                })
2972                .map_err(|_| GuiError::EventsFull)?;
2973            }
2974        }
2975
2976        Ok(out.len())
2977    }
2978
2979    pub fn dispatch_widget_event<const M: usize, F>(
2980        &self,
2981        target: WidgetId,
2982        kind: WidgetEventKind,
2983        scratch: &mut heapless::Vec<WidgetEvent, M>,
2984        mut handler: F,
2985    ) -> Result<(), GuiError>
2986    where
2987        F: FnMut(WidgetEvent) -> EventPolicy,
2988    {
2989        self.widget_event_path(target, kind, scratch)?;
2990        for event in scratch.iter().copied() {
2991            let handler_policy = handler(event);
2992            if matches!(handler_policy, EventPolicy::Stop)
2993                || self.stop_due_to_builtin_widget_behavior(event)
2994                || self.stop_due_to_registered_policy(event)
2995            {
2996                break;
2997            }
2998        }
2999        Ok(())
3000    }
3001
3002    pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
3003        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
3004        self.dirty.add(rect)?;
3005        let child_ids: heapless::Vec<WidgetId, NODES> = self
3006            .widgets
3007            .iter()
3008            .filter(|node| node.parent == Some(id))
3009            .map(|node| node.id)
3010            .collect();
3011        for child in child_ids {
3012            self.mark_subtree_dirty(child)?;
3013        }
3014        Ok(())
3015    }
3016
3017    pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
3018        self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
3019        Ok(())
3020    }
3021
3022    pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
3023        self.active_focus_group = group;
3024        if let Some(focus) = self.focus {
3025            let still_valid = self.node(focus).is_some_and(|node| {
3026                group.is_none_or(|active| node.focus_group == active)
3027                    && self.effective_focusable(focus)
3028            });
3029            if !still_valid {
3030                self.focus = None;
3031                self.ensure_focus();
3032            }
3033        }
3034    }
3035
3036    pub fn apply_layout(
3037        &mut self,
3038        layout: LinearLayout,
3039        area: Rect,
3040        ids: &[WidgetId],
3041    ) -> Result<usize, GuiError> {
3042        let mut rects = [Rect::empty(); 16];
3043        let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
3044        for (id, rect) in ids.iter().copied().zip(rects).take(count) {
3045            self.set_widget_rect(id, rect)?;
3046        }
3047        Ok(count)
3048    }
3049
3050    pub fn apply_layout_flex(
3051        &mut self,
3052        layout: LinearLayout,
3053        area: Rect,
3054        ids: &[WidgetId],
3055        items: &[LayoutItem],
3056        enable_grow: bool,
3057        enable_shrink: bool,
3058    ) -> Result<usize, GuiError> {
3059        let mut rects = [Rect::empty(); 16];
3060        let count = ids.len().min(items.len()).min(rects.len());
3061        let laid_out = layout.arrange_items_flex(
3062            area,
3063            &items[..count],
3064            &mut rects,
3065            enable_grow,
3066            enable_shrink,
3067        );
3068        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3069            self.set_widget_rect(id, rect)?;
3070        }
3071        Ok(laid_out)
3072    }
3073
3074    pub fn apply_layout_intrinsic(
3075        &mut self,
3076        layout: LinearLayout,
3077        area: Rect,
3078        ids: &[WidgetId],
3079    ) -> Result<usize, GuiError> {
3080        self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
3081    }
3082
3083    pub fn apply_layout_intrinsic_with_cross(
3084        &mut self,
3085        layout: LinearLayout,
3086        area: Rect,
3087        ids: &[WidgetId],
3088        preserve_cross: bool,
3089    ) -> Result<usize, GuiError> {
3090        let mut specs = [LayoutItem::fill(); 16];
3091        let mut rects = [Rect::empty(); 16];
3092        let count = ids.len().min(specs.len()).min(rects.len());
3093
3094        for (idx, id) in ids.iter().copied().take(count).enumerate() {
3095            let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
3096            specs[idx] = match layout.axis {
3097                Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
3098                    crate::layout::Constraint::Length(h)
3099                } else {
3100                    crate::layout::Constraint::Fill(1)
3101                }),
3102                Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
3103                    crate::layout::Constraint::Length(w)
3104                } else {
3105                    crate::layout::Constraint::Fill(1)
3106                }),
3107            };
3108        }
3109
3110        let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
3111        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3112            self.set_widget_rect(id, rect)?;
3113        }
3114        Ok(laid_out)
3115    }
3116
3117    pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error>
3118    where
3119        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3120    {
3121        let mut ctx = RenderCtx::new(target, self.viewport);
3122        ctx.set_quality(self.render_quality);
3123        self.render_into(&mut ctx, 0, 0, 255)
3124    }
3125
3126    pub fn render_dirty<D>(&self, target: &mut D) -> Result<(), D::Error>
3127    where
3128        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3129    {
3130        if self.dirty.is_empty() {
3131            return Ok(());
3132        }
3133
3134        for dirty in self.dirty.as_slice() {
3135            let mut ctx = RenderCtx::with_dirty(target, self.viewport, *dirty);
3136            ctx.set_quality(self.render_quality);
3137            self.render_into(&mut ctx, 0, 0, 255)?;
3138        }
3139        Ok(())
3140    }
3141
3142    pub fn render_with_offset<D>(
3143        &self,
3144        target: &mut D,
3145        offset_x: i32,
3146        offset_y: i32,
3147    ) -> Result<(), D::Error>
3148    where
3149        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3150    {
3151        self.render_with_offset_and_opacity(target, offset_x, offset_y, 255)
3152    }
3153
3154    pub fn render_with_offset_and_opacity<D>(
3155        &self,
3156        target: &mut D,
3157        offset_x: i32,
3158        offset_y: i32,
3159        opacity: u8,
3160    ) -> Result<(), D::Error>
3161    where
3162        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3163    {
3164        let mut ctx = RenderCtx::new(target, self.viewport);
3165        ctx.set_quality(self.render_quality);
3166        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3167    }
3168
3169    pub fn render_with_offset_opacity_and_clip<D>(
3170        &self,
3171        target: &mut D,
3172        offset_x: i32,
3173        offset_y: i32,
3174        opacity: u8,
3175        clip: Rect,
3176    ) -> Result<(), D::Error>
3177    where
3178        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3179    {
3180        let mut ctx = RenderCtx::new(target, self.viewport);
3181        ctx.set_quality(self.render_quality);
3182        let old_clip = ctx.clip();
3183        ctx.set_clip(old_clip.intersection(clip));
3184        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3185    }
3186
3187    pub fn handle_input(&mut self, event: InputEvent) -> Result<(), GuiError> {
3188        match event {
3189            InputEvent::Home => {
3190                if let Some(id) = self.focus {
3191                    if matches!(
3192                        self.node(id).map(|n| n.kind),
3193                        Some(WidgetKind::TextArea { .. })
3194                    ) {
3195                        self.set_textarea_cursor_line_home(id)?;
3196                        return Ok(());
3197                    }
3198                }
3199                Ok(())
3200            }
3201            InputEvent::End => {
3202                if let Some(id) = self.focus {
3203                    if matches!(
3204                        self.node(id).map(|n| n.kind),
3205                        Some(WidgetKind::TextArea { .. })
3206                    ) {
3207                        self.set_textarea_cursor_line_end(id)?;
3208                        return Ok(());
3209                    }
3210                }
3211                Ok(())
3212            }
3213            InputEvent::WordLeft => {
3214                if let Some(id) = self.focus {
3215                    if matches!(
3216                        self.node(id).map(|n| n.kind),
3217                        Some(WidgetKind::TextArea { .. })
3218                    ) {
3219                        self.move_textarea_cursor_word(id, -1)?;
3220                        return Ok(());
3221                    }
3222                }
3223                Ok(())
3224            }
3225            InputEvent::WordRight => {
3226                if let Some(id) = self.focus {
3227                    if matches!(
3228                        self.node(id).map(|n| n.kind),
3229                        Some(WidgetKind::TextArea { .. })
3230                    ) {
3231                        self.move_textarea_cursor_word(id, 1)?;
3232                        return Ok(());
3233                    }
3234                }
3235                Ok(())
3236            }
3237            InputEvent::Undo => {
3238                if let Some(id) = self.focus {
3239                    if matches!(
3240                        self.node(id).map(|n| n.kind),
3241                        Some(WidgetKind::TextArea { .. })
3242                    ) {
3243                        self.textarea_undo(id)?;
3244                    }
3245                }
3246                Ok(())
3247            }
3248            InputEvent::Redo => {
3249                if let Some(id) = self.focus {
3250                    if matches!(
3251                        self.node(id).map(|n| n.kind),
3252                        Some(WidgetKind::TextArea { .. })
3253                    ) {
3254                        self.textarea_redo(id)?;
3255                    }
3256                }
3257                Ok(())
3258            }
3259            InputEvent::SelectLeft => {
3260                if let Some(id) = self.focus {
3261                    if matches!(
3262                        self.node(id).map(|n| n.kind),
3263                        Some(WidgetKind::TextArea { .. })
3264                    ) {
3265                        self.move_textarea_cursor_select(id, -1)?;
3266                        return Ok(());
3267                    }
3268                }
3269                Ok(())
3270            }
3271            InputEvent::SelectRight => {
3272                if let Some(id) = self.focus {
3273                    if matches!(
3274                        self.node(id).map(|n| n.kind),
3275                        Some(WidgetKind::TextArea { .. })
3276                    ) {
3277                        self.move_textarea_cursor_select(id, 1)?;
3278                        return Ok(());
3279                    }
3280                }
3281                Ok(())
3282            }
3283            InputEvent::SelectHome => {
3284                if let Some(id) = self.focus {
3285                    if matches!(
3286                        self.node(id).map(|n| n.kind),
3287                        Some(WidgetKind::TextArea { .. })
3288                    ) {
3289                        self.set_textarea_cursor_line_home_select(id)?;
3290                        return Ok(());
3291                    }
3292                }
3293                Ok(())
3294            }
3295            InputEvent::SelectEnd => {
3296                if let Some(id) = self.focus {
3297                    if matches!(
3298                        self.node(id).map(|n| n.kind),
3299                        Some(WidgetKind::TextArea { .. })
3300                    ) {
3301                        self.set_textarea_cursor_line_end_select(id)?;
3302                        return Ok(());
3303                    }
3304                }
3305                Ok(())
3306            }
3307            InputEvent::SelectWordLeft => {
3308                if let Some(id) = self.focus {
3309                    if matches!(
3310                        self.node(id).map(|n| n.kind),
3311                        Some(WidgetKind::TextArea { .. })
3312                    ) {
3313                        self.move_textarea_cursor_word_select(id, -1)?;
3314                        return Ok(());
3315                    }
3316                }
3317                Ok(())
3318            }
3319            InputEvent::SelectWordRight => {
3320                if let Some(id) = self.focus {
3321                    if matches!(
3322                        self.node(id).map(|n| n.kind),
3323                        Some(WidgetKind::TextArea { .. })
3324                    ) {
3325                        self.move_textarea_cursor_word_select(id, 1)?;
3326                        return Ok(());
3327                    }
3328                }
3329                Ok(())
3330            }
3331            InputEvent::Up => {
3332                if !self.adjust_focused_selection(-1)? {
3333                    self.focus_prev()?;
3334                }
3335                Ok(())
3336            }
3337            InputEvent::Down => {
3338                if !self.adjust_focused_selection(1)? {
3339                    self.focus_next()?;
3340                }
3341                Ok(())
3342            }
3343            InputEvent::Left => {
3344                if !self.adjust_focused_scalar(-1.0)? {
3345                    self.focus_prev()?;
3346                }
3347                Ok(())
3348            }
3349            InputEvent::Right => {
3350                if !self.adjust_focused_scalar(1.0)? {
3351                    self.focus_next()?;
3352                }
3353                Ok(())
3354            }
3355            InputEvent::Encoder { delta } if delta > 0 => {
3356                if !self.adjust_focused_selection(1)? {
3357                    self.focus_next()?;
3358                }
3359                Ok(())
3360            }
3361            InputEvent::Encoder { delta } if delta < 0 => {
3362                if !self.adjust_focused_selection(-1)? {
3363                    self.focus_prev()?;
3364                }
3365                Ok(())
3366            }
3367            InputEvent::Select => {
3368                if let Some(id) = self.focus {
3369                    match self.key_bindings_for(id).select {
3370                        KeyBindingAction::Default | KeyBindingAction::Activate => {
3371                            self.handle_select_activation(id)?
3372                        }
3373                        KeyBindingAction::Back => self.handle_back_action()?,
3374                        KeyBindingAction::Ignore => {}
3375                    }
3376                }
3377                Ok(())
3378            }
3379            InputEvent::SelectPressed => {
3380                if let Some(id) = self.focus {
3381                    if self.key_input_policy_for(id).raw_select {
3382                        self.dispatch_key_pressed(id)?;
3383                    }
3384                }
3385                Ok(())
3386            }
3387            InputEvent::SelectReleased => {
3388                if let Some(id) = self.focus {
3389                    if self.key_input_policy_for(id).raw_select {
3390                        self.dispatch_key_released(id)?;
3391                        self.handle_select_activation(id)?;
3392                    }
3393                }
3394                Ok(())
3395            }
3396            InputEvent::Back => {
3397                if let Some(id) = self.focus {
3398                    match self.key_bindings_for(id).back {
3399                        KeyBindingAction::Default | KeyBindingAction::Back => {
3400                            self.handle_back_action()
3401                        }
3402                        KeyBindingAction::Activate => self.handle_select_activation(id),
3403                        KeyBindingAction::Ignore => Ok(()),
3404                    }
3405                } else {
3406                    self.handle_back_action()
3407                }
3408            }
3409            InputEvent::BackPressed => {
3410                if let Some(id) = self.focus {
3411                    if self.key_input_policy_for(id).raw_back {
3412                        self.dispatch_key_pressed(id)?;
3413                    }
3414                }
3415                Ok(())
3416            }
3417            InputEvent::BackReleased => {
3418                if let Some(id) = self.focus {
3419                    if self.key_input_policy_for(id).raw_back {
3420                        self.dispatch_key_released(id)?;
3421                        return self.handle_back_action();
3422                    }
3423                }
3424                Ok(())
3425            }
3426            InputEvent::Pointer {
3427                x,
3428                y,
3429                state: PointerState::Pressed,
3430                ..
3431            } => self.handle_pointer_pressed(x, y),
3432            InputEvent::Pointer {
3433                x,
3434                y,
3435                state: PointerState::Released,
3436                ..
3437            } => self.handle_pointer_released(x, y),
3438            InputEvent::Pointer {
3439                x,
3440                y,
3441                state: PointerState::Moved,
3442                ..
3443            } => self.handle_pointer_moved(x, y),
3444            _ => Ok(()),
3445        }
3446    }
3447
3448    pub fn tick_input(&mut self, dt_ms: u32) -> Result<(), GuiError> {
3449        if self.last_select_id.is_some() {
3450            self.select_elapsed_ms = self.select_elapsed_ms.saturating_add(dt_ms);
3451            if self.select_elapsed_ms > self.select_double_window_ms {
3452                self.last_select_id = None;
3453                self.select_elapsed_ms = 0;
3454            }
3455        }
3456        if self.last_pointer_id.is_some() {
3457            self.pointer_elapsed_ms = self.pointer_elapsed_ms.saturating_add(dt_ms);
3458            if self.pointer_elapsed_ms > self.pointer_double_window_ms {
3459                self.last_pointer_id = None;
3460                self.pointer_elapsed_ms = 0;
3461            }
3462        }
3463        self.tick_state_transitions(dt_ms)?;
3464        if let Some(mut inertia) = self.inertia_scroll {
3465            if inertia.velocity.abs() < self.scroll_physics.velocity_threshold {
3466                self.inertia_scroll = None;
3467            } else {
3468                let current = self.scroll_offset(inertia.id).unwrap_or(0);
3469                let delta = (inertia.velocity * (dt_ms as f32 / 16.0)).round() as i32;
3470                if delta != 0 {
3471                    let next = current.saturating_sub(delta);
3472                    if next != current {
3473                        self.set_scroll_offset(inertia.id, next)?;
3474                        self.push_event(UiEvent::Scroll {
3475                            id: inertia.id,
3476                            delta: next - current,
3477                        })?;
3478                    }
3479                }
3480                inertia.velocity *= self
3481                    .scroll_physics
3482                    .velocity_decay
3483                    .powf((dt_ms as f32 / 16.0).max(1.0));
3484                self.inertia_scroll = Some(inertia);
3485            }
3486        }
3487        self.tick_textarea_cursor_blink(dt_ms)?;
3488        let Some(mut pressed) = self.pressed else {
3489            return Ok(());
3490        };
3491        if !self.effective_visible(pressed.id) || !self.effective_enabled(pressed.id) {
3492            self.pressed = None;
3493            return Ok(());
3494        }
3495        let timing = self.press_timing_for(pressed.id);
3496        pressed.elapsed_ms = pressed.elapsed_ms.saturating_add(dt_ms);
3497        pressed.repeat_elapsed_ms = pressed.repeat_elapsed_ms.saturating_add(dt_ms);
3498        if !pressed.long_emitted && pressed.elapsed_ms >= timing.long_press_ms {
3499            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
3500            self.dispatch_widget_event(
3501                pressed.id,
3502                WidgetEventKind::LongPressed,
3503                &mut events,
3504                |_| EventPolicy::Continue,
3505            )?;
3506            self.push_event(UiEvent::LongPressed(pressed.id))?;
3507            pressed.long_emitted = true;
3508        }
3509        if pressed.repeat_elapsed_ms >= timing.repeat_delay_ms
3510            && self.repeatable_widget(pressed.id)
3511            && pressed.long_emitted
3512        {
3513            let intervals =
3514                (pressed.repeat_elapsed_ms - timing.repeat_delay_ms) / timing.repeat_interval_ms;
3515            if intervals > 0 {
3516                self.dispatch_repeat_activation(pressed.id)?;
3517                pressed.repeat_elapsed_ms = timing.repeat_delay_ms;
3518            }
3519        }
3520        self.pressed = Some(pressed);
3521        Ok(())
3522    }
3523
3524    pub fn pop_event(&mut self) -> Option<UiEvent> {
3525        if self.events.is_empty() {
3526            None
3527        } else {
3528            Some(self.events.remove(0))
3529        }
3530    }
3531
3532    pub fn set_event_filter(
3533        &mut self,
3534        id: WidgetId,
3535        filter: UiEventFilter,
3536    ) -> Result<(), GuiError> {
3537        self.node(id).ok_or(GuiError::NotFound)?;
3538        if let Some((_, current)) = self
3539            .subscriptions
3540            .iter_mut()
3541            .find(|(sub_id, _)| *sub_id == id)
3542        {
3543            *current = filter;
3544            return Ok(());
3545        }
3546        self.subscriptions
3547            .push((id, filter))
3548            .map_err(|_| GuiError::WidgetsFull)
3549    }
3550
3551    pub fn event_filter(&self, id: WidgetId) -> Result<UiEventFilter, GuiError> {
3552        self.node(id).ok_or(GuiError::NotFound)?;
3553        Ok(self
3554            .subscriptions
3555            .iter()
3556            .find(|(sub_id, _)| *sub_id == id)
3557            .map(|(_, filter)| *filter)
3558            .unwrap_or(UiEventFilter::ALL))
3559    }
3560
3561    pub fn clear_event_filter(&mut self, id: WidgetId) -> Result<(), GuiError> {
3562        self.node(id).ok_or(GuiError::NotFound)?;
3563        if let Some(pos) = self
3564            .subscriptions
3565            .iter()
3566            .position(|(sub_id, _)| *sub_id == id)
3567        {
3568            self.subscriptions.remove(pos);
3569        }
3570        Ok(())
3571    }
3572
3573    pub fn set_dispatch_policy(
3574        &mut self,
3575        id: WidgetId,
3576        policy: WidgetDispatchPolicy,
3577    ) -> Result<(), GuiError> {
3578        self.node(id).ok_or(GuiError::NotFound)?;
3579        if let Some((_, current)) = self
3580            .dispatch_policies
3581            .iter_mut()
3582            .find(|(policy_id, _)| *policy_id == id)
3583        {
3584            *current = policy;
3585            return Ok(());
3586        }
3587        self.dispatch_policies
3588            .push((id, policy))
3589            .map_err(|_| GuiError::WidgetsFull)
3590    }
3591
3592    pub fn dispatch_policy(&self, id: WidgetId) -> Result<Option<WidgetDispatchPolicy>, GuiError> {
3593        self.node(id).ok_or(GuiError::NotFound)?;
3594        Ok(self
3595            .dispatch_policies
3596            .iter()
3597            .find(|(policy_id, _)| *policy_id == id)
3598            .map(|(_, policy)| *policy))
3599    }
3600
3601    pub fn clear_dispatch_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
3602        self.node(id).ok_or(GuiError::NotFound)?;
3603        if let Some(pos) = self
3604            .dispatch_policies
3605            .iter()
3606            .position(|(policy_id, _)| *policy_id == id)
3607        {
3608            self.dispatch_policies.remove(pos);
3609        }
3610        Ok(())
3611    }
3612
3613    fn add_widget<S>(
3614        &mut self,
3615        rect: Rect,
3616        kind: WidgetKind<'a>,
3617        style: S,
3618    ) -> Result<WidgetId, GuiError>
3619    where
3620        S: Into<WidgetStyle>,
3621    {
3622        let id = WidgetId::new(self.next_id);
3623        self.next_id = self.next_id.saturating_add(1).max(1);
3624        let node = WidgetNode::new(id, rect, kind, style);
3625        self.widgets.push(node).map_err(|_| GuiError::WidgetsFull)?;
3626        self.dirty.add(rect)?;
3627        Ok(id)
3628    }
3629
3630    fn render_into<D>(
3631        &self,
3632        ctx: &mut RenderCtx<'_, D>,
3633        offset_x: i32,
3634        offset_y: i32,
3635        opacity: u8,
3636    ) -> Result<(), D::Error>
3637    where
3638        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3639    {
3640        for node in &self.widgets {
3641            if !self.effective_visible(node.id) {
3642                continue;
3643            }
3644            let Some(base_rect) = self.absolute_rect(node.id) else {
3645                continue;
3646            };
3647            let rect = Rect::new(
3648                base_rect.x + offset_x,
3649                base_rect.y + offset_y,
3650                base_rect.w,
3651                base_rect.h,
3652            );
3653            let base_clip = self.inherited_clip(node.id).unwrap_or(self.viewport);
3654            let clip = Rect::new(
3655                base_clip.x + offset_x,
3656                base_clip.y + offset_y,
3657                base_clip.w,
3658                base_clip.h,
3659            );
3660            if rect.intersection(clip).is_empty() {
3661                continue;
3662            }
3663            let old_clip = ctx.clip();
3664            ctx.set_clip(old_clip.intersection(clip));
3665            let state = if self.pressed.is_some_and(|pressed| pressed.id == node.id) {
3666                VisualState::Pressed
3667            } else if Some(node.id) == self.focus {
3668                VisualState::Focused
3669            } else if !self.effective_enabled(node.id) {
3670                VisualState::Disabled
3671            } else {
3672                VisualState::Normal
3673            };
3674            let mut render_node = *node;
3675            let class_style = node.style_class.and_then(|class| {
3676                self.class_styles
3677                    .iter()
3678                    .find(|(id, _)| *id == class)
3679                    .map(|(_, style)| *style)
3680            });
3681            let resolve_state_style = |vs: VisualState| {
3682                class_style
3683                    .map(|style| style.resolve(vs))
3684                    .unwrap_or_else(|| render_node.style.resolve(vs))
3685            };
3686            let active_style = if let Some((from, to, t)) = self.state_transition_progress(node.id)
3687            {
3688                lerp_style(resolve_state_style(from), resolve_state_style(to), t)
3689            } else {
3690                resolve_state_style(state)
3691            };
3692            render_node.style = render_node.style.with_state_override(state, active_style);
3693            if opacity < 255 {
3694                let apply = |v: u8| -> u8 { ((v as u16 * opacity as u16) / 255) as u8 };
3695                render_node.style.normal.opacity = apply(render_node.style.normal.opacity);
3696                render_node.style.focused.opacity = apply(render_node.style.focused.opacity);
3697                render_node.style.pressed.opacity = apply(render_node.style.pressed.opacity);
3698                render_node.style.disabled.opacity = apply(render_node.style.disabled.opacity);
3699            }
3700            render_node.render_at(ctx, rect, state)?;
3701            ctx.set_clip(old_clip);
3702        }
3703        Ok(())
3704    }
3705
3706    fn node(&self, id: WidgetId) -> Option<&WidgetNode<'a>> {
3707        self.widgets.iter().find(|node| node.id == id)
3708    }
3709
3710    fn intrinsic_size(&self, id: WidgetId) -> Option<(u32, u32)> {
3711        let node = self.node(id)?;
3712        let style = node.style.resolve(VisualState::Normal);
3713        let pad_x = style.padding.left.max(0) as u32 + style.padding.right.max(0) as u32;
3714        let pad_y = style.padding.top.max(0) as u32 + style.padding.bottom.max(0) as u32;
3715        let border = style.border.width as u32 * 2;
3716        let text_width = |text: &str| text.chars().count() as u32 * style.font.advance();
3717        let text_height = style.font.line_height();
3718
3719        let content = match node.kind {
3720            WidgetKind::Label(text) => (text_width(text), text_height),
3721            WidgetKind::Button(text) => (text_width(text).saturating_add(6), text_height),
3722            WidgetKind::Toggle { label, .. } => (text_width(label).saturating_add(12), text_height),
3723            WidgetKind::Checkbox { label, .. } => {
3724                (text_width(label).saturating_add(10), text_height)
3725            }
3726            WidgetKind::ValueLabel { label, .. } => {
3727                (text_width(label).saturating_add(16), text_height)
3728            }
3729            WidgetKind::IconButton { label, .. } => {
3730                (text_width(label).saturating_add(10), text_height)
3731            }
3732            WidgetKind::Tabs { labels, .. } => {
3733                let max = labels.iter().map(|s| text_width(s)).max().unwrap_or(0);
3734                (
3735                    max.saturating_mul(labels.len() as u32).saturating_add(4),
3736                    text_height,
3737                )
3738            }
3739            WidgetKind::Dialog { title, body } => {
3740                let w = text_width(title).max(text_width(body)).saturating_add(8);
3741                (w, text_height.saturating_mul(3))
3742            }
3743            WidgetKind::Toast { text, .. } => (
3744                text_width(text).saturating_add(8),
3745                text_height.saturating_add(2),
3746            ),
3747            WidgetKind::Dropdown {
3748                items, selected, ..
3749            } => (
3750                text_width(items.get(selected).copied().unwrap_or("-")).saturating_add(10),
3751                text_height.saturating_add(2),
3752            ),
3753            WidgetKind::TextArea {
3754                text_buf,
3755                text_len,
3756                placeholder,
3757                ..
3758            } => (
3759                text_width(if text_len == 0 {
3760                    placeholder
3761                } else {
3762                    textarea_text(&text_buf, text_len)
3763                })
3764                .saturating_add(10),
3765                text_height.saturating_add(4),
3766            ),
3767            WidgetKind::Keyboard { keys, cols, .. } => {
3768                let cols = cols.max(1) as u32;
3769                let rows = (keys.len() as u32).div_ceil(cols).max(1);
3770                (
3771                    cols.saturating_mul(style.font.advance().saturating_add(4)),
3772                    rows.saturating_mul(style.font.line_height().saturating_add(4)),
3773                )
3774            }
3775            WidgetKind::List {
3776                items,
3777                visible_rows,
3778                ..
3779            } => {
3780                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3781                (
3782                    max.saturating_add(6),
3783                    (text_height.saturating_add(2))
3784                        .saturating_mul(visible_rows as u32)
3785                        .max(text_height),
3786                )
3787            }
3788            WidgetKind::Menu { items, .. } => {
3789                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3790                (
3791                    max.saturating_add(6),
3792                    (text_height.saturating_add(2))
3793                        .saturating_mul(items.len() as u32)
3794                        .max(text_height),
3795                )
3796            }
3797            WidgetKind::FeedTimeline {
3798                items,
3799                visible_rows,
3800                expanded,
3801                ..
3802            } => {
3803                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3804                let row_h = if expanded {
3805                    text_height.saturating_mul(2).saturating_add(2)
3806                } else {
3807                    text_height.saturating_add(2)
3808                };
3809                (
3810                    max.saturating_add(8),
3811                    row_h.saturating_mul(visible_rows as u32).max(text_height),
3812                )
3813            }
3814            _ => (node.rect.w.max(1), node.rect.h.max(1)),
3815        };
3816
3817        Some((
3818            content
3819                .0
3820                .saturating_add(pad_x)
3821                .saturating_add(border)
3822                .max(1),
3823            content
3824                .1
3825                .saturating_add(pad_y)
3826                .saturating_add(border)
3827                .max(1),
3828        ))
3829    }
3830
3831    fn node_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode<'a>> {
3832        self.widgets.iter_mut().find(|node| node.id == id)
3833    }
3834
3835    fn effective_visible(&self, id: WidgetId) -> bool {
3836        let mut current = Some(id);
3837        let mut depth = 0;
3838        while let Some(widget_id) = current {
3839            if depth >= NODES {
3840                return false;
3841            }
3842            let Some(node) = self.node(widget_id) else {
3843                return false;
3844            };
3845            if node.hidden() {
3846                return false;
3847            }
3848            current = node.parent;
3849            depth += 1;
3850        }
3851        true
3852    }
3853
3854    fn inherited_clip(&self, id: WidgetId) -> Option<Rect> {
3855        let mut clip = self.viewport;
3856        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
3857        let mut current = Some(id);
3858        while let Some(widget_id) = current {
3859            chain.push(widget_id).ok()?;
3860            current = self.node(widget_id)?.parent;
3861        }
3862        for widget_id in chain.iter().rev().copied() {
3863            let node = self.node(widget_id)?;
3864            if widget_id == id || node.clips_children() {
3865                clip = clip.intersection(self.absolute_rect(widget_id)?);
3866            }
3867            if clip.is_empty() {
3868                return None;
3869            }
3870        }
3871        Some(clip)
3872    }
3873
3874    fn effective_enabled(&self, id: WidgetId) -> bool {
3875        let mut current = Some(id);
3876        let mut depth = 0;
3877        while let Some(widget_id) = current {
3878            if depth >= NODES {
3879                return false;
3880            }
3881            let Some(node) = self.node(widget_id) else {
3882                return false;
3883            };
3884            if node.disabled() {
3885                return false;
3886            }
3887            current = node.parent;
3888            depth += 1;
3889        }
3890        true
3891    }
3892
3893    fn effective_focusable(&self, id: WidgetId) -> bool {
3894        self.node(id).is_some_and(|node| {
3895            self.node_in_active_group(node)
3896                && node.focusable()
3897                && self.effective_visible(id)
3898                && self.effective_enabled(id)
3899        })
3900    }
3901
3902    fn ensure_focus(&mut self) {
3903        if self.focus.is_none() {
3904            self.focus = self
3905                .widgets
3906                .iter()
3907                .find(|node| self.effective_focusable(node.id))
3908                .map(|n| n.id);
3909        }
3910    }
3911
3912    fn focus_next(&mut self) -> Result<(), GuiError> {
3913        self.move_focus(1)
3914    }
3915
3916    fn focus_prev(&mut self) -> Result<(), GuiError> {
3917        self.move_focus(-1)
3918    }
3919
3920    fn move_focus(&mut self, delta: i8) -> Result<(), GuiError> {
3921        let focusable = self
3922            .widgets
3923            .iter()
3924            .filter(|node| self.effective_focusable(node.id))
3925            .count();
3926        if focusable == 0 {
3927            return Ok(());
3928        }
3929
3930        let current_pos = self
3931            .widgets
3932            .iter()
3933            .filter(|node| self.effective_focusable(node.id))
3934            .position(|node| Some(node.id) == self.focus)
3935            .unwrap_or(0);
3936
3937        let next_pos = if delta >= 0 {
3938            (current_pos + 1) % focusable
3939        } else if current_pos == 0 {
3940            focusable - 1
3941        } else {
3942            current_pos - 1
3943        };
3944
3945        let next = self
3946            .widgets
3947            .iter()
3948            .filter(|node| self.effective_focusable(node.id))
3949            .nth(next_pos)
3950            .map(|node| node.id);
3951        self.set_focus(next)
3952    }
3953
3954    fn adjust_focused_selection(&mut self, delta: i8) -> Result<bool, GuiError> {
3955        let Some(id) = self.focus else {
3956            return Ok(false);
3957        };
3958        let wrap_navigation = self.menu_contract.wrap_navigation;
3959
3960        let mut changed_rect = None;
3961        let mut changed = false;
3962
3963        if let Some(node) = self.node_mut(id) {
3964            match node.kind {
3965                WidgetKind::Menu {
3966                    items,
3967                    selected: ref mut current,
3968                } => {
3969                    if items.is_empty() {
3970                        return Ok(true);
3971                    }
3972                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3973                    changed_rect = changed.then_some(node.rect);
3974                }
3975                WidgetKind::Dropdown {
3976                    items,
3977                    selected: ref mut current,
3978                    open,
3979                } => {
3980                    if !open {
3981                        return Ok(false);
3982                    }
3983                    if items.is_empty() {
3984                        return Ok(true);
3985                    }
3986                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3987                    changed_rect = changed.then_some(node.rect);
3988                }
3989                WidgetKind::Roller {
3990                    items,
3991                    selected: ref mut current,
3992                } => {
3993                    if items.is_empty() {
3994                        return Ok(true);
3995                    }
3996                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3997                    changed_rect = changed.then_some(node.rect);
3998                }
3999                WidgetKind::Keyboard {
4000                    keys,
4001                    selected: ref mut current,
4002                    ..
4003                } => {
4004                    if keys.is_empty() {
4005                        return Ok(true);
4006                    }
4007                    changed = bump_index_with_wrap(current, keys.len(), delta, wrap_navigation);
4008                    changed_rect = changed.then_some(node.rect);
4009                }
4010                WidgetKind::List {
4011                    items,
4012                    selected: ref mut current,
4013                    ref mut offset,
4014                    visible_rows,
4015                } => {
4016                    if items.is_empty() {
4017                        return Ok(true);
4018                    }
4019                    let mut state = ListState::new(*current, *offset, visible_rows);
4020                    let mut next = state.selected;
4021                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4022                    if changed {
4023                        let _ = state.set_selected(next, items.len());
4024                    }
4025                    *current = state.selected;
4026                    *offset = state.offset;
4027                    changed_rect = changed.then_some(node.rect);
4028                }
4029                WidgetKind::FeedTimeline {
4030                    items,
4031                    selected: ref mut current,
4032                    ref mut offset,
4033                    visible_rows,
4034                    expanded,
4035                } => {
4036                    if items.is_empty() {
4037                        return Ok(true);
4038                    }
4039                    let mut state =
4040                        FeedTimelineState::new(*current, *offset, visible_rows, expanded);
4041                    let mut next = state.selected;
4042                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4043                    if changed {
4044                        let _ = state.set_selected(next, items.len());
4045                    }
4046                    *current = state.selected;
4047                    *offset = state.offset;
4048                    changed_rect = changed.then_some(node.rect);
4049                }
4050                WidgetKind::ScrollView {
4051                    offset_y: ref mut offset,
4052                    content_h,
4053                } => {
4054                    let mut state = ScrollState::new(*offset, content_h);
4055                    changed = state.scroll_by(delta as i32 * 8);
4056                    *offset = state.offset_y;
4057                    changed_rect = changed.then_some(node.rect);
4058                }
4059                _ => return Ok(false),
4060            }
4061        }
4062
4063        if let Some(rect) = changed_rect {
4064            self.dirty.add(rect)?;
4065        }
4066        if changed {
4067            self.push_event(UiEvent::ValueChanged(id))?;
4068        }
4069        Ok(true)
4070    }
4071
4072    fn adjust_focused_scalar(&mut self, direction: f32) -> Result<bool, GuiError> {
4073        let Some(id) = self.focus else {
4074            return Ok(false);
4075        };
4076
4077        let mut changed_rect = None;
4078        let mut changed = false;
4079
4080        if let Some(node) = self.node_mut(id) {
4081            match node.kind {
4082                WidgetKind::Slider {
4083                    value: ref mut current,
4084                    min,
4085                    max,
4086                } => {
4087                    let mut state = SliderState::new(*current, min, max);
4088                    changed = state.step_by(direction);
4089                    *current = state.value;
4090                    changed_rect = changed.then_some(node.rect);
4091                }
4092                WidgetKind::Tabs {
4093                    labels,
4094                    selected: ref mut current,
4095                } => {
4096                    if labels.is_empty() {
4097                        return Ok(true);
4098                    }
4099                    let mut state = TabsState::new(*current);
4100                    changed = state.bump(labels.len(), if direction >= 0.0 { 1 } else { -1 });
4101                    *current = state.selected;
4102                    changed_rect = changed.then_some(node.rect);
4103                }
4104                WidgetKind::TextArea {
4105                    text_buf,
4106                    text_len,
4107                    cursor: ref mut current,
4108                    ..
4109                } => {
4110                    let text = textarea_text(&text_buf, text_len);
4111                    let len = text.chars().count();
4112                    if direction >= 0.0 {
4113                        let next = (*current + 1).min(len);
4114                        changed = next != *current;
4115                        *current = next;
4116                    } else {
4117                        let next = current.saturating_sub(1);
4118                        changed = next != *current;
4119                        *current = next;
4120                    }
4121                    changed_rect = changed.then_some(node.rect);
4122                }
4123                _ => return Ok(false),
4124            }
4125        }
4126
4127        if let Some(rect) = changed_rect {
4128            self.dirty.add(rect)?;
4129        }
4130        if changed {
4131            self.push_event(UiEvent::ValueChanged(id))?;
4132        }
4133        Ok(true)
4134    }
4135
4136    fn activate_focused(&mut self, id: WidgetId) -> Result<(), GuiError> {
4137        let mut changed_rect = None;
4138        let mut changed = false;
4139        let mut dropdown_state_event = None;
4140        let select_opens_dropdown = self.menu_contract.select_opens_dropdown;
4141
4142        if let Some(node) = self.node_mut(id) {
4143            match node.kind {
4144                WidgetKind::Toggle { on: ref mut v, .. } => {
4145                    *v = !*v;
4146                    changed = true;
4147                    changed_rect = Some(node.rect);
4148                }
4149                WidgetKind::Checkbox {
4150                    checked: ref mut v, ..
4151                } => {
4152                    *v = !*v;
4153                    changed = true;
4154                    changed_rect = Some(node.rect);
4155                }
4156                WidgetKind::Keyboard {
4157                    keys,
4158                    alt_keys,
4159                    selected,
4160                    layout,
4161                    target,
4162                    ..
4163                } => {
4164                    if let Some(ch) = keyboard_char_for_layout(keys, alt_keys, selected, layout) {
4165                        changed = true;
4166                        changed_rect = Some(node.rect);
4167                        if let Some(target) = target {
4168                            let _ = self.push_event(UiEvent::TextInput { id: target, ch });
4169                            let _ = self.push_event(UiEvent::ValueChanged(target));
4170                        }
4171                    }
4172                }
4173                WidgetKind::Dropdown {
4174                    open: ref mut is_open,
4175                    ..
4176                } if select_opens_dropdown => {
4177                    *is_open = !*is_open;
4178                    changed = true;
4179                    changed_rect = Some(node.rect);
4180                    dropdown_state_event = Some(*is_open);
4181                }
4182                _ => {}
4183            }
4184        }
4185
4186        if let Some(open) = dropdown_state_event {
4187            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4188            self.dispatch_widget_event(
4189                id,
4190                if open {
4191                    WidgetEventKind::Opened
4192                } else {
4193                    WidgetEventKind::Closed
4194                },
4195                &mut events,
4196                |_| EventPolicy::Continue,
4197            )?;
4198            self.push_event(if open {
4199                UiEvent::Opened(id)
4200            } else {
4201                UiEvent::Closed(id)
4202            })?;
4203        }
4204
4205        if let Some(rect) = changed_rect {
4206            self.dirty.add(rect)?;
4207        }
4208        if changed {
4209            self.push_event(UiEvent::ValueChanged(id))?;
4210        }
4211        Ok(())
4212    }
4213
4214    fn node_in_active_group(&self, node: &WidgetNode<'_>) -> bool {
4215        self.active_focus_group
4216            .is_none_or(|group| node.focus_group == group)
4217    }
4218
4219    fn handle_pointer_pressed(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4220        let hit = self.pointer_hit(x, y, true);
4221
4222        if let Some(id) = hit {
4223            self.dispatch_activation(id, true)?;
4224            self.pressed = Some(PressTracker {
4225                id,
4226                start_x: x,
4227                start_y: y,
4228                last_x: x,
4229                last_y: y,
4230                elapsed_ms: 0,
4231                long_emitted: false,
4232                gesture_emitted: false,
4233                repeat_elapsed_ms: 0,
4234                scroll_velocity: 0.0,
4235            });
4236            self.inertia_scroll = None;
4237        }
4238        Ok(())
4239    }
4240
4241    fn handle_pointer_released(&mut self, _x: i32, _y: i32) -> Result<(), GuiError> {
4242        let mut released_id = None;
4243        if let Some(pressed) = self.pressed {
4244            if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4245                if pressed.scroll_velocity.abs() > self.scroll_physics.velocity_threshold {
4246                    self.inertia_scroll = Some(InertiaScroll {
4247                        id: scroll_id,
4248                        velocity: pressed.scroll_velocity,
4249                    });
4250                }
4251            }
4252            released_id = Some(pressed.id);
4253        }
4254        self.pressed = None;
4255        if let Some(id) = released_id {
4256            let to = if !self.effective_enabled(id) {
4257                VisualState::Disabled
4258            } else if Some(id) == self.focus {
4259                VisualState::Focused
4260            } else {
4261                VisualState::Normal
4262            };
4263            self.start_state_transition(id, VisualState::Pressed, to);
4264            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4265            self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4266                EventPolicy::Continue
4267            })?;
4268            self.push_event(UiEvent::Released(id))?;
4269            self.push_event(UiEvent::PointerReleased(id))?;
4270            let double_pointer = self.last_pointer_id == Some(id)
4271                && self.pointer_elapsed_ms <= self.pointer_double_window_ms;
4272            if double_pointer {
4273                self.dispatch_double_clicked(id)?;
4274                self.last_pointer_id = None;
4275                self.pointer_elapsed_ms = 0;
4276            } else {
4277                self.last_pointer_id = Some(id);
4278                self.pointer_elapsed_ms = 0;
4279            }
4280        }
4281        Ok(())
4282    }
4283
4284    fn handle_pointer_moved(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4285        let Some(mut pressed) = self.pressed else {
4286            return Ok(());
4287        };
4288        let dy = y - pressed.last_y;
4289        pressed.last_x = x;
4290        pressed.last_y = y;
4291
4292        let moved_from_start =
4293            (x - pressed.start_x).unsigned_abs() + (y - pressed.start_y).unsigned_abs();
4294        if !pressed.gesture_emitted && moved_from_start >= 6 {
4295            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4296            self.dispatch_widget_event(pressed.id, WidgetEventKind::Gesture, &mut events, |_| {
4297                EventPolicy::Continue
4298            })?;
4299            self.push_event(UiEvent::Gesture(pressed.id))?;
4300            pressed.gesture_emitted = true;
4301        }
4302
4303        if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4304            let current = self.scroll_offset(scroll_id).unwrap_or(0);
4305            let next = current.saturating_sub(dy);
4306            if next != current {
4307                self.set_scroll_offset(scroll_id, next)?;
4308                self.push_event(UiEvent::Scroll {
4309                    id: scroll_id,
4310                    delta: next - current,
4311                })?;
4312                let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4313                self.dispatch_widget_event(
4314                    scroll_id,
4315                    WidgetEventKind::Scroll {
4316                        delta: next - current,
4317                    },
4318                    &mut events,
4319                    |_| EventPolicy::Continue,
4320                )?;
4321            }
4322            let blend = self.scroll_physics.drag_velocity_blend;
4323            pressed.scroll_velocity = pressed.scroll_velocity * (1.0 - blend) + (dy as f32) * blend;
4324        }
4325        self.pressed = Some(pressed);
4326        Ok(())
4327    }
4328
4329    fn dispatch_activation(&mut self, id: WidgetId, is_pointer: bool) -> Result<(), GuiError> {
4330        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4331        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4332            EventPolicy::Continue
4333        })?;
4334        if self.effective_focusable(id) {
4335            self.set_focus(Some(id))?;
4336        }
4337        self.push_event(UiEvent::Pressed(id))?;
4338        if is_pointer {
4339            self.push_event(UiEvent::PointerPressed(id))?;
4340        }
4341        let from = if Some(id) == self.focus {
4342            VisualState::Focused
4343        } else {
4344            VisualState::Normal
4345        };
4346        self.start_state_transition(id, from, VisualState::Pressed);
4347
4348        self.activate_focused(id)?;
4349        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4350            EventPolicy::Continue
4351        })?;
4352        self.push_event(UiEvent::Clicked(id))?;
4353        self.push_event(UiEvent::Activate(id))?;
4354        Ok(())
4355    }
4356
4357    fn dispatch_repeat_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4358        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4359        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4360            EventPolicy::Continue
4361        })?;
4362        self.push_event(UiEvent::Clicked(id))?;
4363        self.push_event(UiEvent::Activate(id))
4364    }
4365
4366    fn dispatch_double_clicked(&mut self, id: WidgetId) -> Result<(), GuiError> {
4367        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4368        self.dispatch_widget_event(id, WidgetEventKind::DoubleClicked, &mut events, |_| {
4369            EventPolicy::Continue
4370        })?;
4371        self.push_event(UiEvent::DoubleClicked(id))
4372    }
4373
4374    fn dispatch_key_pressed(&mut self, id: WidgetId) -> Result<(), GuiError> {
4375        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4376        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4377            EventPolicy::Continue
4378        })?;
4379        self.push_event(UiEvent::Pressed(id))
4380    }
4381
4382    fn dispatch_key_released(&mut self, id: WidgetId) -> Result<(), GuiError> {
4383        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4384        self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4385            EventPolicy::Continue
4386        })?;
4387        self.push_event(UiEvent::Released(id))
4388    }
4389
4390    fn repeatable_widget(&self, id: WidgetId) -> bool {
4391        self.node(id).is_some_and(|node| {
4392            matches!(
4393                node.kind,
4394                WidgetKind::Button(_) | WidgetKind::IconButton { .. }
4395            )
4396        })
4397    }
4398
4399    fn pointer_hit(&self, x: i32, y: i32, clickable_only: bool) -> Option<WidgetId> {
4400        self.widgets
4401            .iter()
4402            .rev()
4403            .find(|node| {
4404                (!clickable_only || node.clickable())
4405                    && self.effective_visible(node.id)
4406                    && self.effective_enabled(node.id)
4407                    && self
4408                        .absolute_rect(node.id)
4409                        .is_some_and(|rect| rect.contains(x, y))
4410            })
4411            .map(|node| node.id)
4412    }
4413
4414    fn scrollable_ancestor(&self, id: WidgetId) -> Option<WidgetId> {
4415        let mut current = Some(id);
4416        let mut depth = 0usize;
4417        while let Some(widget_id) = current {
4418            if depth >= NODES {
4419                return None;
4420            }
4421            let node = self.node(widget_id)?;
4422            if node.scrollable() {
4423                return Some(widget_id);
4424            }
4425            current = node.parent;
4426            depth += 1;
4427        }
4428        None
4429    }
4430
4431    fn mark_focus_pair(
4432        &mut self,
4433        old: Option<WidgetId>,
4434        new: Option<WidgetId>,
4435    ) -> Result<(), GuiError> {
4436        if let Some(id) = old {
4437            if let Some(rect) = self.absolute_rect(id) {
4438                self.dirty.add(rect)?;
4439            }
4440        }
4441        if let Some(id) = new {
4442            if let Some(rect) = self.absolute_rect(id) {
4443                self.dirty.add(rect)?;
4444            }
4445        }
4446        Ok(())
4447    }
4448
4449    fn start_focus_transitions(&mut self, old: Option<WidgetId>, new: Option<WidgetId>) {
4450        if self.state_transition_ms == 0 {
4451            return;
4452        }
4453        if let Some(id) = old {
4454            self.start_state_transition(id, VisualState::Focused, VisualState::Normal);
4455        }
4456        if let Some(id) = new {
4457            self.start_state_transition(id, VisualState::Normal, VisualState::Focused);
4458        }
4459    }
4460
4461    fn start_state_transition(&mut self, id: WidgetId, from: VisualState, to: VisualState) {
4462        if self.state_transition_ms == 0 || from == to {
4463            return;
4464        }
4465        if let Some(entry) = self
4466            .state_transitions
4467            .iter_mut()
4468            .find(|entry| entry.id == id)
4469        {
4470            *entry = StateTransition {
4471                id,
4472                from,
4473                to,
4474                elapsed_ms: 0,
4475            };
4476            return;
4477        }
4478        if self.state_transitions.len() == self.state_transitions.capacity() {
4479            self.state_transitions.remove(0);
4480        }
4481        let _ = self.state_transitions.push(StateTransition {
4482            id,
4483            from,
4484            to,
4485            elapsed_ms: 0,
4486        });
4487    }
4488
4489    fn tick_state_transitions(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4490        if self.state_transitions.is_empty() || self.state_transition_ms == 0 {
4491            return Ok(());
4492        }
4493        let mut i = 0usize;
4494        let mut completed_pressed = heapless::Vec::<WidgetId, NODES>::new();
4495        while i < self.state_transitions.len() {
4496            let mut remove = false;
4497            let id;
4498            let to;
4499            {
4500                let entry = &mut self.state_transitions[i];
4501                entry.elapsed_ms = entry.elapsed_ms.saturating_add(dt_ms);
4502                if entry.elapsed_ms >= self.state_transition_ms {
4503                    remove = true;
4504                }
4505                id = entry.id;
4506                to = entry.to;
4507            }
4508            if let Some(rect) = self.absolute_rect(id) {
4509                self.dirty.add(rect)?;
4510            }
4511            if remove {
4512                if to == VisualState::Pressed {
4513                    let _ = completed_pressed.push(id);
4514                }
4515                self.state_transitions.remove(i);
4516            } else {
4517                i += 1;
4518            }
4519        }
4520        for id in completed_pressed {
4521            // Pointer-held presses keep visual pressed state until release.
4522            if self.pressed.is_some_and(|pressed| pressed.id == id) {
4523                continue;
4524            }
4525            let to = self.resting_visual_state(id);
4526            self.start_state_transition(id, VisualState::Pressed, to);
4527        }
4528        Ok(())
4529    }
4530
4531    fn state_transition_progress(&self, id: WidgetId) -> Option<(VisualState, VisualState, f32)> {
4532        let duration = self.state_transition_ms.max(1);
4533        self.state_transitions
4534            .iter()
4535            .find(|entry| entry.id == id)
4536            .map(|entry| {
4537                let t = (entry.elapsed_ms as f32 / duration as f32).clamp(0.0, 1.0);
4538                (entry.from, entry.to, t)
4539            })
4540    }
4541
4542    fn set_textarea_cursor_visible(&mut self, id: Option<WidgetId>, visible: bool) {
4543        let Some(id) = id else {
4544            return;
4545        };
4546        let Some(rect) = self.absolute_rect(id) else {
4547            return;
4548        };
4549        let Some(node) = self.node_mut(id) else {
4550            return;
4551        };
4552        if let WidgetKind::TextArea {
4553            cursor_visible: ref mut current,
4554            ..
4555        } = node.kind
4556        {
4557            *current = visible;
4558            let _ = self.dirty.add(rect);
4559        }
4560    }
4561
4562    fn tick_textarea_cursor_blink(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4563        let Some(id) = self.focus else {
4564            return Ok(());
4565        };
4566        let is_textarea = matches!(
4567            self.node(id).map(|n| n.kind),
4568            Some(WidgetKind::TextArea { .. })
4569        );
4570        if !is_textarea {
4571            return Ok(());
4572        }
4573        self.textarea_cursor_blink_elapsed_ms =
4574            self.textarea_cursor_blink_elapsed_ms.saturating_add(dt_ms);
4575        if self.textarea_cursor_blink_elapsed_ms < self.textarea_cursor_blink_ms {
4576            return Ok(());
4577        }
4578        self.textarea_cursor_blink_elapsed_ms = 0;
4579        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
4580        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
4581        if let WidgetKind::TextArea {
4582            cursor_visible: ref mut visible,
4583            ..
4584        } = node.kind
4585        {
4586            *visible = !*visible;
4587            self.dirty.add(rect)?;
4588        }
4589        Ok(())
4590    }
4591
4592    fn push_event(&mut self, event: UiEvent) -> Result<(), GuiError> {
4593        if self.should_emit_event(event)? {
4594            self.events.push(event).map_err(|_| GuiError::EventsFull)?;
4595        }
4596        Ok(())
4597    }
4598
4599    fn should_emit_event(&self, event: UiEvent) -> Result<bool, GuiError> {
4600        let Some(target) = event.target() else {
4601            return Ok(true);
4602        };
4603        let filter = self.event_filter(target)?;
4604        Ok(filter.contains(event.filter()))
4605    }
4606
4607    fn stop_due_to_builtin_widget_behavior(&self, event: WidgetEvent) -> bool {
4608        if event.phase != EventPhase::Capture || event.current == event.target {
4609            return false;
4610        }
4611        let is_pointer_kind = matches!(
4612            event.kind,
4613            WidgetEventKind::Pressed | WidgetEventKind::Released | WidgetEventKind::Clicked
4614        );
4615        is_pointer_kind
4616            && self
4617                .node(event.current)
4618                .is_some_and(|node| matches!(node.kind, WidgetKind::ScrollView { .. }))
4619    }
4620
4621    fn stop_due_to_registered_policy(&self, event: WidgetEvent) -> bool {
4622        self.dispatch_policies
4623            .iter()
4624            .find(|(id, _)| *id == event.current)
4625            .is_some_and(|(_, policy)| policy.stop && policy.allows(event.kind, event.phase))
4626    }
4627
4628    fn resting_visual_state(&self, id: WidgetId) -> VisualState {
4629        if !self.effective_enabled(id) {
4630            VisualState::Disabled
4631        } else if Some(id) == self.focus {
4632            VisualState::Focused
4633        } else {
4634            VisualState::Normal
4635        }
4636    }
4637
4638    fn current_visual_state(&self, id: WidgetId) -> VisualState {
4639        if self.pressed.is_some_and(|pressed| pressed.id == id) {
4640            VisualState::Pressed
4641        } else {
4642            self.resting_visual_state(id)
4643        }
4644    }
4645
4646    fn press_timing_for(&self, id: WidgetId) -> PressTiming {
4647        self.widget_press_timings
4648            .iter()
4649            .find(|(timing_id, _)| *timing_id == id)
4650            .map(|(_, timing)| *timing)
4651            .unwrap_or(PressTiming {
4652                long_press_ms: self.long_press_ms,
4653                repeat_delay_ms: self.press_repeat_delay_ms,
4654                repeat_interval_ms: self.press_repeat_interval_ms,
4655            })
4656    }
4657
4658    fn key_input_policy_for(&self, id: WidgetId) -> WidgetKeyInputPolicy {
4659        self.widget_key_policies
4660            .iter()
4661            .find(|(policy_id, _)| *policy_id == id)
4662            .map(|(_, policy)| *policy)
4663            .unwrap_or_default()
4664    }
4665
4666    fn key_bindings_for(&self, id: WidgetId) -> WidgetKeyBindings {
4667        self.widget_key_bindings
4668            .iter()
4669            .find(|(binding_id, _)| *binding_id == id)
4670            .map(|(_, bindings)| *bindings)
4671            .unwrap_or_default()
4672    }
4673
4674    fn handle_select_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4675        if let Some(node) = self.node(id) {
4676            if self.menu_contract.select_toggles_feed_expanded
4677                && matches!(node.kind, WidgetKind::FeedTimeline { .. })
4678            {
4679                let expanded = if let WidgetKind::FeedTimeline { expanded, .. } = node.kind {
4680                    expanded
4681                } else {
4682                    false
4683                };
4684                self.set_feed_expanded(id, !expanded)?;
4685                self.push_event(UiEvent::ValueChanged(id))?;
4686            }
4687        }
4688        let double_select = self.last_select_id == Some(id)
4689            && self.select_elapsed_ms <= self.select_double_window_ms;
4690        self.dispatch_activation(id, false)?;
4691        if double_select {
4692            self.dispatch_double_clicked(id)?;
4693            self.last_select_id = None;
4694            self.select_elapsed_ms = 0;
4695        } else {
4696            self.last_select_id = Some(id);
4697            self.select_elapsed_ms = 0;
4698        }
4699        Ok(())
4700    }
4701
4702    fn handle_back_action(&mut self) -> Result<(), GuiError> {
4703        if let Some(id) = self.focus {
4704            if matches!(
4705                self.node(id).map(|n| n.kind),
4706                Some(WidgetKind::TextArea { .. })
4707            ) {
4708                self.textarea_backspace(id)?;
4709                return Ok(());
4710            }
4711            if matches!(
4712                self.node(id).map(|n| n.kind),
4713                Some(WidgetKind::Dropdown { open: true, .. })
4714            ) && self.menu_contract.back_closes_dropdown
4715            {
4716                self.set_dropdown_open(id, false)?;
4717                return Ok(());
4718            }
4719            if matches!(
4720                self.node(id).map(|n| n.kind),
4721                Some(WidgetKind::NotificationActionSheet { open: true, .. })
4722            ) && self.menu_contract.back_closes_notification_sheet
4723            {
4724                self.set_notification_sheet_open(id, false)?;
4725                return Ok(());
4726            }
4727        }
4728        self.push_event(UiEvent::Back)
4729    }
4730}
4731
4732fn bump_index_with_wrap(current: &mut usize, len: usize, delta: i8, wrap: bool) -> bool {
4733    if len == 0 {
4734        return false;
4735    }
4736    let next = if delta >= 0 {
4737        if *current + 1 >= len {
4738            if wrap { 0 } else { *current }
4739        } else {
4740            *current + 1
4741        }
4742    } else if *current == 0 {
4743        if wrap { len - 1 } else { *current }
4744    } else {
4745        *current - 1
4746    };
4747    if next != *current {
4748        *current = next;
4749        true
4750    } else {
4751        false
4752    }
4753}
4754
4755fn keyboard_char_for_layout(
4756    keys: &[char],
4757    alt_keys: Option<&[char]>,
4758    selected: usize,
4759    layout: KeyboardLayout,
4760) -> Option<char> {
4761    let base = keys.get(selected).copied()?;
4762    Some(match layout {
4763        KeyboardLayout::Normal => base,
4764        KeyboardLayout::Shift => {
4765            if base.is_ascii_alphabetic() {
4766                base.to_ascii_uppercase()
4767            } else {
4768                base
4769            }
4770        }
4771        KeyboardLayout::Symbols => alt_keys
4772            .and_then(|keys| keys.get(selected).copied())
4773            .unwrap_or('#'),
4774    })
4775}
4776
4777fn textarea_text(buf: &[u8; TEXTAREA_CAPACITY], len: u8) -> &str {
4778    let used = (len as usize).min(TEXTAREA_CAPACITY);
4779    core::str::from_utf8(&buf[..used]).unwrap_or("")
4780}
4781
4782fn textarea_storage_from_str(text: &str) -> ([u8; TEXTAREA_CAPACITY], u8) {
4783    let mut out = [0u8; TEXTAREA_CAPACITY];
4784    let mut len = 0usize;
4785    for ch in text.chars() {
4786        let mut tmp = [0u8; 4];
4787        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4788        if len + enc.len() > TEXTAREA_CAPACITY {
4789            break;
4790        }
4791        out[len..len + enc.len()].copy_from_slice(enc);
4792        len += enc.len();
4793    }
4794    (out, len as u8)
4795}
4796
4797fn textarea_storage_from_chars(
4798    chars: &heapless::Vec<char, TEXTAREA_CAPACITY>,
4799) -> ([u8; TEXTAREA_CAPACITY], u8) {
4800    let mut out = [0u8; TEXTAREA_CAPACITY];
4801    let mut len = 0usize;
4802    for ch in chars {
4803        let mut tmp = [0u8; 4];
4804        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4805        if len + enc.len() > TEXTAREA_CAPACITY {
4806            break;
4807        }
4808        out[len..len + enc.len()].copy_from_slice(enc);
4809        len += enc.len();
4810    }
4811    (out, len as u8)
4812}
4813
4814fn char_at(text: &str, idx: usize) -> Option<char> {
4815    text.chars().nth(idx)
4816}
4817
4818fn prev_word_boundary(text: &str, cursor: usize) -> usize {
4819    let mut pos = cursor.min(text.chars().count());
4820    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| ch.is_whitespace()) {
4821        pos -= 1;
4822    }
4823    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| !ch.is_whitespace()) {
4824        pos -= 1;
4825    }
4826    pos
4827}
4828
4829fn next_word_boundary(text: &str, cursor: usize) -> usize {
4830    let len = text.chars().count();
4831    let mut pos = cursor.min(len);
4832    while pos < len && char_at(text, pos).is_some_and(|ch| !ch.is_whitespace()) {
4833        pos += 1;
4834    }
4835    while pos < len && char_at(text, pos).is_some_and(|ch| ch.is_whitespace()) {
4836        pos += 1;
4837    }
4838    pos
4839}
4840
4841fn delete_selection_if_any(
4842    chars: &mut heapless::Vec<char, TEXTAREA_CAPACITY>,
4843    cursor: &mut usize,
4844    selection: &mut Option<(usize, usize)>,
4845) -> bool {
4846    let Some((start, end)) = *selection else {
4847        return false;
4848    };
4849    let start = start.min(end).min(chars.len());
4850    let end = end.max(start).min(chars.len());
4851    if end <= start {
4852        *selection = None;
4853        *cursor = start;
4854        return false;
4855    }
4856    for _ in start..end {
4857        chars.remove(start);
4858    }
4859    *cursor = start;
4860    *selection = None;
4861    true
4862}
4863
4864fn textarea_row_col_at_cursor(text: &str, cursor: usize, wrap_cols: usize) -> (usize, usize) {
4865    let mut row = 0usize;
4866    let mut col = 0usize;
4867    for ch in text.chars().take(cursor) {
4868        if ch == '\n' {
4869            row += 1;
4870            col = 0;
4871            continue;
4872        }
4873        col += 1;
4874        if col >= wrap_cols {
4875            row += 1;
4876            col = 0;
4877        }
4878    }
4879    (row, col)
4880}
4881
4882fn textarea_cursor_from_row_col(
4883    text: &str,
4884    target_row: usize,
4885    target_col: usize,
4886    wrap_cols: usize,
4887) -> usize {
4888    let mut row = 0usize;
4889    let mut col = 0usize;
4890    let mut idx = 0usize;
4891    for ch in text.chars() {
4892        if row == target_row && col >= target_col {
4893            break;
4894        }
4895        if ch == '\n' {
4896            if row == target_row {
4897                break;
4898            }
4899            row += 1;
4900            col = 0;
4901            idx += 1;
4902            continue;
4903        }
4904        idx += 1;
4905        col += 1;
4906        if col >= wrap_cols {
4907            if row == target_row {
4908                break;
4909            }
4910            row += 1;
4911            col = 0;
4912        }
4913    }
4914    idx
4915}
4916
4917fn textarea_row_end_col(text: &str, target_row: usize, wrap_cols: usize) -> usize {
4918    let mut row = 0usize;
4919    let mut col = 0usize;
4920    for ch in text.chars() {
4921        if row == target_row {
4922            if ch == '\n' {
4923                break;
4924            }
4925            col += 1;
4926            if col >= wrap_cols {
4927                break;
4928            }
4929        } else if ch == '\n' {
4930            row += 1;
4931            col = 0;
4932        } else {
4933            col += 1;
4934            if col >= wrap_cols {
4935                row += 1;
4936                col = 0;
4937            }
4938        }
4939    }
4940    col
4941}