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    #[inline]
2835    pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
2836        let node = self.node(id)?;
2837        if node.parent.is_none() {
2838            return Some(node.rect);
2839        }
2840        let mut rect = node.rect;
2841        let mut parent = node.parent;
2842        let mut depth = 0;
2843        while let Some(parent_id) = parent {
2844            if depth >= NODES {
2845                return None;
2846            }
2847            let parent_node = self.node(parent_id)?;
2848            rect.x += parent_node.rect.x;
2849            rect.y += parent_node.rect.y;
2850            parent = parent_node.parent;
2851            depth += 1;
2852        }
2853        Some(rect)
2854    }
2855
2856    pub fn set_flag(
2857        &mut self,
2858        id: WidgetId,
2859        flag: WidgetFlags,
2860        enabled: bool,
2861    ) -> Result<(), GuiError> {
2862        let was_set = self.has_flag(id, flag)?;
2863        let before_state = self.current_visual_state(id);
2864        self.mark_subtree_dirty(id)?;
2865        self.node_mut(id)
2866            .ok_or(GuiError::NotFound)?
2867            .flags
2868            .set(flag, enabled);
2869        if flag == WidgetFlags::DISABLED
2870            && enabled
2871            && self.pressed.is_some_and(|pressed| pressed.id == id)
2872        {
2873            self.pressed = None;
2874        }
2875        self.mark_subtree_dirty(id)?;
2876        if self
2877            .focus
2878            .is_some_and(|focus| !self.effective_focusable(focus))
2879        {
2880            self.focus = None;
2881            self.ensure_focus();
2882        }
2883        if flag == WidgetFlags::DISABLED && was_set != enabled {
2884            let after_state = self.current_visual_state(id);
2885            self.start_state_transition(id, before_state, after_state);
2886        }
2887        Ok(())
2888    }
2889
2890    pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
2891        Ok(self
2892            .node(id)
2893            .ok_or(GuiError::NotFound)?
2894            .flags
2895            .contains(flag))
2896    }
2897
2898    pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2899        self.set_flag(id, flag, true)
2900    }
2901
2902    pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2903        self.set_flag(id, flag, false)
2904    }
2905
2906    pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
2907        self.set_flag(id, WidgetFlags::HIDDEN, hidden)
2908    }
2909
2910    pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
2911        self.set_flag(id, WidgetFlags::DISABLED, disabled)
2912    }
2913
2914    pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
2915        self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
2916    }
2917
2918    pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
2919        self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
2920    }
2921
2922    pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
2923        self.set_hidden(id, !visible)
2924    }
2925
2926    pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
2927        self.set_disabled(id, !enabled)
2928    }
2929
2930    pub fn event_path<const M: usize>(
2931        &self,
2932        target: WidgetId,
2933        out: &mut heapless::Vec<EventContext, M>,
2934    ) -> Result<usize, GuiError> {
2935        self.node(target).ok_or(GuiError::NotFound)?;
2936        out.clear();
2937
2938        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2939        let mut current = Some(target);
2940        while let Some(id) = current {
2941            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2942            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2943        }
2944
2945        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2946            out.push(EventContext {
2947                target,
2948                current: id,
2949                phase: EventPhase::Capture,
2950            })
2951            .map_err(|_| GuiError::EventsFull)?;
2952        }
2953
2954        out.push(EventContext {
2955            target,
2956            current: target,
2957            phase: EventPhase::Target,
2958        })
2959        .map_err(|_| GuiError::EventsFull)?;
2960
2961        for id in chain.iter().copied().skip(1) {
2962            out.push(EventContext {
2963                target,
2964                current: id,
2965                phase: EventPhase::Bubble,
2966            })
2967            .map_err(|_| GuiError::EventsFull)?;
2968        }
2969
2970        Ok(out.len())
2971    }
2972
2973    pub fn widget_event_path<const M: usize>(
2974        &self,
2975        target: WidgetId,
2976        kind: WidgetEventKind,
2977        out: &mut heapless::Vec<WidgetEvent, M>,
2978    ) -> Result<usize, GuiError> {
2979        self.node(target).ok_or(GuiError::NotFound)?;
2980        out.clear();
2981
2982        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2983        let mut current = Some(target);
2984        while let Some(id) = current {
2985            chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2986            current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2987        }
2988
2989        for id in chain.iter().rev().copied().filter(|&id| id != target) {
2990            out.push(WidgetEvent {
2991                target,
2992                current: id,
2993                phase: EventPhase::Capture,
2994                kind,
2995            })
2996            .map_err(|_| GuiError::EventsFull)?;
2997        }
2998
2999        out.push(WidgetEvent {
3000            target,
3001            current: target,
3002            phase: EventPhase::Target,
3003            kind,
3004        })
3005        .map_err(|_| GuiError::EventsFull)?;
3006
3007        if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
3008            for id in chain.iter().copied().skip(1) {
3009                out.push(WidgetEvent {
3010                    target,
3011                    current: id,
3012                    phase: EventPhase::Bubble,
3013                    kind,
3014                })
3015                .map_err(|_| GuiError::EventsFull)?;
3016            }
3017        }
3018
3019        Ok(out.len())
3020    }
3021
3022    pub fn dispatch_widget_event<const M: usize, F>(
3023        &self,
3024        target: WidgetId,
3025        kind: WidgetEventKind,
3026        scratch: &mut heapless::Vec<WidgetEvent, M>,
3027        mut handler: F,
3028    ) -> Result<(), GuiError>
3029    where
3030        F: FnMut(WidgetEvent) -> EventPolicy,
3031    {
3032        self.widget_event_path(target, kind, scratch)?;
3033        for event in scratch.iter().copied() {
3034            let handler_policy = handler(event);
3035            if matches!(handler_policy, EventPolicy::Stop)
3036                || self.stop_due_to_builtin_widget_behavior(event)
3037                || self.stop_due_to_registered_policy(event)
3038            {
3039                break;
3040            }
3041        }
3042        Ok(())
3043    }
3044
3045    pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
3046        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
3047        self.dirty.add(rect)?;
3048        let child_ids: heapless::Vec<WidgetId, NODES> = self
3049            .widgets
3050            .iter()
3051            .filter(|node| node.parent == Some(id))
3052            .map(|node| node.id)
3053            .collect();
3054        for child in child_ids {
3055            self.mark_subtree_dirty(child)?;
3056        }
3057        Ok(())
3058    }
3059
3060    pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
3061        self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
3062        Ok(())
3063    }
3064
3065    pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
3066        self.active_focus_group = group;
3067        if let Some(focus) = self.focus {
3068            let still_valid = self.node(focus).is_some_and(|node| {
3069                group.is_none_or(|active| node.focus_group == active)
3070                    && self.effective_focusable(focus)
3071            });
3072            if !still_valid {
3073                self.focus = None;
3074                self.ensure_focus();
3075            }
3076        }
3077    }
3078
3079    pub fn apply_layout(
3080        &mut self,
3081        layout: LinearLayout,
3082        area: Rect,
3083        ids: &[WidgetId],
3084    ) -> Result<usize, GuiError> {
3085        let mut rects = [Rect::empty(); 16];
3086        let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
3087        for (id, rect) in ids.iter().copied().zip(rects).take(count) {
3088            self.set_widget_rect(id, rect)?;
3089        }
3090        Ok(count)
3091    }
3092
3093    pub fn apply_layout_flex(
3094        &mut self,
3095        layout: LinearLayout,
3096        area: Rect,
3097        ids: &[WidgetId],
3098        items: &[LayoutItem],
3099        enable_grow: bool,
3100        enable_shrink: bool,
3101    ) -> Result<usize, GuiError> {
3102        let mut rects = [Rect::empty(); 16];
3103        let count = ids.len().min(items.len()).min(rects.len());
3104        let laid_out = layout.arrange_items_flex(
3105            area,
3106            &items[..count],
3107            &mut rects,
3108            enable_grow,
3109            enable_shrink,
3110        );
3111        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3112            self.set_widget_rect(id, rect)?;
3113        }
3114        Ok(laid_out)
3115    }
3116
3117    pub fn apply_layout_intrinsic(
3118        &mut self,
3119        layout: LinearLayout,
3120        area: Rect,
3121        ids: &[WidgetId],
3122    ) -> Result<usize, GuiError> {
3123        self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
3124    }
3125
3126    pub fn apply_layout_intrinsic_with_cross(
3127        &mut self,
3128        layout: LinearLayout,
3129        area: Rect,
3130        ids: &[WidgetId],
3131        preserve_cross: bool,
3132    ) -> Result<usize, GuiError> {
3133        let mut specs = [LayoutItem::fill(); 16];
3134        let mut rects = [Rect::empty(); 16];
3135        let count = ids.len().min(specs.len()).min(rects.len());
3136
3137        for (idx, id) in ids.iter().copied().take(count).enumerate() {
3138            let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
3139            specs[idx] = match layout.axis {
3140                Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
3141                    crate::layout::Constraint::Length(h)
3142                } else {
3143                    crate::layout::Constraint::Fill(1)
3144                }),
3145                Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
3146                    crate::layout::Constraint::Length(w)
3147                } else {
3148                    crate::layout::Constraint::Fill(1)
3149                }),
3150            };
3151        }
3152
3153        let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
3154        for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3155            self.set_widget_rect(id, rect)?;
3156        }
3157        Ok(laid_out)
3158    }
3159
3160    pub fn render<D>(&self, target: &mut D) -> Result<(), D::Error>
3161    where
3162        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3163    {
3164        let mut ctx = RenderCtx::new(target, self.viewport);
3165        ctx.set_quality(self.render_quality);
3166        self.render_into(&mut ctx, 0, 0, 255)
3167    }
3168
3169    /// Like [`GuiContext::render`], but every widget's translucency is composited
3170    /// with true per-pixel alpha blending instead of ordered dithering. Requires
3171    /// a readback-capable target ([`PixelRead`](crate::PixelRead)), e.g. a
3172    /// [`Framebuffer`](crate::Framebuffer) used as a software back buffer.
3173    pub fn render_composited<D>(&self, target: &mut D) -> Result<(), D::Error>
3174    where
3175        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
3176            + crate::render::PixelRead,
3177    {
3178        let mut ctx = RenderCtx::compositing(target, self.viewport);
3179        ctx.set_quality(self.render_quality);
3180        self.render_into::<D, crate::render::Blend>(&mut ctx, 0, 0, 255)
3181    }
3182
3183    pub fn render_dirty<D>(&self, target: &mut D) -> Result<(), D::Error>
3184    where
3185        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3186    {
3187        let slice = self.dirty.as_slice();
3188        if slice.is_empty() {
3189            return Ok(());
3190        }
3191
3192        if slice.len() == 1 {
3193            let mut ctx = RenderCtx::with_dirty(target, self.viewport, slice[0]);
3194            ctx.set_quality(self.render_quality);
3195            return self.render_into(&mut ctx, 0, 0, 255);
3196        }
3197
3198        if let Some(bound) = self.dirty.bounding_rect() {
3199            let area_bound = bound.w as u64 * bound.h as u64;
3200            let sum_area: u64 = slice.iter().map(|r| r.w as u64 * r.h as u64).sum();
3201            // If bounding rect area is close to sum of individual areas (overlap or cluster),
3202            // render bounding rect in 1 pass to avoid multiple tree sweeps
3203            if area_bound <= sum_area + (sum_area / 2) {
3204                let mut ctx = RenderCtx::with_dirty(target, self.viewport, bound);
3205                ctx.set_quality(self.render_quality);
3206                return self.render_into(&mut ctx, 0, 0, 255);
3207            }
3208        }
3209
3210        for dirty in slice {
3211            let mut ctx = RenderCtx::with_dirty(target, self.viewport, *dirty);
3212            ctx.set_quality(self.render_quality);
3213            self.render_into(&mut ctx, 0, 0, 255)?;
3214        }
3215        Ok(())
3216    }
3217
3218    /// Renders dirty regions to a hardware display controller implementing [`WindowedDrawTarget`].
3219    /// Sets the physical controller's column/row window bounds (`set_window`) before rendering,
3220    /// enabling hardware SPI/DMA transfers exclusively to the dirty sub-window.
3221    pub fn render_dirty_windowed<D>(&self, target: &mut D) -> Result<(), D::Error>
3222    where
3223        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
3224            + crate::render::WindowedDrawTarget,
3225    {
3226        let slice = self.dirty.as_slice();
3227        if slice.is_empty() {
3228            return Ok(());
3229        }
3230
3231        for dirty in slice {
3232            let eg_rect = embedded_graphics_core::primitives::Rectangle::new(
3233                embedded_graphics_core::geometry::Point::new(dirty.x, dirty.y),
3234                embedded_graphics_core::geometry::Size::new(dirty.w, dirty.h),
3235            );
3236            target.set_window(&eg_rect)?;
3237            let mut ctx = RenderCtx::with_dirty(target, self.viewport, *dirty);
3238            ctx.set_quality(self.render_quality);
3239            self.render_into(&mut ctx, 0, 0, 255)?;
3240        }
3241        Ok(())
3242    }
3243
3244    pub fn render_with_offset<D>(
3245        &self,
3246        target: &mut D,
3247        offset_x: i32,
3248        offset_y: i32,
3249    ) -> Result<(), D::Error>
3250    where
3251        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3252    {
3253        self.render_with_offset_and_opacity(target, offset_x, offset_y, 255)
3254    }
3255
3256    pub fn render_with_offset_and_opacity<D>(
3257        &self,
3258        target: &mut D,
3259        offset_x: i32,
3260        offset_y: i32,
3261        opacity: u8,
3262    ) -> Result<(), D::Error>
3263    where
3264        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3265    {
3266        let mut ctx = RenderCtx::new(target, self.viewport);
3267        ctx.set_quality(self.render_quality);
3268        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3269    }
3270
3271    pub fn render_with_offset_opacity_and_clip<D>(
3272        &self,
3273        target: &mut D,
3274        offset_x: i32,
3275        offset_y: i32,
3276        opacity: u8,
3277        clip: Rect,
3278    ) -> Result<(), D::Error>
3279    where
3280        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3281    {
3282        let mut ctx = RenderCtx::new(target, self.viewport);
3283        ctx.set_quality(self.render_quality);
3284        let old_clip = ctx.clip();
3285        ctx.set_clip(old_clip.intersection(clip));
3286        self.render_into(&mut ctx, offset_x, offset_y, opacity)
3287    }
3288
3289    pub fn handle_input(&mut self, event: InputEvent) -> Result<(), GuiError> {
3290        match event {
3291            InputEvent::Home => {
3292                if let Some(id) = self.focus {
3293                    if matches!(
3294                        self.node(id).map(|n| n.kind),
3295                        Some(WidgetKind::TextArea { .. })
3296                    ) {
3297                        self.set_textarea_cursor_line_home(id)?;
3298                        return Ok(());
3299                    }
3300                }
3301                Ok(())
3302            }
3303            InputEvent::End => {
3304                if let Some(id) = self.focus {
3305                    if matches!(
3306                        self.node(id).map(|n| n.kind),
3307                        Some(WidgetKind::TextArea { .. })
3308                    ) {
3309                        self.set_textarea_cursor_line_end(id)?;
3310                        return Ok(());
3311                    }
3312                }
3313                Ok(())
3314            }
3315            InputEvent::WordLeft => {
3316                if let Some(id) = self.focus {
3317                    if matches!(
3318                        self.node(id).map(|n| n.kind),
3319                        Some(WidgetKind::TextArea { .. })
3320                    ) {
3321                        self.move_textarea_cursor_word(id, -1)?;
3322                        return Ok(());
3323                    }
3324                }
3325                Ok(())
3326            }
3327            InputEvent::WordRight => {
3328                if let Some(id) = self.focus {
3329                    if matches!(
3330                        self.node(id).map(|n| n.kind),
3331                        Some(WidgetKind::TextArea { .. })
3332                    ) {
3333                        self.move_textarea_cursor_word(id, 1)?;
3334                        return Ok(());
3335                    }
3336                }
3337                Ok(())
3338            }
3339            InputEvent::Undo => {
3340                if let Some(id) = self.focus {
3341                    if matches!(
3342                        self.node(id).map(|n| n.kind),
3343                        Some(WidgetKind::TextArea { .. })
3344                    ) {
3345                        self.textarea_undo(id)?;
3346                    }
3347                }
3348                Ok(())
3349            }
3350            InputEvent::Redo => {
3351                if let Some(id) = self.focus {
3352                    if matches!(
3353                        self.node(id).map(|n| n.kind),
3354                        Some(WidgetKind::TextArea { .. })
3355                    ) {
3356                        self.textarea_redo(id)?;
3357                    }
3358                }
3359                Ok(())
3360            }
3361            InputEvent::SelectLeft => {
3362                if let Some(id) = self.focus {
3363                    if matches!(
3364                        self.node(id).map(|n| n.kind),
3365                        Some(WidgetKind::TextArea { .. })
3366                    ) {
3367                        self.move_textarea_cursor_select(id, -1)?;
3368                        return Ok(());
3369                    }
3370                }
3371                Ok(())
3372            }
3373            InputEvent::SelectRight => {
3374                if let Some(id) = self.focus {
3375                    if matches!(
3376                        self.node(id).map(|n| n.kind),
3377                        Some(WidgetKind::TextArea { .. })
3378                    ) {
3379                        self.move_textarea_cursor_select(id, 1)?;
3380                        return Ok(());
3381                    }
3382                }
3383                Ok(())
3384            }
3385            InputEvent::SelectHome => {
3386                if let Some(id) = self.focus {
3387                    if matches!(
3388                        self.node(id).map(|n| n.kind),
3389                        Some(WidgetKind::TextArea { .. })
3390                    ) {
3391                        self.set_textarea_cursor_line_home_select(id)?;
3392                        return Ok(());
3393                    }
3394                }
3395                Ok(())
3396            }
3397            InputEvent::SelectEnd => {
3398                if let Some(id) = self.focus {
3399                    if matches!(
3400                        self.node(id).map(|n| n.kind),
3401                        Some(WidgetKind::TextArea { .. })
3402                    ) {
3403                        self.set_textarea_cursor_line_end_select(id)?;
3404                        return Ok(());
3405                    }
3406                }
3407                Ok(())
3408            }
3409            InputEvent::SelectWordLeft => {
3410                if let Some(id) = self.focus {
3411                    if matches!(
3412                        self.node(id).map(|n| n.kind),
3413                        Some(WidgetKind::TextArea { .. })
3414                    ) {
3415                        self.move_textarea_cursor_word_select(id, -1)?;
3416                        return Ok(());
3417                    }
3418                }
3419                Ok(())
3420            }
3421            InputEvent::SelectWordRight => {
3422                if let Some(id) = self.focus {
3423                    if matches!(
3424                        self.node(id).map(|n| n.kind),
3425                        Some(WidgetKind::TextArea { .. })
3426                    ) {
3427                        self.move_textarea_cursor_word_select(id, 1)?;
3428                        return Ok(());
3429                    }
3430                }
3431                Ok(())
3432            }
3433            InputEvent::Up => {
3434                if !self.adjust_focused_selection(-1)? {
3435                    self.focus_prev()?;
3436                }
3437                Ok(())
3438            }
3439            InputEvent::Down => {
3440                if !self.adjust_focused_selection(1)? {
3441                    self.focus_next()?;
3442                }
3443                Ok(())
3444            }
3445            InputEvent::Left => {
3446                if !self.adjust_focused_scalar(-1.0)? {
3447                    self.focus_prev()?;
3448                }
3449                Ok(())
3450            }
3451            InputEvent::Right => {
3452                if !self.adjust_focused_scalar(1.0)? {
3453                    self.focus_next()?;
3454                }
3455                Ok(())
3456            }
3457            InputEvent::Encoder { delta } if delta > 0 => {
3458                if !self.adjust_focused_selection(1)? {
3459                    self.focus_next()?;
3460                }
3461                Ok(())
3462            }
3463            InputEvent::Encoder { delta } if delta < 0 => {
3464                if !self.adjust_focused_selection(-1)? {
3465                    self.focus_prev()?;
3466                }
3467                Ok(())
3468            }
3469            InputEvent::Select => {
3470                if let Some(id) = self.focus {
3471                    match self.key_bindings_for(id).select {
3472                        KeyBindingAction::Default | KeyBindingAction::Activate => {
3473                            self.handle_select_activation(id)?
3474                        }
3475                        KeyBindingAction::Back => self.handle_back_action()?,
3476                        KeyBindingAction::Ignore => {}
3477                    }
3478                }
3479                Ok(())
3480            }
3481            InputEvent::SelectPressed => {
3482                if let Some(id) = self.focus {
3483                    if self.key_input_policy_for(id).raw_select {
3484                        self.dispatch_key_pressed(id)?;
3485                    }
3486                }
3487                Ok(())
3488            }
3489            InputEvent::SelectReleased => {
3490                if let Some(id) = self.focus {
3491                    if self.key_input_policy_for(id).raw_select {
3492                        self.dispatch_key_released(id)?;
3493                        self.handle_select_activation(id)?;
3494                    }
3495                }
3496                Ok(())
3497            }
3498            InputEvent::Back => {
3499                if let Some(id) = self.focus {
3500                    match self.key_bindings_for(id).back {
3501                        KeyBindingAction::Default | KeyBindingAction::Back => {
3502                            self.handle_back_action()
3503                        }
3504                        KeyBindingAction::Activate => self.handle_select_activation(id),
3505                        KeyBindingAction::Ignore => Ok(()),
3506                    }
3507                } else {
3508                    self.handle_back_action()
3509                }
3510            }
3511            InputEvent::BackPressed => {
3512                if let Some(id) = self.focus {
3513                    if self.key_input_policy_for(id).raw_back {
3514                        self.dispatch_key_pressed(id)?;
3515                    }
3516                }
3517                Ok(())
3518            }
3519            InputEvent::BackReleased => {
3520                if let Some(id) = self.focus {
3521                    if self.key_input_policy_for(id).raw_back {
3522                        self.dispatch_key_released(id)?;
3523                        return self.handle_back_action();
3524                    }
3525                }
3526                Ok(())
3527            }
3528            InputEvent::Pointer {
3529                x,
3530                y,
3531                state: PointerState::Pressed,
3532                ..
3533            } => self.handle_pointer_pressed(x, y),
3534            InputEvent::Pointer {
3535                x,
3536                y,
3537                state: PointerState::Released,
3538                ..
3539            } => self.handle_pointer_released(x, y),
3540            InputEvent::Pointer {
3541                x,
3542                y,
3543                state: PointerState::Moved,
3544                ..
3545            } => self.handle_pointer_moved(x, y),
3546            _ => Ok(()),
3547        }
3548    }
3549
3550    pub fn tick_input(&mut self, dt_ms: u32) -> Result<(), GuiError> {
3551        if self.last_select_id.is_some() {
3552            self.select_elapsed_ms = self.select_elapsed_ms.saturating_add(dt_ms);
3553            if self.select_elapsed_ms > self.select_double_window_ms {
3554                self.last_select_id = None;
3555                self.select_elapsed_ms = 0;
3556            }
3557        }
3558        if self.last_pointer_id.is_some() {
3559            self.pointer_elapsed_ms = self.pointer_elapsed_ms.saturating_add(dt_ms);
3560            if self.pointer_elapsed_ms > self.pointer_double_window_ms {
3561                self.last_pointer_id = None;
3562                self.pointer_elapsed_ms = 0;
3563            }
3564        }
3565        self.tick_state_transitions(dt_ms)?;
3566        if let Some(mut inertia) = self.inertia_scroll {
3567            if inertia.velocity.abs() < self.scroll_physics.velocity_threshold {
3568                self.inertia_scroll = None;
3569            } else {
3570                let current = self.scroll_offset(inertia.id).unwrap_or(0);
3571                let delta = (inertia.velocity * (dt_ms as f32 / 16.0)).round() as i32;
3572                if delta != 0 {
3573                    let next = current.saturating_sub(delta);
3574                    if next != current {
3575                        self.set_scroll_offset(inertia.id, next)?;
3576                        self.push_event(UiEvent::Scroll {
3577                            id: inertia.id,
3578                            delta: next - current,
3579                        })?;
3580                    }
3581                }
3582                inertia.velocity *= self
3583                    .scroll_physics
3584                    .velocity_decay
3585                    .powf((dt_ms as f32 / 16.0).max(1.0));
3586                self.inertia_scroll = Some(inertia);
3587            }
3588        }
3589        self.tick_textarea_cursor_blink(dt_ms)?;
3590        let Some(mut pressed) = self.pressed else {
3591            return Ok(());
3592        };
3593        if !self.effective_visible(pressed.id) || !self.effective_enabled(pressed.id) {
3594            self.pressed = None;
3595            return Ok(());
3596        }
3597        let timing = self.press_timing_for(pressed.id);
3598        pressed.elapsed_ms = pressed.elapsed_ms.saturating_add(dt_ms);
3599        pressed.repeat_elapsed_ms = pressed.repeat_elapsed_ms.saturating_add(dt_ms);
3600        if !pressed.long_emitted && pressed.elapsed_ms >= timing.long_press_ms {
3601            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
3602            self.dispatch_widget_event(
3603                pressed.id,
3604                WidgetEventKind::LongPressed,
3605                &mut events,
3606                |_| EventPolicy::Continue,
3607            )?;
3608            self.push_event(UiEvent::LongPressed(pressed.id))?;
3609            pressed.long_emitted = true;
3610        }
3611        if pressed.repeat_elapsed_ms >= timing.repeat_delay_ms
3612            && self.repeatable_widget(pressed.id)
3613            && pressed.long_emitted
3614        {
3615            let intervals =
3616                (pressed.repeat_elapsed_ms - timing.repeat_delay_ms) / timing.repeat_interval_ms;
3617            if intervals > 0 {
3618                self.dispatch_repeat_activation(pressed.id)?;
3619                pressed.repeat_elapsed_ms = timing.repeat_delay_ms;
3620            }
3621        }
3622        self.pressed = Some(pressed);
3623        Ok(())
3624    }
3625
3626    pub fn pop_event(&mut self) -> Option<UiEvent> {
3627        if self.events.is_empty() {
3628            None
3629        } else {
3630            Some(self.events.remove(0))
3631        }
3632    }
3633
3634    pub fn set_event_filter(
3635        &mut self,
3636        id: WidgetId,
3637        filter: UiEventFilter,
3638    ) -> Result<(), GuiError> {
3639        self.node(id).ok_or(GuiError::NotFound)?;
3640        if let Some((_, current)) = self
3641            .subscriptions
3642            .iter_mut()
3643            .find(|(sub_id, _)| *sub_id == id)
3644        {
3645            *current = filter;
3646            return Ok(());
3647        }
3648        self.subscriptions
3649            .push((id, filter))
3650            .map_err(|_| GuiError::WidgetsFull)
3651    }
3652
3653    pub fn event_filter(&self, id: WidgetId) -> Result<UiEventFilter, GuiError> {
3654        self.node(id).ok_or(GuiError::NotFound)?;
3655        Ok(self
3656            .subscriptions
3657            .iter()
3658            .find(|(sub_id, _)| *sub_id == id)
3659            .map(|(_, filter)| *filter)
3660            .unwrap_or(UiEventFilter::ALL))
3661    }
3662
3663    pub fn clear_event_filter(&mut self, id: WidgetId) -> Result<(), GuiError> {
3664        self.node(id).ok_or(GuiError::NotFound)?;
3665        if let Some(pos) = self
3666            .subscriptions
3667            .iter()
3668            .position(|(sub_id, _)| *sub_id == id)
3669        {
3670            self.subscriptions.remove(pos);
3671        }
3672        Ok(())
3673    }
3674
3675    pub fn set_dispatch_policy(
3676        &mut self,
3677        id: WidgetId,
3678        policy: WidgetDispatchPolicy,
3679    ) -> Result<(), GuiError> {
3680        self.node(id).ok_or(GuiError::NotFound)?;
3681        if let Some((_, current)) = self
3682            .dispatch_policies
3683            .iter_mut()
3684            .find(|(policy_id, _)| *policy_id == id)
3685        {
3686            *current = policy;
3687            return Ok(());
3688        }
3689        self.dispatch_policies
3690            .push((id, policy))
3691            .map_err(|_| GuiError::WidgetsFull)
3692    }
3693
3694    pub fn dispatch_policy(&self, id: WidgetId) -> Result<Option<WidgetDispatchPolicy>, GuiError> {
3695        self.node(id).ok_or(GuiError::NotFound)?;
3696        Ok(self
3697            .dispatch_policies
3698            .iter()
3699            .find(|(policy_id, _)| *policy_id == id)
3700            .map(|(_, policy)| *policy))
3701    }
3702
3703    pub fn clear_dispatch_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
3704        self.node(id).ok_or(GuiError::NotFound)?;
3705        if let Some(pos) = self
3706            .dispatch_policies
3707            .iter()
3708            .position(|(policy_id, _)| *policy_id == id)
3709        {
3710            self.dispatch_policies.remove(pos);
3711        }
3712        Ok(())
3713    }
3714
3715    fn add_widget<S>(
3716        &mut self,
3717        rect: Rect,
3718        kind: WidgetKind<'a>,
3719        style: S,
3720    ) -> Result<WidgetId, GuiError>
3721    where
3722        S: Into<WidgetStyle>,
3723    {
3724        let id = WidgetId::new(self.next_id);
3725        self.next_id = self.next_id.saturating_add(1).max(1);
3726        let node = WidgetNode::new(id, rect, kind, style);
3727        self.widgets.push(node).map_err(|_| GuiError::WidgetsFull)?;
3728        self.dirty.add(rect)?;
3729        Ok(id)
3730    }
3731
3732    fn render_into<D, C>(
3733        &self,
3734        ctx: &mut RenderCtx<'_, D, C>,
3735        offset_x: i32,
3736        offset_y: i32,
3737        opacity: u8,
3738    ) -> Result<(), D::Error>
3739    where
3740        D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3741        C: crate::render::Compositor<D>,
3742    {
3743        for node in &self.widgets {
3744            if !self.effective_visible(node.id) {
3745                continue;
3746            }
3747            let Some(base_rect) = self.absolute_rect(node.id) else {
3748                continue;
3749            };
3750            let rect = Rect::new(
3751                base_rect.x + offset_x,
3752                base_rect.y + offset_y,
3753                base_rect.w,
3754                base_rect.h,
3755            );
3756            let base_clip = self.inherited_clip(node.id).unwrap_or(self.viewport);
3757            let clip = Rect::new(
3758                base_clip.x + offset_x,
3759                base_clip.y + offset_y,
3760                base_clip.w,
3761                base_clip.h,
3762            );
3763            if rect.intersection(clip).is_empty() {
3764                continue;
3765            }
3766            let old_clip = ctx.clip();
3767            ctx.set_clip(old_clip.intersection(clip));
3768            let state = if self.pressed.is_some_and(|pressed| pressed.id == node.id) {
3769                VisualState::Pressed
3770            } else if Some(node.id) == self.focus {
3771                VisualState::Focused
3772            } else if !self.effective_enabled(node.id) {
3773                VisualState::Disabled
3774            } else {
3775                VisualState::Normal
3776            };
3777            let mut render_node = *node;
3778            let class_style = node.style_class.and_then(|class| {
3779                self.class_styles
3780                    .iter()
3781                    .find(|(id, _)| *id == class)
3782                    .map(|(_, style)| *style)
3783            });
3784            let resolve_state_style = |vs: VisualState| {
3785                class_style
3786                    .map(|style| style.resolve(vs))
3787                    .unwrap_or_else(|| render_node.style.resolve(vs))
3788            };
3789            let active_style = if let Some((from, to, t)) = self.state_transition_progress(node.id)
3790            {
3791                lerp_style(resolve_state_style(from), resolve_state_style(to), t)
3792            } else {
3793                resolve_state_style(state)
3794            };
3795            render_node.style = render_node.style.with_state_override(state, active_style);
3796            if opacity < 255 {
3797                let apply = |v: u8| -> u8 { ((v as u16 * opacity as u16) / 255) as u8 };
3798                render_node.style.normal.opacity = apply(render_node.style.normal.opacity);
3799                render_node.style.focused.opacity = apply(render_node.style.focused.opacity);
3800                render_node.style.pressed.opacity = apply(render_node.style.pressed.opacity);
3801                render_node.style.disabled.opacity = apply(render_node.style.disabled.opacity);
3802            }
3803            render_node.render_at(ctx, rect, state)?;
3804            ctx.set_clip(old_clip);
3805        }
3806        Ok(())
3807    }
3808
3809    fn node(&self, id: WidgetId) -> Option<&WidgetNode<'a>> {
3810        self.widgets.iter().find(|node| node.id == id)
3811    }
3812
3813    fn intrinsic_size(&self, id: WidgetId) -> Option<(u32, u32)> {
3814        let node = self.node(id)?;
3815        let style = node.style.resolve(VisualState::Normal);
3816        let pad_x = style.padding.left.max(0) as u32 + style.padding.right.max(0) as u32;
3817        let pad_y = style.padding.top.max(0) as u32 + style.padding.bottom.max(0) as u32;
3818        let border = style.border.width as u32 * 2;
3819        let text_width = |text: &str| text.chars().count() as u32 * style.font.advance();
3820        let text_height = style.font.line_height();
3821
3822        let content = match node.kind {
3823            WidgetKind::Label(text) => (text_width(text), text_height),
3824            WidgetKind::Button(text) => (text_width(text).saturating_add(6), text_height),
3825            WidgetKind::Toggle { label, .. } => (text_width(label).saturating_add(12), text_height),
3826            WidgetKind::Checkbox { label, .. } => {
3827                (text_width(label).saturating_add(10), text_height)
3828            }
3829            WidgetKind::ValueLabel { label, .. } => {
3830                (text_width(label).saturating_add(16), text_height)
3831            }
3832            WidgetKind::IconButton { label, .. } => {
3833                (text_width(label).saturating_add(10), text_height)
3834            }
3835            WidgetKind::Tabs { labels, .. } => {
3836                let max = labels.iter().map(|s| text_width(s)).max().unwrap_or(0);
3837                (
3838                    max.saturating_mul(labels.len() as u32).saturating_add(4),
3839                    text_height,
3840                )
3841            }
3842            WidgetKind::Dialog { title, body } => {
3843                let w = text_width(title).max(text_width(body)).saturating_add(8);
3844                (w, text_height.saturating_mul(3))
3845            }
3846            WidgetKind::Toast { text, .. } => (
3847                text_width(text).saturating_add(8),
3848                text_height.saturating_add(2),
3849            ),
3850            WidgetKind::Dropdown {
3851                items, selected, ..
3852            } => (
3853                text_width(items.get(selected).copied().unwrap_or("-")).saturating_add(10),
3854                text_height.saturating_add(2),
3855            ),
3856            WidgetKind::TextArea {
3857                text_buf,
3858                text_len,
3859                placeholder,
3860                ..
3861            } => (
3862                text_width(if text_len == 0 {
3863                    placeholder
3864                } else {
3865                    textarea_text(&text_buf, text_len)
3866                })
3867                .saturating_add(10),
3868                text_height.saturating_add(4),
3869            ),
3870            WidgetKind::Keyboard { keys, cols, .. } => {
3871                let cols = cols.max(1) as u32;
3872                let rows = (keys.len() as u32).div_ceil(cols).max(1);
3873                (
3874                    cols.saturating_mul(style.font.advance().saturating_add(4)),
3875                    rows.saturating_mul(style.font.line_height().saturating_add(4)),
3876                )
3877            }
3878            WidgetKind::List {
3879                items,
3880                visible_rows,
3881                ..
3882            } => {
3883                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3884                (
3885                    max.saturating_add(6),
3886                    (text_height.saturating_add(2))
3887                        .saturating_mul(visible_rows as u32)
3888                        .max(text_height),
3889                )
3890            }
3891            WidgetKind::Menu { items, .. } => {
3892                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3893                (
3894                    max.saturating_add(6),
3895                    (text_height.saturating_add(2))
3896                        .saturating_mul(items.len() as u32)
3897                        .max(text_height),
3898                )
3899            }
3900            WidgetKind::FeedTimeline {
3901                items,
3902                visible_rows,
3903                expanded,
3904                ..
3905            } => {
3906                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3907                let row_h = if expanded {
3908                    text_height.saturating_mul(2).saturating_add(2)
3909                } else {
3910                    text_height.saturating_add(2)
3911                };
3912                (
3913                    max.saturating_add(8),
3914                    row_h.saturating_mul(visible_rows as u32).max(text_height),
3915                )
3916            }
3917            _ => (node.rect.w.max(1), node.rect.h.max(1)),
3918        };
3919
3920        Some((
3921            content
3922                .0
3923                .saturating_add(pad_x)
3924                .saturating_add(border)
3925                .max(1),
3926            content
3927                .1
3928                .saturating_add(pad_y)
3929                .saturating_add(border)
3930                .max(1),
3931        ))
3932    }
3933
3934    fn node_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode<'a>> {
3935        self.widgets.iter_mut().find(|node| node.id == id)
3936    }
3937
3938    fn effective_visible(&self, id: WidgetId) -> bool {
3939        let mut current = Some(id);
3940        let mut depth = 0;
3941        while let Some(widget_id) = current {
3942            if depth >= NODES {
3943                return false;
3944            }
3945            let Some(node) = self.node(widget_id) else {
3946                return false;
3947            };
3948            if node.hidden() {
3949                return false;
3950            }
3951            current = node.parent;
3952            depth += 1;
3953        }
3954        true
3955    }
3956
3957    fn inherited_clip(&self, id: WidgetId) -> Option<Rect> {
3958        let mut clip = self.viewport;
3959        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
3960        let mut current = Some(id);
3961        while let Some(widget_id) = current {
3962            chain.push(widget_id).ok()?;
3963            current = self.node(widget_id)?.parent;
3964        }
3965        for widget_id in chain.iter().rev().copied() {
3966            let node = self.node(widget_id)?;
3967            if widget_id == id || node.clips_children() {
3968                clip = clip.intersection(self.absolute_rect(widget_id)?);
3969            }
3970            if clip.is_empty() {
3971                return None;
3972            }
3973        }
3974        Some(clip)
3975    }
3976
3977    fn effective_enabled(&self, id: WidgetId) -> bool {
3978        let mut current = Some(id);
3979        let mut depth = 0;
3980        while let Some(widget_id) = current {
3981            if depth >= NODES {
3982                return false;
3983            }
3984            let Some(node) = self.node(widget_id) else {
3985                return false;
3986            };
3987            if node.disabled() {
3988                return false;
3989            }
3990            current = node.parent;
3991            depth += 1;
3992        }
3993        true
3994    }
3995
3996    fn effective_focusable(&self, id: WidgetId) -> bool {
3997        self.node(id).is_some_and(|node| {
3998            self.node_in_active_group(node)
3999                && node.focusable()
4000                && self.effective_visible(id)
4001                && self.effective_enabled(id)
4002        })
4003    }
4004
4005    fn ensure_focus(&mut self) {
4006        if self.focus.is_none() {
4007            self.focus = self
4008                .widgets
4009                .iter()
4010                .find(|node| self.effective_focusable(node.id))
4011                .map(|n| n.id);
4012        }
4013    }
4014
4015    fn focus_next(&mut self) -> Result<(), GuiError> {
4016        self.move_focus(1)
4017    }
4018
4019    fn focus_prev(&mut self) -> Result<(), GuiError> {
4020        self.move_focus(-1)
4021    }
4022
4023    fn move_focus(&mut self, delta: i8) -> Result<(), GuiError> {
4024        let focusable = self
4025            .widgets
4026            .iter()
4027            .filter(|node| self.effective_focusable(node.id))
4028            .count();
4029        if focusable == 0 {
4030            return Ok(());
4031        }
4032
4033        let current_pos = self
4034            .widgets
4035            .iter()
4036            .filter(|node| self.effective_focusable(node.id))
4037            .position(|node| Some(node.id) == self.focus)
4038            .unwrap_or(0);
4039
4040        let next_pos = if delta >= 0 {
4041            (current_pos + 1) % focusable
4042        } else if current_pos == 0 {
4043            focusable - 1
4044        } else {
4045            current_pos - 1
4046        };
4047
4048        let next = self
4049            .widgets
4050            .iter()
4051            .filter(|node| self.effective_focusable(node.id))
4052            .nth(next_pos)
4053            .map(|node| node.id);
4054        self.set_focus(next)
4055    }
4056
4057    fn adjust_focused_selection(&mut self, delta: i8) -> Result<bool, GuiError> {
4058        let Some(id) = self.focus else {
4059            return Ok(false);
4060        };
4061        let wrap_navigation = self.menu_contract.wrap_navigation;
4062
4063        let mut changed_rect = None;
4064        let mut changed = false;
4065
4066        if let Some(node) = self.node_mut(id) {
4067            match node.kind {
4068                WidgetKind::Menu {
4069                    items,
4070                    selected: ref mut current,
4071                } => {
4072                    if items.is_empty() {
4073                        return Ok(true);
4074                    }
4075                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4076                    changed_rect = changed.then_some(node.rect);
4077                }
4078                WidgetKind::Dropdown {
4079                    items,
4080                    selected: ref mut current,
4081                    open,
4082                } => {
4083                    if !open {
4084                        return Ok(false);
4085                    }
4086                    if items.is_empty() {
4087                        return Ok(true);
4088                    }
4089                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4090                    changed_rect = changed.then_some(node.rect);
4091                }
4092                WidgetKind::Roller {
4093                    items,
4094                    selected: ref mut current,
4095                } => {
4096                    if items.is_empty() {
4097                        return Ok(true);
4098                    }
4099                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
4100                    changed_rect = changed.then_some(node.rect);
4101                }
4102                WidgetKind::Keyboard {
4103                    keys,
4104                    selected: ref mut current,
4105                    ..
4106                } => {
4107                    if keys.is_empty() {
4108                        return Ok(true);
4109                    }
4110                    changed = bump_index_with_wrap(current, keys.len(), delta, wrap_navigation);
4111                    changed_rect = changed.then_some(node.rect);
4112                }
4113                WidgetKind::List {
4114                    items,
4115                    selected: ref mut current,
4116                    ref mut offset,
4117                    visible_rows,
4118                } => {
4119                    if items.is_empty() {
4120                        return Ok(true);
4121                    }
4122                    let mut state = ListState::new(*current, *offset, visible_rows);
4123                    let mut next = state.selected;
4124                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4125                    if changed {
4126                        let _ = state.set_selected(next, items.len());
4127                    }
4128                    *current = state.selected;
4129                    *offset = state.offset;
4130                    changed_rect = changed.then_some(node.rect);
4131                }
4132                WidgetKind::FeedTimeline {
4133                    items,
4134                    selected: ref mut current,
4135                    ref mut offset,
4136                    visible_rows,
4137                    expanded,
4138                } => {
4139                    if items.is_empty() {
4140                        return Ok(true);
4141                    }
4142                    let mut state =
4143                        FeedTimelineState::new(*current, *offset, visible_rows, expanded);
4144                    let mut next = state.selected;
4145                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4146                    if changed {
4147                        let _ = state.set_selected(next, items.len());
4148                    }
4149                    *current = state.selected;
4150                    *offset = state.offset;
4151                    changed_rect = changed.then_some(node.rect);
4152                }
4153                WidgetKind::ScrollView {
4154                    offset_y: ref mut offset,
4155                    content_h,
4156                } => {
4157                    let mut state = ScrollState::new(*offset, content_h);
4158                    changed = state.scroll_by(delta as i32 * 8);
4159                    *offset = state.offset_y;
4160                    changed_rect = changed.then_some(node.rect);
4161                }
4162                _ => return Ok(false),
4163            }
4164        }
4165
4166        if let Some(rect) = changed_rect {
4167            self.dirty.add(rect)?;
4168        }
4169        if changed {
4170            self.push_event(UiEvent::ValueChanged(id))?;
4171        }
4172        Ok(true)
4173    }
4174
4175    fn adjust_focused_scalar(&mut self, direction: f32) -> Result<bool, GuiError> {
4176        let Some(id) = self.focus else {
4177            return Ok(false);
4178        };
4179
4180        let mut changed_rect = None;
4181        let mut changed = false;
4182
4183        if let Some(node) = self.node_mut(id) {
4184            match node.kind {
4185                WidgetKind::Slider {
4186                    value: ref mut current,
4187                    min,
4188                    max,
4189                } => {
4190                    let mut state = SliderState::new(*current, min, max);
4191                    changed = state.step_by(direction);
4192                    *current = state.value;
4193                    changed_rect = changed.then_some(node.rect);
4194                }
4195                WidgetKind::Tabs {
4196                    labels,
4197                    selected: ref mut current,
4198                } => {
4199                    if labels.is_empty() {
4200                        return Ok(true);
4201                    }
4202                    let mut state = TabsState::new(*current);
4203                    changed = state.bump(labels.len(), if direction >= 0.0 { 1 } else { -1 });
4204                    *current = state.selected;
4205                    changed_rect = changed.then_some(node.rect);
4206                }
4207                WidgetKind::TextArea {
4208                    text_buf,
4209                    text_len,
4210                    cursor: ref mut current,
4211                    ..
4212                } => {
4213                    let text = textarea_text(&text_buf, text_len);
4214                    let len = text.chars().count();
4215                    if direction >= 0.0 {
4216                        let next = (*current + 1).min(len);
4217                        changed = next != *current;
4218                        *current = next;
4219                    } else {
4220                        let next = current.saturating_sub(1);
4221                        changed = next != *current;
4222                        *current = next;
4223                    }
4224                    changed_rect = changed.then_some(node.rect);
4225                }
4226                _ => return Ok(false),
4227            }
4228        }
4229
4230        if let Some(rect) = changed_rect {
4231            self.dirty.add(rect)?;
4232        }
4233        if changed {
4234            self.push_event(UiEvent::ValueChanged(id))?;
4235        }
4236        Ok(true)
4237    }
4238
4239    fn activate_focused(&mut self, id: WidgetId) -> Result<(), GuiError> {
4240        let mut changed_rect = None;
4241        let mut changed = false;
4242        let mut dropdown_state_event = None;
4243        let select_opens_dropdown = self.menu_contract.select_opens_dropdown;
4244
4245        if let Some(node) = self.node_mut(id) {
4246            match node.kind {
4247                WidgetKind::Toggle { on: ref mut v, .. } => {
4248                    *v = !*v;
4249                    changed = true;
4250                    changed_rect = Some(node.rect);
4251                }
4252                WidgetKind::Checkbox {
4253                    checked: ref mut v, ..
4254                } => {
4255                    *v = !*v;
4256                    changed = true;
4257                    changed_rect = Some(node.rect);
4258                }
4259                WidgetKind::Keyboard {
4260                    keys,
4261                    alt_keys,
4262                    selected,
4263                    layout,
4264                    target,
4265                    ..
4266                } => {
4267                    if let Some(ch) = keyboard_char_for_layout(keys, alt_keys, selected, layout) {
4268                        changed = true;
4269                        changed_rect = Some(node.rect);
4270                        if let Some(target) = target {
4271                            let _ = self.push_event(UiEvent::TextInput { id: target, ch });
4272                            let _ = self.push_event(UiEvent::ValueChanged(target));
4273                        }
4274                    }
4275                }
4276                WidgetKind::Dropdown {
4277                    open: ref mut is_open,
4278                    ..
4279                } if select_opens_dropdown => {
4280                    *is_open = !*is_open;
4281                    changed = true;
4282                    changed_rect = Some(node.rect);
4283                    dropdown_state_event = Some(*is_open);
4284                }
4285                _ => {}
4286            }
4287        }
4288
4289        if let Some(open) = dropdown_state_event {
4290            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4291            self.dispatch_widget_event(
4292                id,
4293                if open {
4294                    WidgetEventKind::Opened
4295                } else {
4296                    WidgetEventKind::Closed
4297                },
4298                &mut events,
4299                |_| EventPolicy::Continue,
4300            )?;
4301            self.push_event(if open {
4302                UiEvent::Opened(id)
4303            } else {
4304                UiEvent::Closed(id)
4305            })?;
4306        }
4307
4308        if let Some(rect) = changed_rect {
4309            self.dirty.add(rect)?;
4310        }
4311        if changed {
4312            self.push_event(UiEvent::ValueChanged(id))?;
4313        }
4314        Ok(())
4315    }
4316
4317    fn node_in_active_group(&self, node: &WidgetNode<'_>) -> bool {
4318        self.active_focus_group
4319            .is_none_or(|group| node.focus_group == group)
4320    }
4321
4322    fn handle_pointer_pressed(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4323        let hit = self.pointer_hit(x, y, true);
4324
4325        if let Some(id) = hit {
4326            self.dispatch_activation(id, true)?;
4327            self.pressed = Some(PressTracker {
4328                id,
4329                start_x: x,
4330                start_y: y,
4331                last_x: x,
4332                last_y: y,
4333                elapsed_ms: 0,
4334                long_emitted: false,
4335                gesture_emitted: false,
4336                repeat_elapsed_ms: 0,
4337                scroll_velocity: 0.0,
4338            });
4339            self.inertia_scroll = None;
4340        }
4341        Ok(())
4342    }
4343
4344    fn handle_pointer_released(&mut self, _x: i32, _y: i32) -> Result<(), GuiError> {
4345        let mut released_id = None;
4346        if let Some(pressed) = self.pressed {
4347            if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4348                if pressed.scroll_velocity.abs() > self.scroll_physics.velocity_threshold {
4349                    self.inertia_scroll = Some(InertiaScroll {
4350                        id: scroll_id,
4351                        velocity: pressed.scroll_velocity,
4352                    });
4353                }
4354            }
4355            released_id = Some(pressed.id);
4356        }
4357        self.pressed = None;
4358        if let Some(id) = released_id {
4359            let to = if !self.effective_enabled(id) {
4360                VisualState::Disabled
4361            } else if Some(id) == self.focus {
4362                VisualState::Focused
4363            } else {
4364                VisualState::Normal
4365            };
4366            self.start_state_transition(id, VisualState::Pressed, to);
4367            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4368            self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4369                EventPolicy::Continue
4370            })?;
4371            self.push_event(UiEvent::Released(id))?;
4372            self.push_event(UiEvent::PointerReleased(id))?;
4373            let double_pointer = self.last_pointer_id == Some(id)
4374                && self.pointer_elapsed_ms <= self.pointer_double_window_ms;
4375            if double_pointer {
4376                self.dispatch_double_clicked(id)?;
4377                self.last_pointer_id = None;
4378                self.pointer_elapsed_ms = 0;
4379            } else {
4380                self.last_pointer_id = Some(id);
4381                self.pointer_elapsed_ms = 0;
4382            }
4383        }
4384        Ok(())
4385    }
4386
4387    fn handle_pointer_moved(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4388        let Some(mut pressed) = self.pressed else {
4389            return Ok(());
4390        };
4391        let dy = y - pressed.last_y;
4392        pressed.last_x = x;
4393        pressed.last_y = y;
4394
4395        let moved_from_start =
4396            (x - pressed.start_x).unsigned_abs() + (y - pressed.start_y).unsigned_abs();
4397        if !pressed.gesture_emitted && moved_from_start >= 6 {
4398            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4399            self.dispatch_widget_event(pressed.id, WidgetEventKind::Gesture, &mut events, |_| {
4400                EventPolicy::Continue
4401            })?;
4402            self.push_event(UiEvent::Gesture(pressed.id))?;
4403            pressed.gesture_emitted = true;
4404        }
4405
4406        if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4407            let current = self.scroll_offset(scroll_id).unwrap_or(0);
4408            let next = current.saturating_sub(dy);
4409            if next != current {
4410                self.set_scroll_offset(scroll_id, next)?;
4411                self.push_event(UiEvent::Scroll {
4412                    id: scroll_id,
4413                    delta: next - current,
4414                })?;
4415                let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4416                self.dispatch_widget_event(
4417                    scroll_id,
4418                    WidgetEventKind::Scroll {
4419                        delta: next - current,
4420                    },
4421                    &mut events,
4422                    |_| EventPolicy::Continue,
4423                )?;
4424            }
4425            let blend = self.scroll_physics.drag_velocity_blend;
4426            pressed.scroll_velocity = pressed.scroll_velocity * (1.0 - blend) + (dy as f32) * blend;
4427        }
4428        self.pressed = Some(pressed);
4429        Ok(())
4430    }
4431
4432    fn dispatch_activation(&mut self, id: WidgetId, is_pointer: bool) -> Result<(), GuiError> {
4433        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4434        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4435            EventPolicy::Continue
4436        })?;
4437        if self.effective_focusable(id) {
4438            self.set_focus(Some(id))?;
4439        }
4440        self.push_event(UiEvent::Pressed(id))?;
4441        if is_pointer {
4442            self.push_event(UiEvent::PointerPressed(id))?;
4443        }
4444        let from = if Some(id) == self.focus {
4445            VisualState::Focused
4446        } else {
4447            VisualState::Normal
4448        };
4449        self.start_state_transition(id, from, VisualState::Pressed);
4450
4451        self.activate_focused(id)?;
4452        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4453            EventPolicy::Continue
4454        })?;
4455        self.push_event(UiEvent::Clicked(id))?;
4456        self.push_event(UiEvent::Activate(id))?;
4457        Ok(())
4458    }
4459
4460    fn dispatch_repeat_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4461        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4462        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4463            EventPolicy::Continue
4464        })?;
4465        self.push_event(UiEvent::Clicked(id))?;
4466        self.push_event(UiEvent::Activate(id))
4467    }
4468
4469    fn dispatch_double_clicked(&mut self, id: WidgetId) -> Result<(), GuiError> {
4470        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4471        self.dispatch_widget_event(id, WidgetEventKind::DoubleClicked, &mut events, |_| {
4472            EventPolicy::Continue
4473        })?;
4474        self.push_event(UiEvent::DoubleClicked(id))
4475    }
4476
4477    fn dispatch_key_pressed(&mut self, id: WidgetId) -> Result<(), GuiError> {
4478        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4479        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4480            EventPolicy::Continue
4481        })?;
4482        self.push_event(UiEvent::Pressed(id))
4483    }
4484
4485    fn dispatch_key_released(&mut self, id: WidgetId) -> Result<(), GuiError> {
4486        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4487        self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4488            EventPolicy::Continue
4489        })?;
4490        self.push_event(UiEvent::Released(id))
4491    }
4492
4493    fn repeatable_widget(&self, id: WidgetId) -> bool {
4494        self.node(id).is_some_and(|node| {
4495            matches!(
4496                node.kind,
4497                WidgetKind::Button(_) | WidgetKind::IconButton { .. }
4498            )
4499        })
4500    }
4501
4502    fn pointer_hit(&self, x: i32, y: i32, clickable_only: bool) -> Option<WidgetId> {
4503        self.widgets
4504            .iter()
4505            .rev()
4506            .find(|node| {
4507                (!clickable_only || node.clickable())
4508                    && self.effective_visible(node.id)
4509                    && self.effective_enabled(node.id)
4510                    && self
4511                        .absolute_rect(node.id)
4512                        .is_some_and(|rect| rect.contains(x, y))
4513            })
4514            .map(|node| node.id)
4515    }
4516
4517    fn scrollable_ancestor(&self, id: WidgetId) -> Option<WidgetId> {
4518        let mut current = Some(id);
4519        let mut depth = 0usize;
4520        while let Some(widget_id) = current {
4521            if depth >= NODES {
4522                return None;
4523            }
4524            let node = self.node(widget_id)?;
4525            if node.scrollable() {
4526                return Some(widget_id);
4527            }
4528            current = node.parent;
4529            depth += 1;
4530        }
4531        None
4532    }
4533
4534    fn mark_focus_pair(
4535        &mut self,
4536        old: Option<WidgetId>,
4537        new: Option<WidgetId>,
4538    ) -> Result<(), GuiError> {
4539        if let Some(id) = old {
4540            if let Some(rect) = self.absolute_rect(id) {
4541                self.dirty.add(rect)?;
4542            }
4543        }
4544        if let Some(id) = new {
4545            if let Some(rect) = self.absolute_rect(id) {
4546                self.dirty.add(rect)?;
4547            }
4548        }
4549        Ok(())
4550    }
4551
4552    fn start_focus_transitions(&mut self, old: Option<WidgetId>, new: Option<WidgetId>) {
4553        if self.state_transition_ms == 0 {
4554            return;
4555        }
4556        if let Some(id) = old {
4557            self.start_state_transition(id, VisualState::Focused, VisualState::Normal);
4558        }
4559        if let Some(id) = new {
4560            self.start_state_transition(id, VisualState::Normal, VisualState::Focused);
4561        }
4562    }
4563
4564    fn start_state_transition(&mut self, id: WidgetId, from: VisualState, to: VisualState) {
4565        if self.state_transition_ms == 0 || from == to {
4566            return;
4567        }
4568        if let Some(entry) = self
4569            .state_transitions
4570            .iter_mut()
4571            .find(|entry| entry.id == id)
4572        {
4573            *entry = StateTransition {
4574                id,
4575                from,
4576                to,
4577                elapsed_ms: 0,
4578            };
4579            return;
4580        }
4581        if self.state_transitions.len() == self.state_transitions.capacity() {
4582            self.state_transitions.remove(0);
4583        }
4584        let _ = self.state_transitions.push(StateTransition {
4585            id,
4586            from,
4587            to,
4588            elapsed_ms: 0,
4589        });
4590    }
4591
4592    fn tick_state_transitions(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4593        if self.state_transitions.is_empty() || self.state_transition_ms == 0 {
4594            return Ok(());
4595        }
4596        let mut i = 0usize;
4597        let mut completed_pressed = heapless::Vec::<WidgetId, NODES>::new();
4598        while i < self.state_transitions.len() {
4599            let mut remove = false;
4600            let id;
4601            let to;
4602            {
4603                let entry = &mut self.state_transitions[i];
4604                entry.elapsed_ms = entry.elapsed_ms.saturating_add(dt_ms);
4605                if entry.elapsed_ms >= self.state_transition_ms {
4606                    remove = true;
4607                }
4608                id = entry.id;
4609                to = entry.to;
4610            }
4611            if let Some(rect) = self.absolute_rect(id) {
4612                self.dirty.add(rect)?;
4613            }
4614            if remove {
4615                if to == VisualState::Pressed {
4616                    let _ = completed_pressed.push(id);
4617                }
4618                self.state_transitions.remove(i);
4619            } else {
4620                i += 1;
4621            }
4622        }
4623        for id in completed_pressed {
4624            // Pointer-held presses keep visual pressed state until release.
4625            if self.pressed.is_some_and(|pressed| pressed.id == id) {
4626                continue;
4627            }
4628            let to = self.resting_visual_state(id);
4629            self.start_state_transition(id, VisualState::Pressed, to);
4630        }
4631        Ok(())
4632    }
4633
4634    fn state_transition_progress(&self, id: WidgetId) -> Option<(VisualState, VisualState, f32)> {
4635        let duration = self.state_transition_ms.max(1);
4636        self.state_transitions
4637            .iter()
4638            .find(|entry| entry.id == id)
4639            .map(|entry| {
4640                let t = (entry.elapsed_ms as f32 / duration as f32).clamp(0.0, 1.0);
4641                (entry.from, entry.to, t)
4642            })
4643    }
4644
4645    fn set_textarea_cursor_visible(&mut self, id: Option<WidgetId>, visible: bool) {
4646        let Some(id) = id else {
4647            return;
4648        };
4649        let Some(rect) = self.absolute_rect(id) else {
4650            return;
4651        };
4652        let Some(node) = self.node_mut(id) else {
4653            return;
4654        };
4655        if let WidgetKind::TextArea {
4656            cursor_visible: ref mut current,
4657            ..
4658        } = node.kind
4659        {
4660            *current = visible;
4661            let _ = self.dirty.add(rect);
4662        }
4663    }
4664
4665    fn tick_textarea_cursor_blink(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4666        let Some(id) = self.focus else {
4667            return Ok(());
4668        };
4669        let is_textarea = matches!(
4670            self.node(id).map(|n| n.kind),
4671            Some(WidgetKind::TextArea { .. })
4672        );
4673        if !is_textarea {
4674            return Ok(());
4675        }
4676        self.textarea_cursor_blink_elapsed_ms =
4677            self.textarea_cursor_blink_elapsed_ms.saturating_add(dt_ms);
4678        if self.textarea_cursor_blink_elapsed_ms < self.textarea_cursor_blink_ms {
4679            return Ok(());
4680        }
4681        self.textarea_cursor_blink_elapsed_ms = 0;
4682        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
4683        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
4684        if let WidgetKind::TextArea {
4685            cursor_visible: ref mut visible,
4686            ..
4687        } = node.kind
4688        {
4689            *visible = !*visible;
4690            self.dirty.add(rect)?;
4691        }
4692        Ok(())
4693    }
4694
4695    fn push_event(&mut self, event: UiEvent) -> Result<(), GuiError> {
4696        if self.should_emit_event(event)? {
4697            self.events.push(event).map_err(|_| GuiError::EventsFull)?;
4698        }
4699        Ok(())
4700    }
4701
4702    fn should_emit_event(&self, event: UiEvent) -> Result<bool, GuiError> {
4703        let Some(target) = event.target() else {
4704            return Ok(true);
4705        };
4706        let filter = self.event_filter(target)?;
4707        Ok(filter.contains(event.filter()))
4708    }
4709
4710    fn stop_due_to_builtin_widget_behavior(&self, event: WidgetEvent) -> bool {
4711        if event.phase != EventPhase::Capture || event.current == event.target {
4712            return false;
4713        }
4714        let is_pointer_kind = matches!(
4715            event.kind,
4716            WidgetEventKind::Pressed | WidgetEventKind::Released | WidgetEventKind::Clicked
4717        );
4718        is_pointer_kind
4719            && self
4720                .node(event.current)
4721                .is_some_and(|node| matches!(node.kind, WidgetKind::ScrollView { .. }))
4722    }
4723
4724    fn stop_due_to_registered_policy(&self, event: WidgetEvent) -> bool {
4725        self.dispatch_policies
4726            .iter()
4727            .find(|(id, _)| *id == event.current)
4728            .is_some_and(|(_, policy)| policy.stop && policy.allows(event.kind, event.phase))
4729    }
4730
4731    fn resting_visual_state(&self, id: WidgetId) -> VisualState {
4732        if !self.effective_enabled(id) {
4733            VisualState::Disabled
4734        } else if Some(id) == self.focus {
4735            VisualState::Focused
4736        } else {
4737            VisualState::Normal
4738        }
4739    }
4740
4741    fn current_visual_state(&self, id: WidgetId) -> VisualState {
4742        if self.pressed.is_some_and(|pressed| pressed.id == id) {
4743            VisualState::Pressed
4744        } else {
4745            self.resting_visual_state(id)
4746        }
4747    }
4748
4749    fn press_timing_for(&self, id: WidgetId) -> PressTiming {
4750        self.widget_press_timings
4751            .iter()
4752            .find(|(timing_id, _)| *timing_id == id)
4753            .map(|(_, timing)| *timing)
4754            .unwrap_or(PressTiming {
4755                long_press_ms: self.long_press_ms,
4756                repeat_delay_ms: self.press_repeat_delay_ms,
4757                repeat_interval_ms: self.press_repeat_interval_ms,
4758            })
4759    }
4760
4761    fn key_input_policy_for(&self, id: WidgetId) -> WidgetKeyInputPolicy {
4762        self.widget_key_policies
4763            .iter()
4764            .find(|(policy_id, _)| *policy_id == id)
4765            .map(|(_, policy)| *policy)
4766            .unwrap_or_default()
4767    }
4768
4769    fn key_bindings_for(&self, id: WidgetId) -> WidgetKeyBindings {
4770        self.widget_key_bindings
4771            .iter()
4772            .find(|(binding_id, _)| *binding_id == id)
4773            .map(|(_, bindings)| *bindings)
4774            .unwrap_or_default()
4775    }
4776
4777    fn handle_select_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4778        if let Some(node) = self.node(id) {
4779            if self.menu_contract.select_toggles_feed_expanded
4780                && matches!(node.kind, WidgetKind::FeedTimeline { .. })
4781            {
4782                let expanded = if let WidgetKind::FeedTimeline { expanded, .. } = node.kind {
4783                    expanded
4784                } else {
4785                    false
4786                };
4787                self.set_feed_expanded(id, !expanded)?;
4788                self.push_event(UiEvent::ValueChanged(id))?;
4789            }
4790        }
4791        let double_select = self.last_select_id == Some(id)
4792            && self.select_elapsed_ms <= self.select_double_window_ms;
4793        self.dispatch_activation(id, false)?;
4794        if double_select {
4795            self.dispatch_double_clicked(id)?;
4796            self.last_select_id = None;
4797            self.select_elapsed_ms = 0;
4798        } else {
4799            self.last_select_id = Some(id);
4800            self.select_elapsed_ms = 0;
4801        }
4802        Ok(())
4803    }
4804
4805    fn handle_back_action(&mut self) -> Result<(), GuiError> {
4806        if let Some(id) = self.focus {
4807            if matches!(
4808                self.node(id).map(|n| n.kind),
4809                Some(WidgetKind::TextArea { .. })
4810            ) {
4811                self.textarea_backspace(id)?;
4812                return Ok(());
4813            }
4814            if matches!(
4815                self.node(id).map(|n| n.kind),
4816                Some(WidgetKind::Dropdown { open: true, .. })
4817            ) && self.menu_contract.back_closes_dropdown
4818            {
4819                self.set_dropdown_open(id, false)?;
4820                return Ok(());
4821            }
4822            if matches!(
4823                self.node(id).map(|n| n.kind),
4824                Some(WidgetKind::NotificationActionSheet { open: true, .. })
4825            ) && self.menu_contract.back_closes_notification_sheet
4826            {
4827                self.set_notification_sheet_open(id, false)?;
4828                return Ok(());
4829            }
4830        }
4831        self.push_event(UiEvent::Back)
4832    }
4833}
4834
4835fn bump_index_with_wrap(current: &mut usize, len: usize, delta: i8, wrap: bool) -> bool {
4836    if len == 0 {
4837        return false;
4838    }
4839    let next = if delta >= 0 {
4840        if *current + 1 >= len {
4841            if wrap { 0 } else { *current }
4842        } else {
4843            *current + 1
4844        }
4845    } else if *current == 0 {
4846        if wrap { len - 1 } else { *current }
4847    } else {
4848        *current - 1
4849    };
4850    if next != *current {
4851        *current = next;
4852        true
4853    } else {
4854        false
4855    }
4856}
4857
4858fn keyboard_char_for_layout(
4859    keys: &[char],
4860    alt_keys: Option<&[char]>,
4861    selected: usize,
4862    layout: KeyboardLayout,
4863) -> Option<char> {
4864    let base = keys.get(selected).copied()?;
4865    Some(match layout {
4866        KeyboardLayout::Normal => base,
4867        KeyboardLayout::Shift => {
4868            if base.is_ascii_alphabetic() {
4869                base.to_ascii_uppercase()
4870            } else {
4871                base
4872            }
4873        }
4874        KeyboardLayout::Symbols => alt_keys
4875            .and_then(|keys| keys.get(selected).copied())
4876            .unwrap_or('#'),
4877    })
4878}
4879
4880fn textarea_text(buf: &[u8; TEXTAREA_CAPACITY], len: u8) -> &str {
4881    let used = (len as usize).min(TEXTAREA_CAPACITY);
4882    core::str::from_utf8(&buf[..used]).unwrap_or("")
4883}
4884
4885fn textarea_storage_from_str(text: &str) -> ([u8; TEXTAREA_CAPACITY], u8) {
4886    let mut out = [0u8; TEXTAREA_CAPACITY];
4887    let mut len = 0usize;
4888    for ch in text.chars() {
4889        let mut tmp = [0u8; 4];
4890        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4891        if len + enc.len() > TEXTAREA_CAPACITY {
4892            break;
4893        }
4894        out[len..len + enc.len()].copy_from_slice(enc);
4895        len += enc.len();
4896    }
4897    (out, len as u8)
4898}
4899
4900fn textarea_storage_from_chars(
4901    chars: &heapless::Vec<char, TEXTAREA_CAPACITY>,
4902) -> ([u8; TEXTAREA_CAPACITY], u8) {
4903    let mut out = [0u8; TEXTAREA_CAPACITY];
4904    let mut len = 0usize;
4905    for ch in chars {
4906        let mut tmp = [0u8; 4];
4907        let enc = ch.encode_utf8(&mut tmp).as_bytes();
4908        if len + enc.len() > TEXTAREA_CAPACITY {
4909            break;
4910        }
4911        out[len..len + enc.len()].copy_from_slice(enc);
4912        len += enc.len();
4913    }
4914    (out, len as u8)
4915}
4916
4917fn char_at(text: &str, idx: usize) -> Option<char> {
4918    text.chars().nth(idx)
4919}
4920
4921fn prev_word_boundary(text: &str, cursor: usize) -> usize {
4922    let mut pos = cursor.min(text.chars().count());
4923    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| ch.is_whitespace()) {
4924        pos -= 1;
4925    }
4926    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| !ch.is_whitespace()) {
4927        pos -= 1;
4928    }
4929    pos
4930}
4931
4932fn next_word_boundary(text: &str, cursor: usize) -> usize {
4933    let len = text.chars().count();
4934    let mut pos = cursor.min(len);
4935    while pos < len && char_at(text, pos).is_some_and(|ch| !ch.is_whitespace()) {
4936        pos += 1;
4937    }
4938    while pos < len && char_at(text, pos).is_some_and(|ch| ch.is_whitespace()) {
4939        pos += 1;
4940    }
4941    pos
4942}
4943
4944fn delete_selection_if_any(
4945    chars: &mut heapless::Vec<char, TEXTAREA_CAPACITY>,
4946    cursor: &mut usize,
4947    selection: &mut Option<(usize, usize)>,
4948) -> bool {
4949    let Some((start, end)) = *selection else {
4950        return false;
4951    };
4952    let start = start.min(end).min(chars.len());
4953    let end = end.max(start).min(chars.len());
4954    if end <= start {
4955        *selection = None;
4956        *cursor = start;
4957        return false;
4958    }
4959    for _ in start..end {
4960        chars.remove(start);
4961    }
4962    *cursor = start;
4963    *selection = None;
4964    true
4965}
4966
4967fn textarea_row_col_at_cursor(text: &str, cursor: usize, wrap_cols: usize) -> (usize, usize) {
4968    let mut row = 0usize;
4969    let mut col = 0usize;
4970    for ch in text.chars().take(cursor) {
4971        if ch == '\n' {
4972            row += 1;
4973            col = 0;
4974            continue;
4975        }
4976        col += 1;
4977        if col >= wrap_cols {
4978            row += 1;
4979            col = 0;
4980        }
4981    }
4982    (row, col)
4983}
4984
4985fn textarea_cursor_from_row_col(
4986    text: &str,
4987    target_row: usize,
4988    target_col: usize,
4989    wrap_cols: usize,
4990) -> usize {
4991    let mut row = 0usize;
4992    let mut col = 0usize;
4993    let mut idx = 0usize;
4994    for ch in text.chars() {
4995        if row == target_row && col >= target_col {
4996            break;
4997        }
4998        if ch == '\n' {
4999            if row == target_row {
5000                break;
5001            }
5002            row += 1;
5003            col = 0;
5004            idx += 1;
5005            continue;
5006        }
5007        idx += 1;
5008        col += 1;
5009        if col >= wrap_cols {
5010            if row == target_row {
5011                break;
5012            }
5013            row += 1;
5014            col = 0;
5015        }
5016    }
5017    idx
5018}
5019
5020fn textarea_row_end_col(text: &str, target_row: usize, wrap_cols: usize) -> usize {
5021    let mut row = 0usize;
5022    let mut col = 0usize;
5023    for ch in text.chars() {
5024        if row == target_row {
5025            if ch == '\n' {
5026                break;
5027            }
5028            col += 1;
5029            if col >= wrap_cols {
5030                break;
5031            }
5032        } else if ch == '\n' {
5033            row += 1;
5034            col = 0;
5035        } else {
5036            col += 1;
5037            if col >= wrap_cols {
5038                row += 1;
5039                col = 0;
5040            }
5041        }
5042    }
5043    col
5044}