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_gauge_needle<S>(
1031 &mut self,
1032 rect: Rect,
1033 value: f32,
1034 min: f32,
1035 max: f32,
1036 start_deg: i32,
1037 end_deg: i32,
1038 style: S,
1039 ) -> Result<WidgetId, GuiError>
1040 where
1041 S: Into<WidgetStyle>,
1042 {
1043 self.add_widget(
1044 rect,
1045 WidgetKind::GaugeNeedle {
1046 value,
1047 min,
1048 max,
1049 start_deg,
1050 end_deg,
1051 },
1052 style,
1053 )
1054 }
1055
1056 pub fn add_chart<S>(
1057 &mut self,
1058 rect: Rect,
1059 values: &'a [f32],
1060 min: f32,
1061 max: f32,
1062 style: S,
1063 ) -> Result<WidgetId, GuiError>
1064 where
1065 S: Into<WidgetStyle>,
1066 {
1067 self.add_widget(
1068 rect,
1069 WidgetKind::Chart {
1070 values,
1071 min,
1072 max,
1073 thickness: 1,
1074 fill_under: false,
1075 markers: false,
1076 mode: ChartMode::Line,
1077 show_grid: false,
1078 show_axes: false,
1079 show_labels: false,
1080 },
1081 style,
1082 )
1083 }
1084
1085 pub fn set_chart_style(
1086 &mut self,
1087 id: WidgetId,
1088 thickness: u8,
1089 fill_under: bool,
1090 markers: bool,
1091 ) -> Result<(), GuiError> {
1092 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1093 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1094 match node.kind {
1095 WidgetKind::Chart {
1096 thickness: ref mut t,
1097 fill_under: ref mut fill,
1098 markers: ref mut mark,
1099 ..
1100 } => {
1101 *t = thickness.max(1);
1102 *fill = fill_under;
1103 *mark = markers;
1104 self.dirty.add(rect)?;
1105 Ok(())
1106 }
1107 _ => Err(GuiError::NotFound),
1108 }
1109 }
1110
1111 pub fn set_chart_decoration(
1112 &mut self,
1113 id: WidgetId,
1114 mode: ChartMode,
1115 show_grid: bool,
1116 show_axes: bool,
1117 show_labels: bool,
1118 ) -> Result<(), GuiError> {
1119 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1120 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1121 match node.kind {
1122 WidgetKind::Chart {
1123 mode: ref mut chart_mode,
1124 show_grid: ref mut grid,
1125 show_axes: ref mut axes,
1126 show_labels: ref mut labels,
1127 ..
1128 } => {
1129 *chart_mode = mode;
1130 *grid = show_grid;
1131 *axes = show_axes;
1132 *labels = show_labels;
1133 self.dirty.add(rect)?;
1134 Ok(())
1135 }
1136 _ => Err(GuiError::NotFound),
1137 }
1138 }
1139
1140 pub fn add_spinner<S>(&mut self, rect: Rect, phase: f32, style: S) -> Result<WidgetId, GuiError>
1141 where
1142 S: Into<WidgetStyle>,
1143 {
1144 self.add_widget(rect, WidgetKind::Spinner { phase }, style)
1145 }
1146
1147 pub fn add_dropdown<S>(
1148 &mut self,
1149 rect: Rect,
1150 items: &'a [&'a str],
1151 selected: usize,
1152 style: S,
1153 ) -> Result<WidgetId, GuiError>
1154 where
1155 S: Into<WidgetStyle>,
1156 {
1157 let selected = selected.min(items.len().saturating_sub(1));
1158 let id = self.add_widget(
1159 rect,
1160 WidgetKind::Dropdown {
1161 items,
1162 selected,
1163 open: false,
1164 },
1165 style,
1166 )?;
1167 self.ensure_focus();
1168 Ok(id)
1169 }
1170
1171 pub fn add_roller<S>(
1172 &mut self,
1173 rect: Rect,
1174 items: &'a [&'a str],
1175 selected: usize,
1176 style: S,
1177 ) -> Result<WidgetId, GuiError>
1178 where
1179 S: Into<WidgetStyle>,
1180 {
1181 let selected = selected.min(items.len().saturating_sub(1));
1182 let id = self.add_widget(rect, WidgetKind::Roller { items, selected }, style)?;
1183 self.ensure_focus();
1184 Ok(id)
1185 }
1186
1187 pub fn add_table<S>(
1188 &mut self,
1189 rect: Rect,
1190 rows: &'a [&'a [&'a str]],
1191 style: S,
1192 ) -> Result<WidgetId, GuiError>
1193 where
1194 S: Into<WidgetStyle>,
1195 {
1196 self.add_widget(
1197 rect,
1198 WidgetKind::Table {
1199 rows,
1200 separators: true,
1201 cell_padding: 1,
1202 align: TextAlign::Left,
1203 },
1204 style,
1205 )
1206 }
1207
1208 pub fn set_table_style(
1209 &mut self,
1210 id: WidgetId,
1211 separators: bool,
1212 cell_padding: u8,
1213 align: TextAlign,
1214 ) -> Result<(), GuiError> {
1215 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1216 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1217 match node.kind {
1218 WidgetKind::Table {
1219 separators: ref mut cell_sep,
1220 cell_padding: ref mut pad,
1221 align: ref mut table_align,
1222 ..
1223 } => {
1224 *cell_sep = separators;
1225 *pad = cell_padding.min(6);
1226 *table_align = align;
1227 self.dirty.add(rect)?;
1228 Ok(())
1229 }
1230 _ => Err(GuiError::NotFound),
1231 }
1232 }
1233
1234 pub fn add_textarea<S>(
1235 &mut self,
1236 rect: Rect,
1237 text: &'a str,
1238 placeholder: &'a str,
1239 style: S,
1240 ) -> Result<WidgetId, GuiError>
1241 where
1242 S: Into<WidgetStyle>,
1243 {
1244 let cursor = text.chars().count();
1245 let (text_buf, text_len) = textarea_storage_from_str(text);
1246 let id = self.add_widget(
1247 rect,
1248 WidgetKind::TextArea {
1249 text_buf,
1250 text_len,
1251 cursor,
1252 placeholder,
1253 selection: None,
1254 cursor_visible: true,
1255 read_only: false,
1256 single_line: false,
1257 accept_newline: true,
1258 },
1259 style,
1260 )?;
1261 self.ensure_focus();
1262 Ok(id)
1263 }
1264
1265 pub fn add_keyboard<S>(
1266 &mut self,
1267 rect: Rect,
1268 keys: &'a [char],
1269 cols: u8,
1270 target: Option<WidgetId>,
1271 style: S,
1272 ) -> Result<WidgetId, GuiError>
1273 where
1274 S: Into<WidgetStyle>,
1275 {
1276 self.add_keyboard_with_alt(rect, keys, None, cols, target, style)
1277 }
1278
1279 pub fn add_keyboard_with_alt<S>(
1280 &mut self,
1281 rect: Rect,
1282 keys: &'a [char],
1283 alt_keys: Option<&'a [char]>,
1284 cols: u8,
1285 target: Option<WidgetId>,
1286 style: S,
1287 ) -> Result<WidgetId, GuiError>
1288 where
1289 S: Into<WidgetStyle>,
1290 {
1291 let id = self.add_widget(
1292 rect,
1293 WidgetKind::Keyboard {
1294 keys,
1295 selected: 0,
1296 cols: cols.max(1),
1297 alt_keys,
1298 layout: KeyboardLayout::Normal,
1299 target,
1300 },
1301 style,
1302 )?;
1303 self.ensure_focus();
1304 Ok(id)
1305 }
1306
1307 pub fn add_image<S>(
1308 &mut self,
1309 rect: Rect,
1310 image: ImageRef<'a>,
1311 fit: ImageFit,
1312 style: S,
1313 ) -> Result<WidgetId, GuiError>
1314 where
1315 S: Into<WidgetStyle>,
1316 {
1317 self.add_widget(rect, WidgetKind::Image { image, fit }, style)
1318 }
1319
1320 pub fn add_peek_reveal<S>(
1321 &mut self,
1322 rect: Rect,
1323 icon: ImageRef<'a>,
1324 title: &'a str,
1325 subtitle: &'a str,
1326 style: S,
1327 ) -> Result<WidgetId, GuiError>
1328 where
1329 S: Into<WidgetStyle>,
1330 {
1331 self.add_widget(
1332 rect,
1333 WidgetKind::PeekReveal {
1334 icon,
1335 title,
1336 subtitle,
1337 progress: 0.0,
1338 },
1339 style,
1340 )
1341 }
1342
1343 pub fn add_glance_tile<S>(
1344 &mut self,
1345 rect: Rect,
1346 icon: char,
1347 title: &'a str,
1348 subtitle: &'a str,
1349 style: S,
1350 ) -> Result<WidgetId, GuiError>
1351 where
1352 S: Into<WidgetStyle>,
1353 {
1354 let id = self.add_widget(
1355 rect,
1356 WidgetKind::GlanceTile {
1357 icon,
1358 title,
1359 subtitle,
1360 highlighted: false,
1361 },
1362 style,
1363 )?;
1364 self.ensure_focus();
1365 Ok(id)
1366 }
1367
1368 pub fn add_card_deck<S>(
1369 &mut self,
1370 rect: Rect,
1371 titles: &'a [&'a str],
1372 selected: usize,
1373 style: S,
1374 ) -> Result<WidgetId, GuiError>
1375 where
1376 S: Into<WidgetStyle>,
1377 {
1378 self.add_widget(
1379 rect,
1380 WidgetKind::CardDeck {
1381 titles,
1382 selected: selected.min(titles.len().saturating_sub(1)),
1383 },
1384 style,
1385 )
1386 }
1387
1388 pub fn add_reel<S>(
1389 &mut self,
1390 rect: Rect,
1391 player: ReelPlayer<'a>,
1392 fit: ImageFit,
1393 style: S,
1394 ) -> Result<WidgetId, GuiError>
1395 where
1396 S: Into<WidgetStyle>,
1397 {
1398 self.add_widget(rect, WidgetKind::Reel { player, fit }, style)
1399 }
1400
1401 pub fn add_state_surface<S>(
1402 &mut self,
1403 rect: Rect,
1404 state: SurfaceState,
1405 title: &'a str,
1406 message: &'a str,
1407 action: Option<&'a str>,
1408 style: S,
1409 ) -> Result<WidgetId, GuiError>
1410 where
1411 S: Into<WidgetStyle>,
1412 {
1413 self.add_widget(
1414 rect,
1415 WidgetKind::StateSurface {
1416 state,
1417 title,
1418 message,
1419 action,
1420 busy_phase: 0.0,
1421 },
1422 style,
1423 )
1424 }
1425
1426 pub fn add_heads_up_banner<S>(
1427 &mut self,
1428 rect: Rect,
1429 level: NotificationLevel,
1430 text: &'a str,
1431 ttl_ms: u32,
1432 style: S,
1433 ) -> Result<WidgetId, GuiError>
1434 where
1435 S: Into<WidgetStyle>,
1436 {
1437 self.add_widget(
1438 rect,
1439 WidgetKind::HeadsUpBanner {
1440 level,
1441 text,
1442 ttl_ms,
1443 },
1444 style,
1445 )
1446 }
1447
1448 #[allow(clippy::too_many_arguments)]
1449 pub fn add_notification_action_sheet<S>(
1450 &mut self,
1451 rect: Rect,
1452 level: NotificationLevel,
1453 title: &'a str,
1454 body: &'a str,
1455 actions: &'a [&'a str],
1456 selected: usize,
1457 open: bool,
1458 style: S,
1459 ) -> Result<WidgetId, GuiError>
1460 where
1461 S: Into<WidgetStyle>,
1462 {
1463 self.add_widget(
1464 rect,
1465 WidgetKind::NotificationActionSheet {
1466 level,
1467 title,
1468 body,
1469 actions,
1470 selected: selected.min(actions.len().saturating_sub(1)),
1471 open,
1472 },
1473 style,
1474 )
1475 }
1476
1477 pub fn add_border<S>(&mut self, rect: Rect, style: S) -> Result<WidgetId, GuiError>
1478 where
1479 S: Into<WidgetStyle>,
1480 {
1481 self.add_widget(rect, WidgetKind::Border, style)
1482 }
1483
1484 pub fn add_spacer(&mut self, rect: Rect) -> Result<WidgetId, GuiError> {
1485 self.add_widget(rect, WidgetKind::Spacer, Style::default())
1486 }
1487
1488 pub fn add_menu<S>(
1489 &mut self,
1490 rect: Rect,
1491 items: &'a [&'a str],
1492 selected: usize,
1493 style: S,
1494 ) -> Result<WidgetId, GuiError>
1495 where
1496 S: Into<WidgetStyle>,
1497 {
1498 let selected = selected.min(items.len().saturating_sub(1));
1499 let id = self.add_widget(rect, WidgetKind::Menu { items, selected }, style)?;
1500 self.ensure_focus();
1501 Ok(id)
1502 }
1503
1504 pub fn set_progress(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1505 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1506 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1507 match node.kind {
1508 WidgetKind::ProgressBar { value: ref mut v } => {
1509 *v = value.clamp(0.0, 1.0);
1510 self.dirty.add(rect)?;
1511 Ok(())
1512 }
1513 WidgetKind::PeekReveal {
1514 progress: ref mut v,
1515 ..
1516 } => {
1517 *v = value.clamp(0.0, 1.0);
1518 self.dirty.add(rect)?;
1519 Ok(())
1520 }
1521 _ => Err(GuiError::NotFound),
1522 }
1523 }
1524
1525 pub fn set_glance_highlighted(
1526 &mut self,
1527 id: WidgetId,
1528 highlighted: bool,
1529 ) -> Result<(), GuiError> {
1530 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1531 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1532 match node.kind {
1533 WidgetKind::GlanceTile {
1534 highlighted: ref mut h,
1535 ..
1536 } => {
1537 *h = highlighted;
1538 self.dirty.add(rect)?;
1539 Ok(())
1540 }
1541 _ => Err(GuiError::NotFound),
1542 }
1543 }
1544
1545 pub fn set_card_deck_selected(
1546 &mut self,
1547 id: WidgetId,
1548 selected: usize,
1549 ) -> Result<(), GuiError> {
1550 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1551 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1552 match node.kind {
1553 WidgetKind::CardDeck {
1554 titles,
1555 selected: ref mut current,
1556 } => {
1557 *current = selected.min(titles.len().saturating_sub(1));
1558 self.dirty.add(rect)?;
1559 Ok(())
1560 }
1561 _ => Err(GuiError::NotFound),
1562 }
1563 }
1564
1565 pub fn tick_reel(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1566 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1567 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1568 match node.kind {
1569 WidgetKind::Reel {
1570 player: ref mut reel,
1571 ..
1572 } => {
1573 reel.tick(dt_ms);
1574 self.dirty.add(rect)?;
1575 Ok(())
1576 }
1577 _ => Err(GuiError::NotFound),
1578 }
1579 }
1580
1581 pub fn set_state_surface_state(
1582 &mut self,
1583 id: WidgetId,
1584 state: SurfaceState,
1585 ) -> Result<(), GuiError> {
1586 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1587 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1588 match node.kind {
1589 WidgetKind::StateSurface {
1590 state: ref mut current,
1591 ..
1592 } => {
1593 *current = state;
1594 self.dirty.add(rect)?;
1595 Ok(())
1596 }
1597 _ => Err(GuiError::NotFound),
1598 }
1599 }
1600
1601 pub fn set_state_surface_message(
1602 &mut self,
1603 id: WidgetId,
1604 message: &'a str,
1605 ) -> Result<(), GuiError> {
1606 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1607 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1608 match node.kind {
1609 WidgetKind::StateSurface {
1610 message: ref mut current,
1611 ..
1612 } => {
1613 *current = message;
1614 self.dirty.add(rect)?;
1615 Ok(())
1616 }
1617 _ => Err(GuiError::NotFound),
1618 }
1619 }
1620
1621 pub fn set_state_surface_action(
1622 &mut self,
1623 id: WidgetId,
1624 action: Option<&'a str>,
1625 ) -> Result<(), GuiError> {
1626 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1627 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1628 match node.kind {
1629 WidgetKind::StateSurface {
1630 action: ref mut current,
1631 ..
1632 } => {
1633 *current = action;
1634 self.dirty.add(rect)?;
1635 Ok(())
1636 }
1637 _ => Err(GuiError::NotFound),
1638 }
1639 }
1640
1641 pub fn set_state_surface_busy_phase(
1642 &mut self,
1643 id: WidgetId,
1644 phase: f32,
1645 ) -> Result<(), GuiError> {
1646 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1647 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1648 match node.kind {
1649 WidgetKind::StateSurface {
1650 busy_phase: ref mut current,
1651 ..
1652 } => {
1653 *current = phase;
1654 self.dirty.add(rect)?;
1655 Ok(())
1656 }
1657 _ => Err(GuiError::NotFound),
1658 }
1659 }
1660
1661 pub fn tick_state_surface(
1662 &mut self,
1663 id: WidgetId,
1664 dt_ms: u32,
1665 cycles_per_sec: f32,
1666 ) -> Result<(), GuiError> {
1667 let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1668 WidgetKind::StateSurface { busy_phase, .. } => {
1669 busy_phase + (dt_ms as f32 / 1000.0) * cycles_per_sec
1670 }
1671 _ => return Err(GuiError::NotFound),
1672 };
1673 self.set_state_surface_busy_phase(id, phase)
1674 }
1675
1676 pub fn set_heads_up_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
1677 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1678 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1679 match node.kind {
1680 WidgetKind::HeadsUpBanner {
1681 ttl_ms: ref mut current,
1682 ..
1683 } => {
1684 *current = ttl_ms;
1685 self.dirty.add(rect)?;
1686 Ok(())
1687 }
1688 _ => Err(GuiError::NotFound),
1689 }
1690 }
1691
1692 pub fn tick_heads_up(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1693 let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1694 WidgetKind::HeadsUpBanner { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
1695 _ => return Err(GuiError::NotFound),
1696 };
1697 self.set_heads_up_ttl(id, ttl)
1698 }
1699
1700 pub fn set_notification_sheet_open(
1701 &mut self,
1702 id: WidgetId,
1703 open: bool,
1704 ) -> Result<(), GuiError> {
1705 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1706 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1707 match node.kind {
1708 WidgetKind::NotificationActionSheet {
1709 open: ref mut current,
1710 ..
1711 } => {
1712 *current = open;
1713 self.dirty.add(rect)?;
1714 Ok(())
1715 }
1716 _ => Err(GuiError::NotFound),
1717 }
1718 }
1719
1720 pub fn set_notification_sheet_selected(
1721 &mut self,
1722 id: WidgetId,
1723 selected: usize,
1724 ) -> Result<(), GuiError> {
1725 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1726 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1727 match node.kind {
1728 WidgetKind::NotificationActionSheet {
1729 actions,
1730 selected: ref mut current,
1731 ..
1732 } => {
1733 *current = selected.min(actions.len().saturating_sub(1));
1734 self.dirty.add(rect)?;
1735 Ok(())
1736 }
1737 _ => Err(GuiError::NotFound),
1738 }
1739 }
1740
1741 pub fn set_menu_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1742 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1743 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1744 match node.kind {
1745 WidgetKind::Menu {
1746 items,
1747 selected: ref mut current,
1748 } => {
1749 *current = selected.min(items.len().saturating_sub(1));
1750 self.dirty.add(rect)?;
1751 Ok(())
1752 }
1753 _ => Err(GuiError::NotFound),
1754 }
1755 }
1756
1757 pub fn menu_selected(&self, id: WidgetId) -> Option<usize> {
1758 match self.node(id)?.kind {
1759 WidgetKind::Menu { selected, .. } => Some(selected),
1760 _ => None,
1761 }
1762 }
1763
1764 pub fn list_selected(&self, id: WidgetId) -> Option<usize> {
1765 match self.node(id)?.kind {
1766 WidgetKind::List { selected, .. } => Some(selected),
1767 _ => None,
1768 }
1769 }
1770
1771 pub fn set_list_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1772 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1773 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1774 match node.kind {
1775 WidgetKind::List {
1776 items,
1777 selected: ref mut current,
1778 ref mut offset,
1779 visible_rows,
1780 } => {
1781 let mut state = ListState::new(*current, *offset, visible_rows);
1782 state.set_selected(selected, items.len());
1783 *current = state.selected;
1784 *offset = state.offset;
1785 self.dirty.add(rect)?;
1786 Ok(())
1787 }
1788 _ => Err(GuiError::NotFound),
1789 }
1790 }
1791
1792 pub fn feed_selected(&self, id: WidgetId) -> Option<usize> {
1793 match self.node(id)?.kind {
1794 WidgetKind::FeedTimeline { selected, .. } => Some(selected),
1795 _ => None,
1796 }
1797 }
1798
1799 pub fn set_feed_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
1800 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1801 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1802 match node.kind {
1803 WidgetKind::FeedTimeline {
1804 items,
1805 selected: ref mut current,
1806 ref mut offset,
1807 visible_rows,
1808 ..
1809 } => {
1810 let mut state = FeedTimelineState::new(*current, *offset, visible_rows, false);
1811 state.set_selected(selected, items.len());
1812 *current = state.selected;
1813 *offset = state.offset;
1814 self.dirty.add(rect)?;
1815 Ok(())
1816 }
1817 _ => Err(GuiError::NotFound),
1818 }
1819 }
1820
1821 pub fn set_feed_expanded(&mut self, id: WidgetId, expanded: bool) -> Result<(), GuiError> {
1822 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1823 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1824 match node.kind {
1825 WidgetKind::FeedTimeline {
1826 expanded: ref mut current,
1827 ..
1828 } => {
1829 *current = expanded;
1830 self.dirty.add(rect)?;
1831 Ok(())
1832 }
1833 _ => Err(GuiError::NotFound),
1834 }
1835 }
1836
1837 pub fn set_toggle(&mut self, id: WidgetId, on: bool) -> Result<(), GuiError> {
1838 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1839 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1840 match node.kind {
1841 WidgetKind::Toggle { on: ref mut v, .. } => {
1842 *v = on;
1843 self.dirty.add(rect)?;
1844 Ok(())
1845 }
1846 _ => Err(GuiError::NotFound),
1847 }
1848 }
1849
1850 pub fn toggle_value(&self, id: WidgetId) -> Option<bool> {
1851 match self.node(id)?.kind {
1852 WidgetKind::Toggle { on, .. } => Some(on),
1853 _ => None,
1854 }
1855 }
1856
1857 pub fn set_checked(&mut self, id: WidgetId, checked: bool) -> Result<(), GuiError> {
1858 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1859 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1860 match node.kind {
1861 WidgetKind::Checkbox {
1862 checked: ref mut v, ..
1863 } => {
1864 *v = checked;
1865 self.dirty.add(rect)?;
1866 Ok(())
1867 }
1868 _ => Err(GuiError::NotFound),
1869 }
1870 }
1871
1872 pub fn checked_value(&self, id: WidgetId) -> Option<bool> {
1873 match self.node(id)?.kind {
1874 WidgetKind::Checkbox { checked, .. } => Some(checked),
1875 _ => None,
1876 }
1877 }
1878
1879 pub fn set_slider_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1880 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1881 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1882 match node.kind {
1883 WidgetKind::Slider {
1884 value: ref mut v,
1885 min,
1886 max,
1887 } => {
1888 let mut state = SliderState::new(*v, min, max);
1889 state.set_value(value);
1890 *v = state.value;
1891 self.dirty.add(rect)?;
1892 Ok(())
1893 }
1894 _ => Err(GuiError::NotFound),
1895 }
1896 }
1897
1898 pub fn slider_value(&self, id: WidgetId) -> Option<f32> {
1899 match self.node(id)?.kind {
1900 WidgetKind::Slider { value, .. } => Some(value),
1901 _ => None,
1902 }
1903 }
1904
1905 pub fn set_value_label(&mut self, id: WidgetId, value: i32) -> Result<(), GuiError> {
1906 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1907 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1908 match node.kind {
1909 WidgetKind::ValueLabel {
1910 value: ref mut v, ..
1911 } => {
1912 *v = value;
1913 self.dirty.add(rect)?;
1914 Ok(())
1915 }
1916 _ => Err(GuiError::NotFound),
1917 }
1918 }
1919
1920 pub fn set_scroll_offset(&mut self, id: WidgetId, offset_y: i32) -> Result<(), GuiError> {
1921 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1922 match node.kind {
1923 WidgetKind::ScrollView {
1924 offset_y: ref mut v,
1925 content_h,
1926 } => {
1927 let mut state = ScrollState::new(*v, content_h);
1928 state.set_offset(offset_y);
1929 *v = state.offset_y;
1930 self.mark_subtree_dirty(id)?;
1931 Ok(())
1932 }
1933 _ => Err(GuiError::NotFound),
1934 }
1935 }
1936
1937 pub fn scroll_offset(&self, id: WidgetId) -> Option<i32> {
1938 match self.node(id)?.kind {
1939 WidgetKind::ScrollView { offset_y, .. } => Some(offset_y),
1940 _ => None,
1941 }
1942 }
1943
1944 pub fn set_tab_selected(&mut self, id: WidgetId, selected: usize) -> 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::Tabs {
1949 labels,
1950 selected: ref mut v,
1951 } => {
1952 let mut state = TabsState::new(*v);
1953 state.set_selected(selected, labels.len());
1954 *v = state.selected;
1955 self.dirty.add(rect)?;
1956 Ok(())
1957 }
1958 _ => Err(GuiError::NotFound),
1959 }
1960 }
1961
1962 pub fn tab_selected(&self, id: WidgetId) -> Option<usize> {
1963 match self.node(id)?.kind {
1964 WidgetKind::Tabs { selected, .. } => Some(selected),
1965 _ => None,
1966 }
1967 }
1968
1969 pub fn set_toast_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
1970 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1971 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1972 match node.kind {
1973 WidgetKind::Toast {
1974 ttl_ms: ref mut v, ..
1975 } => {
1976 *v = ttl_ms;
1977 self.dirty.add(rect)?;
1978 Ok(())
1979 }
1980 _ => Err(GuiError::NotFound),
1981 }
1982 }
1983
1984 pub fn tick_toast(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
1985 let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
1986 WidgetKind::Toast { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
1987 _ => return Err(GuiError::NotFound),
1988 };
1989 self.set_toast_ttl(id, ttl)
1990 }
1991
1992 pub fn set_meter_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1993 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1994 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1995 match node.kind {
1996 WidgetKind::Meter {
1997 value: ref mut v,
1998 min,
1999 max,
2000 } => {
2001 *v = value.clamp(min.min(max), min.max(max));
2002 self.dirty.add(rect)?;
2003 Ok(())
2004 }
2005 _ => Err(GuiError::NotFound),
2006 }
2007 }
2008
2009 pub fn set_spinner_phase(&mut self, id: WidgetId, phase: f32) -> Result<(), GuiError> {
2010 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2011 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2012 match node.kind {
2013 WidgetKind::Spinner { phase: ref mut v } => {
2014 *v = phase;
2015 self.dirty.add(rect)?;
2016 Ok(())
2017 }
2018 _ => Err(GuiError::NotFound),
2019 }
2020 }
2021
2022 pub fn tick_spinner(
2023 &mut self,
2024 id: WidgetId,
2025 dt_ms: u32,
2026 cycles_per_sec: f32,
2027 ) -> Result<(), GuiError> {
2028 let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
2029 WidgetKind::Spinner { phase } => phase + (dt_ms as f32 / 1000.0) * cycles_per_sec,
2030 _ => return Err(GuiError::NotFound),
2031 };
2032 self.set_spinner_phase(id, phase)
2033 }
2034
2035 pub fn set_dropdown_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2036 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2037 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2038 match node.kind {
2039 WidgetKind::Dropdown {
2040 items,
2041 selected: ref mut current,
2042 ..
2043 } => {
2044 *current = selected.min(items.len().saturating_sub(1));
2045 self.dirty.add(rect)?;
2046 Ok(())
2047 }
2048 _ => Err(GuiError::NotFound),
2049 }
2050 }
2051
2052 pub fn dropdown_selected(&self, id: WidgetId) -> Option<usize> {
2053 match self.node(id)?.kind {
2054 WidgetKind::Dropdown { selected, .. } => Some(selected),
2055 _ => None,
2056 }
2057 }
2058
2059 pub fn set_dropdown_open(&mut self, id: WidgetId, open: bool) -> Result<(), GuiError> {
2060 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2061 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2062 match node.kind {
2063 WidgetKind::Dropdown {
2064 open: ref mut is_open,
2065 ..
2066 } => {
2067 if *is_open != open {
2068 *is_open = open;
2069 self.dirty.add(rect)?;
2070 self.push_event(if open {
2071 UiEvent::Opened(id)
2072 } else {
2073 UiEvent::Closed(id)
2074 })?;
2075 }
2076 Ok(())
2077 }
2078 _ => Err(GuiError::NotFound),
2079 }
2080 }
2081
2082 pub fn dropdown_open(&self, id: WidgetId) -> Option<bool> {
2083 match self.node(id)?.kind {
2084 WidgetKind::Dropdown { open, .. } => Some(open),
2085 _ => None,
2086 }
2087 }
2088
2089 pub fn set_roller_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
2090 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2091 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2092 match node.kind {
2093 WidgetKind::Roller {
2094 items,
2095 selected: ref mut current,
2096 } => {
2097 *current = selected.min(items.len().saturating_sub(1));
2098 self.dirty.add(rect)?;
2099 Ok(())
2100 }
2101 _ => Err(GuiError::NotFound),
2102 }
2103 }
2104
2105 pub fn roller_selected(&self, id: WidgetId) -> Option<usize> {
2106 match self.node(id)?.kind {
2107 WidgetKind::Roller { selected, .. } => Some(selected),
2108 _ => None,
2109 }
2110 }
2111
2112 pub fn set_textarea_text(&mut self, id: WidgetId, text: &'a str) -> Result<(), GuiError> {
2113 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2114 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2115 match node.kind {
2116 WidgetKind::TextArea {
2117 text_buf: ref mut buf,
2118 text_len: ref mut len,
2119 cursor: ref mut c,
2120 ..
2121 } => {
2122 let (next_buf, next_len) = textarea_storage_from_str(text);
2123 *buf = next_buf;
2124 *len = next_len;
2125 *c = (*c).min(textarea_text(buf, *len).chars().count());
2126 self.dirty.add(rect)?;
2127 Ok(())
2128 }
2129 _ => Err(GuiError::NotFound),
2130 }
2131 }
2132
2133 pub fn textarea_text(&self, id: WidgetId) -> Option<&str> {
2134 match &self.node(id)?.kind {
2135 WidgetKind::TextArea {
2136 text_buf, text_len, ..
2137 } => Some(textarea_text(text_buf, *text_len)),
2138 _ => None,
2139 }
2140 }
2141
2142 pub fn set_textarea_cursor(&mut self, id: WidgetId, cursor: usize) -> Result<(), GuiError> {
2143 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2144 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2145 match node.kind {
2146 WidgetKind::TextArea {
2147 text_buf,
2148 text_len,
2149 cursor: ref mut current,
2150 ..
2151 } => {
2152 let text = textarea_text(&text_buf, text_len);
2153 *current = cursor.min(text.chars().count());
2154 self.dirty.add(rect)?;
2155 Ok(())
2156 }
2157 _ => Err(GuiError::NotFound),
2158 }
2159 }
2160
2161 pub fn move_textarea_cursor(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2162 let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2163 self.set_textarea_cursor_with_extend(id, next.max(0) as usize, false)
2164 }
2165
2166 pub fn move_textarea_cursor_select(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2167 let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
2168 self.set_textarea_cursor_with_extend(id, next.max(0) as usize, true)
2169 }
2170
2171 pub fn move_textarea_cursor_word(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
2172 let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2173 WidgetKind::TextArea {
2174 text_buf,
2175 text_len,
2176 cursor,
2177 ..
2178 } => (textarea_text(text_buf, *text_len), *cursor),
2179 _ => return Err(GuiError::NotFound),
2180 };
2181 let next = if delta >= 0 {
2182 next_word_boundary(text, cursor)
2183 } else {
2184 prev_word_boundary(text, cursor)
2185 };
2186 self.set_textarea_cursor_with_extend(id, next, false)
2187 }
2188
2189 pub fn move_textarea_cursor_word_select(
2190 &mut self,
2191 id: WidgetId,
2192 delta: i8,
2193 ) -> Result<(), GuiError> {
2194 let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
2195 WidgetKind::TextArea {
2196 text_buf,
2197 text_len,
2198 cursor,
2199 ..
2200 } => (textarea_text(text_buf, *text_len), *cursor),
2201 _ => return Err(GuiError::NotFound),
2202 };
2203 let next = if delta >= 0 {
2204 next_word_boundary(text, cursor)
2205 } else {
2206 prev_word_boundary(text, cursor)
2207 };
2208 self.set_textarea_cursor_with_extend(id, next, true)
2209 }
2210
2211 pub fn set_textarea_cursor_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2212 self.set_textarea_cursor(id, 0)
2213 }
2214
2215 pub fn set_textarea_cursor_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2216 let len = self
2217 .textarea_text(id)
2218 .map(|text| text.chars().count())
2219 .ok_or(GuiError::NotFound)?;
2220 self.set_textarea_cursor(id, len)
2221 }
2222
2223 pub fn set_textarea_cursor_line_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
2224 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2225 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2226 let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2227 self.set_textarea_cursor_with_extend(id, next, false)
2228 }
2229
2230 pub fn set_textarea_cursor_line_home_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2231 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2232 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2233 let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
2234 self.set_textarea_cursor_with_extend(id, next, true)
2235 }
2236
2237 pub fn set_textarea_cursor_line_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
2238 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2239 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2240 let row_end = textarea_row_end_col(text, row, wrap_cols);
2241 let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2242 self.set_textarea_cursor_with_extend(id, next, false)
2243 }
2244
2245 pub fn set_textarea_cursor_line_end_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
2246 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
2247 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
2248 let row_end = textarea_row_end_col(text, row, wrap_cols);
2249 let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
2250 self.set_textarea_cursor_with_extend(id, next, true)
2251 }
2252
2253 pub fn textarea_cursor(&self, id: WidgetId) -> Option<usize> {
2254 match self.node(id)?.kind {
2255 WidgetKind::TextArea { cursor, .. } => Some(cursor),
2256 _ => None,
2257 }
2258 }
2259
2260 pub fn set_textarea_selection(
2261 &mut self,
2262 id: WidgetId,
2263 start: usize,
2264 end: usize,
2265 ) -> Result<(), GuiError> {
2266 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2267 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2268 match node.kind {
2269 WidgetKind::TextArea {
2270 text_buf,
2271 text_len,
2272 selection: ref mut current,
2273 ..
2274 } => {
2275 let text = textarea_text(&text_buf, text_len);
2276 let len = text.chars().count();
2277 let start = start.min(len);
2278 let end = end.min(len);
2279 *current = Some((start.min(end), start.max(end)));
2280 self.dirty.add(rect)?;
2281 Ok(())
2282 }
2283 _ => Err(GuiError::NotFound),
2284 }
2285 }
2286
2287 pub fn clear_textarea_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
2288 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2289 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2290 match node.kind {
2291 WidgetKind::TextArea {
2292 selection: ref mut current,
2293 ..
2294 } => {
2295 *current = None;
2296 self.dirty.add(rect)?;
2297 Ok(())
2298 }
2299 _ => Err(GuiError::NotFound),
2300 }
2301 }
2302
2303 pub fn textarea_selection(&self, id: WidgetId) -> Option<(usize, usize)> {
2304 match self.node(id)?.kind {
2305 WidgetKind::TextArea { selection, .. } => selection,
2306 _ => None,
2307 }
2308 }
2309
2310 pub fn textarea_cursor_visible(&self, id: WidgetId) -> Option<bool> {
2311 match self.node(id)?.kind {
2312 WidgetKind::TextArea { cursor_visible, .. } => Some(cursor_visible),
2313 _ => None,
2314 }
2315 }
2316
2317 pub fn set_textarea_capabilities(
2318 &mut self,
2319 id: WidgetId,
2320 read_only: bool,
2321 single_line: bool,
2322 accept_newline: bool,
2323 ) -> Result<(), GuiError> {
2324 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2325 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2326 match node.kind {
2327 WidgetKind::TextArea {
2328 read_only: ref mut ro,
2329 single_line: ref mut sl,
2330 accept_newline: ref mut an,
2331 ..
2332 } => {
2333 *ro = read_only;
2334 *sl = single_line;
2335 *an = accept_newline && !single_line;
2336 self.dirty.add(rect)?;
2337 Ok(())
2338 }
2339 _ => Err(GuiError::NotFound),
2340 }
2341 }
2342
2343 pub fn textarea_insert_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
2344 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2345 let before = self.capture_textarea_snapshot(id)?;
2346 let mut emit = false;
2347 if let Some(node) = self.node_mut(id) {
2348 if let WidgetKind::TextArea {
2349 text_buf,
2350 text_len,
2351 cursor,
2352 selection,
2353 read_only,
2354 single_line,
2355 accept_newline,
2356 ..
2357 } = &mut node.kind
2358 {
2359 if *read_only {
2360 return Ok(());
2361 }
2362 if ch == '\n' && (*single_line || !*accept_newline) {
2363 return Ok(());
2364 }
2365 let mut chars: heapless::Vec<char, TEXTAREA_CAPACITY> = heapless::Vec::new();
2366 for c in textarea_text(text_buf, *text_len).chars() {
2367 let _ = chars.push(c);
2368 }
2369 let original_len = chars.len();
2370 let original_cursor = *cursor;
2371
2372 if ch == '\u{8}' {
2373 let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2374 if !removed_selection && *cursor > 0 && *cursor <= chars.len() {
2375 chars.remove(*cursor - 1);
2376 *cursor -= 1;
2377 }
2378 if removed_selection
2379 || *cursor != original_cursor
2380 || chars.len() != original_len
2381 {
2382 *selection = None;
2383 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2384 *text_buf = next_buf;
2385 *text_len = next_len;
2386 emit = true;
2387 }
2388 } else if ch == '\u{7f}' {
2389 let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
2390 if !removed_selection && *cursor < chars.len() {
2391 chars.remove(*cursor);
2392 }
2393 if removed_selection || chars.len() != original_len {
2394 *selection = None;
2395 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2396 *text_buf = next_buf;
2397 *text_len = next_len;
2398 emit = true;
2399 }
2400 } else if ch != '\n' || *cursor < TEXTAREA_CAPACITY {
2401 if delete_selection_if_any(&mut chars, cursor, selection) {
2402 *selection = None;
2403 }
2404 if chars.len() < TEXTAREA_CAPACITY && *cursor <= chars.len() {
2405 let _ = chars.insert(*cursor, ch);
2406 *cursor += 1;
2407 *selection = None;
2408 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
2409 *text_buf = next_buf;
2410 *text_len = next_len;
2411 emit = true;
2412 }
2413 }
2414 } else {
2415 return Err(GuiError::NotFound);
2416 }
2417 }
2418 if emit {
2419 self.push_textarea_undo(id, before);
2420 self.clear_textarea_redo_for(id);
2421 self.dirty.add(rect)?;
2422 self.push_event(UiEvent::TextInput { id, ch })?;
2423 self.push_event(UiEvent::ValueChanged(id))?;
2424 }
2425 Ok(())
2426 }
2427
2428 fn textarea_line_context(&self, id: WidgetId) -> Result<(&str, usize, usize), GuiError> {
2429 let node = self.node(id).ok_or(GuiError::NotFound)?;
2430 match &node.kind {
2431 WidgetKind::TextArea {
2432 text_buf,
2433 text_len,
2434 cursor,
2435 ..
2436 } => {
2437 let font = node.style.normal.font;
2438 let inner_w = node.rect.w.saturating_sub(2);
2439 let cols = (inner_w / font.advance()).max(1) as usize;
2440 Ok((textarea_text(text_buf, *text_len), *cursor, cols))
2441 }
2442 _ => Err(GuiError::NotFound),
2443 }
2444 }
2445
2446 fn set_textarea_cursor_with_extend(
2447 &mut self,
2448 id: WidgetId,
2449 cursor: usize,
2450 extend_selection: bool,
2451 ) -> Result<(), GuiError> {
2452 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2453 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2454 match node.kind {
2455 WidgetKind::TextArea {
2456 text_buf,
2457 text_len,
2458 cursor: ref mut current_cursor,
2459 ref mut selection,
2460 ..
2461 } => {
2462 let len = textarea_text(&text_buf, text_len).chars().count();
2463 let next = cursor.min(len);
2464 if extend_selection {
2465 let anchor = match *selection {
2466 Some((start, end)) => {
2467 if *current_cursor == start {
2468 end
2469 } else {
2470 start
2471 }
2472 }
2473 None => *current_cursor,
2474 };
2475 if anchor == next {
2476 *selection = None;
2477 } else {
2478 *selection = Some((anchor.min(next), anchor.max(next)));
2479 }
2480 } else {
2481 *selection = None;
2482 }
2483 *current_cursor = next;
2484 self.dirty.add(rect)?;
2485 Ok(())
2486 }
2487 _ => Err(GuiError::NotFound),
2488 }
2489 }
2490
2491 fn capture_textarea_snapshot(&self, id: WidgetId) -> Result<TextareaSnapshot, GuiError> {
2492 match self.node(id).ok_or(GuiError::NotFound)?.kind {
2493 WidgetKind::TextArea {
2494 text_buf,
2495 text_len,
2496 cursor,
2497 selection,
2498 ..
2499 } => Ok(TextareaSnapshot {
2500 text_buf,
2501 text_len,
2502 cursor,
2503 selection,
2504 }),
2505 _ => Err(GuiError::NotFound),
2506 }
2507 }
2508
2509 fn apply_textarea_snapshot(
2510 &mut self,
2511 id: WidgetId,
2512 snap: TextareaSnapshot,
2513 ) -> Result<(), GuiError> {
2514 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2515 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2516 match node.kind {
2517 WidgetKind::TextArea {
2518 text_buf: ref mut buf,
2519 text_len: ref mut len,
2520 cursor: ref mut c,
2521 selection: ref mut sel,
2522 ..
2523 } => {
2524 *buf = snap.text_buf;
2525 *len = snap.text_len;
2526 *c = snap.cursor;
2527 *sel = snap.selection;
2528 self.dirty.add(rect)?;
2529 self.push_event(UiEvent::ValueChanged(id))
2530 }
2531 _ => Err(GuiError::NotFound),
2532 }
2533 }
2534
2535 fn push_textarea_undo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2536 if self.textarea_undo.len() == self.textarea_undo.capacity() {
2537 self.textarea_undo.remove(0);
2538 }
2539 let _ = self
2540 .textarea_undo
2541 .push(TextareaHistoryEntry { id, snapshot });
2542 }
2543
2544 fn push_textarea_redo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
2545 if self.textarea_redo.len() == self.textarea_redo.capacity() {
2546 self.textarea_redo.remove(0);
2547 }
2548 let _ = self
2549 .textarea_redo
2550 .push(TextareaHistoryEntry { id, snapshot });
2551 }
2552
2553 fn clear_textarea_redo_for(&mut self, id: WidgetId) {
2554 let mut i = 0usize;
2555 while i < self.textarea_redo.len() {
2556 if self.textarea_redo[i].id == id {
2557 self.textarea_redo.remove(i);
2558 } else {
2559 i += 1;
2560 }
2561 }
2562 }
2563
2564 fn textarea_undo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2565 let Some(pos) = self.textarea_undo.iter().rposition(|entry| entry.id == id) else {
2566 return Ok(());
2567 };
2568 let current = self.capture_textarea_snapshot(id)?;
2569 let prior = self.textarea_undo.remove(pos).snapshot;
2570 self.push_textarea_redo(id, current);
2571 self.apply_textarea_snapshot(id, prior)
2572 }
2573
2574 fn textarea_redo(&mut self, id: WidgetId) -> Result<(), GuiError> {
2575 let Some(pos) = self.textarea_redo.iter().rposition(|entry| entry.id == id) else {
2576 return Ok(());
2577 };
2578 let current = self.capture_textarea_snapshot(id)?;
2579 let next = self.textarea_redo.remove(pos).snapshot;
2580 self.push_textarea_undo(id, current);
2581 self.apply_textarea_snapshot(id, next)
2582 }
2583
2584 pub fn textarea_backspace(&mut self, id: WidgetId) -> Result<(), GuiError> {
2585 self.textarea_insert_char(id, '\u{8}')
2586 }
2587
2588 pub fn textarea_delete_forward(&mut self, id: WidgetId) -> Result<(), GuiError> {
2589 self.textarea_insert_char(id, '\u{7f}')
2590 }
2591
2592 pub fn keyboard_selected_key(&self, id: WidgetId) -> Option<char> {
2593 match self.node(id)?.kind {
2594 WidgetKind::Keyboard {
2595 keys,
2596 alt_keys,
2597 selected,
2598 layout,
2599 ..
2600 } => keyboard_char_for_layout(keys, alt_keys, selected, layout),
2601 _ => None,
2602 }
2603 }
2604
2605 pub fn keyboard_layout(&self, id: WidgetId) -> Option<KeyboardLayout> {
2606 match self.node(id)?.kind {
2607 WidgetKind::Keyboard { layout, .. } => Some(layout),
2608 _ => None,
2609 }
2610 }
2611
2612 pub fn set_keyboard_layout(
2613 &mut self,
2614 id: WidgetId,
2615 layout: KeyboardLayout,
2616 ) -> Result<(), GuiError> {
2617 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2618 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2619 match node.kind {
2620 WidgetKind::Keyboard {
2621 layout: ref mut current,
2622 ..
2623 } => {
2624 *current = layout;
2625 self.dirty.add(rect)?;
2626 Ok(())
2627 }
2628 _ => Err(GuiError::NotFound),
2629 }
2630 }
2631
2632 pub fn set_keyboard_target(
2633 &mut self,
2634 id: WidgetId,
2635 target: Option<WidgetId>,
2636 ) -> Result<(), GuiError> {
2637 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2638 match node.kind {
2639 WidgetKind::Keyboard {
2640 target: ref mut current,
2641 ..
2642 } => {
2643 *current = target;
2644 Ok(())
2645 }
2646 _ => Err(GuiError::NotFound),
2647 }
2648 }
2649
2650 pub fn set_gauge_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
2651 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2652 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2653 match node.kind {
2654 WidgetKind::Gauge {
2655 value: ref mut v,
2656 min,
2657 max,
2658 ..
2659 }
2660 | WidgetKind::ArcGauge {
2661 value: ref mut v,
2662 min,
2663 max,
2664 ..
2665 }
2666 | WidgetKind::GaugeNeedle {
2667 value: ref mut v,
2668 min,
2669 max,
2670 ..
2671 } => {
2672 *v = value.clamp(min.min(max), min.max(max));
2673 self.dirty.add(rect)?;
2674 Ok(())
2675 }
2676 _ => Err(GuiError::NotFound),
2677 }
2678 }
2679
2680 pub fn set_gauge_ticks(
2681 &mut self,
2682 id: WidgetId,
2683 major_ticks: u8,
2684 minor_ticks: u8,
2685 show_value: bool,
2686 ) -> Result<(), GuiError> {
2687 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2688 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2689 match node.kind {
2690 WidgetKind::Gauge {
2691 major_ticks: ref mut major,
2692 minor_ticks: ref mut minor,
2693 show_value: ref mut show,
2694 ..
2695 }
2696 | WidgetKind::ArcGauge {
2697 major_ticks: ref mut major,
2698 minor_ticks: ref mut minor,
2699 show_value: ref mut show,
2700 ..
2701 } => {
2702 *major = major_ticks.max(1);
2703 *minor = minor_ticks.max(1);
2704 *show = show_value;
2705 self.dirty.add(rect)?;
2706 Ok(())
2707 }
2708 _ => Err(GuiError::NotFound),
2709 }
2710 }
2711
2712 pub fn set_widget_rect(&mut self, id: WidgetId, rect: Rect) -> Result<(), GuiError> {
2713 let old = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
2714 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2715 node.rect = rect;
2716 self.dirty.add(old)?;
2717 self.mark_subtree_dirty(id)?;
2718 Ok(())
2719 }
2720
2721 pub fn set_widget_x(&mut self, id: WidgetId, x: i32) -> Result<(), GuiError> {
2722 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2723 rect.x = x;
2724 self.set_widget_rect(id, rect)
2725 }
2726
2727 pub fn set_widget_y(&mut self, id: WidgetId, y: i32) -> Result<(), GuiError> {
2728 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2729 rect.y = y;
2730 self.set_widget_rect(id, rect)
2731 }
2732
2733 pub fn set_widget_width(&mut self, id: WidgetId, w: u32) -> Result<(), GuiError> {
2734 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2735 rect.w = w.max(1);
2736 self.set_widget_rect(id, rect)
2737 }
2738
2739 pub fn set_widget_height(&mut self, id: WidgetId, h: u32) -> Result<(), GuiError> {
2740 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
2741 rect.h = h.max(1);
2742 self.set_widget_rect(id, rect)
2743 }
2744
2745 pub fn set_widget_opacity(&mut self, id: WidgetId, opacity: u8) -> Result<(), GuiError> {
2746 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2747 node.style.normal.opacity = opacity;
2748 node.style.focused.opacity = opacity;
2749 node.style.pressed.opacity = opacity;
2750 node.style.disabled.opacity = opacity;
2751 self.mark_subtree_dirty(id)
2752 }
2753
2754 pub fn set_widget_corner_radius(&mut self, id: WidgetId, radius: u8) -> Result<(), GuiError> {
2755 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2756 node.style.normal.corner_radius = radius;
2757 node.style.focused.corner_radius = radius;
2758 node.style.pressed.corner_radius = radius;
2759 node.style.disabled.corner_radius = radius;
2760 self.mark_subtree_dirty(id)
2761 }
2762
2763 pub fn set_widget_accent(&mut self, id: WidgetId, accent: Rgb565) -> Result<(), GuiError> {
2764 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
2765 node.style.normal.accent = accent;
2766 node.style.focused.accent = accent;
2767 node.style.pressed.accent = accent;
2768 node.style.disabled.accent = accent;
2769 self.mark_subtree_dirty(id)
2770 }
2771
2772 pub fn set_widget_parent(
2773 &mut self,
2774 id: WidgetId,
2775 parent: Option<WidgetId>,
2776 ) -> Result<(), GuiError> {
2777 if let Some(parent) = parent {
2778 self.node(parent).ok_or(GuiError::NotFound)?;
2779 }
2780 self.node_mut(id).ok_or(GuiError::NotFound)?.parent = parent;
2781 self.mark_subtree_dirty(id)?;
2782 Ok(())
2783 }
2784
2785 pub fn add_child(&mut self, parent: WidgetId, child: WidgetId) -> Result<(), GuiError> {
2786 self.set_widget_parent(child, Some(parent))
2787 }
2788
2789 pub fn children_of(&self, parent: WidgetId) -> impl Iterator<Item = &WidgetNode<'a>> + '_ {
2790 self.widgets
2791 .iter()
2792 .filter(move |node| node.parent == Some(parent))
2793 }
2794
2795 pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
2796 let node = self.node(id)?;
2797 let mut rect = node.rect;
2798 let mut parent = node.parent;
2799 let mut depth = 0;
2800 while let Some(parent_id) = parent {
2801 if depth >= NODES {
2802 return None;
2803 }
2804 let parent_node = self.node(parent_id)?;
2805 rect.x += parent_node.rect.x;
2806 rect.y += parent_node.rect.y;
2807 parent = parent_node.parent;
2808 depth += 1;
2809 }
2810 Some(rect)
2811 }
2812
2813 pub fn set_flag(
2814 &mut self,
2815 id: WidgetId,
2816 flag: WidgetFlags,
2817 enabled: bool,
2818 ) -> Result<(), GuiError> {
2819 let was_set = self.has_flag(id, flag)?;
2820 let before_state = self.current_visual_state(id);
2821 self.mark_subtree_dirty(id)?;
2822 self.node_mut(id)
2823 .ok_or(GuiError::NotFound)?
2824 .flags
2825 .set(flag, enabled);
2826 if flag == WidgetFlags::DISABLED
2827 && enabled
2828 && self.pressed.is_some_and(|pressed| pressed.id == id)
2829 {
2830 self.pressed = None;
2831 }
2832 self.mark_subtree_dirty(id)?;
2833 if self
2834 .focus
2835 .is_some_and(|focus| !self.effective_focusable(focus))
2836 {
2837 self.focus = None;
2838 self.ensure_focus();
2839 }
2840 if flag == WidgetFlags::DISABLED && was_set != enabled {
2841 let after_state = self.current_visual_state(id);
2842 self.start_state_transition(id, before_state, after_state);
2843 }
2844 Ok(())
2845 }
2846
2847 pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
2848 Ok(self
2849 .node(id)
2850 .ok_or(GuiError::NotFound)?
2851 .flags
2852 .contains(flag))
2853 }
2854
2855 pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2856 self.set_flag(id, flag, true)
2857 }
2858
2859 pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
2860 self.set_flag(id, flag, false)
2861 }
2862
2863 pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
2864 self.set_flag(id, WidgetFlags::HIDDEN, hidden)
2865 }
2866
2867 pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
2868 self.set_flag(id, WidgetFlags::DISABLED, disabled)
2869 }
2870
2871 pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
2872 self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
2873 }
2874
2875 pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
2876 self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
2877 }
2878
2879 pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
2880 self.set_hidden(id, !visible)
2881 }
2882
2883 pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
2884 self.set_disabled(id, !enabled)
2885 }
2886
2887 pub fn event_path<const M: usize>(
2888 &self,
2889 target: WidgetId,
2890 out: &mut heapless::Vec<EventContext, M>,
2891 ) -> Result<usize, GuiError> {
2892 self.node(target).ok_or(GuiError::NotFound)?;
2893 out.clear();
2894
2895 let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2896 let mut current = Some(target);
2897 while let Some(id) = current {
2898 chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2899 current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2900 }
2901
2902 for id in chain.iter().rev().copied().filter(|&id| id != target) {
2903 out.push(EventContext {
2904 target,
2905 current: id,
2906 phase: EventPhase::Capture,
2907 })
2908 .map_err(|_| GuiError::EventsFull)?;
2909 }
2910
2911 out.push(EventContext {
2912 target,
2913 current: target,
2914 phase: EventPhase::Target,
2915 })
2916 .map_err(|_| GuiError::EventsFull)?;
2917
2918 for id in chain.iter().copied().skip(1) {
2919 out.push(EventContext {
2920 target,
2921 current: id,
2922 phase: EventPhase::Bubble,
2923 })
2924 .map_err(|_| GuiError::EventsFull)?;
2925 }
2926
2927 Ok(out.len())
2928 }
2929
2930 pub fn widget_event_path<const M: usize>(
2931 &self,
2932 target: WidgetId,
2933 kind: WidgetEventKind,
2934 out: &mut heapless::Vec<WidgetEvent, M>,
2935 ) -> Result<usize, GuiError> {
2936 self.node(target).ok_or(GuiError::NotFound)?;
2937 out.clear();
2938
2939 let mut chain = heapless::Vec::<WidgetId, NODES>::new();
2940 let mut current = Some(target);
2941 while let Some(id) = current {
2942 chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
2943 current = self.node(id).ok_or(GuiError::NotFound)?.parent;
2944 }
2945
2946 for id in chain.iter().rev().copied().filter(|&id| id != target) {
2947 out.push(WidgetEvent {
2948 target,
2949 current: id,
2950 phase: EventPhase::Capture,
2951 kind,
2952 })
2953 .map_err(|_| GuiError::EventsFull)?;
2954 }
2955
2956 out.push(WidgetEvent {
2957 target,
2958 current: target,
2959 phase: EventPhase::Target,
2960 kind,
2961 })
2962 .map_err(|_| GuiError::EventsFull)?;
2963
2964 if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
2965 for id in chain.iter().copied().skip(1) {
2966 out.push(WidgetEvent {
2967 target,
2968 current: id,
2969 phase: EventPhase::Bubble,
2970 kind,
2971 })
2972 .map_err(|_| GuiError::EventsFull)?;
2973 }
2974 }
2975
2976 Ok(out.len())
2977 }
2978
2979 pub fn dispatch_widget_event<const M: usize, F>(
2980 &self,
2981 target: WidgetId,
2982 kind: WidgetEventKind,
2983 scratch: &mut heapless::Vec<WidgetEvent, M>,
2984 mut handler: F,
2985 ) -> Result<(), GuiError>
2986 where
2987 F: FnMut(WidgetEvent) -> EventPolicy,
2988 {
2989 self.widget_event_path(target, kind, scratch)?;
2990 for event in scratch.iter().copied() {
2991 let handler_policy = handler(event);
2992 if matches!(handler_policy, EventPolicy::Stop)
2993 || self.stop_due_to_builtin_widget_behavior(event)
2994 || self.stop_due_to_registered_policy(event)
2995 {
2996 break;
2997 }
2998 }
2999 Ok(())
3000 }
3001
3002 pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
3003 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
3004 self.dirty.add(rect)?;
3005 let child_ids: heapless::Vec<WidgetId, NODES> = self
3006 .widgets
3007 .iter()
3008 .filter(|node| node.parent == Some(id))
3009 .map(|node| node.id)
3010 .collect();
3011 for child in child_ids {
3012 self.mark_subtree_dirty(child)?;
3013 }
3014 Ok(())
3015 }
3016
3017 pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
3018 self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
3019 Ok(())
3020 }
3021
3022 pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
3023 self.active_focus_group = group;
3024 if let Some(focus) = self.focus {
3025 let still_valid = self.node(focus).is_some_and(|node| {
3026 group.is_none_or(|active| node.focus_group == active)
3027 && self.effective_focusable(focus)
3028 });
3029 if !still_valid {
3030 self.focus = None;
3031 self.ensure_focus();
3032 }
3033 }
3034 }
3035
3036 pub fn apply_layout(
3037 &mut self,
3038 layout: LinearLayout,
3039 area: Rect,
3040 ids: &[WidgetId],
3041 ) -> Result<usize, GuiError> {
3042 let mut rects = [Rect::empty(); 16];
3043 let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
3044 for (id, rect) in ids.iter().copied().zip(rects).take(count) {
3045 self.set_widget_rect(id, rect)?;
3046 }
3047 Ok(count)
3048 }
3049
3050 pub fn apply_layout_flex(
3051 &mut self,
3052 layout: LinearLayout,
3053 area: Rect,
3054 ids: &[WidgetId],
3055 items: &[LayoutItem],
3056 enable_grow: bool,
3057 enable_shrink: bool,
3058 ) -> Result<usize, GuiError> {
3059 let mut rects = [Rect::empty(); 16];
3060 let count = ids.len().min(items.len()).min(rects.len());
3061 let laid_out = layout.arrange_items_flex(
3062 area,
3063 &items[..count],
3064 &mut rects,
3065 enable_grow,
3066 enable_shrink,
3067 );
3068 for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
3069 self.set_widget_rect(id, rect)?;
3070 }
3071 Ok(laid_out)
3072 }
3073
3074 pub fn apply_layout_intrinsic(
3075 &mut self,
3076 layout: LinearLayout,
3077 area: Rect,
3078 ids: &[WidgetId],
3079 ) -> Result<usize, GuiError> {
3080 self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
3081 }
3082
3083 pub fn apply_layout_intrinsic_with_cross(
3084 &mut self,
3085 layout: LinearLayout,
3086 area: Rect,
3087 ids: &[WidgetId],
3088 preserve_cross: bool,
3089 ) -> Result<usize, GuiError> {
3090 let mut specs = [LayoutItem::fill(); 16];
3091 let mut rects = [Rect::empty(); 16];
3092 let count = ids.len().min(specs.len()).min(rects.len());
3093
3094 for (idx, id) in ids.iter().copied().take(count).enumerate() {
3095 let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
3096 specs[idx] = match layout.axis {
3097 Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
3098 crate::layout::Constraint::Length(h)
3099 } else {
3100 crate::layout::Constraint::Fill(1)
3101 }),
3102 Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
3103 crate::layout::Constraint::Length(w)
3104 } else {
3105 crate::layout::Constraint::Fill(1)
3106 }),
3107 };
3108 }
3109
3110 let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
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 render<D>(&self, target: &mut D) -> Result<(), D::Error>
3118 where
3119 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3120 {
3121 let mut ctx = RenderCtx::new(target, self.viewport);
3122 ctx.set_quality(self.render_quality);
3123 self.render_into(&mut ctx, 0, 0, 255)
3124 }
3125
3126 pub fn render_dirty<D>(&self, target: &mut D) -> Result<(), D::Error>
3127 where
3128 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3129 {
3130 if self.dirty.is_empty() {
3131 return Ok(());
3132 }
3133
3134 for dirty in self.dirty.as_slice() {
3135 let mut ctx = RenderCtx::with_dirty(target, self.viewport, *dirty);
3136 ctx.set_quality(self.render_quality);
3137 self.render_into(&mut ctx, 0, 0, 255)?;
3138 }
3139 Ok(())
3140 }
3141
3142 pub fn render_with_offset<D>(
3143 &self,
3144 target: &mut D,
3145 offset_x: i32,
3146 offset_y: i32,
3147 ) -> Result<(), D::Error>
3148 where
3149 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3150 {
3151 self.render_with_offset_and_opacity(target, offset_x, offset_y, 255)
3152 }
3153
3154 pub fn render_with_offset_and_opacity<D>(
3155 &self,
3156 target: &mut D,
3157 offset_x: i32,
3158 offset_y: i32,
3159 opacity: u8,
3160 ) -> 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, offset_x, offset_y, opacity)
3167 }
3168
3169 pub fn render_with_offset_opacity_and_clip<D>(
3170 &self,
3171 target: &mut D,
3172 offset_x: i32,
3173 offset_y: i32,
3174 opacity: u8,
3175 clip: Rect,
3176 ) -> Result<(), D::Error>
3177 where
3178 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3179 {
3180 let mut ctx = RenderCtx::new(target, self.viewport);
3181 ctx.set_quality(self.render_quality);
3182 let old_clip = ctx.clip();
3183 ctx.set_clip(old_clip.intersection(clip));
3184 self.render_into(&mut ctx, offset_x, offset_y, opacity)
3185 }
3186
3187 pub fn handle_input(&mut self, event: InputEvent) -> Result<(), GuiError> {
3188 match event {
3189 InputEvent::Home => {
3190 if let Some(id) = self.focus {
3191 if matches!(
3192 self.node(id).map(|n| n.kind),
3193 Some(WidgetKind::TextArea { .. })
3194 ) {
3195 self.set_textarea_cursor_line_home(id)?;
3196 return Ok(());
3197 }
3198 }
3199 Ok(())
3200 }
3201 InputEvent::End => {
3202 if let Some(id) = self.focus {
3203 if matches!(
3204 self.node(id).map(|n| n.kind),
3205 Some(WidgetKind::TextArea { .. })
3206 ) {
3207 self.set_textarea_cursor_line_end(id)?;
3208 return Ok(());
3209 }
3210 }
3211 Ok(())
3212 }
3213 InputEvent::WordLeft => {
3214 if let Some(id) = self.focus {
3215 if matches!(
3216 self.node(id).map(|n| n.kind),
3217 Some(WidgetKind::TextArea { .. })
3218 ) {
3219 self.move_textarea_cursor_word(id, -1)?;
3220 return Ok(());
3221 }
3222 }
3223 Ok(())
3224 }
3225 InputEvent::WordRight => {
3226 if let Some(id) = self.focus {
3227 if matches!(
3228 self.node(id).map(|n| n.kind),
3229 Some(WidgetKind::TextArea { .. })
3230 ) {
3231 self.move_textarea_cursor_word(id, 1)?;
3232 return Ok(());
3233 }
3234 }
3235 Ok(())
3236 }
3237 InputEvent::Undo => {
3238 if let Some(id) = self.focus {
3239 if matches!(
3240 self.node(id).map(|n| n.kind),
3241 Some(WidgetKind::TextArea { .. })
3242 ) {
3243 self.textarea_undo(id)?;
3244 }
3245 }
3246 Ok(())
3247 }
3248 InputEvent::Redo => {
3249 if let Some(id) = self.focus {
3250 if matches!(
3251 self.node(id).map(|n| n.kind),
3252 Some(WidgetKind::TextArea { .. })
3253 ) {
3254 self.textarea_redo(id)?;
3255 }
3256 }
3257 Ok(())
3258 }
3259 InputEvent::SelectLeft => {
3260 if let Some(id) = self.focus {
3261 if matches!(
3262 self.node(id).map(|n| n.kind),
3263 Some(WidgetKind::TextArea { .. })
3264 ) {
3265 self.move_textarea_cursor_select(id, -1)?;
3266 return Ok(());
3267 }
3268 }
3269 Ok(())
3270 }
3271 InputEvent::SelectRight => {
3272 if let Some(id) = self.focus {
3273 if matches!(
3274 self.node(id).map(|n| n.kind),
3275 Some(WidgetKind::TextArea { .. })
3276 ) {
3277 self.move_textarea_cursor_select(id, 1)?;
3278 return Ok(());
3279 }
3280 }
3281 Ok(())
3282 }
3283 InputEvent::SelectHome => {
3284 if let Some(id) = self.focus {
3285 if matches!(
3286 self.node(id).map(|n| n.kind),
3287 Some(WidgetKind::TextArea { .. })
3288 ) {
3289 self.set_textarea_cursor_line_home_select(id)?;
3290 return Ok(());
3291 }
3292 }
3293 Ok(())
3294 }
3295 InputEvent::SelectEnd => {
3296 if let Some(id) = self.focus {
3297 if matches!(
3298 self.node(id).map(|n| n.kind),
3299 Some(WidgetKind::TextArea { .. })
3300 ) {
3301 self.set_textarea_cursor_line_end_select(id)?;
3302 return Ok(());
3303 }
3304 }
3305 Ok(())
3306 }
3307 InputEvent::SelectWordLeft => {
3308 if let Some(id) = self.focus {
3309 if matches!(
3310 self.node(id).map(|n| n.kind),
3311 Some(WidgetKind::TextArea { .. })
3312 ) {
3313 self.move_textarea_cursor_word_select(id, -1)?;
3314 return Ok(());
3315 }
3316 }
3317 Ok(())
3318 }
3319 InputEvent::SelectWordRight => {
3320 if let Some(id) = self.focus {
3321 if matches!(
3322 self.node(id).map(|n| n.kind),
3323 Some(WidgetKind::TextArea { .. })
3324 ) {
3325 self.move_textarea_cursor_word_select(id, 1)?;
3326 return Ok(());
3327 }
3328 }
3329 Ok(())
3330 }
3331 InputEvent::Up => {
3332 if !self.adjust_focused_selection(-1)? {
3333 self.focus_prev()?;
3334 }
3335 Ok(())
3336 }
3337 InputEvent::Down => {
3338 if !self.adjust_focused_selection(1)? {
3339 self.focus_next()?;
3340 }
3341 Ok(())
3342 }
3343 InputEvent::Left => {
3344 if !self.adjust_focused_scalar(-1.0)? {
3345 self.focus_prev()?;
3346 }
3347 Ok(())
3348 }
3349 InputEvent::Right => {
3350 if !self.adjust_focused_scalar(1.0)? {
3351 self.focus_next()?;
3352 }
3353 Ok(())
3354 }
3355 InputEvent::Encoder { delta } if delta > 0 => {
3356 if !self.adjust_focused_selection(1)? {
3357 self.focus_next()?;
3358 }
3359 Ok(())
3360 }
3361 InputEvent::Encoder { delta } if delta < 0 => {
3362 if !self.adjust_focused_selection(-1)? {
3363 self.focus_prev()?;
3364 }
3365 Ok(())
3366 }
3367 InputEvent::Select => {
3368 if let Some(id) = self.focus {
3369 match self.key_bindings_for(id).select {
3370 KeyBindingAction::Default | KeyBindingAction::Activate => {
3371 self.handle_select_activation(id)?
3372 }
3373 KeyBindingAction::Back => self.handle_back_action()?,
3374 KeyBindingAction::Ignore => {}
3375 }
3376 }
3377 Ok(())
3378 }
3379 InputEvent::SelectPressed => {
3380 if let Some(id) = self.focus {
3381 if self.key_input_policy_for(id).raw_select {
3382 self.dispatch_key_pressed(id)?;
3383 }
3384 }
3385 Ok(())
3386 }
3387 InputEvent::SelectReleased => {
3388 if let Some(id) = self.focus {
3389 if self.key_input_policy_for(id).raw_select {
3390 self.dispatch_key_released(id)?;
3391 self.handle_select_activation(id)?;
3392 }
3393 }
3394 Ok(())
3395 }
3396 InputEvent::Back => {
3397 if let Some(id) = self.focus {
3398 match self.key_bindings_for(id).back {
3399 KeyBindingAction::Default | KeyBindingAction::Back => {
3400 self.handle_back_action()
3401 }
3402 KeyBindingAction::Activate => self.handle_select_activation(id),
3403 KeyBindingAction::Ignore => Ok(()),
3404 }
3405 } else {
3406 self.handle_back_action()
3407 }
3408 }
3409 InputEvent::BackPressed => {
3410 if let Some(id) = self.focus {
3411 if self.key_input_policy_for(id).raw_back {
3412 self.dispatch_key_pressed(id)?;
3413 }
3414 }
3415 Ok(())
3416 }
3417 InputEvent::BackReleased => {
3418 if let Some(id) = self.focus {
3419 if self.key_input_policy_for(id).raw_back {
3420 self.dispatch_key_released(id)?;
3421 return self.handle_back_action();
3422 }
3423 }
3424 Ok(())
3425 }
3426 InputEvent::Pointer {
3427 x,
3428 y,
3429 state: PointerState::Pressed,
3430 ..
3431 } => self.handle_pointer_pressed(x, y),
3432 InputEvent::Pointer {
3433 x,
3434 y,
3435 state: PointerState::Released,
3436 ..
3437 } => self.handle_pointer_released(x, y),
3438 InputEvent::Pointer {
3439 x,
3440 y,
3441 state: PointerState::Moved,
3442 ..
3443 } => self.handle_pointer_moved(x, y),
3444 _ => Ok(()),
3445 }
3446 }
3447
3448 pub fn tick_input(&mut self, dt_ms: u32) -> Result<(), GuiError> {
3449 if self.last_select_id.is_some() {
3450 self.select_elapsed_ms = self.select_elapsed_ms.saturating_add(dt_ms);
3451 if self.select_elapsed_ms > self.select_double_window_ms {
3452 self.last_select_id = None;
3453 self.select_elapsed_ms = 0;
3454 }
3455 }
3456 if self.last_pointer_id.is_some() {
3457 self.pointer_elapsed_ms = self.pointer_elapsed_ms.saturating_add(dt_ms);
3458 if self.pointer_elapsed_ms > self.pointer_double_window_ms {
3459 self.last_pointer_id = None;
3460 self.pointer_elapsed_ms = 0;
3461 }
3462 }
3463 self.tick_state_transitions(dt_ms)?;
3464 if let Some(mut inertia) = self.inertia_scroll {
3465 if inertia.velocity.abs() < self.scroll_physics.velocity_threshold {
3466 self.inertia_scroll = None;
3467 } else {
3468 let current = self.scroll_offset(inertia.id).unwrap_or(0);
3469 let delta = (inertia.velocity * (dt_ms as f32 / 16.0)).round() as i32;
3470 if delta != 0 {
3471 let next = current.saturating_sub(delta);
3472 if next != current {
3473 self.set_scroll_offset(inertia.id, next)?;
3474 self.push_event(UiEvent::Scroll {
3475 id: inertia.id,
3476 delta: next - current,
3477 })?;
3478 }
3479 }
3480 inertia.velocity *= self
3481 .scroll_physics
3482 .velocity_decay
3483 .powf((dt_ms as f32 / 16.0).max(1.0));
3484 self.inertia_scroll = Some(inertia);
3485 }
3486 }
3487 self.tick_textarea_cursor_blink(dt_ms)?;
3488 let Some(mut pressed) = self.pressed else {
3489 return Ok(());
3490 };
3491 if !self.effective_visible(pressed.id) || !self.effective_enabled(pressed.id) {
3492 self.pressed = None;
3493 return Ok(());
3494 }
3495 let timing = self.press_timing_for(pressed.id);
3496 pressed.elapsed_ms = pressed.elapsed_ms.saturating_add(dt_ms);
3497 pressed.repeat_elapsed_ms = pressed.repeat_elapsed_ms.saturating_add(dt_ms);
3498 if !pressed.long_emitted && pressed.elapsed_ms >= timing.long_press_ms {
3499 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
3500 self.dispatch_widget_event(
3501 pressed.id,
3502 WidgetEventKind::LongPressed,
3503 &mut events,
3504 |_| EventPolicy::Continue,
3505 )?;
3506 self.push_event(UiEvent::LongPressed(pressed.id))?;
3507 pressed.long_emitted = true;
3508 }
3509 if pressed.repeat_elapsed_ms >= timing.repeat_delay_ms
3510 && self.repeatable_widget(pressed.id)
3511 && pressed.long_emitted
3512 {
3513 let intervals =
3514 (pressed.repeat_elapsed_ms - timing.repeat_delay_ms) / timing.repeat_interval_ms;
3515 if intervals > 0 {
3516 self.dispatch_repeat_activation(pressed.id)?;
3517 pressed.repeat_elapsed_ms = timing.repeat_delay_ms;
3518 }
3519 }
3520 self.pressed = Some(pressed);
3521 Ok(())
3522 }
3523
3524 pub fn pop_event(&mut self) -> Option<UiEvent> {
3525 if self.events.is_empty() {
3526 None
3527 } else {
3528 Some(self.events.remove(0))
3529 }
3530 }
3531
3532 pub fn set_event_filter(
3533 &mut self,
3534 id: WidgetId,
3535 filter: UiEventFilter,
3536 ) -> Result<(), GuiError> {
3537 self.node(id).ok_or(GuiError::NotFound)?;
3538 if let Some((_, current)) = self
3539 .subscriptions
3540 .iter_mut()
3541 .find(|(sub_id, _)| *sub_id == id)
3542 {
3543 *current = filter;
3544 return Ok(());
3545 }
3546 self.subscriptions
3547 .push((id, filter))
3548 .map_err(|_| GuiError::WidgetsFull)
3549 }
3550
3551 pub fn event_filter(&self, id: WidgetId) -> Result<UiEventFilter, GuiError> {
3552 self.node(id).ok_or(GuiError::NotFound)?;
3553 Ok(self
3554 .subscriptions
3555 .iter()
3556 .find(|(sub_id, _)| *sub_id == id)
3557 .map(|(_, filter)| *filter)
3558 .unwrap_or(UiEventFilter::ALL))
3559 }
3560
3561 pub fn clear_event_filter(&mut self, id: WidgetId) -> Result<(), GuiError> {
3562 self.node(id).ok_or(GuiError::NotFound)?;
3563 if let Some(pos) = self
3564 .subscriptions
3565 .iter()
3566 .position(|(sub_id, _)| *sub_id == id)
3567 {
3568 self.subscriptions.remove(pos);
3569 }
3570 Ok(())
3571 }
3572
3573 pub fn set_dispatch_policy(
3574 &mut self,
3575 id: WidgetId,
3576 policy: WidgetDispatchPolicy,
3577 ) -> Result<(), GuiError> {
3578 self.node(id).ok_or(GuiError::NotFound)?;
3579 if let Some((_, current)) = self
3580 .dispatch_policies
3581 .iter_mut()
3582 .find(|(policy_id, _)| *policy_id == id)
3583 {
3584 *current = policy;
3585 return Ok(());
3586 }
3587 self.dispatch_policies
3588 .push((id, policy))
3589 .map_err(|_| GuiError::WidgetsFull)
3590 }
3591
3592 pub fn dispatch_policy(&self, id: WidgetId) -> Result<Option<WidgetDispatchPolicy>, GuiError> {
3593 self.node(id).ok_or(GuiError::NotFound)?;
3594 Ok(self
3595 .dispatch_policies
3596 .iter()
3597 .find(|(policy_id, _)| *policy_id == id)
3598 .map(|(_, policy)| *policy))
3599 }
3600
3601 pub fn clear_dispatch_policy(&mut self, id: WidgetId) -> Result<(), GuiError> {
3602 self.node(id).ok_or(GuiError::NotFound)?;
3603 if let Some(pos) = self
3604 .dispatch_policies
3605 .iter()
3606 .position(|(policy_id, _)| *policy_id == id)
3607 {
3608 self.dispatch_policies.remove(pos);
3609 }
3610 Ok(())
3611 }
3612
3613 fn add_widget<S>(
3614 &mut self,
3615 rect: Rect,
3616 kind: WidgetKind<'a>,
3617 style: S,
3618 ) -> Result<WidgetId, GuiError>
3619 where
3620 S: Into<WidgetStyle>,
3621 {
3622 let id = WidgetId::new(self.next_id);
3623 self.next_id = self.next_id.saturating_add(1).max(1);
3624 let node = WidgetNode::new(id, rect, kind, style);
3625 self.widgets.push(node).map_err(|_| GuiError::WidgetsFull)?;
3626 self.dirty.add(rect)?;
3627 Ok(id)
3628 }
3629
3630 fn render_into<D>(
3631 &self,
3632 ctx: &mut RenderCtx<'_, D>,
3633 offset_x: i32,
3634 offset_y: i32,
3635 opacity: u8,
3636 ) -> Result<(), D::Error>
3637 where
3638 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
3639 {
3640 for node in &self.widgets {
3641 if !self.effective_visible(node.id) {
3642 continue;
3643 }
3644 let Some(base_rect) = self.absolute_rect(node.id) else {
3645 continue;
3646 };
3647 let rect = Rect::new(
3648 base_rect.x + offset_x,
3649 base_rect.y + offset_y,
3650 base_rect.w,
3651 base_rect.h,
3652 );
3653 let base_clip = self.inherited_clip(node.id).unwrap_or(self.viewport);
3654 let clip = Rect::new(
3655 base_clip.x + offset_x,
3656 base_clip.y + offset_y,
3657 base_clip.w,
3658 base_clip.h,
3659 );
3660 if rect.intersection(clip).is_empty() {
3661 continue;
3662 }
3663 let old_clip = ctx.clip();
3664 ctx.set_clip(old_clip.intersection(clip));
3665 let state = if self.pressed.is_some_and(|pressed| pressed.id == node.id) {
3666 VisualState::Pressed
3667 } else if Some(node.id) == self.focus {
3668 VisualState::Focused
3669 } else if !self.effective_enabled(node.id) {
3670 VisualState::Disabled
3671 } else {
3672 VisualState::Normal
3673 };
3674 let mut render_node = *node;
3675 let class_style = node.style_class.and_then(|class| {
3676 self.class_styles
3677 .iter()
3678 .find(|(id, _)| *id == class)
3679 .map(|(_, style)| *style)
3680 });
3681 let resolve_state_style = |vs: VisualState| {
3682 class_style
3683 .map(|style| style.resolve(vs))
3684 .unwrap_or_else(|| render_node.style.resolve(vs))
3685 };
3686 let active_style = if let Some((from, to, t)) = self.state_transition_progress(node.id)
3687 {
3688 lerp_style(resolve_state_style(from), resolve_state_style(to), t)
3689 } else {
3690 resolve_state_style(state)
3691 };
3692 render_node.style = render_node.style.with_state_override(state, active_style);
3693 if opacity < 255 {
3694 let apply = |v: u8| -> u8 { ((v as u16 * opacity as u16) / 255) as u8 };
3695 render_node.style.normal.opacity = apply(render_node.style.normal.opacity);
3696 render_node.style.focused.opacity = apply(render_node.style.focused.opacity);
3697 render_node.style.pressed.opacity = apply(render_node.style.pressed.opacity);
3698 render_node.style.disabled.opacity = apply(render_node.style.disabled.opacity);
3699 }
3700 render_node.render_at(ctx, rect, state)?;
3701 ctx.set_clip(old_clip);
3702 }
3703 Ok(())
3704 }
3705
3706 fn node(&self, id: WidgetId) -> Option<&WidgetNode<'a>> {
3707 self.widgets.iter().find(|node| node.id == id)
3708 }
3709
3710 fn intrinsic_size(&self, id: WidgetId) -> Option<(u32, u32)> {
3711 let node = self.node(id)?;
3712 let style = node.style.resolve(VisualState::Normal);
3713 let pad_x = style.padding.left.max(0) as u32 + style.padding.right.max(0) as u32;
3714 let pad_y = style.padding.top.max(0) as u32 + style.padding.bottom.max(0) as u32;
3715 let border = style.border.width as u32 * 2;
3716 let text_width = |text: &str| text.chars().count() as u32 * style.font.advance();
3717 let text_height = style.font.line_height();
3718
3719 let content = match node.kind {
3720 WidgetKind::Label(text) => (text_width(text), text_height),
3721 WidgetKind::Button(text) => (text_width(text).saturating_add(6), text_height),
3722 WidgetKind::Toggle { label, .. } => (text_width(label).saturating_add(12), text_height),
3723 WidgetKind::Checkbox { label, .. } => {
3724 (text_width(label).saturating_add(10), text_height)
3725 }
3726 WidgetKind::ValueLabel { label, .. } => {
3727 (text_width(label).saturating_add(16), text_height)
3728 }
3729 WidgetKind::IconButton { label, .. } => {
3730 (text_width(label).saturating_add(10), text_height)
3731 }
3732 WidgetKind::Tabs { labels, .. } => {
3733 let max = labels.iter().map(|s| text_width(s)).max().unwrap_or(0);
3734 (
3735 max.saturating_mul(labels.len() as u32).saturating_add(4),
3736 text_height,
3737 )
3738 }
3739 WidgetKind::Dialog { title, body } => {
3740 let w = text_width(title).max(text_width(body)).saturating_add(8);
3741 (w, text_height.saturating_mul(3))
3742 }
3743 WidgetKind::Toast { text, .. } => (
3744 text_width(text).saturating_add(8),
3745 text_height.saturating_add(2),
3746 ),
3747 WidgetKind::Dropdown {
3748 items, selected, ..
3749 } => (
3750 text_width(items.get(selected).copied().unwrap_or("-")).saturating_add(10),
3751 text_height.saturating_add(2),
3752 ),
3753 WidgetKind::TextArea {
3754 text_buf,
3755 text_len,
3756 placeholder,
3757 ..
3758 } => (
3759 text_width(if text_len == 0 {
3760 placeholder
3761 } else {
3762 textarea_text(&text_buf, text_len)
3763 })
3764 .saturating_add(10),
3765 text_height.saturating_add(4),
3766 ),
3767 WidgetKind::Keyboard { keys, cols, .. } => {
3768 let cols = cols.max(1) as u32;
3769 let rows = (keys.len() as u32).div_ceil(cols).max(1);
3770 (
3771 cols.saturating_mul(style.font.advance().saturating_add(4)),
3772 rows.saturating_mul(style.font.line_height().saturating_add(4)),
3773 )
3774 }
3775 WidgetKind::List {
3776 items,
3777 visible_rows,
3778 ..
3779 } => {
3780 let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3781 (
3782 max.saturating_add(6),
3783 (text_height.saturating_add(2))
3784 .saturating_mul(visible_rows as u32)
3785 .max(text_height),
3786 )
3787 }
3788 WidgetKind::Menu { items, .. } => {
3789 let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3790 (
3791 max.saturating_add(6),
3792 (text_height.saturating_add(2))
3793 .saturating_mul(items.len() as u32)
3794 .max(text_height),
3795 )
3796 }
3797 WidgetKind::FeedTimeline {
3798 items,
3799 visible_rows,
3800 expanded,
3801 ..
3802 } => {
3803 let max = items.iter().map(|s| text_width(s)).max().unwrap_or(0);
3804 let row_h = if expanded {
3805 text_height.saturating_mul(2).saturating_add(2)
3806 } else {
3807 text_height.saturating_add(2)
3808 };
3809 (
3810 max.saturating_add(8),
3811 row_h.saturating_mul(visible_rows as u32).max(text_height),
3812 )
3813 }
3814 _ => (node.rect.w.max(1), node.rect.h.max(1)),
3815 };
3816
3817 Some((
3818 content
3819 .0
3820 .saturating_add(pad_x)
3821 .saturating_add(border)
3822 .max(1),
3823 content
3824 .1
3825 .saturating_add(pad_y)
3826 .saturating_add(border)
3827 .max(1),
3828 ))
3829 }
3830
3831 fn node_mut(&mut self, id: WidgetId) -> Option<&mut WidgetNode<'a>> {
3832 self.widgets.iter_mut().find(|node| node.id == id)
3833 }
3834
3835 fn effective_visible(&self, id: WidgetId) -> bool {
3836 let mut current = Some(id);
3837 let mut depth = 0;
3838 while let Some(widget_id) = current {
3839 if depth >= NODES {
3840 return false;
3841 }
3842 let Some(node) = self.node(widget_id) else {
3843 return false;
3844 };
3845 if node.hidden() {
3846 return false;
3847 }
3848 current = node.parent;
3849 depth += 1;
3850 }
3851 true
3852 }
3853
3854 fn inherited_clip(&self, id: WidgetId) -> Option<Rect> {
3855 let mut clip = self.viewport;
3856 let mut chain = heapless::Vec::<WidgetId, NODES>::new();
3857 let mut current = Some(id);
3858 while let Some(widget_id) = current {
3859 chain.push(widget_id).ok()?;
3860 current = self.node(widget_id)?.parent;
3861 }
3862 for widget_id in chain.iter().rev().copied() {
3863 let node = self.node(widget_id)?;
3864 if widget_id == id || node.clips_children() {
3865 clip = clip.intersection(self.absolute_rect(widget_id)?);
3866 }
3867 if clip.is_empty() {
3868 return None;
3869 }
3870 }
3871 Some(clip)
3872 }
3873
3874 fn effective_enabled(&self, id: WidgetId) -> bool {
3875 let mut current = Some(id);
3876 let mut depth = 0;
3877 while let Some(widget_id) = current {
3878 if depth >= NODES {
3879 return false;
3880 }
3881 let Some(node) = self.node(widget_id) else {
3882 return false;
3883 };
3884 if node.disabled() {
3885 return false;
3886 }
3887 current = node.parent;
3888 depth += 1;
3889 }
3890 true
3891 }
3892
3893 fn effective_focusable(&self, id: WidgetId) -> bool {
3894 self.node(id).is_some_and(|node| {
3895 self.node_in_active_group(node)
3896 && node.focusable()
3897 && self.effective_visible(id)
3898 && self.effective_enabled(id)
3899 })
3900 }
3901
3902 fn ensure_focus(&mut self) {
3903 if self.focus.is_none() {
3904 self.focus = self
3905 .widgets
3906 .iter()
3907 .find(|node| self.effective_focusable(node.id))
3908 .map(|n| n.id);
3909 }
3910 }
3911
3912 fn focus_next(&mut self) -> Result<(), GuiError> {
3913 self.move_focus(1)
3914 }
3915
3916 fn focus_prev(&mut self) -> Result<(), GuiError> {
3917 self.move_focus(-1)
3918 }
3919
3920 fn move_focus(&mut self, delta: i8) -> Result<(), GuiError> {
3921 let focusable = self
3922 .widgets
3923 .iter()
3924 .filter(|node| self.effective_focusable(node.id))
3925 .count();
3926 if focusable == 0 {
3927 return Ok(());
3928 }
3929
3930 let current_pos = self
3931 .widgets
3932 .iter()
3933 .filter(|node| self.effective_focusable(node.id))
3934 .position(|node| Some(node.id) == self.focus)
3935 .unwrap_or(0);
3936
3937 let next_pos = if delta >= 0 {
3938 (current_pos + 1) % focusable
3939 } else if current_pos == 0 {
3940 focusable - 1
3941 } else {
3942 current_pos - 1
3943 };
3944
3945 let next = self
3946 .widgets
3947 .iter()
3948 .filter(|node| self.effective_focusable(node.id))
3949 .nth(next_pos)
3950 .map(|node| node.id);
3951 self.set_focus(next)
3952 }
3953
3954 fn adjust_focused_selection(&mut self, delta: i8) -> Result<bool, GuiError> {
3955 let Some(id) = self.focus else {
3956 return Ok(false);
3957 };
3958 let wrap_navigation = self.menu_contract.wrap_navigation;
3959
3960 let mut changed_rect = None;
3961 let mut changed = false;
3962
3963 if let Some(node) = self.node_mut(id) {
3964 match node.kind {
3965 WidgetKind::Menu {
3966 items,
3967 selected: ref mut current,
3968 } => {
3969 if items.is_empty() {
3970 return Ok(true);
3971 }
3972 changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3973 changed_rect = changed.then_some(node.rect);
3974 }
3975 WidgetKind::Dropdown {
3976 items,
3977 selected: ref mut current,
3978 open,
3979 } => {
3980 if !open {
3981 return Ok(false);
3982 }
3983 if items.is_empty() {
3984 return Ok(true);
3985 }
3986 changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3987 changed_rect = changed.then_some(node.rect);
3988 }
3989 WidgetKind::Roller {
3990 items,
3991 selected: ref mut current,
3992 } => {
3993 if items.is_empty() {
3994 return Ok(true);
3995 }
3996 changed = bump_index_with_wrap(current, items.len(), delta, wrap_navigation);
3997 changed_rect = changed.then_some(node.rect);
3998 }
3999 WidgetKind::Keyboard {
4000 keys,
4001 selected: ref mut current,
4002 ..
4003 } => {
4004 if keys.is_empty() {
4005 return Ok(true);
4006 }
4007 changed = bump_index_with_wrap(current, keys.len(), delta, wrap_navigation);
4008 changed_rect = changed.then_some(node.rect);
4009 }
4010 WidgetKind::List {
4011 items,
4012 selected: ref mut current,
4013 ref mut offset,
4014 visible_rows,
4015 } => {
4016 if items.is_empty() {
4017 return Ok(true);
4018 }
4019 let mut state = ListState::new(*current, *offset, visible_rows);
4020 let mut next = state.selected;
4021 changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4022 if changed {
4023 let _ = state.set_selected(next, items.len());
4024 }
4025 *current = state.selected;
4026 *offset = state.offset;
4027 changed_rect = changed.then_some(node.rect);
4028 }
4029 WidgetKind::FeedTimeline {
4030 items,
4031 selected: ref mut current,
4032 ref mut offset,
4033 visible_rows,
4034 expanded,
4035 } => {
4036 if items.is_empty() {
4037 return Ok(true);
4038 }
4039 let mut state =
4040 FeedTimelineState::new(*current, *offset, visible_rows, expanded);
4041 let mut next = state.selected;
4042 changed = bump_index_with_wrap(&mut next, items.len(), delta, wrap_navigation);
4043 if changed {
4044 let _ = state.set_selected(next, items.len());
4045 }
4046 *current = state.selected;
4047 *offset = state.offset;
4048 changed_rect = changed.then_some(node.rect);
4049 }
4050 WidgetKind::ScrollView {
4051 offset_y: ref mut offset,
4052 content_h,
4053 } => {
4054 let mut state = ScrollState::new(*offset, content_h);
4055 changed = state.scroll_by(delta as i32 * 8);
4056 *offset = state.offset_y;
4057 changed_rect = changed.then_some(node.rect);
4058 }
4059 _ => return Ok(false),
4060 }
4061 }
4062
4063 if let Some(rect) = changed_rect {
4064 self.dirty.add(rect)?;
4065 }
4066 if changed {
4067 self.push_event(UiEvent::ValueChanged(id))?;
4068 }
4069 Ok(true)
4070 }
4071
4072 fn adjust_focused_scalar(&mut self, direction: f32) -> Result<bool, GuiError> {
4073 let Some(id) = self.focus else {
4074 return Ok(false);
4075 };
4076
4077 let mut changed_rect = None;
4078 let mut changed = false;
4079
4080 if let Some(node) = self.node_mut(id) {
4081 match node.kind {
4082 WidgetKind::Slider {
4083 value: ref mut current,
4084 min,
4085 max,
4086 } => {
4087 let mut state = SliderState::new(*current, min, max);
4088 changed = state.step_by(direction);
4089 *current = state.value;
4090 changed_rect = changed.then_some(node.rect);
4091 }
4092 WidgetKind::Tabs {
4093 labels,
4094 selected: ref mut current,
4095 } => {
4096 if labels.is_empty() {
4097 return Ok(true);
4098 }
4099 let mut state = TabsState::new(*current);
4100 changed = state.bump(labels.len(), if direction >= 0.0 { 1 } else { -1 });
4101 *current = state.selected;
4102 changed_rect = changed.then_some(node.rect);
4103 }
4104 WidgetKind::TextArea {
4105 text_buf,
4106 text_len,
4107 cursor: ref mut current,
4108 ..
4109 } => {
4110 let text = textarea_text(&text_buf, text_len);
4111 let len = text.chars().count();
4112 if direction >= 0.0 {
4113 let next = (*current + 1).min(len);
4114 changed = next != *current;
4115 *current = next;
4116 } else {
4117 let next = current.saturating_sub(1);
4118 changed = next != *current;
4119 *current = next;
4120 }
4121 changed_rect = changed.then_some(node.rect);
4122 }
4123 _ => return Ok(false),
4124 }
4125 }
4126
4127 if let Some(rect) = changed_rect {
4128 self.dirty.add(rect)?;
4129 }
4130 if changed {
4131 self.push_event(UiEvent::ValueChanged(id))?;
4132 }
4133 Ok(true)
4134 }
4135
4136 fn activate_focused(&mut self, id: WidgetId) -> Result<(), GuiError> {
4137 let mut changed_rect = None;
4138 let mut changed = false;
4139 let mut dropdown_state_event = None;
4140 let select_opens_dropdown = self.menu_contract.select_opens_dropdown;
4141
4142 if let Some(node) = self.node_mut(id) {
4143 match node.kind {
4144 WidgetKind::Toggle { on: ref mut v, .. } => {
4145 *v = !*v;
4146 changed = true;
4147 changed_rect = Some(node.rect);
4148 }
4149 WidgetKind::Checkbox {
4150 checked: ref mut v, ..
4151 } => {
4152 *v = !*v;
4153 changed = true;
4154 changed_rect = Some(node.rect);
4155 }
4156 WidgetKind::Keyboard {
4157 keys,
4158 alt_keys,
4159 selected,
4160 layout,
4161 target,
4162 ..
4163 } => {
4164 if let Some(ch) = keyboard_char_for_layout(keys, alt_keys, selected, layout) {
4165 changed = true;
4166 changed_rect = Some(node.rect);
4167 if let Some(target) = target {
4168 let _ = self.push_event(UiEvent::TextInput { id: target, ch });
4169 let _ = self.push_event(UiEvent::ValueChanged(target));
4170 }
4171 }
4172 }
4173 WidgetKind::Dropdown {
4174 open: ref mut is_open,
4175 ..
4176 } if select_opens_dropdown => {
4177 *is_open = !*is_open;
4178 changed = true;
4179 changed_rect = Some(node.rect);
4180 dropdown_state_event = Some(*is_open);
4181 }
4182 _ => {}
4183 }
4184 }
4185
4186 if let Some(open) = dropdown_state_event {
4187 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4188 self.dispatch_widget_event(
4189 id,
4190 if open {
4191 WidgetEventKind::Opened
4192 } else {
4193 WidgetEventKind::Closed
4194 },
4195 &mut events,
4196 |_| EventPolicy::Continue,
4197 )?;
4198 self.push_event(if open {
4199 UiEvent::Opened(id)
4200 } else {
4201 UiEvent::Closed(id)
4202 })?;
4203 }
4204
4205 if let Some(rect) = changed_rect {
4206 self.dirty.add(rect)?;
4207 }
4208 if changed {
4209 self.push_event(UiEvent::ValueChanged(id))?;
4210 }
4211 Ok(())
4212 }
4213
4214 fn node_in_active_group(&self, node: &WidgetNode<'_>) -> bool {
4215 self.active_focus_group
4216 .is_none_or(|group| node.focus_group == group)
4217 }
4218
4219 fn handle_pointer_pressed(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4220 let hit = self.pointer_hit(x, y, true);
4221
4222 if let Some(id) = hit {
4223 self.dispatch_activation(id, true)?;
4224 self.pressed = Some(PressTracker {
4225 id,
4226 start_x: x,
4227 start_y: y,
4228 last_x: x,
4229 last_y: y,
4230 elapsed_ms: 0,
4231 long_emitted: false,
4232 gesture_emitted: false,
4233 repeat_elapsed_ms: 0,
4234 scroll_velocity: 0.0,
4235 });
4236 self.inertia_scroll = None;
4237 }
4238 Ok(())
4239 }
4240
4241 fn handle_pointer_released(&mut self, _x: i32, _y: i32) -> Result<(), GuiError> {
4242 let mut released_id = None;
4243 if let Some(pressed) = self.pressed {
4244 if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4245 if pressed.scroll_velocity.abs() > self.scroll_physics.velocity_threshold {
4246 self.inertia_scroll = Some(InertiaScroll {
4247 id: scroll_id,
4248 velocity: pressed.scroll_velocity,
4249 });
4250 }
4251 }
4252 released_id = Some(pressed.id);
4253 }
4254 self.pressed = None;
4255 if let Some(id) = released_id {
4256 let to = if !self.effective_enabled(id) {
4257 VisualState::Disabled
4258 } else if Some(id) == self.focus {
4259 VisualState::Focused
4260 } else {
4261 VisualState::Normal
4262 };
4263 self.start_state_transition(id, VisualState::Pressed, to);
4264 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4265 self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4266 EventPolicy::Continue
4267 })?;
4268 self.push_event(UiEvent::Released(id))?;
4269 self.push_event(UiEvent::PointerReleased(id))?;
4270 let double_pointer = self.last_pointer_id == Some(id)
4271 && self.pointer_elapsed_ms <= self.pointer_double_window_ms;
4272 if double_pointer {
4273 self.dispatch_double_clicked(id)?;
4274 self.last_pointer_id = None;
4275 self.pointer_elapsed_ms = 0;
4276 } else {
4277 self.last_pointer_id = Some(id);
4278 self.pointer_elapsed_ms = 0;
4279 }
4280 }
4281 Ok(())
4282 }
4283
4284 fn handle_pointer_moved(&mut self, x: i32, y: i32) -> Result<(), GuiError> {
4285 let Some(mut pressed) = self.pressed else {
4286 return Ok(());
4287 };
4288 let dy = y - pressed.last_y;
4289 pressed.last_x = x;
4290 pressed.last_y = y;
4291
4292 let moved_from_start =
4293 (x - pressed.start_x).unsigned_abs() + (y - pressed.start_y).unsigned_abs();
4294 if !pressed.gesture_emitted && moved_from_start >= 6 {
4295 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4296 self.dispatch_widget_event(pressed.id, WidgetEventKind::Gesture, &mut events, |_| {
4297 EventPolicy::Continue
4298 })?;
4299 self.push_event(UiEvent::Gesture(pressed.id))?;
4300 pressed.gesture_emitted = true;
4301 }
4302
4303 if let Some(scroll_id) = self.scrollable_ancestor(pressed.id) {
4304 let current = self.scroll_offset(scroll_id).unwrap_or(0);
4305 let next = current.saturating_sub(dy);
4306 if next != current {
4307 self.set_scroll_offset(scroll_id, next)?;
4308 self.push_event(UiEvent::Scroll {
4309 id: scroll_id,
4310 delta: next - current,
4311 })?;
4312 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4313 self.dispatch_widget_event(
4314 scroll_id,
4315 WidgetEventKind::Scroll {
4316 delta: next - current,
4317 },
4318 &mut events,
4319 |_| EventPolicy::Continue,
4320 )?;
4321 }
4322 let blend = self.scroll_physics.drag_velocity_blend;
4323 pressed.scroll_velocity = pressed.scroll_velocity * (1.0 - blend) + (dy as f32) * blend;
4324 }
4325 self.pressed = Some(pressed);
4326 Ok(())
4327 }
4328
4329 fn dispatch_activation(&mut self, id: WidgetId, is_pointer: bool) -> Result<(), GuiError> {
4330 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4331 self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4332 EventPolicy::Continue
4333 })?;
4334 if self.effective_focusable(id) {
4335 self.set_focus(Some(id))?;
4336 }
4337 self.push_event(UiEvent::Pressed(id))?;
4338 if is_pointer {
4339 self.push_event(UiEvent::PointerPressed(id))?;
4340 }
4341 let from = if Some(id) == self.focus {
4342 VisualState::Focused
4343 } else {
4344 VisualState::Normal
4345 };
4346 self.start_state_transition(id, from, VisualState::Pressed);
4347
4348 self.activate_focused(id)?;
4349 self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4350 EventPolicy::Continue
4351 })?;
4352 self.push_event(UiEvent::Clicked(id))?;
4353 self.push_event(UiEvent::Activate(id))?;
4354 Ok(())
4355 }
4356
4357 fn dispatch_repeat_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4358 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4359 self.dispatch_widget_event(id, WidgetEventKind::Clicked, &mut events, |_| {
4360 EventPolicy::Continue
4361 })?;
4362 self.push_event(UiEvent::Clicked(id))?;
4363 self.push_event(UiEvent::Activate(id))
4364 }
4365
4366 fn dispatch_double_clicked(&mut self, id: WidgetId) -> Result<(), GuiError> {
4367 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4368 self.dispatch_widget_event(id, WidgetEventKind::DoubleClicked, &mut events, |_| {
4369 EventPolicy::Continue
4370 })?;
4371 self.push_event(UiEvent::DoubleClicked(id))
4372 }
4373
4374 fn dispatch_key_pressed(&mut self, id: WidgetId) -> Result<(), GuiError> {
4375 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4376 self.dispatch_widget_event(id, WidgetEventKind::Pressed, &mut events, |_| {
4377 EventPolicy::Continue
4378 })?;
4379 self.push_event(UiEvent::Pressed(id))
4380 }
4381
4382 fn dispatch_key_released(&mut self, id: WidgetId) -> Result<(), GuiError> {
4383 let mut events = heapless::Vec::<WidgetEvent, NODES>::new();
4384 self.dispatch_widget_event(id, WidgetEventKind::Released, &mut events, |_| {
4385 EventPolicy::Continue
4386 })?;
4387 self.push_event(UiEvent::Released(id))
4388 }
4389
4390 fn repeatable_widget(&self, id: WidgetId) -> bool {
4391 self.node(id).is_some_and(|node| {
4392 matches!(
4393 node.kind,
4394 WidgetKind::Button(_) | WidgetKind::IconButton { .. }
4395 )
4396 })
4397 }
4398
4399 fn pointer_hit(&self, x: i32, y: i32, clickable_only: bool) -> Option<WidgetId> {
4400 self.widgets
4401 .iter()
4402 .rev()
4403 .find(|node| {
4404 (!clickable_only || node.clickable())
4405 && self.effective_visible(node.id)
4406 && self.effective_enabled(node.id)
4407 && self
4408 .absolute_rect(node.id)
4409 .is_some_and(|rect| rect.contains(x, y))
4410 })
4411 .map(|node| node.id)
4412 }
4413
4414 fn scrollable_ancestor(&self, id: WidgetId) -> Option<WidgetId> {
4415 let mut current = Some(id);
4416 let mut depth = 0usize;
4417 while let Some(widget_id) = current {
4418 if depth >= NODES {
4419 return None;
4420 }
4421 let node = self.node(widget_id)?;
4422 if node.scrollable() {
4423 return Some(widget_id);
4424 }
4425 current = node.parent;
4426 depth += 1;
4427 }
4428 None
4429 }
4430
4431 fn mark_focus_pair(
4432 &mut self,
4433 old: Option<WidgetId>,
4434 new: Option<WidgetId>,
4435 ) -> Result<(), GuiError> {
4436 if let Some(id) = old {
4437 if let Some(rect) = self.absolute_rect(id) {
4438 self.dirty.add(rect)?;
4439 }
4440 }
4441 if let Some(id) = new {
4442 if let Some(rect) = self.absolute_rect(id) {
4443 self.dirty.add(rect)?;
4444 }
4445 }
4446 Ok(())
4447 }
4448
4449 fn start_focus_transitions(&mut self, old: Option<WidgetId>, new: Option<WidgetId>) {
4450 if self.state_transition_ms == 0 {
4451 return;
4452 }
4453 if let Some(id) = old {
4454 self.start_state_transition(id, VisualState::Focused, VisualState::Normal);
4455 }
4456 if let Some(id) = new {
4457 self.start_state_transition(id, VisualState::Normal, VisualState::Focused);
4458 }
4459 }
4460
4461 fn start_state_transition(&mut self, id: WidgetId, from: VisualState, to: VisualState) {
4462 if self.state_transition_ms == 0 || from == to {
4463 return;
4464 }
4465 if let Some(entry) = self
4466 .state_transitions
4467 .iter_mut()
4468 .find(|entry| entry.id == id)
4469 {
4470 *entry = StateTransition {
4471 id,
4472 from,
4473 to,
4474 elapsed_ms: 0,
4475 };
4476 return;
4477 }
4478 if self.state_transitions.len() == self.state_transitions.capacity() {
4479 self.state_transitions.remove(0);
4480 }
4481 let _ = self.state_transitions.push(StateTransition {
4482 id,
4483 from,
4484 to,
4485 elapsed_ms: 0,
4486 });
4487 }
4488
4489 fn tick_state_transitions(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4490 if self.state_transitions.is_empty() || self.state_transition_ms == 0 {
4491 return Ok(());
4492 }
4493 let mut i = 0usize;
4494 let mut completed_pressed = heapless::Vec::<WidgetId, NODES>::new();
4495 while i < self.state_transitions.len() {
4496 let mut remove = false;
4497 let id;
4498 let to;
4499 {
4500 let entry = &mut self.state_transitions[i];
4501 entry.elapsed_ms = entry.elapsed_ms.saturating_add(dt_ms);
4502 if entry.elapsed_ms >= self.state_transition_ms {
4503 remove = true;
4504 }
4505 id = entry.id;
4506 to = entry.to;
4507 }
4508 if let Some(rect) = self.absolute_rect(id) {
4509 self.dirty.add(rect)?;
4510 }
4511 if remove {
4512 if to == VisualState::Pressed {
4513 let _ = completed_pressed.push(id);
4514 }
4515 self.state_transitions.remove(i);
4516 } else {
4517 i += 1;
4518 }
4519 }
4520 for id in completed_pressed {
4521 if self.pressed.is_some_and(|pressed| pressed.id == id) {
4523 continue;
4524 }
4525 let to = self.resting_visual_state(id);
4526 self.start_state_transition(id, VisualState::Pressed, to);
4527 }
4528 Ok(())
4529 }
4530
4531 fn state_transition_progress(&self, id: WidgetId) -> Option<(VisualState, VisualState, f32)> {
4532 let duration = self.state_transition_ms.max(1);
4533 self.state_transitions
4534 .iter()
4535 .find(|entry| entry.id == id)
4536 .map(|entry| {
4537 let t = (entry.elapsed_ms as f32 / duration as f32).clamp(0.0, 1.0);
4538 (entry.from, entry.to, t)
4539 })
4540 }
4541
4542 fn set_textarea_cursor_visible(&mut self, id: Option<WidgetId>, visible: bool) {
4543 let Some(id) = id else {
4544 return;
4545 };
4546 let Some(rect) = self.absolute_rect(id) else {
4547 return;
4548 };
4549 let Some(node) = self.node_mut(id) else {
4550 return;
4551 };
4552 if let WidgetKind::TextArea {
4553 cursor_visible: ref mut current,
4554 ..
4555 } = node.kind
4556 {
4557 *current = visible;
4558 let _ = self.dirty.add(rect);
4559 }
4560 }
4561
4562 fn tick_textarea_cursor_blink(&mut self, dt_ms: u32) -> Result<(), GuiError> {
4563 let Some(id) = self.focus else {
4564 return Ok(());
4565 };
4566 let is_textarea = matches!(
4567 self.node(id).map(|n| n.kind),
4568 Some(WidgetKind::TextArea { .. })
4569 );
4570 if !is_textarea {
4571 return Ok(());
4572 }
4573 self.textarea_cursor_blink_elapsed_ms =
4574 self.textarea_cursor_blink_elapsed_ms.saturating_add(dt_ms);
4575 if self.textarea_cursor_blink_elapsed_ms < self.textarea_cursor_blink_ms {
4576 return Ok(());
4577 }
4578 self.textarea_cursor_blink_elapsed_ms = 0;
4579 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
4580 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
4581 if let WidgetKind::TextArea {
4582 cursor_visible: ref mut visible,
4583 ..
4584 } = node.kind
4585 {
4586 *visible = !*visible;
4587 self.dirty.add(rect)?;
4588 }
4589 Ok(())
4590 }
4591
4592 fn push_event(&mut self, event: UiEvent) -> Result<(), GuiError> {
4593 if self.should_emit_event(event)? {
4594 self.events.push(event).map_err(|_| GuiError::EventsFull)?;
4595 }
4596 Ok(())
4597 }
4598
4599 fn should_emit_event(&self, event: UiEvent) -> Result<bool, GuiError> {
4600 let Some(target) = event.target() else {
4601 return Ok(true);
4602 };
4603 let filter = self.event_filter(target)?;
4604 Ok(filter.contains(event.filter()))
4605 }
4606
4607 fn stop_due_to_builtin_widget_behavior(&self, event: WidgetEvent) -> bool {
4608 if event.phase != EventPhase::Capture || event.current == event.target {
4609 return false;
4610 }
4611 let is_pointer_kind = matches!(
4612 event.kind,
4613 WidgetEventKind::Pressed | WidgetEventKind::Released | WidgetEventKind::Clicked
4614 );
4615 is_pointer_kind
4616 && self
4617 .node(event.current)
4618 .is_some_and(|node| matches!(node.kind, WidgetKind::ScrollView { .. }))
4619 }
4620
4621 fn stop_due_to_registered_policy(&self, event: WidgetEvent) -> bool {
4622 self.dispatch_policies
4623 .iter()
4624 .find(|(id, _)| *id == event.current)
4625 .is_some_and(|(_, policy)| policy.stop && policy.allows(event.kind, event.phase))
4626 }
4627
4628 fn resting_visual_state(&self, id: WidgetId) -> VisualState {
4629 if !self.effective_enabled(id) {
4630 VisualState::Disabled
4631 } else if Some(id) == self.focus {
4632 VisualState::Focused
4633 } else {
4634 VisualState::Normal
4635 }
4636 }
4637
4638 fn current_visual_state(&self, id: WidgetId) -> VisualState {
4639 if self.pressed.is_some_and(|pressed| pressed.id == id) {
4640 VisualState::Pressed
4641 } else {
4642 self.resting_visual_state(id)
4643 }
4644 }
4645
4646 fn press_timing_for(&self, id: WidgetId) -> PressTiming {
4647 self.widget_press_timings
4648 .iter()
4649 .find(|(timing_id, _)| *timing_id == id)
4650 .map(|(_, timing)| *timing)
4651 .unwrap_or(PressTiming {
4652 long_press_ms: self.long_press_ms,
4653 repeat_delay_ms: self.press_repeat_delay_ms,
4654 repeat_interval_ms: self.press_repeat_interval_ms,
4655 })
4656 }
4657
4658 fn key_input_policy_for(&self, id: WidgetId) -> WidgetKeyInputPolicy {
4659 self.widget_key_policies
4660 .iter()
4661 .find(|(policy_id, _)| *policy_id == id)
4662 .map(|(_, policy)| *policy)
4663 .unwrap_or_default()
4664 }
4665
4666 fn key_bindings_for(&self, id: WidgetId) -> WidgetKeyBindings {
4667 self.widget_key_bindings
4668 .iter()
4669 .find(|(binding_id, _)| *binding_id == id)
4670 .map(|(_, bindings)| *bindings)
4671 .unwrap_or_default()
4672 }
4673
4674 fn handle_select_activation(&mut self, id: WidgetId) -> Result<(), GuiError> {
4675 if let Some(node) = self.node(id) {
4676 if self.menu_contract.select_toggles_feed_expanded
4677 && matches!(node.kind, WidgetKind::FeedTimeline { .. })
4678 {
4679 let expanded = if let WidgetKind::FeedTimeline { expanded, .. } = node.kind {
4680 expanded
4681 } else {
4682 false
4683 };
4684 self.set_feed_expanded(id, !expanded)?;
4685 self.push_event(UiEvent::ValueChanged(id))?;
4686 }
4687 }
4688 let double_select = self.last_select_id == Some(id)
4689 && self.select_elapsed_ms <= self.select_double_window_ms;
4690 self.dispatch_activation(id, false)?;
4691 if double_select {
4692 self.dispatch_double_clicked(id)?;
4693 self.last_select_id = None;
4694 self.select_elapsed_ms = 0;
4695 } else {
4696 self.last_select_id = Some(id);
4697 self.select_elapsed_ms = 0;
4698 }
4699 Ok(())
4700 }
4701
4702 fn handle_back_action(&mut self) -> Result<(), GuiError> {
4703 if let Some(id) = self.focus {
4704 if matches!(
4705 self.node(id).map(|n| n.kind),
4706 Some(WidgetKind::TextArea { .. })
4707 ) {
4708 self.textarea_backspace(id)?;
4709 return Ok(());
4710 }
4711 if matches!(
4712 self.node(id).map(|n| n.kind),
4713 Some(WidgetKind::Dropdown { open: true, .. })
4714 ) && self.menu_contract.back_closes_dropdown
4715 {
4716 self.set_dropdown_open(id, false)?;
4717 return Ok(());
4718 }
4719 if matches!(
4720 self.node(id).map(|n| n.kind),
4721 Some(WidgetKind::NotificationActionSheet { open: true, .. })
4722 ) && self.menu_contract.back_closes_notification_sheet
4723 {
4724 self.set_notification_sheet_open(id, false)?;
4725 return Ok(());
4726 }
4727 }
4728 self.push_event(UiEvent::Back)
4729 }
4730}
4731
4732fn bump_index_with_wrap(current: &mut usize, len: usize, delta: i8, wrap: bool) -> bool {
4733 if len == 0 {
4734 return false;
4735 }
4736 let next = if delta >= 0 {
4737 if *current + 1 >= len {
4738 if wrap { 0 } else { *current }
4739 } else {
4740 *current + 1
4741 }
4742 } else if *current == 0 {
4743 if wrap { len - 1 } else { *current }
4744 } else {
4745 *current - 1
4746 };
4747 if next != *current {
4748 *current = next;
4749 true
4750 } else {
4751 false
4752 }
4753}
4754
4755fn keyboard_char_for_layout(
4756 keys: &[char],
4757 alt_keys: Option<&[char]>,
4758 selected: usize,
4759 layout: KeyboardLayout,
4760) -> Option<char> {
4761 let base = keys.get(selected).copied()?;
4762 Some(match layout {
4763 KeyboardLayout::Normal => base,
4764 KeyboardLayout::Shift => {
4765 if base.is_ascii_alphabetic() {
4766 base.to_ascii_uppercase()
4767 } else {
4768 base
4769 }
4770 }
4771 KeyboardLayout::Symbols => alt_keys
4772 .and_then(|keys| keys.get(selected).copied())
4773 .unwrap_or('#'),
4774 })
4775}
4776
4777fn textarea_text(buf: &[u8; TEXTAREA_CAPACITY], len: u8) -> &str {
4778 let used = (len as usize).min(TEXTAREA_CAPACITY);
4779 core::str::from_utf8(&buf[..used]).unwrap_or("")
4780}
4781
4782fn textarea_storage_from_str(text: &str) -> ([u8; TEXTAREA_CAPACITY], u8) {
4783 let mut out = [0u8; TEXTAREA_CAPACITY];
4784 let mut len = 0usize;
4785 for ch in text.chars() {
4786 let mut tmp = [0u8; 4];
4787 let enc = ch.encode_utf8(&mut tmp).as_bytes();
4788 if len + enc.len() > TEXTAREA_CAPACITY {
4789 break;
4790 }
4791 out[len..len + enc.len()].copy_from_slice(enc);
4792 len += enc.len();
4793 }
4794 (out, len as u8)
4795}
4796
4797fn textarea_storage_from_chars(
4798 chars: &heapless::Vec<char, TEXTAREA_CAPACITY>,
4799) -> ([u8; TEXTAREA_CAPACITY], u8) {
4800 let mut out = [0u8; TEXTAREA_CAPACITY];
4801 let mut len = 0usize;
4802 for ch in chars {
4803 let mut tmp = [0u8; 4];
4804 let enc = ch.encode_utf8(&mut tmp).as_bytes();
4805 if len + enc.len() > TEXTAREA_CAPACITY {
4806 break;
4807 }
4808 out[len..len + enc.len()].copy_from_slice(enc);
4809 len += enc.len();
4810 }
4811 (out, len as u8)
4812}
4813
4814fn char_at(text: &str, idx: usize) -> Option<char> {
4815 text.chars().nth(idx)
4816}
4817
4818fn prev_word_boundary(text: &str, cursor: usize) -> usize {
4819 let mut pos = cursor.min(text.chars().count());
4820 while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| ch.is_whitespace()) {
4821 pos -= 1;
4822 }
4823 while pos > 0 && char_at(text, pos - 1).is_some_and(|ch| !ch.is_whitespace()) {
4824 pos -= 1;
4825 }
4826 pos
4827}
4828
4829fn next_word_boundary(text: &str, cursor: usize) -> usize {
4830 let len = text.chars().count();
4831 let mut pos = cursor.min(len);
4832 while pos < len && char_at(text, pos).is_some_and(|ch| !ch.is_whitespace()) {
4833 pos += 1;
4834 }
4835 while pos < len && char_at(text, pos).is_some_and(|ch| ch.is_whitespace()) {
4836 pos += 1;
4837 }
4838 pos
4839}
4840
4841fn delete_selection_if_any(
4842 chars: &mut heapless::Vec<char, TEXTAREA_CAPACITY>,
4843 cursor: &mut usize,
4844 selection: &mut Option<(usize, usize)>,
4845) -> bool {
4846 let Some((start, end)) = *selection else {
4847 return false;
4848 };
4849 let start = start.min(end).min(chars.len());
4850 let end = end.max(start).min(chars.len());
4851 if end <= start {
4852 *selection = None;
4853 *cursor = start;
4854 return false;
4855 }
4856 for _ in start..end {
4857 chars.remove(start);
4858 }
4859 *cursor = start;
4860 *selection = None;
4861 true
4862}
4863
4864fn textarea_row_col_at_cursor(text: &str, cursor: usize, wrap_cols: usize) -> (usize, usize) {
4865 let mut row = 0usize;
4866 let mut col = 0usize;
4867 for ch in text.chars().take(cursor) {
4868 if ch == '\n' {
4869 row += 1;
4870 col = 0;
4871 continue;
4872 }
4873 col += 1;
4874 if col >= wrap_cols {
4875 row += 1;
4876 col = 0;
4877 }
4878 }
4879 (row, col)
4880}
4881
4882fn textarea_cursor_from_row_col(
4883 text: &str,
4884 target_row: usize,
4885 target_col: usize,
4886 wrap_cols: usize,
4887) -> usize {
4888 let mut row = 0usize;
4889 let mut col = 0usize;
4890 let mut idx = 0usize;
4891 for ch in text.chars() {
4892 if row == target_row && col >= target_col {
4893 break;
4894 }
4895 if ch == '\n' {
4896 if row == target_row {
4897 break;
4898 }
4899 row += 1;
4900 col = 0;
4901 idx += 1;
4902 continue;
4903 }
4904 idx += 1;
4905 col += 1;
4906 if col >= wrap_cols {
4907 if row == target_row {
4908 break;
4909 }
4910 row += 1;
4911 col = 0;
4912 }
4913 }
4914 idx
4915}
4916
4917fn textarea_row_end_col(text: &str, target_row: usize, wrap_cols: usize) -> usize {
4918 let mut row = 0usize;
4919 let mut col = 0usize;
4920 for ch in text.chars() {
4921 if row == target_row {
4922 if ch == '\n' {
4923 break;
4924 }
4925 col += 1;
4926 if col >= wrap_cols {
4927 break;
4928 }
4929 } else if ch == '\n' {
4930 row += 1;
4931 col = 0;
4932 } else {
4933 col += 1;
4934 if col >= wrap_cols {
4935 row += 1;
4936 col = 0;
4937 }
4938 }
4939 }
4940 col
4941}