Skip to main content

embedded_gui/context/
input.rs

1#[cfg(not(feature = "std"))]
2use crate::math::F32Ext as _;
3use crate::{
4    geometry::Rect,
5    input::{
6        InputEvent, PointerState, UiEvent, UiEventFilter, WidgetDispatchPolicy, WidgetEvent,
7        WidgetEventKind,
8    },
9    state::{FeedTimelineState, ListState, ScrollState, SliderState, TabsState},
10    style::{VisualState, WidgetStyle},
11    widget::{EventPhase, EventPolicy, WidgetId},
12    widgets::{KeyboardLayout, TEXTAREA_CAPACITY, WidgetKind, WidgetNode},
13};
14
15use super::*;
16
17impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
18    GuiContext<'a, NODES, EVENTS, DIRTY>
19{
20    pub fn handle_input(&mut self, event: InputEvent) -> Result<(), GuiError> {
21        match event {
22            InputEvent::Home => {
23                if let Some(id) = self.focus {
24                    #[cfg(feature = "rich-widgets")]
25                    if matches!(
26                        self.node(id).map(|n| n.kind),
27                        Some(WidgetKind::TextArea { .. })
28                    ) {
29                        self.set_textarea_cursor_line_home(id)?;
30                        return Ok(());
31                    }
32                }
33                Ok(())
34            }
35            InputEvent::End => {
36                if let Some(id) = self.focus {
37                    #[cfg(feature = "rich-widgets")]
38                    if matches!(
39                        self.node(id).map(|n| n.kind),
40                        Some(WidgetKind::TextArea { .. })
41                    ) {
42                        self.set_textarea_cursor_line_end(id)?;
43                        return Ok(());
44                    }
45                }
46                Ok(())
47            }
48            InputEvent::WordLeft => {
49                if let Some(id) = self.focus {
50                    #[cfg(feature = "rich-widgets")]
51                    if matches!(
52                        self.node(id).map(|n| n.kind),
53                        Some(WidgetKind::TextArea { .. })
54                    ) {
55                        self.move_textarea_cursor_word(id, -1)?;
56                        return Ok(());
57                    }
58                }
59                Ok(())
60            }
61            InputEvent::WordRight => {
62                if let Some(id) = self.focus {
63                    #[cfg(feature = "rich-widgets")]
64                    if matches!(
65                        self.node(id).map(|n| n.kind),
66                        Some(WidgetKind::TextArea { .. })
67                    ) {
68                        self.move_textarea_cursor_word(id, 1)?;
69                        return Ok(());
70                    }
71                }
72                Ok(())
73            }
74            InputEvent::Undo => {
75                if let Some(id) = self.focus {
76                    #[cfg(feature = "rich-widgets")]
77                    if matches!(
78                        self.node(id).map(|n| n.kind),
79                        Some(WidgetKind::TextArea { .. })
80                    ) {
81                        self.textarea_undo(id)?;
82                    }
83                }
84                Ok(())
85            }
86            InputEvent::Redo => {
87                if let Some(id) = self.focus {
88                    #[cfg(feature = "rich-widgets")]
89                    if matches!(
90                        self.node(id).map(|n| n.kind),
91                        Some(WidgetKind::TextArea { .. })
92                    ) {
93                        self.textarea_redo(id)?;
94                    }
95                }
96                Ok(())
97            }
98            InputEvent::SelectLeft => {
99                if let Some(id) = self.focus {
100                    #[cfg(feature = "rich-widgets")]
101                    if matches!(
102                        self.node(id).map(|n| n.kind),
103                        Some(WidgetKind::TextArea { .. })
104                    ) {
105                        self.move_textarea_cursor_select(id, -1)?;
106                        return Ok(());
107                    }
108                }
109                Ok(())
110            }
111            InputEvent::SelectRight => {
112                if let Some(id) = self.focus {
113                    #[cfg(feature = "rich-widgets")]
114                    if matches!(
115                        self.node(id).map(|n| n.kind),
116                        Some(WidgetKind::TextArea { .. })
117                    ) {
118                        self.move_textarea_cursor_select(id, 1)?;
119                        return Ok(());
120                    }
121                }
122                Ok(())
123            }
124            InputEvent::SelectHome => {
125                if let Some(id) = self.focus {
126                    #[cfg(feature = "rich-widgets")]
127                    if matches!(
128                        self.node(id).map(|n| n.kind),
129                        Some(WidgetKind::TextArea { .. })
130                    ) {
131                        self.set_textarea_cursor_line_home_select(id)?;
132                        return Ok(());
133                    }
134                }
135                Ok(())
136            }
137            InputEvent::SelectEnd => {
138                if let Some(id) = self.focus {
139                    #[cfg(feature = "rich-widgets")]
140                    if matches!(
141                        self.node(id).map(|n| n.kind),
142                        Some(WidgetKind::TextArea { .. })
143                    ) {
144                        self.set_textarea_cursor_line_end_select(id)?;
145                        return Ok(());
146                    }
147                }
148                Ok(())
149            }
150            InputEvent::SelectWordLeft => {
151                if let Some(id) = self.focus {
152                    #[cfg(feature = "rich-widgets")]
153                    if matches!(
154                        self.node(id).map(|n| n.kind),
155                        Some(WidgetKind::TextArea { .. })
156                    ) {
157                        self.move_textarea_cursor_word_select(id, -1)?;
158                        return Ok(());
159                    }
160                }
161                Ok(())
162            }
163            InputEvent::SelectWordRight => {
164                if let Some(id) = self.focus {
165                    #[cfg(feature = "rich-widgets")]
166                    if matches!(
167                        self.node(id).map(|n| n.kind),
168                        Some(WidgetKind::TextArea { .. })
169                    ) {
170                        self.move_textarea_cursor_word_select(id, 1)?;
171                        return Ok(());
172                    }
173                }
174                Ok(())
175            }
176            InputEvent::Up => {
177                if !self.adjust_focused_selection(-1)? {
178                    self.focus_prev()?;
179                }
180                Ok(())
181            }
182            InputEvent::Down => {
183                if !self.adjust_focused_selection(1)? {
184                    self.focus_next()?;
185                }
186                Ok(())
187            }
188            InputEvent::Left => {
189                if !self.adjust_focused_scalar(-1.0)? {
190                    self.focus_prev()?;
191                }
192                Ok(())
193            }
194            InputEvent::Right => {
195                if !self.adjust_focused_scalar(1.0)? {
196                    self.focus_next()?;
197                }
198                Ok(())
199            }
200            InputEvent::Encoder { delta } if delta > 0 => {
201                if !self.adjust_focused_selection(1)? {
202                    self.focus_next()?;
203                }
204                Ok(())
205            }
206            InputEvent::Encoder { delta } if delta < 0 => {
207                if !self.adjust_focused_selection(-1)? {
208                    self.focus_prev()?;
209                }
210                Ok(())
211            }
212            InputEvent::Select => {
213                if let Some(id) = self.focus {
214                    match self.key_bindings_for(id).select {
215                        KeyBindingAction::Default | KeyBindingAction::Activate => {
216                            self.handle_select_activation(id)?
217                        }
218                        KeyBindingAction::Back => self.handle_back_action()?,
219                        KeyBindingAction::Ignore => {}
220                    }
221                }
222                Ok(())
223            }
224            InputEvent::SelectPressed => {
225                if let Some(id) = self.focus {
226                    if self.key_input_policy_for(id).raw_select {
227                        self.dispatch_key_pressed(id)?;
228                    }
229                }
230                Ok(())
231            }
232            InputEvent::SelectReleased => {
233                if let Some(id) = self.focus {
234                    if self.key_input_policy_for(id).raw_select {
235                        self.dispatch_key_released(id)?;
236                        self.handle_select_activation(id)?;
237                    }
238                }
239                Ok(())
240            }
241            InputEvent::Back => {
242                if let Some(id) = self.focus {
243                    match self.key_bindings_for(id).back {
244                        KeyBindingAction::Default | KeyBindingAction::Back => {
245                            self.handle_back_action()
246                        }
247                        KeyBindingAction::Activate => self.handle_select_activation(id),
248                        KeyBindingAction::Ignore => Ok(()),
249                    }
250                } else {
251                    self.handle_back_action()
252                }
253            }
254            InputEvent::BackPressed => {
255                if let Some(id) = self.focus {
256                    if self.key_input_policy_for(id).raw_back {
257                        self.dispatch_key_pressed(id)?;
258                    }
259                }
260                Ok(())
261            }
262            InputEvent::BackReleased => {
263                if let Some(id) = self.focus {
264                    if self.key_input_policy_for(id).raw_back {
265                        self.dispatch_key_released(id)?;
266                        return self.handle_back_action();
267                    }
268                }
269                Ok(())
270            }
271            InputEvent::Pointer {
272                x,
273                y,
274                state: PointerState::Pressed,
275                ..
276            } => self.handle_pointer_pressed(x, y),
277            InputEvent::Pointer {
278                x,
279                y,
280                state: PointerState::Released,
281                ..
282            } => self.handle_pointer_released(x, y),
283            InputEvent::Pointer {
284                x,
285                y,
286                state: PointerState::Moved,
287                ..
288            } => self.handle_pointer_moved(x, y),
289            _ => Ok(()),
290        }
291    }
292
293    pub fn tick_input(&mut self, dt_ms: u32) -> Result<(), GuiError> {
294        if self.last_select_id.is_some() {
295            self.select_elapsed_ms = self.select_elapsed_ms.saturating_add(dt_ms);
296            if self.select_elapsed_ms > self.select_double_window_ms {
297                self.last_select_id = None;
298                self.select_elapsed_ms = 0;
299            }
300        }
301        if self.last_pointer_id.is_some() {
302            self.pointer_elapsed_ms = self.pointer_elapsed_ms.saturating_add(dt_ms);
303            if self.pointer_elapsed_ms > self.pointer_double_window_ms {
304                self.last_pointer_id = None;
305                self.pointer_elapsed_ms = 0;
306            }
307        }
308        self.tick_state_transitions(dt_ms)?;
309        #[cfg(feature = "rich-widgets")]
310        if let Some(mut inertia) = self.inertia_scroll {
311            if inertia.velocity.abs() < self.scroll_physics.velocity_threshold {
312                self.inertia_scroll = None;
313            } else {
314                let current = self.scroll_offset(inertia.id).unwrap_or(0);
315                let delta = (inertia.velocity * (dt_ms as f32 / 16.0)).round() as i32;
316                if delta != 0 {
317                    let next = current.saturating_sub(delta);
318                    if next != current {
319                        self.set_scroll_offset(inertia.id, next)?;
320                        self.push_event(UiEvent::Scroll {
321                            id: inertia.id,
322                            delta: next - current,
323                        })?;
324                    }
325                }
326                inertia.velocity *= self
327                    .scroll_physics
328                    .velocity_decay
329                    .powf((dt_ms as f32 / 16.0).max(1.0));
330                self.inertia_scroll = Some(inertia);
331            }
332        }
333        self.tick_textarea_cursor_blink(dt_ms)?;
334        let Some(mut pressed) = self.pressed else {
335            return Ok(());
336        };
337        if !self.effective_visible(pressed.id) || !self.effective_enabled(pressed.id) {
338            self.pressed = None;
339            return Ok(());
340        }
341        let timing = self.press_timing_for(pressed.id);
342        pressed.elapsed_ms = pressed.elapsed_ms.saturating_add(dt_ms);
343        pressed.repeat_elapsed_ms = pressed.repeat_elapsed_ms.saturating_add(dt_ms);
344        if !pressed.long_emitted && pressed.elapsed_ms >= timing.long_press_ms {
345            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
346            self.dispatch_widget_event(
347                pressed.id,
348                WidgetEventKind::LongPressed,
349                &mut events,
350                |_| EventPolicy::Continue,
351            )?;
352            self.push_event(UiEvent::LongPressed(pressed.id))?;
353            pressed.long_emitted = true;
354        }
355        if pressed.repeat_elapsed_ms >= timing.repeat_delay_ms
356            && self.repeatable_widget(pressed.id)
357            && pressed.long_emitted
358        {
359            let intervals =
360                (pressed.repeat_elapsed_ms - timing.repeat_delay_ms) / timing.repeat_interval_ms;
361            if intervals > 0 {
362                self.dispatch_repeat_activation(pressed.id)?;
363                pressed.repeat_elapsed_ms = timing.repeat_delay_ms;
364            }
365        }
366        self.pressed = Some(pressed);
367        Ok(())
368    }
369
370    pub fn pop_event(&mut self) -> Option<UiEvent> {
371        if self.events.is_empty() {
372            None
373        } else {
374            Some(self.events.remove(0))
375        }
376    }
377
378    pub fn set_event_filter(
379        &mut self,
380        id: WidgetId,
381        filter: UiEventFilter,
382    ) -> Result<(), GuiError> {
383        self.node(id).ok_or(GuiError::NotFound)?;
384        if let Some((_, current)) = self
385            .subscriptions
386            .iter_mut()
387            .find(|(sub_id, _)| *sub_id == id)
388        {
389            *current = filter;
390            return Ok(());
391        }
392        self.subscriptions
393            .push((id, filter))
394            .map_err(|_| GuiError::WidgetsFull)
395    }
396
397    pub fn event_filter(&self, id: WidgetId) -> Result<UiEventFilter, GuiError> {
398        self.node(id).ok_or(GuiError::NotFound)?;
399        Ok(self
400            .subscriptions
401            .iter()
402            .find(|(sub_id, _)| *sub_id == id)
403            .map(|(_, filter)| *filter)
404            .unwrap_or(UiEventFilter::ALL))
405    }
406
407    pub fn clear_event_filter(&mut self, id: WidgetId) -> Result<(), GuiError> {
408        self.node(id).ok_or(GuiError::NotFound)?;
409        if let Some(pos) = self
410            .subscriptions
411            .iter()
412            .position(|(sub_id, _)| *sub_id == id)
413        {
414            self.subscriptions.remove(pos);
415        }
416        Ok(())
417    }
418
419    pub fn set_dispatch_policy(
420        &mut self,
421        id: WidgetId,
422        policy: WidgetDispatchPolicy,
423    ) -> Result<(), GuiError> {
424        self.node(id).ok_or(GuiError::NotFound)?;
425        if let Some((_, current)) = self
426            .dispatch_policies
427            .iter_mut()
428            .find(|(policy_id, _)| *policy_id == id)
429        {
430            *current = policy;
431            return Ok(());
432        }
433        self.dispatch_policies
434            .push((id, policy))
435            .map_err(|_| GuiError::WidgetsFull)
436    }
437
438    pub fn dispatch_policy(&self, id: WidgetId) -> Result<Option<WidgetDispatchPolicy>, GuiError> {
439        self.node(id).ok_or(GuiError::NotFound)?;
440        Ok(self
441            .dispatch_policies
442            .iter()
443            .find(|(policy_id, _)| *policy_id == id)
444            .map(|(_, policy)| *policy))
445    }
446
447    pub fn clear_dispatch_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
448        self.node(id).ok_or(GuiError::NotFound)?;
449        if let Some(pos) = self
450            .dispatch_policies
451            .iter()
452            .position(|(policy_id, _)| *policy_id == id)
453        {
454            self.dispatch_policies.remove(pos);
455        }
456        Ok(())
457    }
458
459    pub(crate) fn add_widget<S>(
460        &mut self,
461        rect: Rect,
462        kind: WidgetKind<'a>,
463        style: S,
464    ) -> Result<WidgetId, GuiError>
465    where
466        S: Into<WidgetStyle>,
467    {
468        let id = WidgetId::new(self.next_id);
469        self.next_id = self.next_id.saturating_add(1).max(1);
470        let node = WidgetNode::new(id, rect, kind, style);
471        self.widgets.push(node).map_err(|_| GuiError::WidgetsFull)?;
472        self.dirty.add(rect)?;
473        Ok(id)
474    }
475
476    pub(crate) fn node(&self, id: WidgetId) -> Option<&WidgetNode<'a>> {
477        self.widgets.iter().find(|node| node.id == id)
478    }
479
480    pub(crate) fn intrinsic_size(&self, id: WidgetId) -> Option<(u32, u32)> {
481        let node = self.node(id)?;
482        let style = node.style.resolve(VisualState::Normal);
483        let pad_x = style.padding.left.max(0) as u32 + style.padding.right.max(0) as u32;
484        let pad_y = style.padding.top.max(0) as u32 + style.padding.bottom.max(0) as u32;
485        let border = style.border.width as u32 * 2;
486        let text_width = |text: &str| text.chars().count() as u32 * style.font.advance();
487        let text_height = style.font.line_height();
488
489        let content = match node.kind {
490            WidgetKind::Label(text) => (text_width(text), text_height),
491            WidgetKind::Button(text) => (text_width(text).saturating_add(6), text_height),
492            #[cfg(feature = "rich-widgets")]
493            WidgetKind::Toggle { label, .. } => (text_width(label).saturating_add(12), text_height),
494            #[cfg(feature = "rich-widgets")]
495            WidgetKind::Checkbox { label, .. } => {
496                (text_width(label).saturating_add(10), text_height)
497            }
498            #[cfg(feature = "rich-widgets")]
499            WidgetKind::ValueLabel { label, .. } => {
500                (text_width(label).saturating_add(16), text_height)
501            }
502            #[cfg(feature = "rich-widgets")]
503            WidgetKind::IconButton { label, .. } => {
504                (text_width(label).saturating_add(10), text_height)
505            }
506            #[cfg(feature = "rich-widgets")]
507            WidgetKind::Tabs { labels, .. } => {
508                let max = labels.iter().map(|s| text_width(s)).max().unwrap_or(0);
509                (
510                    max.saturating_mul(labels.len() as u32).saturating_add(4),
511                    text_height,
512                )
513            }
514            #[cfg(feature = "rich-widgets")]
515            WidgetKind::Dialog { title, body } => {
516                let w = text_width(title).max(text_width(body)).saturating_add(8);
517                (w, text_height.saturating_mul(3))
518            }
519            #[cfg(feature = "rich-widgets")]
520            WidgetKind::Toast { text, .. } => (
521                text_width(text).saturating_add(8),
522                text_height.saturating_add(2),
523            ),
524            #[cfg(feature = "rich-widgets")]
525            WidgetKind::Dropdown {
526                items, selected, ..
527            } => (
528                text_width(items.get(selected).copied().unwrap_or("-")).saturating_add(10),
529                text_height.saturating_add(2),
530            ),
531            #[cfg(feature = "rich-widgets")]
532            WidgetKind::TextArea {
533                text_buf,
534                text_len,
535                placeholder,
536                ..
537            } => (
538                text_width(if text_len == 0 {
539                    placeholder
540                } else {
541                    textarea_text(&text_buf, text_len)
542                })
543                .saturating_add(10),
544                text_height.saturating_add(4),
545            ),
546            #[cfg(feature = "rich-widgets")]
547            WidgetKind::Keyboard { keys, cols, .. } => {
548                let cols = cols.max(1) as u32;
549                let rows = (keys.len() as u32).div_ceil(cols).max(1);
550                (
551                    cols.saturating_mul(style.font.advance().saturating_add(4)),
552                    rows.saturating_mul(style.font.line_height().saturating_add(4)),
553                )
554            }
555            #[cfg(feature = "rich-widgets")]
556            WidgetKind::List {
557                items,
558                visible_rows,
559                ..
560            } => {
561                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
562                (
563                    max.saturating_add(6),
564                    (text_height.saturating_add(2))
565                        .saturating_mul(visible_rows as u32)
566                        .max(text_height),
567                )
568            }
569            #[cfg(feature = "rich-widgets")]
570            WidgetKind::Menu { items, .. } => {
571                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
572                (
573                    max.saturating_add(6),
574                    (text_height.saturating_add(2))
575                        .saturating_mul(items.len() as u32)
576                        .max(text_height),
577                )
578            }
579            #[cfg(feature = "rich-widgets")]
580            WidgetKind::FeedTimeline {
581                items,
582                visible_rows,
583                expanded,
584                ..
585            } => {
586                let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
587                let row_h = if expanded {
588                    text_height.saturating_mul(2).saturating_add(2)
589                } else {
590                    text_height.saturating_add(2)
591                };
592                (
593                    max.saturating_add(8),
594                    row_h.saturating_mul(visible_rows as u32).max(text_height),
595                )
596            }
597            _ => (node.rect.w.max(1), node.rect.h.max(1)),
598        };
599
600        Some((
601            content
602                .0
603                .saturating_add(pad_x)
604                .saturating_add(border)
605                .max(1),
606            content
607                .1
608                .saturating_add(pad_y)
609                .saturating_add(border)
610                .max(1),
611        ))
612    }
613
614    pub(crate) fn node_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode<'a>> {
615        self.widgets.iter_mut().find(|node| node.id == id)
616    }
617
618    pub(crate) fn effective_visible(&self, id: WidgetId) -> bool {
619        let mut current = Some(id);
620        let mut depth = 0;
621        while let Some(widget_id) = current {
622            if depth >= NODES {
623                return false;
624            }
625            let Some(node) = self.node(widget_id) else {
626                return false;
627            };
628            if node.hidden() {
629                return false;
630            }
631            current = node.parent;
632            depth += 1;
633        }
634        true
635    }
636
637    pub(crate) fn inherited_clip(&self, id: WidgetId) -> Option<Rect> {
638        let mut clip = self.viewport;
639        let mut chain = heapless::Vec::<WidgetId, NODES>::new();
640        let mut current = Some(id);
641        while let Some(widget_id) = current {
642            chain.push(widget_id).ok()?;
643            current = self.node(widget_id)?.parent;
644        }
645        for widget_id in chain.iter().rev().copied() {
646            let node = self.node(widget_id)?;
647            if widget_id == id || node.clips_children() {
648                clip = clip.intersection(self.absolute_rect(widget_id)?);
649            }
650            if clip.is_empty() {
651                return None;
652            }
653        }
654        Some(clip)
655    }
656
657    pub(crate) fn effective_enabled(&self, id: WidgetId) -> bool {
658        let mut current = Some(id);
659        let mut depth = 0;
660        while let Some(widget_id) = current {
661            if depth >= NODES {
662                return false;
663            }
664            let Some(node) = self.node(widget_id) else {
665                return false;
666            };
667            if node.disabled() {
668                return false;
669            }
670            current = node.parent;
671            depth += 1;
672        }
673        true
674    }
675
676    pub(crate) fn effective_focusable(&self, id: WidgetId) -> bool {
677        self.node(id).is_some_and(|node| {
678            self.node_in_active_group(node)
679                && node.focusable()
680                && self.effective_visible(id)
681                && self.effective_enabled(id)
682        })
683    }
684
685    pub(crate) fn ensure_focus(&mut self) {
686        if self.focus.is_none() {
687            self.focus = self
688                .widgets
689                .iter()
690                .find(|node| self.effective_focusable(node.id))
691                .map(|n| n.id);
692        }
693    }
694
695    pub(crate) fn focus_next(&mut self) -> Result<(), GuiError> {
696        self.move_focus(1)
697    }
698
699    pub(crate) fn focus_prev(&mut self) -> Result<(), GuiError> {
700        self.move_focus(-1)
701    }
702
703    pub(crate) fn move_focus(&mut self, delta: i8) -> Result<(), GuiError> {
704        let focusable = self
705            .widgets
706            .iter()
707            .filter(|node| self.effective_focusable(node.id))
708            .count();
709        if focusable == 0 {
710            return Ok(());
711        }
712
713        let current_pos = self
714            .widgets
715            .iter()
716            .filter(|node| self.effective_focusable(node.id))
717            .position(|node| Some(node.id) == self.focus)
718            .unwrap_or(0);
719
720        let next_pos = if delta >= 0 {
721            (current_pos + 1) % focusable
722        } else if current_pos == 0 {
723            focusable - 1
724        } else {
725            current_pos - 1
726        };
727
728        let next = self
729            .widgets
730            .iter()
731            .filter(|node| self.effective_focusable(node.id))
732            .nth(next_pos)
733            .map(|node| node.id);
734        self.set_focus(next)
735    }
736
737    /// Stub for the `rich-widgets`-off build: none of the gated kinds this
738    /// handles (List/Menu/Dropdown/Roller/ScrollView/FeedTimeline/Keyboard)
739    /// exist, so there's nothing to adjust -- always fall through to the
740    /// caller's plain focus_prev()/focus_next().
741    #[cfg(not(feature = "rich-widgets"))]
742    pub(crate) fn adjust_focused_selection(&mut self, _delta: i8) -> Result<bool, GuiError> {
743        Ok(false)
744    }
745
746    #[cfg(feature = "rich-widgets")]
747    pub(crate) fn adjust_focused_selection(&mut self, delta: i8) -> Result<bool, GuiError> {
748        let Some(id) = self.focus else {
749            return Ok(false);
750        };
751        let wrap_navigation = self.menu_contract.wrap_navigation;
752
753        let mut changed_rect = None;
754        let mut changed = false;
755
756        if let Some(node) = self.node_mut(id) {
757            match node.kind {
758                WidgetKind::Menu {
759                    items,
760                    selected: ref mut current,
761                } => {
762                    if items.is_empty() {
763                        return Ok(true);
764                    }
765                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
766                    changed_rect = changed.then_some(node.rect);
767                }
768                WidgetKind::Dropdown {
769                    items,
770                    selected: ref mut current,
771                    open,
772                } => {
773                    if !open {
774                        return Ok(false);
775                    }
776                    if items.is_empty() {
777                        return Ok(true);
778                    }
779                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
780                    changed_rect = changed.then_some(node.rect);
781                }
782                WidgetKind::Roller {
783                    items,
784                    selected: ref mut current,
785                } => {
786                    if items.is_empty() {
787                        return Ok(true);
788                    }
789                    changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
790                    changed_rect = changed.then_some(node.rect);
791                }
792                WidgetKind::Keyboard {
793                    keys,
794                    selected: ref mut current,
795                    ..
796                } => {
797                    if keys.is_empty() {
798                        return Ok(true);
799                    }
800                    changed = bump_index_with_wrap(current, keys.len(), delta, wrap_navigation);
801                    changed_rect = changed.then_some(node.rect);
802                }
803                WidgetKind::List {
804                    items,
805                    selected: ref mut current,
806                    ref mut offset,
807                    visible_rows,
808                } => {
809                    if items.is_empty() {
810                        return Ok(true);
811                    }
812                    let mut state = ListState::new(*current, *offset, visible_rows);
813                    let mut next = state.selected;
814                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
815                    if changed {
816                        let _ = state.set_selected(next, items.len());
817                    }
818                    *current = state.selected;
819                    *offset = state.offset;
820                    changed_rect = changed.then_some(node.rect);
821                }
822                WidgetKind::FeedTimeline {
823                    items,
824                    selected: ref mut current,
825                    ref mut offset,
826                    visible_rows,
827                    expanded,
828                } => {
829                    if items.is_empty() {
830                        return Ok(true);
831                    }
832                    let mut state =
833                        FeedTimelineState::new(*current, *offset, visible_rows, expanded);
834                    let mut next = state.selected;
835                    changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
836                    if changed {
837                        let _ = state.set_selected(next, items.len());
838                    }
839                    *current = state.selected;
840                    *offset = state.offset;
841                    changed_rect = changed.then_some(node.rect);
842                }
843                WidgetKind::ScrollView {
844                    offset_y: ref mut offset,
845                    content_h,
846                } => {
847                    let mut state = ScrollState::new(*offset, content_h);
848                    changed = state.scroll_by(delta as i32 * 8);
849                    *offset = state.offset_y;
850                    changed_rect = changed.then_some(node.rect);
851                }
852                _ => return Ok(false),
853            }
854        }
855
856        if let Some(rect) = changed_rect {
857            self.dirty.add(rect)?;
858        }
859        if changed {
860            self.push_event(UiEvent::ValueChanged(id))?;
861        }
862        Ok(true)
863    }
864
865    /// Stub for the `rich-widgets`-off build: none of the gated kinds this
866    /// handles (Slider/Tabs/TextArea) exist, so always fall through to the
867    /// caller's plain focus_prev()/focus_next().
868    #[cfg(not(feature = "rich-widgets"))]
869    pub(crate) fn adjust_focused_scalar(&mut self, _direction: f32) -> Result<bool, GuiError> {
870        Ok(false)
871    }
872
873    #[cfg(feature = "rich-widgets")]
874    pub(crate) fn adjust_focused_scalar(&mut self, direction: f32) -> Result<bool, GuiError> {
875        let Some(id) = self.focus else {
876            return Ok(false);
877        };
878
879        let mut changed_rect = None;
880        let mut changed = false;
881
882        if let Some(node) = self.node_mut(id) {
883            match node.kind {
884                WidgetKind::Slider {
885                    value: ref mut current,
886                    min,
887                    max,
888                } => {
889                    let mut state = SliderState::new(*current, min, max);
890                    changed = state.step_by(direction);
891                    *current = state.value;
892                    changed_rect = changed.then_some(node.rect);
893                }
894                WidgetKind::Tabs {
895                    labels,
896                    selected: ref mut current,
897                } => {
898                    if labels.is_empty() {
899                        return Ok(true);
900                    }
901                    let mut state = TabsState::new(*current);
902                    changed = state.bump(labels.len(), if direction >= 0.0 { 1 } else { -1 });
903                    *current = state.selected;
904                    changed_rect = changed.then_some(node.rect);
905                }
906                WidgetKind::TextArea {
907                    text_buf,
908                    text_len,
909                    cursor: ref mut current,
910                    ..
911                } => {
912                    let text = textarea_text(&text_buf, text_len);
913                    let len = text.chars().count();
914                    if direction >= 0.0 {
915                        let next = (*current + 1).min(len);
916                        changed = next != *current;
917                        *current = next;
918                    } else {
919                        let next = current.saturating_sub(1);
920                        changed = next != *current;
921                        *current = next;
922                    }
923                    changed_rect = changed.then_some(node.rect);
924                }
925                _ => return Ok(false),
926            }
927        }
928
929        if let Some(rect) = changed_rect {
930            self.dirty.add(rect)?;
931        }
932        if changed {
933            self.push_event(UiEvent::ValueChanged(id))?;
934        }
935        Ok(true)
936    }
937
938    /// Stub for the `rich-widgets`-off build: none of the gated kinds this
939    /// handles (Checkbox/Dropdown/Keyboard/Toggle) exist, so activation for
940    /// whatever's focused is just the generic click dispatch already done
941    /// by the caller.
942    #[cfg(not(feature = "rich-widgets"))]
943    pub(crate) fn activate_focused(&mut self, _id: WidgetId) -> Result<(), GuiError> {
944        Ok(())
945    }
946
947    #[cfg(feature = "rich-widgets")]
948    pub(crate) fn activate_focused(&mut self, id: WidgetId) -> Result<(), GuiError> {
949        let mut changed_rect = None;
950        let mut changed = false;
951        let mut dropdown_state_event = None;
952        let select_opens_dropdown = self.menu_contract.select_opens_dropdown;
953
954        if let Some(node) = self.node_mut(id) {
955            match node.kind {
956                WidgetKind::Toggle { on: ref mut v, .. } => {
957                    *v = !*v;
958                    changed = true;
959                    changed_rect = Some(node.rect);
960                }
961                WidgetKind::Checkbox {
962                    checked: ref mut v, ..
963                } => {
964                    *v = !*v;
965                    changed = true;
966                    changed_rect = Some(node.rect);
967                }
968                WidgetKind::Keyboard {
969                    keys,
970                    alt_keys,
971                    selected,
972                    layout,
973                    target,
974                    ..
975                } => {
976                    if let Some(ch) = keyboard_char_for_layout(keys, alt_keys, selected, layout) {
977                        changed = true;
978                        changed_rect = Some(node.rect);
979                        if let Some(target) = target {
980                            let _ = self.push_event(UiEvent::TextInput { id: target, ch });
981                            let _ = self.push_event(UiEvent::ValueChanged(target));
982                        }
983                    }
984                }
985                WidgetKind::Dropdown {
986                    open: ref mut is_open,
987                    ..
988                } if select_opens_dropdown => {
989                    *is_open = !*is_open;
990                    changed = true;
991                    changed_rect = Some(node.rect);
992                    dropdown_state_event = Some(*is_open);
993                }
994                _ => {}
995            }
996        }
997
998        if let Some(open) = dropdown_state_event {
999            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1000            self.dispatch_widget_event(
1001                id,
1002                if open {
1003                    WidgetEventKind::Opened
1004                } else {
1005                    WidgetEventKind::Closed
1006                },
1007                &mut events,
1008                |_| EventPolicy::Continue,
1009            )?;
1010            self.push_event(if open {
1011                UiEvent::Opened(id)
1012            } else {
1013                UiEvent::Closed(id)
1014            })?;
1015        }
1016
1017        if let Some(rect) = changed_rect {
1018            self.dirty.add(rect)?;
1019        }
1020        if changed {
1021            self.push_event(UiEvent::ValueChanged(id))?;
1022        }
1023        Ok(())
1024    }
1025
1026    pub(crate) fn node_in_active_group(&self, node: &WidgetNode<'_>) -> bool {
1027        self.active_focus_group
1028            .is_none_or(|group| node.focus_group == group)
1029    }
1030
1031    pub(crate) fn handle_pointer_pressed(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
1032        let hit = self.pointer_hit(x, y, true);
1033
1034        if let Some(id) = hit {
1035            self.dispatch_activation(id, true)?;
1036            self.pressed = Some(PressTracker {
1037                id,
1038                start_x: x,
1039                start_y: y,
1040                last_x: x,
1041                last_y: y,
1042                elapsed_ms: 0,
1043                long_emitted: false,
1044                gesture_emitted: false,
1045                repeat_elapsed_ms: 0,
1046                scroll_velocity: 0.0,
1047            });
1048            self.inertia_scroll = None;
1049        }
1050        Ok(())
1051    }
1052
1053    pub(crate) fn handle_pointer_released(&mut self, _x: i32, _y: i32) -> Result<(), GuiError> {
1054        let mut released_id = None;
1055        if let Some(pressed) = self.pressed {
1056            if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
1057                if pressed.scroll_velocity.abs() > self.scroll_physics.velocity_threshold {
1058                    self.inertia_scroll = Some(InertiaScroll {
1059                        id: scroll_id,
1060                        velocity: pressed.scroll_velocity,
1061                    });
1062                }
1063            }
1064            released_id = Some(pressed.id);
1065        }
1066        self.pressed = None;
1067        if let Some(id) = released_id {
1068            let to = if !self.effective_enabled(id) {
1069                VisualState::Disabled
1070            } else if Some(id) == self.focus {
1071                VisualState::Focused
1072            } else {
1073                VisualState::Normal
1074            };
1075            self.start_state_transition(id, VisualState::Pressed, to);
1076            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1077            self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
1078                EventPolicy::Continue
1079            })?;
1080            self.push_event(UiEvent::Released(id))?;
1081            self.push_event(UiEvent::PointerReleased(id))?;
1082            let double_pointer = self.last_pointer_id == Some(id)
1083                && self.pointer_elapsed_ms <= self.pointer_double_window_ms;
1084            if double_pointer {
1085                self.dispatch_double_clicked(id)?;
1086                self.last_pointer_id = None;
1087                self.pointer_elapsed_ms = 0;
1088            } else {
1089                self.last_pointer_id = Some(id);
1090                self.pointer_elapsed_ms = 0;
1091            }
1092        }
1093        Ok(())
1094    }
1095
1096    pub(crate) fn handle_pointer_moved(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
1097        let Some(mut pressed) = self.pressed else {
1098            return Ok(());
1099        };
1100        let dy = y - pressed.last_y;
1101        pressed.last_x = x;
1102        pressed.last_y = y;
1103
1104        let moved_from_start =
1105            (x - pressed.start_x).unsigned_abs() + (y - pressed.start_y).unsigned_abs();
1106        if !pressed.gesture_emitted && moved_from_start >= 6 {
1107            let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1108            self.dispatch_widget_event(pressed.id, WidgetEventKind::Gesture, &mut events, |_| {
1109                EventPolicy::Continue
1110            })?;
1111            self.push_event(UiEvent::Gesture(pressed.id))?;
1112            pressed.gesture_emitted = true;
1113        }
1114
1115        #[cfg(feature = "rich-widgets")]
1116        if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
1117            let current = self.scroll_offset(scroll_id).unwrap_or(0);
1118            let next = current.saturating_sub(dy);
1119            if next != current {
1120                self.set_scroll_offset(scroll_id, next)?;
1121                self.push_event(UiEvent::Scroll {
1122                    id: scroll_id,
1123                    delta: next - current,
1124                })?;
1125                let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1126                self.dispatch_widget_event(
1127                    scroll_id,
1128                    WidgetEventKind::Scroll {
1129                        delta: next - current,
1130                    },
1131                    &mut events,
1132                    |_| EventPolicy::Continue,
1133                )?;
1134            }
1135            let blend = self.scroll_physics.drag_velocity_blend;
1136            pressed.scroll_velocity = pressed.scroll_velocity * (1.0 - blend) + (dy as f32) * blend;
1137        }
1138        self.pressed = Some(pressed);
1139        Ok(())
1140    }
1141
1142    pub(crate) fn dispatch_activation(
1143        &mut self,
1144        id: WidgetId,
1145        is_pointer: bool,
1146    ) -> Result<(), GuiError> {
1147        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1148        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
1149            EventPolicy::Continue
1150        })?;
1151        if self.effective_focusable(id) {
1152            self.set_focus(Some(id))?;
1153        }
1154        self.push_event(UiEvent::Pressed(id))?;
1155        if is_pointer {
1156            self.push_event(UiEvent::PointerPressed(id))?;
1157        }
1158        let from = if Some(id) == self.focus {
1159            VisualState::Focused
1160        } else {
1161            VisualState::Normal
1162        };
1163        self.start_state_transition(id, from, VisualState::Pressed);
1164
1165        self.activate_focused(id)?;
1166        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
1167            EventPolicy::Continue
1168        })?;
1169        self.push_event(UiEvent::Clicked(id))?;
1170        self.push_event(UiEvent::Activate(id))?;
1171        Ok(())
1172    }
1173
1174    pub(crate) fn dispatch_repeat_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
1175        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1176        self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
1177            EventPolicy::Continue
1178        })?;
1179        self.push_event(UiEvent::Clicked(id))?;
1180        self.push_event(UiEvent::Activate(id))
1181    }
1182
1183    pub(crate) fn dispatch_double_clicked(&mut self, id: WidgetId) -> Result<(), GuiError> {
1184        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1185        self.dispatch_widget_event(id, WidgetEventKind::DoubleClicked, &mut events, |_| {
1186            EventPolicy::Continue
1187        })?;
1188        self.push_event(UiEvent::DoubleClicked(id))
1189    }
1190
1191    pub(crate) fn dispatch_key_pressed(&mut self, id: WidgetId) -> Result<(), GuiError> {
1192        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1193        self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
1194            EventPolicy::Continue
1195        })?;
1196        self.push_event(UiEvent::Pressed(id))
1197    }
1198
1199    pub(crate) fn dispatch_key_released(&mut self, id: WidgetId) -> Result<(), GuiError> {
1200        let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
1201        self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
1202            EventPolicy::Continue
1203        })?;
1204        self.push_event(UiEvent::Released(id))
1205    }
1206
1207    pub(crate) fn repeatable_widget(&self, id: WidgetId) -> bool {
1208        self.node(id).is_some_and(|node| {
1209            #[cfg(feature = "rich-widgets")]
1210            if matches!(node.kind, WidgetKind::IconButton { .. }) {
1211                return true;
1212            }
1213            matches!(node.kind, WidgetKind::Button(_))
1214        })
1215    }
1216
1217    pub(crate) fn pointer_hit(&self, x: i32, y: i32, clickable_only: bool) -> Option<WidgetId> {
1218        self.widgets
1219            .iter()
1220            .rev()
1221            .find(|node| {
1222                (!clickable_only || node.clickable())
1223                    && self.effective_visible(node.id)
1224                    && self.effective_enabled(node.id)
1225                    && self
1226                        .absolute_rect(node.id)
1227                        .is_some_and(|rect| rect.contains(x, y))
1228            })
1229            .map(|node| node.id)
1230    }
1231
1232    pub(crate) fn scrollable_ancestor(&self, id: WidgetId) -> Option<WidgetId> {
1233        let mut current = Some(id);
1234        let mut depth = 0usize;
1235        while let Some(widget_id) = current {
1236            if depth >= NODES {
1237                return None;
1238            }
1239            let node = self.node(widget_id)?;
1240            if node.scrollable() {
1241                return Some(widget_id);
1242            }
1243            current = node.parent;
1244            depth += 1;
1245        }
1246        None
1247    }
1248
1249    pub(crate) fn mark_focus_pair(
1250        &mut self,
1251        old: Option<WidgetId>,
1252        new: Option<WidgetId>,
1253    ) -> Result<(), GuiError> {
1254        if let Some(id) = old {
1255            if let Some(rect) = self.absolute_rect(id) {
1256                self.dirty.add(rect)?;
1257            }
1258        }
1259        if let Some(id) = new {
1260            if let Some(rect) = self.absolute_rect(id) {
1261                self.dirty.add(rect)?;
1262            }
1263        }
1264        Ok(())
1265    }
1266
1267    pub(crate) fn start_focus_transitions(&mut self, old: Option<WidgetId>, new: Option<WidgetId>) {
1268        if self.state_transition_ms == 0 {
1269            return;
1270        }
1271        if let Some(id) = old {
1272            self.start_state_transition(id, VisualState::Focused, VisualState::Normal);
1273        }
1274        if let Some(id) = new {
1275            self.start_state_transition(id, VisualState::Normal, VisualState::Focused);
1276        }
1277    }
1278
1279    pub(crate) fn start_state_transition(
1280        &mut self,
1281        id: WidgetId,
1282        from: VisualState,
1283        to: VisualState,
1284    ) {
1285        if self.state_transition_ms == 0 || from == to {
1286            return;
1287        }
1288        if let Some(entry) = self
1289            .state_transitions
1290            .iter_mut()
1291            .find(|entry| entry.id == id)
1292        {
1293            *entry = StateTransition {
1294                id,
1295                from,
1296                to,
1297                elapsed_ms: 0,
1298            };
1299            return;
1300        }
1301        if self.state_transitions.len() == self.state_transitions.capacity() {
1302            self.state_transitions.remove(0);
1303        }
1304        let _ = self.state_transitions.push(StateTransition {
1305            id,
1306            from,
1307            to,
1308            elapsed_ms: 0,
1309        });
1310    }
1311
1312    pub(crate) fn tick_state_transitions(&mut self, dt_ms: u32) -> Result<(), GuiError> {
1313        if self.state_transitions.is_empty() || self.state_transition_ms == 0 {
1314            return Ok(());
1315        }
1316        let mut i = 0usize;
1317        let mut completed_pressed = heapless::Vec::<WidgetId, NODES>::new();
1318        while i < self.state_transitions.len() {
1319            let mut remove = false;
1320            let id;
1321            let to;
1322            {
1323                let entry = &mut self.state_transitions[i];
1324                entry.elapsed_ms = entry.elapsed_ms.saturating_add(dt_ms);
1325                if entry.elapsed_ms >= self.state_transition_ms {
1326                    remove = true;
1327                }
1328                id = entry.id;
1329                to = entry.to;
1330            }
1331            if let Some(rect) = self.absolute_rect(id) {
1332                self.dirty.add(rect)?;
1333            }
1334            if remove {
1335                if to == VisualState::Pressed {
1336                    let _ = completed_pressed.push(id);
1337                }
1338                self.state_transitions.remove(i);
1339            } else {
1340                i += 1;
1341            }
1342        }
1343        for id in completed_pressed {
1344            // Pointer-held presses keep visual pressed state until release.
1345            if self.pressed.is_some_and(|pressed| pressed.id == id) {
1346                continue;
1347            }
1348            let to = self.resting_visual_state(id);
1349            self.start_state_transition(id, VisualState::Pressed, to);
1350        }
1351        Ok(())
1352    }
1353
1354    pub(crate) fn state_transition_progress(
1355        &self,
1356        id: WidgetId,
1357    ) -> Option<(VisualState, VisualState, f32)> {
1358        let duration = self.state_transition_ms.max(1);
1359        self.state_transitions
1360            .iter()
1361            .find(|entry| entry.id == id)
1362            .map(|entry| {
1363                let t = (entry.elapsed_ms as f32 / duration as f32).clamp(0.0, 1.0);
1364                (entry.from, entry.to, t)
1365            })
1366    }
1367
1368    /// Stub for the `rich-widgets`-off build: TextArea doesn't exist, so
1369    /// there's no cursor blink state to update.
1370    #[cfg(not(feature = "rich-widgets"))]
1371    pub(crate) fn set_textarea_cursor_visible(&mut self, _id: Option<WidgetId>, _visible: bool) {}
1372
1373    #[cfg(feature = "rich-widgets")]
1374    pub(crate) fn set_textarea_cursor_visible(&mut self, id: Option<WidgetId>, visible: bool) {
1375        let Some(id) = id else {
1376            return;
1377        };
1378        let Some(rect) = self.absolute_rect(id) else {
1379            return;
1380        };
1381        let Some(node) = self.node_mut(id) else {
1382            return;
1383        };
1384        if let WidgetKind::TextArea {
1385            cursor_visible: ref mut current,
1386            ..
1387        } = node.kind
1388        {
1389            *current = visible;
1390            let _ = self.dirty.add(rect);
1391        }
1392    }
1393
1394    /// Stub for the `rich-widgets`-off build: TextArea doesn't exist, so
1395    /// there's no cursor blink to tick.
1396    #[cfg(not(feature = "rich-widgets"))]
1397    pub(crate) fn tick_textarea_cursor_blink(&mut self, _dt_ms: u32) -> Result<(), GuiError> {
1398        Ok(())
1399    }
1400
1401    #[cfg(feature = "rich-widgets")]
1402    pub(crate) fn tick_textarea_cursor_blink(&mut self, dt_ms: u32) -> Result<(), GuiError> {
1403        let Some(id) = self.focus else {
1404            return Ok(());
1405        };
1406        let is_textarea = matches!(
1407            self.node(id).map(|n| n.kind),
1408            Some(WidgetKind::TextArea { .. })
1409        );
1410        if !is_textarea {
1411            return Ok(());
1412        }
1413        self.textarea_cursor_blink_elapsed_ms =
1414            self.textarea_cursor_blink_elapsed_ms.saturating_add(dt_ms);
1415        if self.textarea_cursor_blink_elapsed_ms < self.textarea_cursor_blink_ms {
1416            return Ok(());
1417        }
1418        self.textarea_cursor_blink_elapsed_ms = 0;
1419        let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1420        let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1421        if let WidgetKind::TextArea {
1422            cursor_visible: ref mut visible,
1423            ..
1424        } = node.kind
1425        {
1426            *visible = !*visible;
1427            self.dirty.add(rect)?;
1428        }
1429        Ok(())
1430    }
1431
1432    pub(crate) fn push_event(&mut self, event: UiEvent) -> Result<(), GuiError> {
1433        if self.should_emit_event(event)? {
1434            self.events.push(event).map_err(|_| GuiError::EventsFull)?;
1435        }
1436        Ok(())
1437    }
1438
1439    pub(crate) fn should_emit_event(&self, event: UiEvent) -> Result<bool, GuiError> {
1440        let Some(target) = event.target() else {
1441            return Ok(true);
1442        };
1443        let filter = self.event_filter(target)?;
1444        Ok(filter.contains(event.filter()))
1445    }
1446
1447    #[cfg(feature = "rich-widgets")]
1448    pub(crate) fn stop_due_to_builtin_widget_behavior(&self, event: WidgetEvent) -> bool {
1449        if event.phase != EventPhase::Capture || event.current == event.target {
1450            return false;
1451        }
1452        let is_pointer_kind = matches!(
1453            event.kind,
1454            WidgetEventKind::Pressed | WidgetEventKind::Released | WidgetEventKind::Clicked
1455        );
1456        is_pointer_kind
1457            && self
1458                .node(event.current)
1459                .is_some_and(|node| matches!(node.kind, WidgetKind::ScrollView { .. }))
1460    }
1461
1462    /// Stub for the `rich-widgets`-off build: ScrollView doesn't exist, so
1463    /// no event ever needs stopping for this reason.
1464    #[cfg(not(feature = "rich-widgets"))]
1465    pub(crate) fn stop_due_to_builtin_widget_behavior(&self, _event: WidgetEvent) -> bool {
1466        false
1467    }
1468
1469    pub(crate) fn stop_due_to_registered_policy(&self, event: WidgetEvent) -> bool {
1470        self.dispatch_policies
1471            .iter()
1472            .find(|(id, _)| *id == event.current)
1473            .is_some_and(|(_, policy)| policy.stop && policy.allows(event.kind, event.phase))
1474    }
1475
1476    pub(crate) fn resting_visual_state(&self, id: WidgetId) -> VisualState {
1477        if !self.effective_enabled(id) {
1478            VisualState::Disabled
1479        } else if Some(id) == self.focus {
1480            VisualState::Focused
1481        } else {
1482            VisualState::Normal
1483        }
1484    }
1485
1486    pub(crate) fn current_visual_state(&self, id: WidgetId) -> VisualState {
1487        if self.pressed.is_some_and(|pressed| pressed.id == id) {
1488            VisualState::Pressed
1489        } else {
1490            self.resting_visual_state(id)
1491        }
1492    }
1493
1494    pub(crate) fn press_timing_for(&self, id: WidgetId) -> PressTiming {
1495        self.widget_press_timings
1496            .iter()
1497            .find(|(timing_id, _)| *timing_id == id)
1498            .map(|(_, timing)| *timing)
1499            .unwrap_or(PressTiming {
1500                long_press_ms: self.long_press_ms,
1501                repeat_delay_ms: self.press_repeat_delay_ms,
1502                repeat_interval_ms: self.press_repeat_interval_ms,
1503            })
1504    }
1505
1506    pub(crate) fn key_input_policy_for(&self, id: WidgetId) -> WidgetKeyInputPolicy {
1507        self.widget_key_policies
1508            .iter()
1509            .find(|(policy_id, _)| *policy_id == id)
1510            .map(|(_, policy)| *policy)
1511            .unwrap_or_default()
1512    }
1513
1514    pub(crate) fn key_bindings_for(&self, id: WidgetId) -> WidgetKeyBindings {
1515        self.widget_key_bindings
1516            .iter()
1517            .find(|(binding_id, _)| *binding_id == id)
1518            .map(|(_, bindings)| *bindings)
1519            .unwrap_or_default()
1520    }
1521
1522    pub(crate) fn handle_select_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
1523        #[cfg(feature = "rich-widgets")]
1524        if let Some(node) = self.node(id) {
1525            if self.menu_contract.select_toggles_feed_expanded
1526                && matches!(node.kind, WidgetKind::FeedTimeline { .. })
1527            {
1528                let expanded = if let WidgetKind::FeedTimeline { expanded, .. } = node.kind {
1529                    expanded
1530                } else {
1531                    false
1532                };
1533                self.set_feed_expanded(id, !expanded)?;
1534                self.push_event(UiEvent::ValueChanged(id))?;
1535            }
1536        }
1537        let double_select = self.last_select_id == Some(id)
1538            && self.select_elapsed_ms <= self.select_double_window_ms;
1539        self.dispatch_activation(id, false)?;
1540        if double_select {
1541            self.dispatch_double_clicked(id)?;
1542            self.last_select_id = None;
1543            self.select_elapsed_ms = 0;
1544        } else {
1545            self.last_select_id = Some(id);
1546            self.select_elapsed_ms = 0;
1547        }
1548        Ok(())
1549    }
1550
1551    pub(crate) fn handle_back_action(&mut self) -> Result<(), GuiError> {
1552        #[cfg(feature = "rich-widgets")]
1553        if let Some(id) = self.focus {
1554            if matches!(
1555                self.node(id).map(|n| n.kind),
1556                Some(WidgetKind::TextArea { .. })
1557            ) {
1558                self.textarea_backspace(id)?;
1559                return Ok(());
1560            }
1561            if matches!(
1562                self.node(id).map(|n| n.kind),
1563                Some(WidgetKind::Dropdown { open: true, .. })
1564            ) && self.menu_contract.back_closes_dropdown
1565            {
1566                self.set_dropdown_open(id, false)?;
1567                return Ok(());
1568            }
1569            if matches!(
1570                self.node(id).map(|n| n.kind),
1571                Some(WidgetKind::NotificationActionSheet { open: true, .. })
1572            ) && self.menu_contract.back_closes_notification_sheet
1573            {
1574                self.set_notification_sheet_open(id, false)?;
1575                return Ok(());
1576            }
1577        }
1578        self.push_event(UiEvent::Back)
1579    }
1580}
1581
1582#[cfg(feature = "rich-widgets")]
1583pub(crate) fn bump_index_with_wrap(current: &mut usize, len: usize, delta: i8, wrap: bool) -> bool {
1584    if len == 0 {
1585        return false;
1586    }
1587    let next = if delta >= 0 {
1588        if *current + 1 >= len {
1589            if wrap { 0 } else { *current }
1590        } else {
1591            *current + 1
1592        }
1593    } else if *current == 0 {
1594        if wrap { len - 1 } else { *current }
1595    } else {
1596        *current - 1
1597    };
1598    if next != *current {
1599        *current = next;
1600        true
1601    } else {
1602        false
1603    }
1604}
1605
1606#[cfg(feature = "rich-widgets")]
1607pub(crate) fn keyboard_char_for_layout(
1608    keys: &[char],
1609    alt_keys: Option<&[char]>,
1610    selected: usize,
1611    layout: KeyboardLayout,
1612) -> Option<char> {
1613    let base = keys.get(selected).copied()?;
1614    Some(match layout {
1615        KeyboardLayout::Normal => base,
1616        KeyboardLayout::Shift => {
1617            if base.is_ascii_alphabetic() {
1618                base.to_ascii_uppercase()
1619            } else {
1620                base
1621            }
1622        }
1623        KeyboardLayout::Symbols => alt_keys
1624            .and_then(|keys| keys.get(selected).copied())
1625            .unwrap_or('#'),
1626    })
1627}
1628
1629#[cfg(feature = "rich-widgets")]
1630pub(crate) fn textarea_text(buf: &[u8; TEXTAREA_CAPACITY], len: u8) -> &str {
1631    let used = (len as usize).min(TEXTAREA_CAPACITY);
1632    core::str::from_utf8(&buf[..used]).unwrap_or("")
1633}
1634
1635#[cfg(feature = "rich-widgets")]
1636pub(crate) fn textarea_storage_from_str(text: &str) -> ([u8; TEXTAREA_CAPACITY], u8) {
1637    let mut out = [0u8; TEXTAREA_CAPACITY];
1638    let mut len = 0usize;
1639    for ch in text.chars() {
1640        let mut tmp = [0u8; 4];
1641        let enc = ch.encode_utf8(&mut tmp).as_bytes();
1642        if len + enc.len() > TEXTAREA_CAPACITY {
1643            break;
1644        }
1645        out[len..len + enc.len()].copy_from_slice(enc);
1646        len += enc.len();
1647    }
1648    (out, len as u8)
1649}
1650
1651#[cfg(feature = "rich-widgets")]
1652pub(crate) fn textarea_storage_from_chars(
1653    chars: &heapless::Vec<char, TEXTAREA_CAPACITY>,
1654) -> ([u8; TEXTAREA_CAPACITY], u8) {
1655    let mut out = [0u8; TEXTAREA_CAPACITY];
1656    let mut len = 0usize;
1657    for ch in chars {
1658        let mut tmp = [0u8; 4];
1659        let enc = ch.encode_utf8(&mut tmp).as_bytes();
1660        if len + enc.len() > TEXTAREA_CAPACITY {
1661            break;
1662        }
1663        out[len..len + enc.len()].copy_from_slice(enc);
1664        len += enc.len();
1665    }
1666    (out, len as u8)
1667}
1668
1669#[cfg(feature = "rich-widgets")]
1670pub(crate) fn char_at(text: &str, idx: usize) -> Option<char> {
1671    text.chars().nth(idx)
1672}
1673
1674#[cfg(feature = "rich-widgets")]
1675pub(crate) fn prev_word_boundary(text: &str, cursor: usize) -> usize {
1676    let mut pos = cursor.min(text.chars().count());
1677    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| ch.is_whitespace()) {
1678        pos -= 1;
1679    }
1680    while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| !ch.is_whitespace()) {
1681        pos -= 1;
1682    }
1683    pos
1684}
1685
1686#[cfg(feature = "rich-widgets")]
1687pub(crate) fn next_word_boundary(text: &str, cursor: usize) -> usize {
1688    let len = text.chars().count();
1689    let mut pos = cursor.min(len);
1690    while pos < len && char_at(text, pos).is_some_and(|ch| !ch.is_whitespace()) {
1691        pos += 1;
1692    }
1693    while pos < len && char_at(text, pos).is_some_and(|ch| ch.is_whitespace()) {
1694        pos += 1;
1695    }
1696    pos
1697}
1698
1699#[cfg(feature = "rich-widgets")]
1700pub(crate) fn delete_selection_if_any(
1701    chars: &mut heapless::Vec<char, TEXTAREA_CAPACITY>,
1702    cursor: &mut usize,
1703    selection: &mut Option<(usize, usize)>,
1704) -> bool {
1705    let Some((start, end)) = *selection else {
1706        return false;
1707    };
1708    let start = start.min(end).min(chars.len());
1709    let end = end.max(start).min(chars.len());
1710    if end <= start {
1711        *selection = None;
1712        *cursor = start;
1713        return false;
1714    }
1715    for _ in start..end {
1716        chars.remove(start);
1717    }
1718    *cursor = start;
1719    *selection = None;
1720    true
1721}
1722
1723#[cfg(feature = "rich-widgets")]
1724pub(crate) fn textarea_row_col_at_cursor(
1725    text: &str,
1726    cursor: usize,
1727    wrap_cols: usize,
1728) -> (usize, usize) {
1729    let mut row = 0usize;
1730    let mut col = 0usize;
1731    for ch in text.chars().take(cursor) {
1732        if ch == '\n' {
1733            row += 1;
1734            col = 0;
1735            continue;
1736        }
1737        col += 1;
1738        if col >= wrap_cols {
1739            row += 1;
1740            col = 0;
1741        }
1742    }
1743    (row, col)
1744}
1745
1746#[cfg(feature = "rich-widgets")]
1747pub(crate) fn textarea_cursor_from_row_col(
1748    text: &str,
1749    target_row: usize,
1750    target_col: usize,
1751    wrap_cols: usize,
1752) -> usize {
1753    let mut row = 0usize;
1754    let mut col = 0usize;
1755    let mut idx = 0usize;
1756    for ch in text.chars() {
1757        if row == target_row && col >= target_col {
1758            break;
1759        }
1760        if ch == '\n' {
1761            if row == target_row {
1762                break;
1763            }
1764            row += 1;
1765            col = 0;
1766            idx += 1;
1767            continue;
1768        }
1769        idx += 1;
1770        col += 1;
1771        if col >= wrap_cols {
1772            if row == target_row {
1773                break;
1774            }
1775            row += 1;
1776            col = 0;
1777        }
1778    }
1779    idx
1780}
1781
1782#[cfg(feature = "rich-widgets")]
1783pub(crate) fn textarea_row_end_col(text: &str, target_row: usize, wrap_cols: usize) -> usize {
1784    let mut row = 0usize;
1785    let mut col = 0usize;
1786    for ch in text.chars() {
1787        if row == target_row {
1788            if ch == '\n' {
1789                break;
1790            }
1791            col += 1;
1792            if col >= wrap_cols {
1793                break;
1794            }
1795        } else if ch == '\n' {
1796            row += 1;
1797            col = 0;
1798        } else {
1799            col += 1;
1800            if col >= wrap_cols {
1801                row += 1;
1802                col = 0;
1803            }
1804        }
1805    }
1806    col
1807}