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