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