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 #[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 #[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 #[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 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 #[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 #[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 #[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}