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_sweeping_arc<S>(
1031        &mut self,
1032        rect: Rect,
1033        progress: f32,
1034        arc_radius: u32,
1035        frame_inset: u16,
1036        corner_radius: u8,
1037        bg_color: Rgb565,
1038        arc_color: Rgb565,
1039        frame_color: Rgb565,
1040        style: S,
1041    ) -> Result<WidgetId, GuiError>
1042    where
1043        S: Into<WidgetStyle>,
1044    {
1045        self.add_widget(
1046            rect,
1047            WidgetKind::SweepingArc {
1048                progress: progress.clamp(0.0, 1.0),
1049                arc_radius,
1050                frame_inset,
1051                corner_radius,
1052                bg_color,
1053                arc_color,
1054                frame_color,
1055            },
1056            style,
1057        )
1058    }
1059
1060    #[allow(clippy::too_many_arguments)]
1061    pub fn add_gauge_needle<S>(
1062        &mut self,
1063        rect: Rect,
1064        value: f32,
1065        min: f32,
1066        max: f32,
1067        start_deg: i32,
1068        end_deg: i32,
1069        style: S,
1070    ) -> Result<WidgetId, GuiError>
1071    where
1072        S: Into<WidgetStyle>,
1073    {
1074        self.add_widget(
1075            rect,
1076            WidgetKind::GaugeNeedle {
1077                value,
1078                min,
1079                max,
1080                start_deg,
1081                end_deg,
1082            },
1083            style,
1084        )
1085    }
1086
1087    pub fn add_chart<S>(
1088        &mut self,
1089        rect: Rect,
1090        values: &'a [f32],
1091        min: f32,
1092        max: f32,
1093        style: S,
1094    ) -> Result<WidgetId, GuiError>
1095    where
1096        S: Into<WidgetStyle>,
1097    {
1098        self.add_widget(
1099            rect,
1100            WidgetKind::Chart {
1101                values,
1102                min,
1103                max,
1104                thickness: 1,
1105                fill_under: false,
1106                markers: false,
1107                mode: ChartMode::Line,
1108                show_grid: false,
1109                show_axes: false,
1110                show_labels: false,
1111            },
1112            style,
1113        )
1114    }
1115
1116    pub fn set_chart_style(
1117        &mut self,
1118        id: WidgetId,
1119        thickness: u8,
1120        fill_under: bool,
1121        markers: bool,
1122    ) -> Result<(), GuiError> {
1123        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1124        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1125        match node.kind {
1126            WidgetKind::Chart {
1127                thickness: ref mut t,
1128                fill_under: ref mut fill,
1129                markers: ref mut mark,
1130                ..
1131            } => {
1132                *t = thickness.max(1);
1133                *fill = fill_under;
1134                *mark = markers;
1135                self.dirty.add(rect)?;
1136                Ok(())
1137            }
1138            _ => Err(GuiError::NotFound),
1139        }
1140    }
1141
1142    pub fn set_chart_decoration(
1143        &mut self,
1144        id: WidgetId,
1145        mode: ChartMode,
1146        show_grid: bool,
1147        show_axes: bool,
1148        show_labels: bool,
1149    ) -> Result<(), GuiError> {
1150        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1151        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1152        match node.kind {
1153            WidgetKind::Chart {
1154                mode: ref mut chart_mode,
1155                show_grid: ref mut grid,
1156                show_axes: ref mut axes,
1157                show_labels: ref mut labels,
1158                ..
1159            } => {
1160                *chart_mode = mode;
1161                *grid = show_grid;
1162                *axes = show_axes;
1163                *labels = show_labels;
1164                self.dirty.add(rect)?;
1165                Ok(())
1166            }
1167            _ => Err(GuiError::NotFound),
1168        }
1169    }
1170
1171    pub fn add_spinner<S>(&mut self, rect: Rect, phase: f32, style: S) -> Result<WidgetId, GuiError>
1172    where
1173        S: Into<WidgetStyle>,
1174    {
1175        self.add_widget(rect, WidgetKind::Spinner { phase }, style)
1176    }
1177
1178    pub fn add_dropdown<S>(
1179        &mut self,
1180        rect: Rect,
1181        items: &'a [&'a str],
1182        selected: usize,
1183        style: S,
1184    ) -> Result<WidgetId, GuiError>
1185    where
1186        S: Into<WidgetStyle>,
1187    {
1188        let selected = selected.min(items.len().saturating_sub(1));
1189        let id = self.add_widget(
1190            rect,
1191            WidgetKind::Dropdown {
1192                items,
1193                selected,
1194                open: false,
1195            },
1196            style,
1197        )?;
1198        self.ensure_focus();
1199        Ok(id)
1200    }
1201
1202    pub fn add_roller<S>(
1203        &mut self,
1204        rect: Rect,
1205        items: &'a [&'a str],
1206        selected: usize,
1207        style: S,
1208    ) -> Result<WidgetId, GuiError>
1209    where
1210        S: Into<WidgetStyle>,
1211    {
1212        let selected = selected.min(items.len().saturating_sub(1));
1213        let id = self.add_widget(rect, WidgetKind::Roller { items, selected }, style)?;
1214        self.ensure_focus();
1215        Ok(id)
1216    }
1217
1218    pub fn add_table<S>(
1219        &mut self,
1220        rect: Rect,
1221        rows: &'a [&'a [&'a str]],
1222        style: S,
1223    ) -> Result<WidgetId, GuiError>
1224    where
1225        S: Into<WidgetStyle>,
1226    {
1227        self.add_widget(
1228            rect,
1229            WidgetKind::Table {
1230                rows,
1231                separators: true,
1232                cell_padding: 1,
1233                align: TextAlign::Left,
1234            },
1235            style,
1236        )
1237    }
1238
1239    pub fn set_table_style(
1240        &mut self,
1241        id: WidgetId,
1242        separators: bool,
1243        cell_padding: u8,
1244        align: TextAlign,
1245    ) -> Result<(), GuiError> {
1246        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1247        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1248        match node.kind {
1249            WidgetKind::Table {
1250                separators: ref mut cell_sep,
1251                cell_padding: ref mut pad,
1252                align: ref mut table_align,
1253                ..
1254            } => {
1255                *cell_sep = separators;
1256                *pad = cell_padding.min(6);
1257                *table_align = align;
1258                self.dirty.add(rect)?;
1259                Ok(())
1260            }
1261            _ => Err(GuiError::NotFound),
1262        }
1263    }
1264
1265    pub fn add_textarea<S>(
1266        &mut self,
1267        rect: Rect,
1268        text: &'a str,
1269        placeholder: &'a str,
1270        style: S,
1271    ) -> Result<WidgetId, GuiError>
1272    where
1273        S: Into<WidgetStyle>,
1274    {
1275        let cursor = text.chars().count();
1276        let (text_buf, text_len) = textarea_storage_from_str(text);
1277        let id = self.add_widget(
1278            rect,
1279            WidgetKind::TextArea {
1280                text_buf,
1281                text_len,
1282                cursor,
1283                placeholder,
1284                selection: None,
1285                cursor_visible: true,
1286                read_only: false,
1287                single_line: false,
1288                accept_newline: true,
1289            },
1290            style,
1291        )?;
1292        self.ensure_focus();
1293        Ok(id)
1294    }
1295
1296    pub fn add_keyboard<S>(
1297        &mut self,
1298        rect: Rect,
1299        keys: &'a [char],
1300        cols: u8,
1301        target: Option<WidgetId>,
1302        style: S,
1303    ) -> Result<WidgetId, GuiError>
1304    where
1305        S: Into<WidgetStyle>,
1306    {
1307        self.add_keyboard_with_alt(rect, keys, None, cols, target, style)
1308    }
1309
1310    pub fn add_keyboard_with_alt<S>(
1311        &mut self,
1312        rect: Rect,
1313        keys: &'a [char],
1314        alt_keys: Option<&'a [char]>,
1315        cols: u8,
1316        target: Option<WidgetId>,
1317        style: S,
1318    ) -> Result<WidgetId, GuiError>
1319    where
1320        S: Into<WidgetStyle>,
1321    {
1322        let id = self.add_widget(
1323            rect,
1324            WidgetKind::Keyboard {
1325                keys,
1326                selected: 0,
1327                cols: cols.max(1),
1328                alt_keys,
1329                layout: KeyboardLayout::Normal,
1330                target,
1331            },
1332            style,
1333        )?;
1334        self.ensure_focus();
1335        Ok(id)
1336    }
1337
1338    pub fn add_image<S>(
1339        &mut self,
1340        rect: Rect,
1341        image: ImageRef<'a>,
1342        fit: ImageFit,
1343        style: S,
1344    ) -> Result<WidgetId, GuiError>
1345    where
1346        S: Into<WidgetStyle>,
1347    {
1348        self.add_widget(rect, WidgetKind::Image { image, fit }, style)
1349    }
1350
1351    pub fn add_peek_reveal<S>(
1352        &mut self,
1353        rect: Rect,
1354        icon: ImageRef<'a>,
1355        title: &'a str,
1356        subtitle: &'a str,
1357        style: S,
1358    ) -> Result<WidgetId, GuiError>
1359    where
1360        S: Into<WidgetStyle>,
1361    {
1362        self.add_widget(
1363            rect,
1364            WidgetKind::PeekReveal {
1365                icon,
1366                title,
1367                subtitle,
1368                progress: 0.0,
1369            },
1370            style,
1371        )
1372    }
1373
1374    pub fn add_glance_tile<S>(
1375        &mut self,
1376        rect: Rect,
1377        icon: char,
1378        title: &'a str,
1379        subtitle: &'a str,
1380        style: S,
1381    ) -> Result<WidgetId, GuiError>
1382    where
1383        S: Into<WidgetStyle>,
1384    {
1385        let id = self.add_widget(
1386            rect,
1387            WidgetKind::GlanceTile {
1388                icon,
1389                title,
1390                subtitle,
1391                highlighted: false,
1392            },
1393            style,
1394        )?;
1395        self.ensure_focus();
1396        Ok(id)
1397    }
1398
1399    pub fn add_card_deck<S>(
1400        &mut self,
1401        rect: Rect,
1402        titles: &'a [&'a str],
1403        selected: usize,
1404        style: S,
1405    ) -> Result<WidgetId, GuiError>
1406    where
1407        S: Into<WidgetStyle>,
1408    {
1409        self.add_widget(
1410            rect,
1411            WidgetKind::CardDeck {
1412                titles,
1413                selected: selected.min(titles.len().saturating_sub(1)),
1414            },
1415            style,
1416        )
1417    }
1418
1419    pub fn add_reel<S>(
1420        &mut self,
1421        rect: Rect,
1422        player: ReelPlayer<'a>,
1423        fit: ImageFit,
1424        style: S,
1425    ) -> Result<WidgetId, GuiError>
1426    where
1427        S: Into<WidgetStyle>,
1428    {
1429        self.add_widget(rect, WidgetKind::Reel { player, fit }, style)
1430    }
1431
1432    pub fn add_state_surface<S>(
1433        &mut self,
1434        rect: Rect,
1435        state: SurfaceState,
1436        title: &'a str,
1437        message: &'a str,
1438        action: Option<&'a str>,
1439        style: S,
1440    ) -> Result<WidgetId, GuiError>
1441    where
1442        S: Into<WidgetStyle>,
1443    {
1444        self.add_widget(
1445            rect,
1446            WidgetKind::StateSurface {
1447                state,
1448                title,
1449                message,
1450                action,
1451                busy_phase: 0.0,
1452            },
1453            style,
1454        )
1455    }
1456
1457    pub fn add_heads_up_banner<S>(
1458        &mut self,
1459        rect: Rect,
1460        level: NotificationLevel,
1461        text: &'a str,
1462        ttl_ms: u32,
1463        style: S,
1464    ) -> Result<WidgetId, GuiError>
1465    where
1466        S: Into<WidgetStyle>,
1467    {
1468        self.add_widget(
1469            rect,
1470            WidgetKind::HeadsUpBanner {
1471                level,
1472                text,
1473                ttl_ms,
1474            },
1475            style,
1476        )
1477    }
1478
1479    #[allow(clippy::too_many_arguments)]
1480    pub fn add_notification_action_sheet<S>(
1481        &mut self,
1482        rect: Rect,
1483        level: NotificationLevel,
1484        title: &'a str,
1485        body: &'a str,
1486        actions: &'a [&'a str],
1487        selected: usize,
1488        open: bool,
1489        style: S,
1490    ) -> Result<WidgetId, GuiError>
1491    where
1492        S: Into<WidgetStyle>,
1493    {
1494        self.add_widget(
1495            rect,
1496            WidgetKind::NotificationActionSheet {
1497                level,
1498                title,
1499                body,
1500                actions,
1501                selected: selected.min(actions.len().saturating_sub(1)),
1502                open,
1503            },
1504            style,
1505        )
1506    }
1507
1508    pub fn add_border<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
1509    where
1510        S: Into<WidgetStyle>,
1511    {
1512        self.add_widget(rect, WidgetKind::Border, style)
1513    }
1514
1515    pub fn add_spacer(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
1516        self.add_widget(rect, WidgetKind::Spacer, Style::default())
1517    }
1518
1519    pub fn add_menu<S>(
1520        &mut self,
1521        rect: Rect,
1522        items: &'a [&'a str],
1523        selected: usize,
1524        style: S,
1525    ) -> Result<WidgetId, GuiError>
1526    where
1527        S: Into<WidgetStyle>,
1528    {
1529        let selected = selected.min(items.len().saturating_sub(1));
1530        let id = self.add_widget(rect, WidgetKind::Menu { items, selected }, style)?;
1531        self.ensure_focus();
1532        Ok(id)
1533    }
1534
1535    pub fn set_progress(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1536        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1537        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1538        match node.kind {
1539            WidgetKind::ProgressBar { value: ref mut v } => {
1540                *v = value.clamp(0.0, 1.0);
1541                self.dirty.add(rect)?;
1542                Ok(())
1543            }
1544            WidgetKind::PeekReveal {
1545                progress: ref mut v,
1546                ..
1547            } => {
1548                *v = value.clamp(0.0, 1.0);
1549                self.dirty.add(rect)?;
1550                Ok(())
1551            }
1552            WidgetKind::SweepingArc {
1553                progress: ref mut v,
1554                ..
1555            } => {
1556                *v = value.clamp(0.0, 1.0);
1557                self.dirty.add(rect)?;
1558                Ok(())
1559            }
1560            _ => Err(GuiError::NotFound),
1561        }
1562    }
1563
1564    pub fn set_glance_highlighted(
1565        &mut self,
1566        id: WidgetId,
1567        highlighted: bool,
1568    ) -> Result<(), GuiError> {
1569        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1570        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1571        match node.kind {
1572            WidgetKind::GlanceTile {
1573                highlighted: ref mut h,
1574                ..
1575            } => {
1576                *h = highlighted;
1577                self.dirty.add(rect)?;
1578                Ok(())
1579            }
1580            _ => Err(GuiError::NotFound),
1581        }
1582    }
1583
1584    pub fn set_card_deck_selected(
1585        &mut self,
1586        id: WidgetId,
1587        selected: usize,
1588    ) -> Result<(), GuiError> {
1589        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1590        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1591        match node.kind {
1592            WidgetKind::CardDeck {
1593                titles,
1594                selected: ref mut current,
1595            } => {
1596                *current = selected.min(titles.len().saturating_sub(1));
1597                self.dirty.add(rect)?;
1598                Ok(())
1599            }
1600            _ => Err(GuiError::NotFound),
1601        }
1602    }
1603
1604    pub fn tick_reel(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1605        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1606        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1607        match node.kind {
1608            WidgetKind::Reel {
1609                player: ref mut reel,
1610                ..
1611            } => {
1612                reel.tick(dt_ms);
1613                self.dirty.add(rect)?;
1614                Ok(())
1615            }
1616            _ => Err(GuiError::NotFound),
1617        }
1618    }
1619
1620    pub fn set_state_surface_state(
1621        &mut self,
1622        id: WidgetId,
1623        state: SurfaceState,
1624    ) -> Result<(), GuiError> {
1625        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1626        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1627        match node.kind {
1628            WidgetKind::StateSurface {
1629                state: ref mut current,
1630                ..
1631            } => {
1632                *current = state;
1633                self.dirty.add(rect)?;
1634                Ok(())
1635            }
1636            _ => Err(GuiError::NotFound),
1637        }
1638    }
1639
1640    pub fn set_state_surface_message(
1641        &mut self,
1642        id: WidgetId,
1643        message: &'a str,
1644    ) -> Result<(), GuiError> {
1645        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1646        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1647        match node.kind {
1648            WidgetKind::StateSurface {
1649                message: ref mut current,
1650                ..
1651            } => {
1652                *current = message;
1653                self.dirty.add(rect)?;
1654                Ok(())
1655            }
1656            _ => Err(GuiError::NotFound),
1657        }
1658    }
1659
1660    pub fn set_state_surface_action(
1661        &mut self,
1662        id: WidgetId,
1663        action: Option<&'a str>,
1664    ) -> Result<(), GuiError> {
1665        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1666        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1667        match node.kind {
1668            WidgetKind::StateSurface {
1669                action: ref mut current,
1670                ..
1671            } => {
1672                *current = action;
1673                self.dirty.add(rect)?;
1674                Ok(())
1675            }
1676            _ => Err(GuiError::NotFound),
1677        }
1678    }
1679
1680    pub fn set_state_surface_busy_phase(
1681        &mut self,
1682        id: WidgetId,
1683        phase: f32,
1684    ) -> Result<(), GuiError> {
1685        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1686        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1687        match node.kind {
1688            WidgetKind::StateSurface {
1689                busy_phase: ref mut current,
1690                ..
1691            } => {
1692                *current = phase;
1693                self.dirty.add(rect)?;
1694                Ok(())
1695            }
1696            _ => Err(GuiError::NotFound),
1697        }
1698    }
1699
1700    pub fn tick_state_surface(
1701        &mut self,
1702        id: WidgetId,
1703        dt_ms: u32,
1704        cycles_per_sec: f32,
1705    ) -> Result<(), GuiError> {
1706        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1707            WidgetKind::StateSurface { busy_phase, .. } => {
1708                busy_phase + (dt_ms as f32 / 1000.0) * cycles_per_sec
1709            }
1710            _ => return Err(GuiError::NotFound),
1711        };
1712        self.set_state_surface_busy_phase(id, phase)
1713    }
1714
1715    pub fn set_heads_up_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
1716        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1717        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1718        match node.kind {
1719            WidgetKind::HeadsUpBanner {
1720                ttl_ms: ref mut current,
1721                ..
1722            } => {
1723                *current = ttl_ms;
1724                self.dirty.add(rect)?;
1725                Ok(())
1726            }
1727            _ => Err(GuiError::NotFound),
1728        }
1729    }
1730
1731    pub fn tick_heads_up(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1732        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1733            WidgetKind::HeadsUpBanner { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
1734            _ => return Err(GuiError::NotFound),
1735        };
1736        self.set_heads_up_ttl(id, ttl)
1737    }
1738
1739    pub fn set_notification_sheet_open(
1740        &mut self,
1741        id: WidgetId,
1742        open: bool,
1743    ) -> Result<(), GuiError> {
1744        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1745        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1746        match node.kind {
1747            WidgetKind::NotificationActionSheet {
1748                open: ref mut current,
1749                ..
1750            } => {
1751                *current = open;
1752                self.dirty.add(rect)?;
1753                Ok(())
1754            }
1755            _ => Err(GuiError::NotFound),
1756        }
1757    }
1758
1759    pub fn set_notification_sheet_selected(
1760        &mut self,
1761        id: WidgetId,
1762        selected: usize,
1763    ) -> Result<(), GuiError> {
1764        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1765        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1766        match node.kind {
1767            WidgetKind::NotificationActionSheet {
1768                actions,
1769                selected: ref mut current,
1770                ..
1771            } => {
1772                *current = selected.min(actions.len().saturating_sub(1));
1773                self.dirty.add(rect)?;
1774                Ok(())
1775            }
1776            _ => Err(GuiError::NotFound),
1777        }
1778    }
1779
1780    pub fn set_menu_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1781        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1782        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1783        match node.kind {
1784            WidgetKind::Menu {
1785                items,
1786                selected: ref mut current,
1787            } => {
1788                *current = selected.min(items.len().saturating_sub(1));
1789                self.dirty.add(rect)?;
1790                Ok(())
1791            }
1792            _ => Err(GuiError::NotFound),
1793        }
1794    }
1795
1796    pub fn menu_selected(&self, id: WidgetId) -> Option<usize> {
1797        match self.node(id)?.kind {
1798            WidgetKind::Menu { selected, .. } => Some(selected),
1799            _ => None,
1800        }
1801    }
1802
1803    pub fn list_selected(&self, id: WidgetId) -> Option<usize> {
1804        match self.node(id)?.kind {
1805            WidgetKind::List { selected, .. } => Some(selected),
1806            _ => None,
1807        }
1808    }
1809
1810    pub fn set_list_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1811        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1812        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1813        match node.kind {
1814            WidgetKind::List {
1815                items,
1816                selected: ref mut current,
1817                ref mut offset,
1818                visible_rows,
1819            } => {
1820                let mut state = ListState::new(*current, *offset, visible_rows);
1821                state.set_selected(selected, items.len());
1822                *current = state.selected;
1823                *offset = state.offset;
1824                self.dirty.add(rect)?;
1825                Ok(())
1826            }
1827            _ => Err(GuiError::NotFound),
1828        }
1829    }
1830
1831    pub fn feed_selected(&self, id: WidgetId) -> Option<usize> {
1832        match self.node(id)?.kind {
1833            WidgetKind::FeedTimeline { selected, .. } => Some(selected),
1834            _ => None,
1835        }
1836    }
1837
1838    pub fn set_feed_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1839        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1840        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1841        match node.kind {
1842            WidgetKind::FeedTimeline {
1843                items,
1844                selected: ref mut current,
1845                ref mut offset,
1846                visible_rows,
1847                ..
1848            } => {
1849                let mut state = FeedTimelineState::new(*current, *offset, visible_rows, false);
1850                state.set_selected(selected, items.len());
1851                *current = state.selected;
1852                *offset = state.offset;
1853                self.dirty.add(rect)?;
1854                Ok(())
1855            }
1856            _ => Err(GuiError::NotFound),
1857        }
1858    }
1859
1860    pub fn set_feed_expanded(&mut self, id: WidgetId, expanded: bool) -> Result<(), GuiError> {
1861        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1862        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1863        match node.kind {
1864            WidgetKind::FeedTimeline {
1865                expanded: ref mut current,
1866                ..
1867            } => {
1868                *current = expanded;
1869                self.dirty.add(rect)?;
1870                Ok(())
1871            }
1872            _ => Err(GuiError::NotFound),
1873        }
1874    }
1875
1876    pub fn set_toggle(&mut self, id: WidgetId, on: bool) -> Result<(), GuiError> {
1877        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1878        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1879        match node.kind {
1880            WidgetKind::Toggle { on: ref mut v, .. } => {
1881                *v = on;
1882                self.dirty.add(rect)?;
1883                Ok(())
1884            }
1885            _ => Err(GuiError::NotFound),
1886        }
1887    }
1888
1889    pub fn toggle_value(&self, id: WidgetId) -> Option<bool> {
1890        match self.node(id)?.kind {
1891            WidgetKind::Toggle { on, .. } => Some(on),
1892            _ => None,
1893        }
1894    }
1895
1896    pub fn set_checked(&mut self, id: WidgetId, checked: bool) -> Result<(), GuiError> {
1897        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1898        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1899        match node.kind {
1900            WidgetKind::Checkbox {
1901                checked: ref mut v, ..
1902            } => {
1903                *v = checked;
1904                self.dirty.add(rect)?;
1905                Ok(())
1906            }
1907            _ => Err(GuiError::NotFound),
1908        }
1909    }
1910
1911    pub fn checked_value(&self, id: WidgetId) -> Option<bool> {
1912        match self.node(id)?.kind {
1913            WidgetKind::Checkbox { checked, .. } => Some(checked),
1914            _ => None,
1915        }
1916    }
1917
1918    pub fn set_slider_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1919        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1920        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1921        match node.kind {
1922            WidgetKind::Slider {
1923                value: ref mut v,
1924                min,
1925                max,
1926            } => {
1927                let mut state = SliderState::new(*v, min, max);
1928                state.set_value(value);
1929                *v = state.value;
1930                self.dirty.add(rect)?;
1931                Ok(())
1932            }
1933            _ => Err(GuiError::NotFound),
1934        }
1935    }
1936
1937    pub fn slider_value(&self, id: WidgetId) -> Option<f32> {
1938        match self.node(id)?.kind {
1939            WidgetKind::Slider { value, .. } => Some(value),
1940            _ => None,
1941        }
1942    }
1943
1944    pub fn set_value_label(&mut self, id: WidgetId, value: i32) -> 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::ValueLabel {
1949                value: ref mut v, ..
1950            } => {
1951                *v = value;
1952                self.dirty.add(rect)?;
1953                Ok(())
1954            }
1955            _ => Err(GuiError::NotFound),
1956        }
1957    }
1958
1959    pub fn set_scroll_offset(&mut self, id: WidgetId, offset_y: i32) -> Result<(), GuiError> {
1960        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1961        match node.kind {
1962            WidgetKind::ScrollView {
1963                offset_y: ref mut v,
1964                content_h,
1965            } => {
1966                let mut state = ScrollState::new(*v, content_h);
1967                state.set_offset(offset_y);
1968                *v = state.offset_y;
1969                self.mark_subtree_dirty(id)?;
1970                Ok(())
1971            }
1972            _ => Err(GuiError::NotFound),
1973        }
1974    }
1975
1976    pub fn scroll_offset(&self, id: WidgetId) -> Option<i32> {
1977        match self.node(id)?.kind {
1978            WidgetKind::ScrollView { offset_y, .. } => Some(offset_y),
1979            _ => None,
1980        }
1981    }
1982
1983    pub fn set_tab_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1984        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1985        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1986        match node.kind {
1987            WidgetKind::Tabs {
1988                labels,
1989                selected: ref mut v,
1990            } => {
1991                let mut state = TabsState::new(*v);
1992                state.set_selected(selected, labels.len());
1993                *v = state.selected;
1994                self.dirty.add(rect)?;
1995                Ok(())
1996            }
1997            _ => Err(GuiError::NotFound),
1998        }
1999    }
2000
2001    pub fn tab_selected(&self, id: WidgetId) -> Option<usize> {
2002        match self.node(id)?.kind {
2003            WidgetKind::Tabs { selected, .. } => Some(selected),
2004            _ => None,
2005        }
2006    }
2007
2008    pub fn set_toast_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
2009        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2010        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2011        match node.kind {
2012            WidgetKind::Toast {
2013                ttl_ms: ref mut v, ..
2014            } => {
2015                *v = ttl_ms;
2016                self.dirty.add(rect)?;
2017                Ok(())
2018            }
2019            _ => Err(GuiError::NotFound),
2020        }
2021    }
2022
2023    pub fn tick_toast(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
2024        let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
2025            WidgetKind::Toast { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
2026            _ => return Err(GuiError::NotFound),
2027        };
2028        self.set_toast_ttl(id, ttl)
2029    }
2030
2031    pub fn set_meter_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
2032        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2033        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2034        match node.kind {
2035            WidgetKind::Meter {
2036                value: ref mut v,
2037                min,
2038                max,
2039            } => {
2040                *v = value.clamp(min.min(max), min.max(max));
2041                self.dirty.add(rect)?;
2042                Ok(())
2043            }
2044            _ => Err(GuiError::NotFound),
2045        }
2046    }
2047
2048    pub fn set_spinner_phase(&mut self, id: WidgetId, phase: f32) -> Result<(), GuiError> {
2049        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2050        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2051        match node.kind {
2052            WidgetKind::Spinner { phase: ref mut v } => {
2053                *v = phase;
2054                self.dirty.add(rect)?;
2055                Ok(())
2056            }
2057            _ => Err(GuiError::NotFound),
2058        }
2059    }
2060
2061    pub fn tick_spinner(
2062        &mut self,
2063        id: WidgetId,
2064        dt_ms: u32,
2065        cycles_per_sec: f32,
2066    ) -> Result<(), GuiError> {
2067        let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
2068            WidgetKind::Spinner { phase } => phase + (dt_ms as f32 / 1000.0) * cycles_per_sec,
2069            _ => return Err(GuiError::NotFound),
2070        };
2071        self.set_spinner_phase(id, phase)
2072    }
2073
2074    pub fn set_dropdown_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2075        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2076        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2077        match node.kind {
2078            WidgetKind::Dropdown {
2079                items,
2080                selected: ref mut current,
2081                ..
2082            } => {
2083                *current = selected.min(items.len().saturating_sub(1));
2084                self.dirty.add(rect)?;
2085                Ok(())
2086            }
2087            _ => Err(GuiError::NotFound),
2088        }
2089    }
2090
2091    pub fn dropdown_selected(&self, id: WidgetId) -> Option<usize> {
2092        match self.node(id)?.kind {
2093            WidgetKind::Dropdown { selected, .. } => Some(selected),
2094            _ => None,
2095        }
2096    }
2097
2098    pub fn set_dropdown_open(&mut self, id: WidgetId, open: bool) -> Result<(), GuiError> {
2099        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2100        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2101        match node.kind {
2102            WidgetKind::Dropdown {
2103                open: ref mut is_open,
2104                ..
2105            } => {
2106                if *is_open != open {
2107                    *is_open = open;
2108                    self.dirty.add(rect)?;
2109                    self.push_event(if open {
2110                        UiEvent::Opened(id)
2111                    } else {
2112                        UiEvent::Closed(id)
2113                    })?;
2114                }
2115                Ok(())
2116            }
2117            _ => Err(GuiError::NotFound),
2118        }
2119    }
2120
2121    pub fn dropdown_open(&self, id: WidgetId) -> Option<bool> {
2122        match self.node(id)?.kind {
2123            WidgetKind::Dropdown { open, .. } => Some(open),
2124            _ => None,
2125        }
2126    }
2127
2128    pub fn set_roller_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2129        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2130        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2131        match node.kind {
2132            WidgetKind::Roller {
2133                items,
2134                selected: ref mut current,
2135            } => {
2136                *current = selected.min(items.len().saturating_sub(1));
2137                self.dirty.add(rect)?;
2138                Ok(())
2139            }
2140            _ => Err(GuiError::NotFound),
2141        }
2142    }
2143
2144    pub fn roller_selected(&self, id: WidgetId) -> Option<usize> {
2145        match self.node(id)?.kind {
2146            WidgetKind::Roller { selected, .. } => Some(selected),
2147            _ => None,
2148        }
2149    }
2150
2151    pub fn set_textarea_text(&mut self, id: WidgetId, text: &'a str) -> Result<(), GuiError> {
2152        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2153        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2154        match node.kind {
2155            WidgetKind::TextArea {
2156                text_buf: ref mut buf,
2157                text_len: ref mut len,
2158                cursor: ref mut c,
2159                ..
2160            } => {
2161                let (next_buf, next_len) = textarea_storage_from_str(text);
2162                *buf = next_buf;
2163                *len = next_len;
2164                *c = (*c).min(textarea_text(buf, *len).chars().count());
2165                self.dirty.add(rect)?;
2166                Ok(())
2167            }
2168            _ => Err(GuiError::NotFound),
2169        }
2170    }
2171
2172    pub fn textarea_text(&self, id: WidgetId) -> Option<&str> {
2173        match &self.node(id)?.kind {
2174            WidgetKind::TextArea {
2175                text_buf, text_len, ..
2176            } => Some(textarea_text(text_buf, *text_len)),
2177            _ => None,
2178        }
2179    }
2180
2181    pub fn set_textarea_cursor(&mut self, id: WidgetId, cursor: usize) -> Result<(), GuiError> {
2182        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2183        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2184        match node.kind {
2185            WidgetKind::TextArea {
2186                text_buf,
2187                text_len,
2188                cursor: ref mut current,
2189                ..
2190            } => {
2191                let text = textarea_text(&text_buf, text_len);
2192                *current = cursor.min(text.chars().count());
2193                self.dirty.add(rect)?;
2194                Ok(())
2195            }
2196            _ => Err(GuiError::NotFound),
2197        }
2198    }
2199
2200    pub fn move_textarea_cursor(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2201        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2202        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, false)
2203    }
2204
2205    pub fn move_textarea_cursor_select(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2206        let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2207        self.set_textarea_cursor_with_extend(id, next.max(0) as usize, true)
2208    }
2209
2210    pub fn move_textarea_cursor_word(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2211        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2212            WidgetKind::TextArea {
2213                text_buf,
2214                text_len,
2215                cursor,
2216                ..
2217            } => (textarea_text(text_buf, *text_len), *cursor),
2218            _ => return Err(GuiError::NotFound),
2219        };
2220        let next = if delta >= 0 {
2221            next_word_boundary(text, cursor)
2222        } else {
2223            prev_word_boundary(text, cursor)
2224        };
2225        self.set_textarea_cursor_with_extend(id, next, false)
2226    }
2227
2228    pub fn move_textarea_cursor_word_select(
2229        &mut self,
2230        id: WidgetId,
2231        delta: i8,
2232    ) -> Result<(), GuiError> {
2233        let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2234            WidgetKind::TextArea {
2235                text_buf,
2236                text_len,
2237                cursor,
2238                ..
2239            } => (textarea_text(text_buf, *text_len), *cursor),
2240            _ => return Err(GuiError::NotFound),
2241        };
2242        let next = if delta >= 0 {
2243            next_word_boundary(text, cursor)
2244        } else {
2245            prev_word_boundary(text, cursor)
2246        };
2247        self.set_textarea_cursor_with_extend(id, next, true)
2248    }
2249
2250    pub fn set_textarea_cursor_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2251        self.set_textarea_cursor(id, 0)
2252    }
2253
2254    pub fn set_textarea_cursor_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2255        let len = self
2256            .textarea_text(id)
2257            .map(|text| text.chars().count())
2258            .ok_or(GuiError::NotFound)?;
2259        self.set_textarea_cursor(id, len)
2260    }
2261
2262    pub fn set_textarea_cursor_line_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2263        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2264        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2265        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2266        self.set_textarea_cursor_with_extend(id, next, false)
2267    }
2268
2269    pub fn set_textarea_cursor_line_home_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2270        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2271        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2272        let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2273        self.set_textarea_cursor_with_extend(id, next, true)
2274    }
2275
2276    pub fn set_textarea_cursor_line_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2277        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2278        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2279        let row_end = textarea_row_end_col(text, row, wrap_cols);
2280        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2281        self.set_textarea_cursor_with_extend(id, next, false)
2282    }
2283
2284    pub fn set_textarea_cursor_line_end_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2285        let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2286        let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2287        let row_end = textarea_row_end_col(text, row, wrap_cols);
2288        let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2289        self.set_textarea_cursor_with_extend(id, next, true)
2290    }
2291
2292    pub fn textarea_cursor(&self, id: WidgetId) -> Option<usize> {
2293        match self.node(id)?.kind {
2294            WidgetKind::TextArea { cursor, .. } => Some(cursor),
2295            _ => None,
2296        }
2297    }
2298
2299    pub fn set_textarea_selection(
2300        &mut self,
2301        id: WidgetId,
2302        start: usize,
2303        end: usize,
2304    ) -> Result<(), GuiError> {
2305        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2306        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2307        match node.kind {
2308            WidgetKind::TextArea {
2309                text_buf,
2310                text_len,
2311                selection: ref mut current,
2312                ..
2313            } => {
2314                let text = textarea_text(&text_buf, text_len);
2315                let len = text.chars().count();
2316                let start = start.min(len);
2317                let end = end.min(len);
2318                *current = Some((start.min(end), start.max(end)));
2319                self.dirty.add(rect)?;
2320                Ok(())
2321            }
2322            _ => Err(GuiError::NotFound),
2323        }
2324    }
2325
2326    pub fn clear_textarea_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
2327        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2328        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2329        match node.kind {
2330            WidgetKind::TextArea {
2331                selection: ref mut current,
2332                ..
2333            } => {
2334                *current = None;
2335                self.dirty.add(rect)?;
2336                Ok(())
2337            }
2338            _ => Err(GuiError::NotFound),
2339        }
2340    }
2341
2342    pub fn textarea_selection(&self, id: WidgetId) -> Option<(usize, usize)> {
2343        match self.node(id)?.kind {
2344            WidgetKind::TextArea { selection, .. } => selection,
2345            _ => None,
2346        }
2347    }
2348
2349    pub fn textarea_cursor_visible(&self, id: WidgetId) -> Option<bool> {
2350        match self.node(id)?.kind {
2351            WidgetKind::TextArea { cursor_visible, .. } => Some(cursor_visible),
2352            _ => None,
2353        }
2354    }
2355
2356    pub fn set_textarea_capabilities(
2357        &mut self,
2358        id: WidgetId,
2359        read_only: bool,
2360        single_line: bool,
2361        accept_newline: bool,
2362    ) -> Result<(), GuiError> {
2363        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2364        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2365        match node.kind {
2366            WidgetKind::TextArea {
2367                read_only: ref mut ro,
2368                single_line: ref mut sl,
2369                accept_newline: ref mut an,
2370                ..
2371            } => {
2372                *ro = read_only;
2373                *sl = single_line;
2374                *an = accept_newline && !single_line;
2375                self.dirty.add(rect)?;
2376                Ok(())
2377            }
2378            _ => Err(GuiError::NotFound),
2379        }
2380    }
2381
2382    pub fn textarea_insert_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
2383        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2384        let before = self.capture_textarea_snapshot(id)?;
2385        let mut emit = false;
2386        if let Some(node) = self.node_mut(id) {
2387            if let WidgetKind::TextArea {
2388                text_buf,
2389                text_len,
2390                cursor,
2391                selection,
2392                read_only,
2393                single_line,
2394                accept_newline,
2395                ..
2396            } = &mut node.kind
2397            {
2398                if *read_only {
2399                    return Ok(());
2400                }
2401                if ch == '\n' && (*single_line || !*accept_newline) {
2402                    return Ok(());
2403                }
2404                let mut chars: heapless::Vec<char, TEXTAREA_CAPACITY> = heapless::Vec::new();
2405                for c in textarea_text(text_buf, *text_len).chars() {
2406                    let _ = chars.push(c);
2407                }
2408                let original_len = chars.len();
2409                let original_cursor = *cursor;
2410
2411                if ch == '\u{8}' {
2412                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2413                    if !removed_selection && *cursor > 0 && *cursor <= chars.len() {
2414                        chars.remove(*cursor - 1);
2415                        *cursor -= 1;
2416                    }
2417                    if removed_selection
2418                        || *cursor != original_cursor
2419                        || chars.len() != original_len
2420                    {
2421                        *selection = None;
2422                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2423                        *text_buf = next_buf;
2424                        *text_len = next_len;
2425                        emit = true;
2426                    }
2427                } else if ch == '\u{7f}' {
2428                    let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2429                    if !removed_selection && *cursor < chars.len() {
2430                        chars.remove(*cursor);
2431                    }
2432                    if removed_selection || chars.len() != original_len {
2433                        *selection = None;
2434                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2435                        *text_buf = next_buf;
2436                        *text_len = next_len;
2437                        emit = true;
2438                    }
2439                } else if ch != '\n' || *cursor < TEXTAREA_CAPACITY {
2440                    if delete_selection_if_any(&mut chars, cursor, selection) {
2441                        *selection = None;
2442                    }
2443                    if chars.len() < TEXTAREA_CAPACITY && *cursor <= chars.len() {
2444                        let _ = chars.insert(*cursor, ch);
2445                        *cursor += 1;
2446                        *selection = None;
2447                        let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2448                        *text_buf = next_buf;
2449                        *text_len = next_len;
2450                        emit = true;
2451                    }
2452                }
2453            } else {
2454                return Err(GuiError::NotFound);
2455            }
2456        }
2457        if emit {
2458            self.push_textarea_undo(id, before);
2459            self.clear_textarea_redo_for(id);
2460            self.dirty.add(rect)?;
2461            self.push_event(UiEvent::TextInput { id, ch })?;
2462            self.push_event(UiEvent::ValueChanged(id))?;
2463        }
2464        Ok(())
2465    }
2466
2467    fn textarea_line_context(&self, id: WidgetId) -> Result<(&str, usize, usize), GuiError> {
2468        let node = self.node(id).ok_or(GuiError::NotFound)?;
2469        match &node.kind {
2470            WidgetKind::TextArea {
2471                text_buf,
2472                text_len,
2473                cursor,
2474                ..
2475            } => {
2476                let font = node.style.normal.font;
2477                let inner_w = node.rect.w.saturating_sub(2);
2478                let cols = (inner_w / font.advance()).max(1) as usize;
2479                Ok((textarea_text(text_buf, *text_len), *cursor, cols))
2480            }
2481            _ => Err(GuiError::NotFound),
2482        }
2483    }
2484
2485    fn set_textarea_cursor_with_extend(
2486        &mut self,
2487        id: WidgetId,
2488        cursor: usize,
2489        extend_selection: bool,
2490    ) -> Result<(), GuiError> {
2491        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2492        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2493        match node.kind {
2494            WidgetKind::TextArea {
2495                text_buf,
2496                text_len,
2497                cursor: ref mut current_cursor,
2498                ref mut selection,
2499                ..
2500            } => {
2501                let len = textarea_text(&text_buf, text_len).chars().count();
2502                let next = cursor.min(len);
2503                if extend_selection {
2504                    let anchor = match *selection {
2505                        Some((start, end)) => {
2506                            if *current_cursor == start {
2507                                end
2508                            } else {
2509                                start
2510                            }
2511                        }
2512                        None => *current_cursor,
2513                    };
2514                    if anchor == next {
2515                        *selection = None;
2516                    } else {
2517                        *selection = Some((anchor.min(next), anchor.max(next)));
2518                    }
2519                } else {
2520                    *selection = None;
2521                }
2522                *current_cursor = next;
2523                self.dirty.add(rect)?;
2524                Ok(())
2525            }
2526            _ => Err(GuiError::NotFound),
2527        }
2528    }
2529
2530    fn capture_textarea_snapshot(&self, id: WidgetId) -> Result<TextareaSnapshot, GuiError> {
2531        match self.node(id).ok_or(GuiError::NotFound)?.kind {
2532            WidgetKind::TextArea {
2533                text_buf,
2534                text_len,
2535                cursor,
2536                selection,
2537                ..
2538            } => Ok(TextareaSnapshot {
2539                text_buf,
2540                text_len,
2541                cursor,
2542                selection,
2543            }),
2544            _ => Err(GuiError::NotFound),
2545        }
2546    }
2547
2548    fn apply_textarea_snapshot(
2549        &mut self,
2550        id: WidgetId,
2551        snap: TextareaSnapshot,
2552    ) -> Result<(), GuiError> {
2553        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2554        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2555        match node.kind {
2556            WidgetKind::TextArea {
2557                text_buf: ref mut buf,
2558                text_len: ref mut len,
2559                cursor: ref mut c,
2560                selection: ref mut sel,
2561                ..
2562            } => {
2563                *buf = snap.text_buf;
2564                *len = snap.text_len;
2565                *c = snap.cursor;
2566                *sel = snap.selection;
2567                self.dirty.add(rect)?;
2568                self.push_event(UiEvent::ValueChanged(id))
2569            }
2570            _ => Err(GuiError::NotFound),
2571        }
2572    }
2573
2574    fn push_textarea_undo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2575        if self.textarea_undo.len() == self.textarea_undo.capacity() {
2576            self.textarea_undo.remove(0);
2577        }
2578        let _ = self
2579            .textarea_undo
2580            .push(TextareaHistoryEntry { id, snapshot });
2581    }
2582
2583    fn push_textarea_redo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2584        if self.textarea_redo.len() == self.textarea_redo.capacity() {
2585            self.textarea_redo.remove(0);
2586        }
2587        let _ = self
2588            .textarea_redo
2589            .push(TextareaHistoryEntry { id, snapshot });
2590    }
2591
2592    fn clear_textarea_redo_for(&mut self, id: WidgetId) {
2593        let mut i = 0usize;
2594        while i < self.textarea_redo.len() {
2595            if self.textarea_redo[i].id == id {
2596                self.textarea_redo.remove(i);
2597            } else {
2598                i += 1;
2599            }
2600        }
2601    }
2602
2603    fn textarea_undo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2604        let Some(pos) = self.textarea_undo.iter().rposition(|entry| entry.id == id) else {
2605            return Ok(());
2606        };
2607        let current = self.capture_textarea_snapshot(id)?;
2608        let prior = self.textarea_undo.remove(pos).snapshot;
2609        self.push_textarea_redo(id, current);
2610        self.apply_textarea_snapshot(id, prior)
2611    }
2612
2613    fn textarea_redo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2614        let Some(pos) = self.textarea_redo.iter().rposition(|entry| entry.id == id) else {
2615            return Ok(());
2616        };
2617        let current = self.capture_textarea_snapshot(id)?;
2618        let next = self.textarea_redo.remove(pos).snapshot;
2619        self.push_textarea_undo(id, current);
2620        self.apply_textarea_snapshot(id, next)
2621    }
2622
2623    pub fn textarea_backspace(&mut self, id: WidgetId) -> Result<(), GuiError> {
2624        self.textarea_insert_char(id, '\u{8}')
2625    }
2626
2627    pub fn textarea_delete_forward(&mut self, id: WidgetId) -> Result<(), GuiError> {
2628        self.textarea_insert_char(id, '\u{7f}')
2629    }
2630
2631    pub fn keyboard_selected_key(&self, id: WidgetId) -> Option<char> {
2632        match self.node(id)?.kind {
2633            WidgetKind::Keyboard {
2634                keys,
2635                alt_keys,
2636                selected,
2637                layout,
2638                ..
2639            } => keyboard_char_for_layout(keys, alt_keys, selected, layout),
2640            _ => None,
2641        }
2642    }
2643
2644    pub fn keyboard_layout(&self, id: WidgetId) -> Option<KeyboardLayout> {
2645        match self.node(id)?.kind {
2646            WidgetKind::Keyboard { layout, .. } => Some(layout),
2647            _ => None,
2648        }
2649    }
2650
2651    pub fn set_keyboard_layout(
2652        &mut self,
2653        id: WidgetId,
2654        layout: KeyboardLayout,
2655    ) -> Result<(), GuiError> {
2656        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2657        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2658        match node.kind {
2659            WidgetKind::Keyboard {
2660                layout: ref mut current,
2661                ..
2662            } => {
2663                *current = layout;
2664                self.dirty.add(rect)?;
2665                Ok(())
2666            }
2667            _ => Err(GuiError::NotFound),
2668        }
2669    }
2670
2671    pub fn set_keyboard_target(
2672        &mut self,
2673        id: WidgetId,
2674        target: Option<WidgetId>,
2675    ) -> Result<(), GuiError> {
2676        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2677        match node.kind {
2678            WidgetKind::Keyboard {
2679                target: ref mut current,
2680                ..
2681            } => {
2682                *current = target;
2683                Ok(())
2684            }
2685            _ => Err(GuiError::NotFound),
2686        }
2687    }
2688
2689    pub fn set_gauge_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
2690        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2691        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2692        match node.kind {
2693            WidgetKind::Gauge {
2694                value: ref mut v,
2695                min,
2696                max,
2697                ..
2698            }
2699            | WidgetKind::ArcGauge {
2700                value: ref mut v,
2701                min,
2702                max,
2703                ..
2704            }
2705            | WidgetKind::GaugeNeedle {
2706                value: ref mut v,
2707                min,
2708                max,
2709                ..
2710            } => {
2711                *v = value.clamp(min.min(max), min.max(max));
2712                self.dirty.add(rect)?;
2713                Ok(())
2714            }
2715            _ => Err(GuiError::NotFound),
2716        }
2717    }
2718
2719    pub fn set_gauge_ticks(
2720        &mut self,
2721        id: WidgetId,
2722        major_ticks: u8,
2723        minor_ticks: u8,
2724        show_value: bool,
2725    ) -> Result<(), GuiError> {
2726        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2727        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2728        match node.kind {
2729            WidgetKind::Gauge {
2730                major_ticks: ref mut major,
2731                minor_ticks: ref mut minor,
2732                show_value: ref mut show,
2733                ..
2734            }
2735            | WidgetKind::ArcGauge {
2736                major_ticks: ref mut major,
2737                minor_ticks: ref mut minor,
2738                show_value: ref mut show,
2739                ..
2740            } => {
2741                *major = major_ticks.max(1);
2742                *minor = minor_ticks.max(1);
2743                *show = show_value;
2744                self.dirty.add(rect)?;
2745                Ok(())
2746            }
2747            _ => Err(GuiError::NotFound),
2748        }
2749    }
2750
2751    pub fn set_widget_rect(&mut self, id: WidgetId, rect: Rect) -> Result<(), GuiError> {
2752        let old = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2753        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2754        node.rect = rect;
2755        self.dirty.add(old)?;
2756        self.mark_subtree_dirty(id)?;
2757        Ok(())
2758    }
2759
2760    pub fn set_widget_x(&mut self, id: WidgetId, x: i32) -> Result<(), GuiError> {
2761        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2762        rect.x = x;
2763        self.set_widget_rect(id, rect)
2764    }
2765
2766    pub fn set_widget_y(&mut self, id: WidgetId, y: i32) -> Result<(), GuiError> {
2767        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2768        rect.y = y;
2769        self.set_widget_rect(id, rect)
2770    }
2771
2772    pub fn set_widget_width(&mut self, id: WidgetId, w: u32) -> Result<(), GuiError> {
2773        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2774        rect.w = w.max(1);
2775        self.set_widget_rect(id, rect)
2776    }
2777
2778    pub fn set_widget_height(&mut self, id: WidgetId, h: u32) -> Result<(), GuiError> {
2779        let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2780        rect.h = h.max(1);
2781        self.set_widget_rect(id, rect)
2782    }
2783
2784    pub fn set_widget_opacity(&mut self, id: WidgetId, opacity: u8) -> Result<(), GuiError> {
2785        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2786        node.style.normal.opacity = opacity;
2787        node.style.focused.opacity = opacity;
2788        node.style.pressed.opacity = opacity;
2789        node.style.disabled.opacity = opacity;
2790        self.mark_subtree_dirty(id)
2791    }
2792
2793    pub fn set_widget_corner_radius(&mut self, id: WidgetId, radius: u8) -> Result<(), GuiError> {
2794        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2795        node.style.normal.corner_radius = radius;
2796        node.style.focused.corner_radius = radius;
2797        node.style.pressed.corner_radius = radius;
2798        node.style.disabled.corner_radius = radius;
2799        self.mark_subtree_dirty(id)
2800    }
2801
2802    pub fn set_widget_accent(&mut self, id: WidgetId, accent: Rgb565) -> Result<(), GuiError> {
2803        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2804        node.style.normal.accent = accent;
2805        node.style.focused.accent = accent;
2806        node.style.pressed.accent = accent;
2807        node.style.disabled.accent = accent;
2808        self.mark_subtree_dirty(id)
2809    }
2810
2811    pub fn set_widget_parent(
2812        &mut self,
2813        id: WidgetId,
2814        parent: Option<WidgetId>,
2815    ) -> Result<(), GuiError> {
2816        if let Some(parent) = parent {
2817            self.node(parent).ok_or(GuiError::NotFound)?;
2818        }
2819        self.node_mut(id).ok_or(GuiError::NotFound)?.parent = parent;
2820        self.mark_subtree_dirty(id)?;
2821        Ok(())
2822    }
2823
2824    pub fn add_child(&mut self, parent: WidgetId, child: WidgetId) -> Result<(), GuiError> {
2825        self.set_widget_parent(child, Some(parent))
2826    }
2827
2828    pub fn children_of(&self, parent: WidgetId) -> impl Iterator<Item = &WidgetNode<'a>> + '_ {
2829        self.widgets
2830            .iter()
2831            .filter(move |node| node.parent == Some(parent))
2832    }
2833
2834    pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
2835        let node = self.node(id)?;
2836        let mut rect = node.rect;
2837        let mut parent = node.parent;
2838        let mut depth = 0;
2839        while let Some(parent_id) = parent {
2840            if depth >= NODES {
2841                return None;
2842            }
2843            let parent_node = self.node(parent_id)?;
2844            rect.x += parent_node.rect.x;
2845            rect.y += parent_node.rect.y;
2846            parent = parent_node.parent;
2847            depth += 1;
2848        }
2849        Some(rect)
2850    }
2851
2852    pub fn set_flag(
2853        &mut self,
2854        id: WidgetId,
2855        flag: WidgetFlags,
2856        enabled: bool,
2857    ) -> Result<(), GuiError> {
2858        let was_set = self.has_flag(id, flag)?;
2859        let before_state = self.current_visual_state(id);
2860        self.mark_subtree_dirty(id)?;
2861        self.node_mut(id)
2862            .ok_or(GuiError::NotFound)?
2863            .flags
2864            .set(flag, enabled);
2865        if flag == WidgetFlags::DISABLED
2866            && enabled
2867            && self.pressed.is_some_and(|pressed| pressed.id == id)
2868        {
2869            self.pressed = None;
2870        }
2871        self.mark_subtree_dirty(id)?;
2872        if self
2873            .focus
2874            .is_some_and(|focus| !self.effective_focusable(focus))
2875        {
2876            self.focus = None;
2877            self.ensure_focus();
2878        }
2879        if flag == WidgetFlags::DISABLED && was_set != enabled {
2880            let after_state = self.current_visual_state(id);
2881            self.start_state_transition(id, before_state, after_state);
2882        }
2883        Ok(())
2884    }
2885
2886    pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
2887        Ok(self
2888            .node(id)
2889            .ok_or(GuiError::NotFound)?
2890            .flags
2891            .contains(flag))
2892    }
2893
2894    pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2895        self.set_flag(id, flag, true)
2896    }
2897
2898    pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2899        self.set_flag(id, flag, false)
2900    }
2901
2902    pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
2903        self.set_flag(id, WidgetFlags::HIDDEN, hidden)
2904    }
2905
2906    pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
2907        self.set_flag(id, WidgetFlags::DISABLED, disabled)
2908    }
2909
2910    pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
2911        self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
2912    }
2913
2914    pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
2915        self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
2916    }
2917
2918    pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
2919        self.set_hidden(id, !visible)
2920    }
2921
2922    pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
2923        self.set_disabled(id, !enabled)
2924    }
2925
2926    pub fn event_path<const M: usize>(
2927        &self,
2928        target: WidgetId,
2929        out: &mut heapless::Vec<EventContext, M>,
2930    ) -> Result<usize, GuiError> {
2931        self.node(target).ok_or(GuiError::NotFound)?;
2932        out.clear();
2933
2934        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2935        let mut current = Some(target);
2936        while let Some(id) = current {
2937            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2938            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2939        }
2940
2941        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2942            out.push(EventContext {
2943                target,
2944                current: id,
2945                phase: EventPhase::Capture,
2946            })
2947            .map_err(|_| GuiError::EventsFull)?;
2948        }
2949
2950        out.push(EventContext {
2951            target,
2952            current: target,
2953            phase: EventPhase::Target,
2954        })
2955        .map_err(|_| GuiError::EventsFull)?;
2956
2957        for id in chain.iter().copied().skip(1) {
2958            out.push(EventContext {
2959                target,
2960                current: id,
2961                phase: EventPhase::Bubble,
2962            })
2963            .map_err(|_| GuiError::EventsFull)?;
2964        }
2965
2966        Ok(out.len())
2967    }
2968
2969    pub fn widget_event_path<const M: usize>(
2970        &self,
2971        target: WidgetId,
2972        kind: WidgetEventKind,
2973        out: &mut heapless::Vec<WidgetEvent, M>,
2974    ) -> Result<usize, GuiError> {
2975        self.node(target).ok_or(GuiError::NotFound)?;
2976        out.clear();
2977
2978        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2979        let mut current = Some(target);
2980        while let Some(id) = current {
2981            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2982            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2983        }
2984
2985        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2986            out.push(WidgetEvent {
2987                target,
2988                current: id,
2989                phase: EventPhase::Capture,
2990                kind,
2991            })
2992            .map_err(|_| GuiError::EventsFull)?;
2993        }
2994
2995        out.push(WidgetEvent {
2996            target,
2997            current: target,
2998            phase: EventPhase::Target,
2999            kind,
3000        })
3001        .map_err(|_| GuiError::EventsFull)?;
3002
3003        if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
3004            for id in chain.iter().copied().skip(1) {
3005                out.push(WidgetEvent {
3006                    target,
3007                    current: id,
3008                    phase: EventPhase::Bubble,
3009                    kind,
3010                })
3011                .map_err(|_| GuiError::EventsFull)?;
3012            }
3013        }
3014
3015        Ok(out.len())
3016    }
3017
3018    pub fn dispatch_widget_event<const M: usize, F>(
3019        &self,
3020        target: WidgetId,
3021        kind: WidgetEventKind,
3022        scratch: &mut heapless::Vec<WidgetEvent, M>,
3023        mut handler: F,
3024    ) -> Result<(), GuiError>
3025    where
3026        F: FnMut(WidgetEvent) -> EventPolicy,
3027    {
3028        self.widget_event_path(target, kind, scratch)?;
3029        for event in scratch.iter().copied() {
3030            let handler_policy = handler(event);
3031            if matches!(handler_policy, EventPolicy::Stop)
3032                || self.stop_due_to_builtin_widget_behavior(event)
3033                || self.stop_due_to_registered_policy(event)
3034            {
3035                break;
3036            }
3037        }
3038        Ok(())
3039    }
3040
3041    pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
3042        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
3043        self.dirty.add(rect)?;
3044        let child_ids: heapless::Vec<WidgetId, NODES> = self
3045            .widgets
3046            .iter()
3047            .filter(|node| node.parent == Some(id))
3048            .map(|node| node.id)
3049            .collect();
3050        for child in child_ids {
3051            self.mark_subtree_dirty(child)?;
3052        }
3053        Ok(())
3054    }
3055
3056    pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
3057        self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
3058        Ok(())
3059    }
3060
3061    pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
3062        self.active_focus_group = group;
3063        if let Some(focus) = self.focus {
3064            let still_valid = self.node(focus).is_some_and(|node| {
3065                group.is_none_or(|active| node.focus_group == active)
3066                    && self.effective_focusable(focus)
3067            });
3068            if !still_valid {
3069                self.focus = None;
3070                self.ensure_focus();
3071            }
3072        }
3073    }
3074
3075    pub fn apply_layout(
3076        &mut self,
3077        layout: LinearLayout,
3078        area: Rect,
3079        ids: &[WidgetId],
3080    ) -> Result<usize, GuiError> {
3081        let mut rects = [Rect::empty(); 16];
3082        let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
3083        for (id, rect) in ids.iter().copied().zip(rects).take(count) {
3084            self.set_widget_rect(id, rect)?;
3085        }
3086        Ok(count)
3087    }
3088
3089    pub fn apply_layout_flex(
3090        &mut self,
3091        layout: LinearLayout,
3092        area: Rect,
3093        ids: &[WidgetId],
3094        items: &[LayoutItem],
3095        enable_grow: bool,
3096        enable_shrink: bool,
3097    ) -> Result<usize, GuiError> {
3098        let mut rects = [Rect::empty(); 16];
3099        let count = ids.len().min(items.len()).min(rects.len());
3100        let laid_out = layout.arrange_items_flex(
3101            area,
3102            &items[..count],
3103            &mut rects,
3104            enable_grow,
3105            enable_shrink,
3106        );
3107        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3108            self.set_widget_rect(id, rect)?;
3109        }
3110        Ok(laid_out)
3111    }
3112
3113    pub fn apply_layout_intrinsic(
3114        &mut self,
3115        layout: LinearLayout,
3116        area: Rect,
3117        ids: &[WidgetId],
3118    ) -> Result<usize, GuiError> {
3119        self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
3120    }
3121
3122    pub fn apply_layout_intrinsic_with_cross(
3123        &mut self,
3124        layout: LinearLayout,
3125        area: Rect,
3126        ids: &[WidgetId],
3127        preserve_cross: bool,
3128    ) -> Result<usize, GuiError> {
3129        let mut specs = [LayoutItem::fill(); 16];
3130        let mut rects = [Rect::empty(); 16];
3131        let count = ids.len().min(specs.len()).min(rects.len());
3132
3133        for (idx, id) in ids.iter().copied().take(count).enumerate() {
3134            let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
3135            specs[idx] = match layout.axis {
3136                Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
3137                    crate::layout::Constraint::Length(h)
3138                } else {
3139                    crate::layout::Constraint::Fill(1)
3140                }),
3141                Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
3142                    crate::layout::Constraint::Length(w)
3143                } else {
3144                    crate::layout::Constraint::Fill(1)
3145                }),
3146            };
3147        }
3148
3149        let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
3150        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3151            self.set_widget_rect(id, rect)?;
3152        }
3153        Ok(laid_out)
3154    }
3155
3156    pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error>
3157    where
3158        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3159    {
3160        let mut ctx = RenderCtx::new(target, self.viewport);
3161        ctx.set_quality(self.render_quality);
3162        self.render_into(&mut ctx, 0, 0, 255)
3163    }
3164
3165    /// Like [`GuiContext::render`], but every widget's translucency is composited
3166    /// with true per-pixel alpha blending instead of ordered dithering. Requires
3167    /// a readback-capable target ([`PixelRead`](crate::PixelRead)), e.g. a
3168    /// [`Framebuffer`](crate::Framebuffer) used as a software back buffer.
3169    pub fn render_composited<D>(&self, target: &mut D) -> Result<(), D::Error>
3170    where
3171        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
3172            + crate::render::PixelRead,
3173    {
3174        let mut ctx = RenderCtx::compositing(target, self.viewport);
3175        ctx.set_quality(self.render_quality);
3176        self.render_into::<D, crate::render::Blend>(&mut ctx, 0, 0, 255)
3177    }
3178
3179    pub fn render_dirty<D>(&self, target: &mut D) -> Result<(), D::Error>
3180    where
3181        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3182    {
3183        if self.dirty.is_empty() {
3184            return Ok(());
3185        }
3186
3187        for dirty in self.dirty.as_slice() {
3188            let mut ctx = RenderCtx::with_dirty(target, self.viewport, *dirty);
3189            ctx.set_quality(self.render_quality);
3190            self.render_into(&mut ctx, 0, 0, 255)?;
3191        }
3192        Ok(())
3193    }
3194
3195    pub fn render_with_offset<D>(
3196        &self,
3197        target: &mut D,
3198        offset_x: i32,
3199        offset_y: i32,
3200    ) -> Result<(), D::Error>
3201    where
3202        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3203    {
3204        self.render_with_offset_and_opacity(target, offset_x, offset_y, 255)
3205    }
3206
3207    pub fn render_with_offset_and_opacity<D>(
3208        &self,
3209        target: &mut D,
3210        offset_x: i32,
3211        offset_y: i32,
3212        opacity: u8,
3213    ) -> Result<(), D::Error>
3214    where
3215        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3216    {
3217        let mut ctx = RenderCtx::new(target, self.viewport);
3218        ctx.set_quality(self.render_quality);
3219        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3220    }
3221
3222    pub fn render_with_offset_opacity_and_clip<D>(
3223        &self,
3224        target: &mut D,
3225        offset_x: i32,
3226        offset_y: i32,
3227        opacity: u8,
3228        clip: Rect,
3229    ) -> Result<(), D::Error>
3230    where
3231        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3232    {
3233        let mut ctx = RenderCtx::new(target, self.viewport);
3234        ctx.set_quality(self.render_quality);
3235        let old_clip = ctx.clip();
3236        ctx.set_clip(old_clip.intersection(clip));
3237        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3238    }
3239
3240    pub fn handle_input(&mut self, event: InputEvent) -> Result<(), GuiError> {
3241        match event {
3242            InputEvent::Home => {
3243                if let Some(id) = self.focus {
3244                    if matches!(
3245                        self.node(id).map(|n| n.kind),
3246                        Some(WidgetKind::TextArea { .. })
3247                    ) {
3248                        self.set_textarea_cursor_line_home(id)?;
3249                        return Ok(());
3250                    }
3251                }
3252                Ok(())
3253            }
3254            InputEvent::End => {
3255                if let Some(id) = self.focus {
3256                    if matches!(
3257                        self.node(id).map(|n| n.kind),
3258                        Some(WidgetKind::TextArea { .. })
3259                    ) {
3260                        self.set_textarea_cursor_line_end(id)?;
3261                        return Ok(());
3262                    }
3263                }
3264                Ok(())
3265            }
3266            InputEvent::WordLeft => {
3267                if let Some(id) = self.focus {
3268                    if matches!(
3269                        self.node(id).map(|n| n.kind),
3270                        Some(WidgetKind::TextArea { .. })
3271                    ) {
3272                        self.move_textarea_cursor_word(id, -1)?;
3273                        return Ok(());
3274                    }
3275                }
3276                Ok(())
3277            }
3278            InputEvent::WordRight => {
3279                if let Some(id) = self.focus {
3280                    if matches!(
3281                        self.node(id).map(|n| n.kind),
3282                        Some(WidgetKind::TextArea { .. })
3283                    ) {
3284                        self.move_textarea_cursor_word(id, 1)?;
3285                        return Ok(());
3286                    }
3287                }
3288                Ok(())
3289            }
3290            InputEvent::Undo => {
3291                if let Some(id) = self.focus {
3292                    if matches!(
3293                        self.node(id).map(|n| n.kind),
3294                        Some(WidgetKind::TextArea { .. })
3295                    ) {
3296                        self.textarea_undo(id)?;
3297                    }
3298                }
3299                Ok(())
3300            }
3301            InputEvent::Redo => {
3302                if let Some(id) = self.focus {
3303                    if matches!(
3304                        self.node(id).map(|n| n.kind),
3305                        Some(WidgetKind::TextArea { .. })
3306                    ) {
3307                        self.textarea_redo(id)?;
3308                    }
3309                }
3310                Ok(())
3311            }
3312            InputEvent::SelectLeft => {
3313                if let Some(id) = self.focus {
3314                    if matches!(
3315                        self.node(id).map(|n| n.kind),
3316                        Some(WidgetKind::TextArea { .. })
3317                    ) {
3318                        self.move_textarea_cursor_select(id, -1)?;
3319                        return Ok(());
3320                    }
3321                }
3322                Ok(())
3323            }
3324            InputEvent::SelectRight => {
3325                if let Some(id) = self.focus {
3326                    if matches!(
3327                        self.node(id).map(|n| n.kind),
3328                        Some(WidgetKind::TextArea { .. })
3329                    ) {
3330                        self.move_textarea_cursor_select(id, 1)?;
3331                        return Ok(());
3332                    }
3333                }
3334                Ok(())
3335            }
3336            InputEvent::SelectHome => {
3337                if let Some(id) = self.focus {
3338                    if matches!(
3339                        self.node(id).map(|n| n.kind),
3340                        Some(WidgetKind::TextArea { .. })
3341                    ) {
3342                        self.set_textarea_cursor_line_home_select(id)?;
3343                        return Ok(());
3344                    }
3345                }
3346                Ok(())
3347            }
3348            InputEvent::SelectEnd => {
3349                if let Some(id) = self.focus {
3350                    if matches!(
3351                        self.node(id).map(|n| n.kind),
3352                        Some(WidgetKind::TextArea { .. })
3353                    ) {
3354                        self.set_textarea_cursor_line_end_select(id)?;
3355                        return Ok(());
3356                    }
3357                }
3358                Ok(())
3359            }
3360            InputEvent::SelectWordLeft => {
3361                if let Some(id) = self.focus {
3362                    if matches!(
3363                        self.node(id).map(|n| n.kind),
3364                        Some(WidgetKind::TextArea { .. })
3365                    ) {
3366                        self.move_textarea_cursor_word_select(id, -1)?;
3367                        return Ok(());
3368                    }
3369                }
3370                Ok(())
3371            }
3372            InputEvent::SelectWordRight => {
3373                if let Some(id) = self.focus {
3374                    if matches!(
3375                        self.node(id).map(|n| n.kind),
3376                        Some(WidgetKind::TextArea { .. })
3377                    ) {
3378                        self.move_textarea_cursor_word_select(id, 1)?;
3379                        return Ok(());
3380                    }
3381                }
3382                Ok(())
3383            }
3384            InputEvent::Up => {
3385                if !self.adjust_focused_selection(-1)? {
3386                    self.focus_prev()?;
3387                }
3388                Ok(())
3389            }
3390            InputEvent::Down => {
3391                if !self.adjust_focused_selection(1)? {
3392                    self.focus_next()?;
3393                }
3394                Ok(())
3395            }
3396            InputEvent::Left => {
3397                if !self.adjust_focused_scalar(-1.0)? {
3398                    self.focus_prev()?;
3399                }
3400                Ok(())
3401            }
3402            InputEvent::Right => {
3403                if !self.adjust_focused_scalar(1.0)? {
3404                    self.focus_next()?;
3405                }
3406                Ok(())
3407            }
3408            InputEvent::Encoder { delta } if delta > 0 => {
3409                if !self.adjust_focused_selection(1)? {
3410                    self.focus_next()?;
3411                }
3412                Ok(())
3413            }
3414            InputEvent::Encoder { delta } if delta < 0 => {
3415                if !self.adjust_focused_selection(-1)? {
3416                    self.focus_prev()?;
3417                }
3418                Ok(())
3419            }
3420            InputEvent::Select => {
3421                if let Some(id) = self.focus {
3422                    match self.key_bindings_for(id).select {
3423                        KeyBindingAction::Default | KeyBindingAction::Activate => {
3424                            self.handle_select_activation(id)?
3425                        }
3426                        KeyBindingAction::Back => self.handle_back_action()?,
3427                        KeyBindingAction::Ignore => {}
3428                    }
3429                }
3430                Ok(())
3431            }
3432            InputEvent::SelectPressed => {
3433                if let Some(id) = self.focus {
3434                    if self.key_input_policy_for(id).raw_select {
3435                        self.dispatch_key_pressed(id)?;
3436                    }
3437                }
3438                Ok(())
3439            }
3440            InputEvent::SelectReleased => {
3441                if let Some(id) = self.focus {
3442                    if self.key_input_policy_for(id).raw_select {
3443                        self.dispatch_key_released(id)?;
3444                        self.handle_select_activation(id)?;
3445                    }
3446                }
3447                Ok(())
3448            }
3449            InputEvent::Back => {
3450                if let Some(id) = self.focus {
3451                    match self.key_bindings_for(id).back {
3452                        KeyBindingAction::Default | KeyBindingAction::Back => {
3453                            self.handle_back_action()
3454                        }
3455                        KeyBindingAction::Activate => self.handle_select_activation(id),
3456                        KeyBindingAction::Ignore => Ok(()),
3457                    }
3458                } else {
3459                    self.handle_back_action()
3460                }
3461            }
3462            InputEvent::BackPressed => {
3463                if let Some(id) = self.focus {
3464                    if self.key_input_policy_for(id).raw_back {
3465                        self.dispatch_key_pressed(id)?;
3466                    }
3467                }
3468                Ok(())
3469            }
3470            InputEvent::BackReleased => {
3471                if let Some(id) = self.focus {
3472                    if self.key_input_policy_for(id).raw_back {
3473                        self.dispatch_key_released(id)?;
3474                        return self.handle_back_action();
3475                    }
3476                }
3477                Ok(())
3478            }
3479            InputEvent::Pointer {
3480                x,
3481                y,
3482                state: PointerState::Pressed,
3483                ..
3484            } => self.handle_pointer_pressed(x, y),
3485            InputEvent::Pointer {
3486                x,
3487                y,
3488                state: PointerState::Released,
3489                ..
3490            } => self.handle_pointer_released(x, y),
3491            InputEvent::Pointer {
3492                x,
3493                y,
3494                state: PointerState::Moved,
3495                ..
3496            } => self.handle_pointer_moved(x, y),
3497            _ => Ok(()),
3498        }
3499    }
3500
3501    pub fn tick_input(&mut self, dt_ms: u32) -> Result<(), GuiError> {
3502        if self.last_select_id.is_some() {
3503            self.select_elapsed_ms = self.select_elapsed_ms.saturating_add(dt_ms);
3504            if self.select_elapsed_ms > self.select_double_window_ms {
3505                self.last_select_id = None;
3506                self.select_elapsed_ms = 0;
3507            }
3508        }
3509        if self.last_pointer_id.is_some() {
3510            self.pointer_elapsed_ms = self.pointer_elapsed_ms.saturating_add(dt_ms);
3511            if self.pointer_elapsed_ms > self.pointer_double_window_ms {
3512                self.last_pointer_id = None;
3513                self.pointer_elapsed_ms = 0;
3514            }
3515        }
3516        self.tick_state_transitions(dt_ms)?;
3517        if let Some(mut inertia) = self.inertia_scroll {
3518            if inertia.velocity.abs() < self.scroll_physics.velocity_threshold {
3519                self.inertia_scroll = None;
3520            } else {
3521                let current = self.scroll_offset(inertia.id).unwrap_or(0);
3522                let delta = (inertia.velocity * (dt_ms as f32 / 16.0)).round() as i32;
3523                if delta != 0 {
3524                    let next = current.saturating_sub(delta);
3525                    if next != current {
3526                        self.set_scroll_offset(inertia.id, next)?;
3527                        self.push_event(UiEvent::Scroll {
3528                            id: inertia.id,
3529                            delta: next - current,
3530                        })?;
3531                    }
3532                }
3533                inertia.velocity *= self
3534                    .scroll_physics
3535                    .velocity_decay
3536                    .powf((dt_ms as f32 / 16.0).max(1.0));
3537                self.inertia_scroll = Some(inertia);
3538            }
3539        }
3540        self.tick_textarea_cursor_blink(dt_ms)?;
3541        let Some(mut pressed) = self.pressed else {
3542            return Ok(());
3543        };
3544        if !self.effective_visible(pressed.id) || !self.effective_enabled(pressed.id) {
3545            self.pressed = None;
3546            return Ok(());
3547        }
3548        let timing = self.press_timing_for(pressed.id);
3549        pressed.elapsed_ms = pressed.elapsed_ms.saturating_add(dt_ms);
3550        pressed.repeat_elapsed_ms = pressed.repeat_elapsed_ms.saturating_add(dt_ms);
3551        if !pressed.long_emitted && pressed.elapsed_ms >= timing.long_press_ms {
3552            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
3553            self.dispatch_widget_event(
3554                pressed.id,
3555                WidgetEventKind::LongPressed,
3556                &mut events,
3557                |_| EventPolicy::Continue,
3558            )?;
3559            self.push_event(UiEvent::LongPressed(pressed.id))?;
3560            pressed.long_emitted = true;
3561        }
3562        if pressed.repeat_elapsed_ms >= timing.repeat_delay_ms
3563            && self.repeatable_widget(pressed.id)
3564            && pressed.long_emitted
3565        {
3566            let intervals =
3567                (pressed.repeat_elapsed_ms - timing.repeat_delay_ms) / timing.repeat_interval_ms;
3568            if intervals > 0 {
3569                self.dispatch_repeat_activation(pressed.id)?;
3570                pressed.repeat_elapsed_ms = timing.repeat_delay_ms;
3571            }
3572        }
3573        self.pressed = Some(pressed);
3574        Ok(())
3575    }
3576
3577    pub fn pop_event(&mut self) -> Option<UiEvent> {
3578        if self.events.is_empty() {
3579            None
3580        } else {
3581            Some(self.events.remove(0))
3582        }
3583    }
3584
3585    pub fn set_event_filter(
3586        &mut self,
3587        id: WidgetId,
3588        filter: UiEventFilter,
3589    ) -> Result<(), GuiError> {
3590        self.node(id).ok_or(GuiError::NotFound)?;
3591        if let Some((_, current)) = self
3592            .subscriptions
3593            .iter_mut()
3594            .find(|(sub_id, _)| *sub_id == id)
3595        {
3596            *current = filter;
3597            return Ok(());
3598        }
3599        self.subscriptions
3600            .push((id, filter))
3601            .map_err(|_| GuiError::WidgetsFull)
3602    }
3603
3604    pub fn event_filter(&self, id: WidgetId) -> Result<UiEventFilter, GuiError> {
3605        self.node(id).ok_or(GuiError::NotFound)?;
3606        Ok(self
3607            .subscriptions
3608            .iter()
3609            .find(|(sub_id, _)| *sub_id == id)
3610            .map(|(_, filter)| *filter)
3611            .unwrap_or(UiEventFilter::ALL))
3612    }
3613
3614    pub fn clear_event_filter(&mut self, id: WidgetId) -> Result<(), GuiError> {
3615        self.node(id).ok_or(GuiError::NotFound)?;
3616        if let Some(pos) = self
3617            .subscriptions
3618            .iter()
3619            .position(|(sub_id, _)| *sub_id == id)
3620        {
3621            self.subscriptions.remove(pos);
3622        }
3623        Ok(())
3624    }
3625
3626    pub fn set_dispatch_policy(
3627        &mut self,
3628        id: WidgetId,
3629        policy: WidgetDispatchPolicy,
3630    ) -> Result<(), GuiError> {
3631        self.node(id).ok_or(GuiError::NotFound)?;
3632        if let Some((_, current)) = self
3633            .dispatch_policies
3634            .iter_mut()
3635            .find(|(policy_id, _)| *policy_id == id)
3636        {
3637            *current = policy;
3638            return Ok(());
3639        }
3640        self.dispatch_policies
3641            .push((id, policy))
3642            .map_err(|_| GuiError::WidgetsFull)
3643    }
3644
3645    pub fn dispatch_policy(&self, id: WidgetId) -> Result<Option<WidgetDispatchPolicy>, GuiError> {
3646        self.node(id).ok_or(GuiError::NotFound)?;
3647        Ok(self
3648            .dispatch_policies
3649            .iter()
3650            .find(|(policy_id, _)| *policy_id == id)
3651            .map(|(_, policy)| *policy))
3652    }
3653
3654    pub fn clear_dispatch_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
3655        self.node(id).ok_or(GuiError::NotFound)?;
3656        if let Some(pos) = self
3657            .dispatch_policies
3658            .iter()
3659            .position(|(policy_id, _)| *policy_id == id)
3660        {
3661            self.dispatch_policies.remove(pos);
3662        }
3663        Ok(())
3664    }
3665
3666    fn add_widget<S>(
3667        &mut self,
3668        rect: Rect,
3669        kind: WidgetKind<'a>,
3670        style: S,
3671    ) -> Result<WidgetId, GuiError>
3672    where
3673        S: Into<WidgetStyle>,
3674    {
3675        let id = WidgetId::new(self.next_id);
3676        self.next_id = self.next_id.saturating_add(1).max(1);
3677        let node = WidgetNode::new(id, rect, kind, style);
3678        self.widgets.push(node).map_err(|_| GuiError::WidgetsFull)?;
3679        self.dirty.add(rect)?;
3680        Ok(id)
3681    }
3682
3683    fn render_into<D, C>(
3684        &self,
3685        ctx: &mut RenderCtx<'_, D, C>,
3686        offset_x: i32,
3687        offset_y: i32,
3688        opacity: u8,
3689    ) -> Result<(), D::Error>
3690    where
3691        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3692        C: crate::render::Compositor<D>,
3693    {
3694        for node in &self.widgets {
3695            if !self.effective_visible(node.id) {
3696                continue;
3697            }
3698            let Some(base_rect) = self.absolute_rect(node.id) else {
3699                continue;
3700            };
3701            let rect = Rect::new(
3702                base_rect.x + offset_x,
3703                base_rect.y + offset_y,
3704                base_rect.w,
3705                base_rect.h,
3706            );
3707            let base_clip = self.inherited_clip(node.id).unwrap_or(self.viewport);
3708            let clip = Rect::new(
3709                base_clip.x + offset_x,
3710                base_clip.y + offset_y,
3711                base_clip.w,
3712                base_clip.h,
3713            );
3714            if rect.intersection(clip).is_empty() {
3715                continue;
3716            }
3717            let old_clip = ctx.clip();
3718            ctx.set_clip(old_clip.intersection(clip));
3719            let state = if self.pressed.is_some_and(|pressed| pressed.id == node.id) {
3720                VisualState::Pressed
3721            } else if Some(node.id) == self.focus {
3722                VisualState::Focused
3723            } else if !self.effective_enabled(node.id) {
3724                VisualState::Disabled
3725            } else {
3726                VisualState::Normal
3727            };
3728            let mut render_node = *node;
3729            let class_style = node.style_class.and_then(|class| {
3730                self.class_styles
3731                    .iter()
3732                    .find(|(id, _)| *id == class)
3733                    .map(|(_, style)| *style)
3734            });
3735            let resolve_state_style = |vs: VisualState| {
3736                class_style
3737                    .map(|style| style.resolve(vs))
3738                    .unwrap_or_else(|| render_node.style.resolve(vs))
3739            };
3740            let active_style = if let Some((from, to, t)) = self.state_transition_progress(node.id)
3741            {
3742                lerp_style(resolve_state_style(from), resolve_state_style(to), t)
3743            } else {
3744                resolve_state_style(state)
3745            };
3746            render_node.style = render_node.style.with_state_override(state, active_style);
3747            if opacity < 255 {
3748                let apply = |v: u8| -> u8 { ((v as u16 * opacity as u16) / 255) as u8 };
3749                render_node.style.normal.opacity = apply(render_node.style.normal.opacity);
3750                render_node.style.focused.opacity = apply(render_node.style.focused.opacity);
3751                render_node.style.pressed.opacity = apply(render_node.style.pressed.opacity);
3752                render_node.style.disabled.opacity = apply(render_node.style.disabled.opacity);
3753            }
3754            render_node.render_at(ctx, rect, state)?;
3755            ctx.set_clip(old_clip);
3756        }
3757        Ok(())
3758    }
3759
3760    fn node(&self, id: WidgetId) -> Option<&WidgetNode<'a>> {
3761        self.widgets.iter().find(|node| node.id == id)
3762    }
3763
3764    fn intrinsic_size(&self, id: WidgetId) -> Option<(u32, u32)> {
3765        let node = self.node(id)?;
3766        let style = node.style.resolve(VisualState::Normal);
3767        let pad_x = style.padding.left.max(0) as u32 + style.padding.right.max(0) as u32;
3768        let pad_y = style.padding.top.max(0) as u32 + style.padding.bottom.max(0) as u32;
3769        let border = style.border.width as u32 * 2;
3770        let text_width = |text: &str| text.chars().count() as u32 * style.font.advance();
3771        let text_height = style.font.line_height();
3772
3773        let content = match node.kind {
3774            WidgetKind::Label(text) => (text_width(text), text_height),
3775            WidgetKind::Button(text) => (text_width(text).saturating_add(6), text_height),
3776            WidgetKind::Toggle { label, .. } => (text_width(label).saturating_add(12), text_height),
3777            WidgetKind::Checkbox { label, .. } => {
3778                (text_width(label).saturating_add(10), text_height)
3779            }
3780            WidgetKind::ValueLabel { label, .. } => {
3781                (text_width(label).saturating_add(16), text_height)
3782            }
3783            WidgetKind::IconButton { label, .. } => {
3784                (text_width(label).saturating_add(10), text_height)
3785            }
3786            WidgetKind::Tabs { labels, .. } => {
3787                let max = labels.iter().map(|s| text_width(s)).max().unwrap_or(0);
3788                (
3789                    max.saturating_mul(labels.len() as u32).saturating_add(4),
3790                    text_height,
3791                )
3792            }
3793            WidgetKind::Dialog { title, body } => {
3794                let w = text_width(title).max(text_width(body)).saturating_add(8);
3795                (w, text_height.saturating_mul(3))
3796            }
3797            WidgetKind::Toast { text, .. } => (
3798                text_width(text).saturating_add(8),
3799                text_height.saturating_add(2),
3800            ),
3801            WidgetKind::Dropdown {
3802                items, selected, ..
3803            } => (
3804                text_width(items.get(selected).copied().unwrap_or("-")).saturating_add(10),
3805                text_height.saturating_add(2),
3806            ),
3807            WidgetKind::TextArea {
3808                text_buf,
3809                text_len,
3810                placeholder,
3811                ..
3812            } => (
3813                text_width(if text_len == 0 {
3814                    placeholder
3815                } else {
3816                    textarea_text(&text_buf, text_len)
3817                })
3818                .saturating_add(10),
3819                text_height.saturating_add(4),
3820            ),
3821            WidgetKind::Keyboard { keys, cols, .. } => {
3822                let cols = cols.max(1) as u32;
3823                let rows = (keys.len() as u32).div_ceil(cols).max(1);
3824                (
3825                    cols.saturating_mul(style.font.advance().saturating_add(4)),
3826                    rows.saturating_mul(style.font.line_height().saturating_add(4)),
3827                )
3828            }
3829            WidgetKind::List {
3830                items,
3831                visible_rows,
3832                ..
3833            } => {
3834                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3835                (
3836                    max.saturating_add(6),
3837                    (text_height.saturating_add(2))
3838                        .saturating_mul(visible_rows as u32)
3839                        .max(text_height),
3840                )
3841            }
3842            WidgetKind::Menu { items, .. } => {
3843                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3844                (
3845                    max.saturating_add(6),
3846                    (text_height.saturating_add(2))
3847                        .saturating_mul(items.len() as u32)
3848                        .max(text_height),
3849                )
3850            }
3851            WidgetKind::FeedTimeline {
3852                items,
3853                visible_rows,
3854                expanded,
3855                ..
3856            } => {
3857                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3858                let row_h = if expanded {
3859                    text_height.saturating_mul(2).saturating_add(2)
3860                } else {
3861                    text_height.saturating_add(2)
3862                };
3863                (
3864                    max.saturating_add(8),
3865                    row_h.saturating_mul(visible_rows as u32).max(text_height),
3866                )
3867            }
3868            _ => (node.rect.w.max(1), node.rect.h.max(1)),
3869        };
3870
3871        Some((
3872            content
3873                .0
3874                .saturating_add(pad_x)
3875                .saturating_add(border)
3876                .max(1),
3877            content
3878                .1
3879                .saturating_add(pad_y)
3880                .saturating_add(border)
3881                .max(1),
3882        ))
3883    }
3884
3885    fn node_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode<'a>> {
3886        self.widgets.iter_mut().find(|node| node.id == id)
3887    }
3888
3889    fn effective_visible(&self, id: WidgetId) -> bool {
3890        let mut current = Some(id);
3891        let mut depth = 0;
3892        while let Some(widget_id) = current {
3893            if depth >= NODES {
3894                return false;
3895            }
3896            let Some(node) = self.node(widget_id) else {
3897                return false;
3898            };
3899            if node.hidden() {
3900                return false;
3901            }
3902            current = node.parent;
3903            depth += 1;
3904        }
3905        true
3906    }
3907
3908    fn inherited_clip(&self, id: WidgetId) -> Option<Rect> {
3909        let mut clip = self.viewport;
3910        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
3911        let mut current = Some(id);
3912        while let Some(widget_id) = current {
3913            chain.push(widget_id).ok()?;
3914            current = self.node(widget_id)?.parent;
3915        }
3916        for widget_id in chain.iter().rev().copied() {
3917            let node = self.node(widget_id)?;
3918            if widget_id == id || node.clips_children() {
3919                clip = clip.intersection(self.absolute_rect(widget_id)?);
3920            }
3921            if clip.is_empty() {
3922                return None;
3923            }
3924        }
3925        Some(clip)
3926    }
3927
3928    fn effective_enabled(&self, id: WidgetId) -> bool {
3929        let mut current = Some(id);
3930        let mut depth = 0;
3931        while let Some(widget_id) = current {
3932            if depth >= NODES {
3933                return false;
3934            }
3935            let Some(node) = self.node(widget_id) else {
3936                return false;
3937            };
3938            if node.disabled() {
3939                return false;
3940            }
3941            current = node.parent;
3942            depth += 1;
3943        }
3944        true
3945    }
3946
3947    fn effective_focusable(&self, id: WidgetId) -> bool {
3948        self.node(id).is_some_and(|node| {
3949            self.node_in_active_group(node)
3950                && node.focusable()
3951                && self.effective_visible(id)
3952                && self.effective_enabled(id)
3953        })
3954    }
3955
3956    fn ensure_focus(&mut self) {
3957        if self.focus.is_none() {
3958            self.focus = self
3959                .widgets
3960                .iter()
3961                .find(|node| self.effective_focusable(node.id))
3962                .map(|n| n.id);
3963        }
3964    }
3965
3966    fn focus_next(&mut self) -> Result<(), GuiError> {
3967        self.move_focus(1)
3968    }
3969
3970    fn focus_prev(&mut self) -> Result<(), GuiError> {
3971        self.move_focus(-1)
3972    }
3973
3974    fn move_focus(&mut self, delta: i8) -> Result<(), GuiError> {
3975        let focusable = self
3976            .widgets
3977            .iter()
3978            .filter(|node| self.effective_focusable(node.id))
3979            .count();
3980        if focusable == 0 {
3981            return Ok(());
3982        }
3983
3984        let current_pos = self
3985            .widgets
3986            .iter()
3987            .filter(|node| self.effective_focusable(node.id))
3988            .position(|node| Some(node.id) == self.focus)
3989            .unwrap_or(0);
3990
3991        let next_pos = if delta >= 0 {
3992            (current_pos + 1) % focusable
3993        } else if current_pos == 0 {
3994            focusable - 1
3995        } else {
3996            current_pos - 1
3997        };
3998
3999        let next = self
4000            .widgets
4001            .iter()
4002            .filter(|node| self.effective_focusable(node.id))
4003            .nth(next_pos)
4004            .map(|node| node.id);
4005        self.set_focus(next)
4006    }
4007
4008    fn adjust_focused_selection(&mut self, delta: i8) -> Result<bool, GuiError> {
4009        let Some(id) = self.focus else {
4010            return Ok(false);
4011        };
4012        let wrap_navigation = self.menu_contract.wrap_navigation;
4013
4014        let mut changed_rect = None;
4015        let mut changed = false;
4016
4017        if let Some(node) = self.node_mut(id) {
4018            match node.kind {
4019                WidgetKind::Menu {
4020                    items,
4021                    selected: ref mut current,
4022                } => {
4023                    if items.is_empty() {
4024                        return Ok(true);
4025                    }
4026                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4027                    changed_rect = changed.then_some(node.rect);
4028                }
4029                WidgetKind::Dropdown {
4030                    items,
4031                    selected: ref mut current,
4032                    open,
4033                } => {
4034                    if !open {
4035                        return Ok(false);
4036                    }
4037                    if items.is_empty() {
4038                        return Ok(true);
4039                    }
4040                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4041                    changed_rect = changed.then_some(node.rect);
4042                }
4043                WidgetKind::Roller {
4044                    items,
4045                    selected: ref mut current,
4046                } => {
4047                    if items.is_empty() {
4048                        return Ok(true);
4049                    }
4050                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4051                    changed_rect = changed.then_some(node.rect);
4052                }
4053                WidgetKind::Keyboard {
4054                    keys,
4055                    selected: ref mut current,
4056                    ..
4057                } => {
4058                    if keys.is_empty() {
4059                        return Ok(true);
4060                    }
4061                    changed = bump_index_with_wrap(current, keys.len(), delta, wrap_navigation);
4062                    changed_rect = changed.then_some(node.rect);
4063                }
4064                WidgetKind::List {
4065                    items,
4066                    selected: ref mut current,
4067                    ref mut offset,
4068                    visible_rows,
4069                } => {
4070                    if items.is_empty() {
4071                        return Ok(true);
4072                    }
4073                    let mut state = ListState::new(*current, *offset, visible_rows);
4074                    let mut next = state.selected;
4075                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4076                    if changed {
4077                        let _ = state.set_selected(next, items.len());
4078                    }
4079                    *current = state.selected;
4080                    *offset = state.offset;
4081                    changed_rect = changed.then_some(node.rect);
4082                }
4083                WidgetKind::FeedTimeline {
4084                    items,
4085                    selected: ref mut current,
4086                    ref mut offset,
4087                    visible_rows,
4088                    expanded,
4089                } => {
4090                    if items.is_empty() {
4091                        return Ok(true);
4092                    }
4093                    let mut state =
4094                        FeedTimelineState::new(*current, *offset, visible_rows, expanded);
4095                    let mut next = state.selected;
4096                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4097                    if changed {
4098                        let _ = state.set_selected(next, items.len());
4099                    }
4100                    *current = state.selected;
4101                    *offset = state.offset;
4102                    changed_rect = changed.then_some(node.rect);
4103                }
4104                WidgetKind::ScrollView {
4105                    offset_y: ref mut offset,
4106                    content_h,
4107                } => {
4108                    let mut state = ScrollState::new(*offset, content_h);
4109                    changed = state.scroll_by(delta as i32 * 8);
4110                    *offset = state.offset_y;
4111                    changed_rect = changed.then_some(node.rect);
4112                }
4113                _ => return Ok(false),
4114            }
4115        }
4116
4117        if let Some(rect) = changed_rect {
4118            self.dirty.add(rect)?;
4119        }
4120        if changed {
4121            self.push_event(UiEvent::ValueChanged(id))?;
4122        }
4123        Ok(true)
4124    }
4125
4126    fn adjust_focused_scalar(&mut self, direction: f32) -> Result<bool, GuiError> {
4127        let Some(id) = self.focus else {
4128            return Ok(false);
4129        };
4130
4131        let mut changed_rect = None;
4132        let mut changed = false;
4133
4134        if let Some(node) = self.node_mut(id) {
4135            match node.kind {
4136                WidgetKind::Slider {
4137                    value: ref mut current,
4138                    min,
4139                    max,
4140                } => {
4141                    let mut state = SliderState::new(*current, min, max);
4142                    changed = state.step_by(direction);
4143                    *current = state.value;
4144                    changed_rect = changed.then_some(node.rect);
4145                }
4146                WidgetKind::Tabs {
4147                    labels,
4148                    selected: ref mut current,
4149                } => {
4150                    if labels.is_empty() {
4151                        return Ok(true);
4152                    }
4153                    let mut state = TabsState::new(*current);
4154                    changed = state.bump(labels.len(), if direction >= 0.0 { 1 } else { -1 });
4155                    *current = state.selected;
4156                    changed_rect = changed.then_some(node.rect);
4157                }
4158                WidgetKind::TextArea {
4159                    text_buf,
4160                    text_len,
4161                    cursor: ref mut current,
4162                    ..
4163                } => {
4164                    let text = textarea_text(&text_buf, text_len);
4165                    let len = text.chars().count();
4166                    if direction >= 0.0 {
4167                        let next = (*current + 1).min(len);
4168                        changed = next != *current;
4169                        *current = next;
4170                    } else {
4171                        let next = current.saturating_sub(1);
4172                        changed = next != *current;
4173                        *current = next;
4174                    }
4175                    changed_rect = changed.then_some(node.rect);
4176                }
4177                _ => return Ok(false),
4178            }
4179        }
4180
4181        if let Some(rect) = changed_rect {
4182            self.dirty.add(rect)?;
4183        }
4184        if changed {
4185            self.push_event(UiEvent::ValueChanged(id))?;
4186        }
4187        Ok(true)
4188    }
4189
4190    fn activate_focused(&mut self, id: WidgetId) -> Result<(), GuiError> {
4191        let mut changed_rect = None;
4192        let mut changed = false;
4193        let mut dropdown_state_event = None;
4194        let select_opens_dropdown = self.menu_contract.select_opens_dropdown;
4195
4196        if let Some(node) = self.node_mut(id) {
4197            match node.kind {
4198                WidgetKind::Toggle { on: ref mut v, .. } => {
4199                    *v = !*v;
4200                    changed = true;
4201                    changed_rect = Some(node.rect);
4202                }
4203                WidgetKind::Checkbox {
4204                    checked: ref mut v, ..
4205                } => {
4206                    *v = !*v;
4207                    changed = true;
4208                    changed_rect = Some(node.rect);
4209                }
4210                WidgetKind::Keyboard {
4211                    keys,
4212                    alt_keys,
4213                    selected,
4214                    layout,
4215                    target,
4216                    ..
4217                } => {
4218                    if let Some(ch) = keyboard_char_for_layout(keys, alt_keys, selected, layout) {
4219                        changed = true;
4220                        changed_rect = Some(node.rect);
4221                        if let Some(target) = target {
4222                            let _ = self.push_event(UiEvent::TextInput { id: target, ch });
4223                            let _ = self.push_event(UiEvent::ValueChanged(target));
4224                        }
4225                    }
4226                }
4227                WidgetKind::Dropdown {
4228                    open: ref mut is_open,
4229                    ..
4230                } if select_opens_dropdown => {
4231                    *is_open = !*is_open;
4232                    changed = true;
4233                    changed_rect = Some(node.rect);
4234                    dropdown_state_event = Some(*is_open);
4235                }
4236                _ => {}
4237            }
4238        }
4239
4240        if let Some(open) = dropdown_state_event {
4241            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4242            self.dispatch_widget_event(
4243                id,
4244                if open {
4245                    WidgetEventKind::Opened
4246                } else {
4247                    WidgetEventKind::Closed
4248                },
4249                &mut events,
4250                |_| EventPolicy::Continue,
4251            )?;
4252            self.push_event(if open {
4253                UiEvent::Opened(id)
4254            } else {
4255                UiEvent::Closed(id)
4256            })?;
4257        }
4258
4259        if let Some(rect) = changed_rect {
4260            self.dirty.add(rect)?;
4261        }
4262        if changed {
4263            self.push_event(UiEvent::ValueChanged(id))?;
4264        }
4265        Ok(())
4266    }
4267
4268    fn node_in_active_group(&self, node: &WidgetNode<'_>) -> bool {
4269        self.active_focus_group
4270            .is_none_or(|group| node.focus_group == group)
4271    }
4272
4273    fn handle_pointer_pressed(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4274        let hit = self.pointer_hit(x, y, true);
4275
4276        if let Some(id) = hit {
4277            self.dispatch_activation(id, true)?;
4278            self.pressed = Some(PressTracker {
4279                id,
4280                start_x: x,
4281                start_y: y,
4282                last_x: x,
4283                last_y: y,
4284                elapsed_ms: 0,
4285                long_emitted: false,
4286                gesture_emitted: false,
4287                repeat_elapsed_ms: 0,
4288                scroll_velocity: 0.0,
4289            });
4290            self.inertia_scroll = None;
4291        }
4292        Ok(())
4293    }
4294
4295    fn handle_pointer_released(&mut self, _x: i32, _y: i32) -> Result<(), GuiError> {
4296        let mut released_id = None;
4297        if let Some(pressed) = self.pressed {
4298            if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4299                if pressed.scroll_velocity.abs() > self.scroll_physics.velocity_threshold {
4300                    self.inertia_scroll = Some(InertiaScroll {
4301                        id: scroll_id,
4302                        velocity: pressed.scroll_velocity,
4303                    });
4304                }
4305            }
4306            released_id = Some(pressed.id);
4307        }
4308        self.pressed = None;
4309        if let Some(id) = released_id {
4310            let to = if !self.effective_enabled(id) {
4311                VisualState::Disabled
4312            } else if Some(id) == self.focus {
4313                VisualState::Focused
4314            } else {
4315                VisualState::Normal
4316            };
4317            self.start_state_transition(id, VisualState::Pressed, to);
4318            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4319            self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4320                EventPolicy::Continue
4321            })?;
4322            self.push_event(UiEvent::Released(id))?;
4323            self.push_event(UiEvent::PointerReleased(id))?;
4324            let double_pointer = self.last_pointer_id == Some(id)
4325                && self.pointer_elapsed_ms <= self.pointer_double_window_ms;
4326            if double_pointer {
4327                self.dispatch_double_clicked(id)?;
4328                self.last_pointer_id = None;
4329                self.pointer_elapsed_ms = 0;
4330            } else {
4331                self.last_pointer_id = Some(id);
4332                self.pointer_elapsed_ms = 0;
4333            }
4334        }
4335        Ok(())
4336    }
4337
4338    fn handle_pointer_moved(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4339        let Some(mut pressed) = self.pressed else {
4340            return Ok(());
4341        };
4342        let dy = y - pressed.last_y;
4343        pressed.last_x = x;
4344        pressed.last_y = y;
4345
4346        let moved_from_start =
4347            (x - pressed.start_x).unsigned_abs() + (y - pressed.start_y).unsigned_abs();
4348        if !pressed.gesture_emitted && moved_from_start >= 6 {
4349            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4350            self.dispatch_widget_event(pressed.id, WidgetEventKind::Gesture, &mut events, |_| {
4351                EventPolicy::Continue
4352            })?;
4353            self.push_event(UiEvent::Gesture(pressed.id))?;
4354            pressed.gesture_emitted = true;
4355        }
4356
4357        if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4358            let current = self.scroll_offset(scroll_id).unwrap_or(0);
4359            let next = current.saturating_sub(dy);
4360            if next != current {
4361                self.set_scroll_offset(scroll_id, next)?;
4362                self.push_event(UiEvent::Scroll {
4363                    id: scroll_id,
4364                    delta: next - current,
4365                })?;
4366                let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4367                self.dispatch_widget_event(
4368                    scroll_id,
4369                    WidgetEventKind::Scroll {
4370                        delta: next - current,
4371                    },
4372                    &mut events,
4373                    |_| EventPolicy::Continue,
4374                )?;
4375            }
4376            let blend = self.scroll_physics.drag_velocity_blend;
4377            pressed.scroll_velocity = pressed.scroll_velocity * (1.0 - blend) + (dy as f32) * blend;
4378        }
4379        self.pressed = Some(pressed);
4380        Ok(())
4381    }
4382
4383    fn dispatch_activation(&mut self, id: WidgetId, is_pointer: bool) -> Result<(), GuiError> {
4384        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4385        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4386            EventPolicy::Continue
4387        })?;
4388        if self.effective_focusable(id) {
4389            self.set_focus(Some(id))?;
4390        }
4391        self.push_event(UiEvent::Pressed(id))?;
4392        if is_pointer {
4393            self.push_event(UiEvent::PointerPressed(id))?;
4394        }
4395        let from = if Some(id) == self.focus {
4396            VisualState::Focused
4397        } else {
4398            VisualState::Normal
4399        };
4400        self.start_state_transition(id, from, VisualState::Pressed);
4401
4402        self.activate_focused(id)?;
4403        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4404            EventPolicy::Continue
4405        })?;
4406        self.push_event(UiEvent::Clicked(id))?;
4407        self.push_event(UiEvent::Activate(id))?;
4408        Ok(())
4409    }
4410
4411    fn dispatch_repeat_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4412        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4413        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4414            EventPolicy::Continue
4415        })?;
4416        self.push_event(UiEvent::Clicked(id))?;
4417        self.push_event(UiEvent::Activate(id))
4418    }
4419
4420    fn dispatch_double_clicked(&mut self, id: WidgetId) -> Result<(), GuiError> {
4421        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4422        self.dispatch_widget_event(id, WidgetEventKind::DoubleClicked, &mut events, |_| {
4423            EventPolicy::Continue
4424        })?;
4425        self.push_event(UiEvent::DoubleClicked(id))
4426    }
4427
4428    fn dispatch_key_pressed(&mut self, id: WidgetId) -> Result<(), GuiError> {
4429        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4430        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4431            EventPolicy::Continue
4432        })?;
4433        self.push_event(UiEvent::Pressed(id))
4434    }
4435
4436    fn dispatch_key_released(&mut self, id: WidgetId) -> Result<(), GuiError> {
4437        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4438        self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4439            EventPolicy::Continue
4440        })?;
4441        self.push_event(UiEvent::Released(id))
4442    }
4443
4444    fn repeatable_widget(&self, id: WidgetId) -> bool {
4445        self.node(id).is_some_and(|node| {
4446            matches!(
4447                node.kind,
4448                WidgetKind::Button(_) | WidgetKind::IconButton { .. }
4449            )
4450        })
4451    }
4452
4453    fn pointer_hit(&self, x: i32, y: i32, clickable_only: bool) -> Option<WidgetId> {
4454        self.widgets
4455            .iter()
4456            .rev()
4457            .find(|node| {
4458                (!clickable_only || node.clickable())
4459                    && self.effective_visible(node.id)
4460                    && self.effective_enabled(node.id)
4461                    && self
4462                        .absolute_rect(node.id)
4463                        .is_some_and(|rect| rect.contains(x, y))
4464            })
4465            .map(|node| node.id)
4466    }
4467
4468    fn scrollable_ancestor(&self, id: WidgetId) -> Option<WidgetId> {
4469        let mut current = Some(id);
4470        let mut depth = 0usize;
4471        while let Some(widget_id) = current {
4472            if depth >= NODES {
4473                return None;
4474            }
4475            let node = self.node(widget_id)?;
4476            if node.scrollable() {
4477                return Some(widget_id);
4478            }
4479            current = node.parent;
4480            depth += 1;
4481        }
4482        None
4483    }
4484
4485    fn mark_focus_pair(
4486        &mut self,
4487        old: Option<WidgetId>,
4488        new: Option<WidgetId>,
4489    ) -> Result<(), GuiError> {
4490        if let Some(id) = old {
4491            if let Some(rect) = self.absolute_rect(id) {
4492                self.dirty.add(rect)?;
4493            }
4494        }
4495        if let Some(id) = new {
4496            if let Some(rect) = self.absolute_rect(id) {
4497                self.dirty.add(rect)?;
4498            }
4499        }
4500        Ok(())
4501    }
4502
4503    fn start_focus_transitions(&mut self, old: Option<WidgetId>, new: Option<WidgetId>) {
4504        if self.state_transition_ms == 0 {
4505            return;
4506        }
4507        if let Some(id) = old {
4508            self.start_state_transition(id, VisualState::Focused, VisualState::Normal);
4509        }
4510        if let Some(id) = new {
4511            self.start_state_transition(id, VisualState::Normal, VisualState::Focused);
4512        }
4513    }
4514
4515    fn start_state_transition(&mut self, id: WidgetId, from: VisualState, to: VisualState) {
4516        if self.state_transition_ms == 0 || from == to {
4517            return;
4518        }
4519        if let Some(entry) = self
4520            .state_transitions
4521            .iter_mut()
4522            .find(|entry| entry.id == id)
4523        {
4524            *entry = StateTransition {
4525                id,
4526                from,
4527                to,
4528                elapsed_ms: 0,
4529            };
4530            return;
4531        }
4532        if self.state_transitions.len() == self.state_transitions.capacity() {
4533            self.state_transitions.remove(0);
4534        }
4535        let _ = self.state_transitions.push(StateTransition {
4536            id,
4537            from,
4538            to,
4539            elapsed_ms: 0,
4540        });
4541    }
4542
4543    fn tick_state_transitions(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4544        if self.state_transitions.is_empty() || self.state_transition_ms == 0 {
4545            return Ok(());
4546        }
4547        let mut i = 0usize;
4548        let mut completed_pressed = heapless::Vec::<WidgetId, NODES>::new();
4549        while i < self.state_transitions.len() {
4550            let mut remove = false;
4551            let id;
4552            let to;
4553            {
4554                let entry = &mut self.state_transitions[i];
4555                entry.elapsed_ms = entry.elapsed_ms.saturating_add(dt_ms);
4556                if entry.elapsed_ms >= self.state_transition_ms {
4557                    remove = true;
4558                }
4559                id = entry.id;
4560                to = entry.to;
4561            }
4562            if let Some(rect) = self.absolute_rect(id) {
4563                self.dirty.add(rect)?;
4564            }
4565            if remove {
4566                if to == VisualState::Pressed {
4567                    let _ = completed_pressed.push(id);
4568                }
4569                self.state_transitions.remove(i);
4570            } else {
4571                i += 1;
4572            }
4573        }
4574        for id in completed_pressed {
4575            // Pointer-held presses keep visual pressed state until release.
4576            if self.pressed.is_some_and(|pressed| pressed.id == id) {
4577                continue;
4578            }
4579            let to = self.resting_visual_state(id);
4580            self.start_state_transition(id, VisualState::Pressed, to);
4581        }
4582        Ok(())
4583    }
4584
4585    fn state_transition_progress(&self, id: WidgetId) -> Option<(VisualState, VisualState, f32)> {
4586        let duration = self.state_transition_ms.max(1);
4587        self.state_transitions
4588            .iter()
4589            .find(|entry| entry.id == id)
4590            .map(|entry| {
4591                let t = (entry.elapsed_ms as f32 / duration as f32).clamp(0.0, 1.0);
4592                (entry.from, entry.to, t)
4593            })
4594    }
4595
4596    fn set_textarea_cursor_visible(&mut self, id: Option<WidgetId>, visible: bool) {
4597        let Some(id) = id else {
4598            return;
4599        };
4600        let Some(rect) = self.absolute_rect(id) else {
4601            return;
4602        };
4603        let Some(node) = self.node_mut(id) else {
4604            return;
4605        };
4606        if let WidgetKind::TextArea {
4607            cursor_visible: ref mut current,
4608            ..
4609        } = node.kind
4610        {
4611            *current = visible;
4612            let _ = self.dirty.add(rect);
4613        }
4614    }
4615
4616    fn tick_textarea_cursor_blink(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4617        let Some(id) = self.focus else {
4618            return Ok(());
4619        };
4620        let is_textarea = matches!(
4621            self.node(id).map(|n| n.kind),
4622            Some(WidgetKind::TextArea { .. })
4623        );
4624        if !is_textarea {
4625            return Ok(());
4626        }
4627        self.textarea_cursor_blink_elapsed_ms =
4628            self.textarea_cursor_blink_elapsed_ms.saturating_add(dt_ms);
4629        if self.textarea_cursor_blink_elapsed_ms < self.textarea_cursor_blink_ms {
4630            return Ok(());
4631        }
4632        self.textarea_cursor_blink_elapsed_ms = 0;
4633        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
4634        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
4635        if let WidgetKind::TextArea {
4636            cursor_visible: ref mut visible,
4637            ..
4638        } = node.kind
4639        {
4640            *visible = !*visible;
4641            self.dirty.add(rect)?;
4642        }
4643        Ok(())
4644    }
4645
4646    fn push_event(&mut self, event: UiEvent) -> Result<(), GuiError> {
4647        if self.should_emit_event(event)? {
4648            self.events.push(event).map_err(|_| GuiError::EventsFull)?;
4649        }
4650        Ok(())
4651    }
4652
4653    fn should_emit_event(&self, event: UiEvent) -> Result<bool, GuiError> {
4654        let Some(target) = event.target() else {
4655            return Ok(true);
4656        };
4657        let filter = self.event_filter(target)?;
4658        Ok(filter.contains(event.filter()))
4659    }
4660
4661    fn stop_due_to_builtin_widget_behavior(&self, event: WidgetEvent) -> bool {
4662        if event.phase != EventPhase::Capture || event.current == event.target {
4663            return false;
4664        }
4665        let is_pointer_kind = matches!(
4666            event.kind,
4667            WidgetEventKind::Pressed | WidgetEventKind::Released | WidgetEventKind::Clicked
4668        );
4669        is_pointer_kind
4670            && self
4671                .node(event.current)
4672                .is_some_and(|node| matches!(node.kind, WidgetKind::ScrollView { .. }))
4673    }
4674
4675    fn stop_due_to_registered_policy(&self, event: WidgetEvent) -> bool {
4676        self.dispatch_policies
4677            .iter()
4678            .find(|(id, _)| *id == event.current)
4679            .is_some_and(|(_, policy)| policy.stop && policy.allows(event.kind, event.phase))
4680    }
4681
4682    fn resting_visual_state(&self, id: WidgetId) -> VisualState {
4683        if !self.effective_enabled(id) {
4684            VisualState::Disabled
4685        } else if Some(id) == self.focus {
4686            VisualState::Focused
4687        } else {
4688            VisualState::Normal
4689        }
4690    }
4691
4692    fn current_visual_state(&self, id: WidgetId) -> VisualState {
4693        if self.pressed.is_some_and(|pressed| pressed.id == id) {
4694            VisualState::Pressed
4695        } else {
4696            self.resting_visual_state(id)
4697        }
4698    }
4699
4700    fn press_timing_for(&self, id: WidgetId) -> PressTiming {
4701        self.widget_press_timings
4702            .iter()
4703            .find(|(timing_id, _)| *timing_id == id)
4704            .map(|(_, timing)| *timing)
4705            .unwrap_or(PressTiming {
4706                long_press_ms: self.long_press_ms,
4707                repeat_delay_ms: self.press_repeat_delay_ms,
4708                repeat_interval_ms: self.press_repeat_interval_ms,
4709            })
4710    }
4711
4712    fn key_input_policy_for(&self, id: WidgetId) -> WidgetKeyInputPolicy {
4713        self.widget_key_policies
4714            .iter()
4715            .find(|(policy_id, _)| *policy_id == id)
4716            .map(|(_, policy)| *policy)
4717            .unwrap_or_default()
4718    }
4719
4720    fn key_bindings_for(&self, id: WidgetId) -> WidgetKeyBindings {
4721        self.widget_key_bindings
4722            .iter()
4723            .find(|(binding_id, _)| *binding_id == id)
4724            .map(|(_, bindings)| *bindings)
4725            .unwrap_or_default()
4726    }
4727
4728    fn handle_select_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4729        if let Some(node) = self.node(id) {
4730            if self.menu_contract.select_toggles_feed_expanded
4731                && matches!(node.kind, WidgetKind::FeedTimeline { .. })
4732            {
4733                let expanded = if let WidgetKind::FeedTimeline { expanded, .. } = node.kind {
4734                    expanded
4735                } else {
4736                    false
4737                };
4738                self.set_feed_expanded(id, !expanded)?;
4739                self.push_event(UiEvent::ValueChanged(id))?;
4740            }
4741        }
4742        let double_select = self.last_select_id == Some(id)
4743            && self.select_elapsed_ms <= self.select_double_window_ms;
4744        self.dispatch_activation(id, false)?;
4745        if double_select {
4746            self.dispatch_double_clicked(id)?;
4747            self.last_select_id = None;
4748            self.select_elapsed_ms = 0;
4749        } else {
4750            self.last_select_id = Some(id);
4751            self.select_elapsed_ms = 0;
4752        }
4753        Ok(())
4754    }
4755
4756    fn handle_back_action(&mut self) -> Result<(), GuiError> {
4757        if let Some(id) = self.focus {
4758            if matches!(
4759                self.node(id).map(|n| n.kind),
4760                Some(WidgetKind::TextArea { .. })
4761            ) {
4762                self.textarea_backspace(id)?;
4763                return Ok(());
4764            }
4765            if matches!(
4766                self.node(id).map(|n| n.kind),
4767                Some(WidgetKind::Dropdown { open: true, .. })
4768            ) && self.menu_contract.back_closes_dropdown
4769            {
4770                self.set_dropdown_open(id, false)?;
4771                return Ok(());
4772            }
4773            if matches!(
4774                self.node(id).map(|n| n.kind),
4775                Some(WidgetKind::NotificationActionSheet { open: true, .. })
4776            ) && self.menu_contract.back_closes_notification_sheet
4777            {
4778                self.set_notification_sheet_open(id, false)?;
4779                return Ok(());
4780            }
4781        }
4782        self.push_event(UiEvent::Back)
4783    }
4784}
4785
4786fn bump_index_with_wrap(current: &mut usize, len: usize, delta: i8, wrap: bool) -> bool {
4787    if len == 0 {
4788        return false;
4789    }
4790    let next = if delta >= 0 {
4791        if *current + 1 >= len {
4792            if wrap { 0 } else { *current }
4793        } else {
4794            *current + 1
4795        }
4796    } else if *current == 0 {
4797        if wrap { len - 1 } else { *current }
4798    } else {
4799        *current - 1
4800    };
4801    if next != *current {
4802        *current = next;
4803        true
4804    } else {
4805        false
4806    }
4807}
4808
4809fn keyboard_char_for_layout(
4810    keys: &[char],
4811    alt_keys: Option<&[char]>,
4812    selected: usize,
4813    layout: KeyboardLayout,
4814) -> Option<char> {
4815    let base = keys.get(selected).copied()?;
4816    Some(match layout {
4817        KeyboardLayout::Normal => base,
4818        KeyboardLayout::Shift => {
4819            if base.is_ascii_alphabetic() {
4820                base.to_ascii_uppercase()
4821            } else {
4822                base
4823            }
4824        }
4825        KeyboardLayout::Symbols => alt_keys
4826            .and_then(|keys| keys.get(selected).copied())
4827            .unwrap_or('#'),
4828    })
4829}
4830
4831fn textarea_text(buf: &[u8; TEXTAREA_CAPACITY], len: u8) -> &str {
4832    let used = (len as usize).min(TEXTAREA_CAPACITY);
4833    core::str::from_utf8(&buf[..used]).unwrap_or("")
4834}
4835
4836fn textarea_storage_from_str(text: &str) -> ([u8; TEXTAREA_CAPACITY], u8) {
4837    let mut out = [0u8; TEXTAREA_CAPACITY];
4838    let mut len = 0usize;
4839    for ch in text.chars() {
4840        let mut tmp = [0u8; 4];
4841        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4842        if len + enc.len() > TEXTAREA_CAPACITY {
4843            break;
4844        }
4845        out[len..len + enc.len()].copy_from_slice(enc);
4846        len += enc.len();
4847    }
4848    (out, len as u8)
4849}
4850
4851fn textarea_storage_from_chars(
4852    chars: &heapless::Vec<char, TEXTAREA_CAPACITY>,
4853) -> ([u8; TEXTAREA_CAPACITY], u8) {
4854    let mut out = [0u8; TEXTAREA_CAPACITY];
4855    let mut len = 0usize;
4856    for ch in chars {
4857        let mut tmp = [0u8; 4];
4858        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4859        if len + enc.len() > TEXTAREA_CAPACITY {
4860            break;
4861        }
4862        out[len..len + enc.len()].copy_from_slice(enc);
4863        len += enc.len();
4864    }
4865    (out, len as u8)
4866}
4867
4868fn char_at(text: &str, idx: usize) -> Option<char> {
4869    text.chars().nth(idx)
4870}
4871
4872fn prev_word_boundary(text: &str, cursor: usize) -> usize {
4873    let mut pos = cursor.min(text.chars().count());
4874    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| ch.is_whitespace()) {
4875        pos -= 1;
4876    }
4877    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| !ch.is_whitespace()) {
4878        pos -= 1;
4879    }
4880    pos
4881}
4882
4883fn next_word_boundary(text: &str, cursor: usize) -> usize {
4884    let len = text.chars().count();
4885    let mut pos = cursor.min(len);
4886    while pos < len && char_at(text, pos).is_some_and(|ch| !ch.is_whitespace()) {
4887        pos += 1;
4888    }
4889    while pos < len && char_at(text, pos).is_some_and(|ch| ch.is_whitespace()) {
4890        pos += 1;
4891    }
4892    pos
4893}
4894
4895fn delete_selection_if_any(
4896    chars: &mut heapless::Vec<char, TEXTAREA_CAPACITY>,
4897    cursor: &mut usize,
4898    selection: &mut Option<(usize, usize)>,
4899) -> bool {
4900    let Some((start, end)) = *selection else {
4901        return false;
4902    };
4903    let start = start.min(end).min(chars.len());
4904    let end = end.max(start).min(chars.len());
4905    if end <= start {
4906        *selection = None;
4907        *cursor = start;
4908        return false;
4909    }
4910    for _ in start..end {
4911        chars.remove(start);
4912    }
4913    *cursor = start;
4914    *selection = None;
4915    true
4916}
4917
4918fn textarea_row_col_at_cursor(text: &str, cursor: usize, wrap_cols: usize) -> (usize, usize) {
4919    let mut row = 0usize;
4920    let mut col = 0usize;
4921    for ch in text.chars().take(cursor) {
4922        if ch == '\n' {
4923            row += 1;
4924            col = 0;
4925            continue;
4926        }
4927        col += 1;
4928        if col >= wrap_cols {
4929            row += 1;
4930            col = 0;
4931        }
4932    }
4933    (row, col)
4934}
4935
4936fn textarea_cursor_from_row_col(
4937    text: &str,
4938    target_row: usize,
4939    target_col: usize,
4940    wrap_cols: usize,
4941) -> usize {
4942    let mut row = 0usize;
4943    let mut col = 0usize;
4944    let mut idx = 0usize;
4945    for ch in text.chars() {
4946        if row == target_row && col >= target_col {
4947            break;
4948        }
4949        if ch == '\n' {
4950            if row == target_row {
4951                break;
4952            }
4953            row += 1;
4954            col = 0;
4955            idx += 1;
4956            continue;
4957        }
4958        idx += 1;
4959        col += 1;
4960        if col >= wrap_cols {
4961            if row == target_row {
4962                break;
4963            }
4964            row += 1;
4965            col = 0;
4966        }
4967    }
4968    idx
4969}
4970
4971fn textarea_row_end_col(text: &str, target_row: usize, wrap_cols: usize) -> usize {
4972    let mut row = 0usize;
4973    let mut col = 0usize;
4974    for ch in text.chars() {
4975        if row == target_row {
4976            if ch == '\n' {
4977                break;
4978            }
4979            col += 1;
4980            if col >= wrap_cols {
4981                break;
4982            }
4983        } else if ch == '\n' {
4984            row += 1;
4985            col = 0;
4986        } else {
4987            col += 1;
4988            if col >= wrap_cols {
4989                row += 1;
4990                col = 0;
4991            }
4992        }
4993    }
4994    col
4995}