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