1use crate::{
2 geometry::Rect,
3 input::{UiEvent, WidgetEvent, WidgetEventKind},
4 layout::{Axis, LayoutItem, LinearLayout},
5 state::{FeedTimelineState, ListState, ScrollState, SliderState, TabsState},
6 widget::{
7 EventContext, EventPhase, EventPolicy, FocusGroupId, PropertyKey, PropertyValue,
8 WidgetFlags, WidgetId,
9 },
10 widgets::{KeyboardLayout, SurfaceState, TEXTAREA_CAPACITY, WidgetKind, WidgetNode},
11};
12use embedded_graphics_core::pixelcolor::Rgb565;
13
14use super::*;
15
16impl<'a, const NODES: usize, const EVENTS: usize, const DIRTY: usize>
17 GuiContext<'a, NODES, EVENTS, DIRTY>
18{
19 pub fn get_widget_property(&self, id: WidgetId, key: PropertyKey) -> Option<PropertyValue<'a>> {
20 let node = self.node(id)?;
21 node.get_property(key)
22 }
23
24 pub fn set_widget_property(
25 &mut self,
26 id: WidgetId,
27 key: PropertyKey,
28 val: PropertyValue<'a>,
29 ) -> Result<(), GuiError> {
30 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
31 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
32 node.set_property(key, val)
33 .map_err(|_| GuiError::NotFound)?;
34 self.dirty.add(rect)?;
35 Ok(())
36 }
37 pub fn set_progress(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
38 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
39 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
40 match node.kind {
41 WidgetKind::ProgressBar { value: ref mut v } => {
42 *v = value.clamp(0.0, 1.0);
43 self.dirty.add(rect)?;
44 Ok(())
45 }
46 #[cfg(feature = "rich-widgets")]
47 WidgetKind::PeekReveal {
48 progress: ref mut v,
49 ..
50 } => {
51 *v = value.clamp(0.0, 1.0);
52 self.dirty.add(rect)?;
53 Ok(())
54 }
55 WidgetKind::SweepingArc {
56 progress: ref mut v,
57 ..
58 } => {
59 *v = value.clamp(0.0, 1.0);
60 self.dirty.add(rect)?;
61 Ok(())
62 }
63 _ => Err(GuiError::NotFound),
64 }
65 }
66
67 #[cfg(feature = "rich-widgets")]
68 pub fn set_glance_highlighted(
69 &mut self,
70 id: WidgetId,
71 highlighted: bool,
72 ) -> Result<(), GuiError> {
73 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
74 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
75 match node.kind {
76 WidgetKind::GlanceTile {
77 highlighted: ref mut h,
78 ..
79 } => {
80 *h = highlighted;
81 self.dirty.add(rect)?;
82 Ok(())
83 }
84 _ => Err(GuiError::NotFound),
85 }
86 }
87
88 #[cfg(feature = "rich-widgets")]
89 pub fn set_card_deck_selected(
90 &mut self,
91 id: WidgetId,
92 selected: usize,
93 ) -> Result<(), GuiError> {
94 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
95 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
96 match node.kind {
97 WidgetKind::CardDeck {
98 titles,
99 selected: ref mut current,
100 } => {
101 *current = selected.min(titles.len().saturating_sub(1));
102 self.dirty.add(rect)?;
103 Ok(())
104 }
105 _ => Err(GuiError::NotFound),
106 }
107 }
108
109 #[cfg(feature = "rich-widgets")]
110 pub fn tick_reel(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
111 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
112 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
113 match node.kind {
114 WidgetKind::Reel {
115 player: ref mut reel,
116 ..
117 } => {
118 reel.tick(dt_ms);
119 self.dirty.add(rect)?;
120 Ok(())
121 }
122 _ => Err(GuiError::NotFound),
123 }
124 }
125
126 #[cfg(feature = "rich-widgets")]
127 pub fn set_state_surface_state(
128 &mut self,
129 id: WidgetId,
130 state: SurfaceState,
131 ) -> Result<(), GuiError> {
132 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
133 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
134 match node.kind {
135 WidgetKind::StateSurface {
136 state: ref mut current,
137 ..
138 } => {
139 *current = state;
140 self.dirty.add(rect)?;
141 Ok(())
142 }
143 _ => Err(GuiError::NotFound),
144 }
145 }
146
147 #[cfg(feature = "rich-widgets")]
148 pub fn set_state_surface_message(
149 &mut self,
150 id: WidgetId,
151 message: &'a str,
152 ) -> Result<(), GuiError> {
153 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
154 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
155 match node.kind {
156 WidgetKind::StateSurface {
157 message: ref mut current,
158 ..
159 } => {
160 *current = message;
161 self.dirty.add(rect)?;
162 Ok(())
163 }
164 _ => Err(GuiError::NotFound),
165 }
166 }
167
168 #[cfg(feature = "rich-widgets")]
169 pub fn set_state_surface_action(
170 &mut self,
171 id: WidgetId,
172 action: Option<&'a str>,
173 ) -> Result<(), GuiError> {
174 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
175 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
176 match node.kind {
177 WidgetKind::StateSurface {
178 action: ref mut current,
179 ..
180 } => {
181 *current = action;
182 self.dirty.add(rect)?;
183 Ok(())
184 }
185 _ => Err(GuiError::NotFound),
186 }
187 }
188
189 #[cfg(feature = "rich-widgets")]
190 pub fn set_state_surface_busy_phase(
191 &mut self,
192 id: WidgetId,
193 phase: f32,
194 ) -> Result<(), GuiError> {
195 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
196 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
197 match node.kind {
198 WidgetKind::StateSurface {
199 busy_phase: ref mut current,
200 ..
201 } => {
202 *current = phase;
203 self.dirty.add(rect)?;
204 Ok(())
205 }
206 _ => Err(GuiError::NotFound),
207 }
208 }
209
210 #[cfg(feature = "rich-widgets")]
211 pub fn tick_state_surface(
212 &mut self,
213 id: WidgetId,
214 dt_ms: u32,
215 cycles_per_sec: f32,
216 ) -> Result<(), GuiError> {
217 let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
218 WidgetKind::StateSurface { busy_phase, .. } => {
219 busy_phase + (dt_ms as f32 / 1000.0) * cycles_per_sec
220 }
221 _ => return Err(GuiError::NotFound),
222 };
223 self.set_state_surface_busy_phase(id, phase)
224 }
225
226 #[cfg(feature = "rich-widgets")]
227 pub fn set_heads_up_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
228 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
229 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
230 match node.kind {
231 WidgetKind::HeadsUpBanner {
232 ttl_ms: ref mut current,
233 ..
234 } => {
235 *current = ttl_ms;
236 self.dirty.add(rect)?;
237 Ok(())
238 }
239 _ => Err(GuiError::NotFound),
240 }
241 }
242
243 #[cfg(feature = "rich-widgets")]
244 pub fn tick_heads_up(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
245 let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
246 WidgetKind::HeadsUpBanner { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
247 _ => return Err(GuiError::NotFound),
248 };
249 self.set_heads_up_ttl(id, ttl)
250 }
251
252 #[cfg(feature = "rich-widgets")]
253 pub fn set_notification_sheet_open(
254 &mut self,
255 id: WidgetId,
256 open: bool,
257 ) -> Result<(), GuiError> {
258 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
259 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
260 match node.kind {
261 WidgetKind::NotificationActionSheet {
262 open: ref mut current,
263 ..
264 } => {
265 *current = open;
266 self.dirty.add(rect)?;
267 Ok(())
268 }
269 _ => Err(GuiError::NotFound),
270 }
271 }
272
273 #[cfg(feature = "rich-widgets")]
274 pub fn set_notification_sheet_selected(
275 &mut self,
276 id: WidgetId,
277 selected: usize,
278 ) -> Result<(), GuiError> {
279 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
280 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
281 match node.kind {
282 WidgetKind::NotificationActionSheet {
283 actions,
284 selected: ref mut current,
285 ..
286 } => {
287 *current = selected.min(actions.len().saturating_sub(1));
288 self.dirty.add(rect)?;
289 Ok(())
290 }
291 _ => Err(GuiError::NotFound),
292 }
293 }
294
295 #[cfg(feature = "rich-widgets")]
296 pub fn set_menu_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
297 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
298 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
299 match node.kind {
300 WidgetKind::Menu {
301 items,
302 selected: ref mut current,
303 } => {
304 *current = selected.min(items.len().saturating_sub(1));
305 self.dirty.add(rect)?;
306 Ok(())
307 }
308 _ => Err(GuiError::NotFound),
309 }
310 }
311
312 #[cfg(feature = "rich-widgets")]
313 pub fn menu_selected(&self, id: WidgetId) -> Option<usize> {
314 match self.node(id)?.kind {
315 WidgetKind::Menu { selected, .. } => Some(selected),
316 _ => None,
317 }
318 }
319
320 #[cfg(feature = "rich-widgets")]
321 pub fn list_selected(&self, id: WidgetId) -> Option<usize> {
322 match self.node(id)?.kind {
323 WidgetKind::List { selected, .. } | WidgetKind::CircularList { selected, .. } => {
324 Some(selected)
325 }
326 _ => None,
327 }
328 }
329
330 #[cfg(feature = "rich-widgets")]
331 pub fn set_list_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
332 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
333 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
334 match node.kind {
335 WidgetKind::List {
336 items,
337 selected: ref mut current,
338 ref mut offset,
339 visible_rows,
340 }
341 | WidgetKind::CircularList {
342 items,
343 selected: ref mut current,
344 ref mut offset,
345 visible_rows,
346 } => {
347 let mut state = ListState::new(*current, *offset, visible_rows);
348 state.set_selected(selected, items.len());
349 *current = state.selected;
350 *offset = state.offset;
351 self.dirty.add(rect)?;
352 Ok(())
353 }
354 _ => Err(GuiError::NotFound),
355 }
356 }
357
358 pub fn set_plotter_head(&mut self, id: WidgetId, head: usize) -> Result<(), GuiError> {
359 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
360 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
361 match node.kind {
362 WidgetKind::Plotter {
363 head: ref mut h, ..
364 } => {
365 *h = head;
366 self.dirty.add(rect)?;
367 Ok(())
368 }
369 _ => Err(GuiError::NotFound),
370 }
371 }
372
373 #[cfg(feature = "rich-widgets")]
374 pub fn set_plotter_values(&mut self, id: WidgetId, values: &'a [f32]) -> Result<(), GuiError> {
375 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
376 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
377 match node.kind {
378 WidgetKind::Plotter {
379 values: ref mut v, ..
380 } => {
381 *v = values;
382 self.dirty.add(rect)?;
383 Ok(())
384 }
385 _ => Err(GuiError::NotFound),
386 }
387 }
388
389 #[cfg(feature = "rich-widgets")]
390 pub fn feed_selected(&self, id: WidgetId) -> Option<usize> {
391 match self.node(id)?.kind {
392 WidgetKind::FeedTimeline { selected, .. } => Some(selected),
393 _ => None,
394 }
395 }
396
397 #[cfg(feature = "rich-widgets")]
398 pub fn set_feed_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
399 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
400 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
401 match node.kind {
402 WidgetKind::FeedTimeline {
403 items,
404 selected: ref mut current,
405 ref mut offset,
406 visible_rows,
407 ..
408 } => {
409 let mut state = FeedTimelineState::new(*current, *offset, visible_rows, false);
410 state.set_selected(selected, items.len());
411 *current = state.selected;
412 *offset = state.offset;
413 self.dirty.add(rect)?;
414 Ok(())
415 }
416 _ => Err(GuiError::NotFound),
417 }
418 }
419
420 #[cfg(feature = "rich-widgets")]
421 pub fn set_feed_expanded(&mut self, id: WidgetId, expanded: bool) -> Result<(), GuiError> {
422 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
423 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
424 match node.kind {
425 WidgetKind::FeedTimeline {
426 expanded: ref mut current,
427 ..
428 } => {
429 *current = expanded;
430 self.dirty.add(rect)?;
431 Ok(())
432 }
433 _ => Err(GuiError::NotFound),
434 }
435 }
436
437 #[cfg(feature = "rich-widgets")]
438 pub fn set_toggle(&mut self, id: WidgetId, on: bool) -> Result<(), GuiError> {
439 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
440 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
441 match node.kind {
442 WidgetKind::Toggle { on: ref mut v, .. } => {
443 *v = on;
444 self.dirty.add(rect)?;
445 Ok(())
446 }
447 _ => Err(GuiError::NotFound),
448 }
449 }
450
451 #[cfg(feature = "rich-widgets")]
452 pub fn toggle_value(&self, id: WidgetId) -> Option<bool> {
453 match self.node(id)?.kind {
454 WidgetKind::Toggle { on, .. } => Some(on),
455 _ => None,
456 }
457 }
458
459 #[cfg(feature = "rich-widgets")]
460 pub fn set_checked(&mut self, id: WidgetId, checked: bool) -> Result<(), GuiError> {
461 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
462 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
463 match node.kind {
464 WidgetKind::Checkbox {
465 checked: ref mut v, ..
466 } => {
467 *v = checked;
468 self.dirty.add(rect)?;
469 Ok(())
470 }
471 _ => Err(GuiError::NotFound),
472 }
473 }
474
475 #[cfg(feature = "rich-widgets")]
476 pub fn checked_value(&self, id: WidgetId) -> Option<bool> {
477 match self.node(id)?.kind {
478 WidgetKind::Checkbox { checked, .. } => Some(checked),
479 _ => None,
480 }
481 }
482
483 #[cfg(feature = "rich-widgets")]
484 pub fn set_slider_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
485 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
486 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
487 match node.kind {
488 WidgetKind::Slider {
489 value: ref mut v,
490 min,
491 max,
492 } => {
493 let mut state = SliderState::new(*v, min, max);
494 state.set_value(value);
495 *v = state.value;
496 self.dirty.add(rect)?;
497 Ok(())
498 }
499 _ => Err(GuiError::NotFound),
500 }
501 }
502
503 #[cfg(feature = "rich-widgets")]
504 pub fn slider_value(&self, id: WidgetId) -> Option<f32> {
505 match self.node(id)?.kind {
506 WidgetKind::Slider { value, .. } => Some(value),
507 _ => None,
508 }
509 }
510
511 #[cfg(feature = "rich-widgets")]
512 pub fn set_value_label(&mut self, id: WidgetId, value: i32) -> Result<(), GuiError> {
513 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
514 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
515 match node.kind {
516 WidgetKind::ValueLabel {
517 value: ref mut v, ..
518 } => {
519 *v = value;
520 self.dirty.add(rect)?;
521 Ok(())
522 }
523 _ => Err(GuiError::NotFound),
524 }
525 }
526
527 #[cfg(feature = "rich-widgets")]
528 pub fn set_scroll_offset(&mut self, id: WidgetId, offset_y: i32) -> Result<(), GuiError> {
529 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
530 match node.kind {
531 WidgetKind::ScrollView {
532 offset_y: ref mut v,
533 content_h,
534 } => {
535 let mut state = ScrollState::new(*v, content_h);
536 state.set_offset(offset_y);
537 *v = state.offset_y;
538 self.mark_subtree_dirty(id)?;
539 Ok(())
540 }
541 _ => Err(GuiError::NotFound),
542 }
543 }
544
545 #[cfg(feature = "rich-widgets")]
546 pub fn scroll_offset(&self, id: WidgetId) -> Option<i32> {
547 match self.node(id)?.kind {
548 WidgetKind::ScrollView { offset_y, .. } => Some(offset_y),
549 _ => None,
550 }
551 }
552
553 #[cfg(feature = "rich-widgets")]
554 pub fn set_tab_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
555 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
556 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
557 match node.kind {
558 WidgetKind::Tabs {
559 labels,
560 selected: ref mut v,
561 } => {
562 let mut state = TabsState::new(*v);
563 state.set_selected(selected, labels.len());
564 *v = state.selected;
565 self.dirty.add(rect)?;
566 Ok(())
567 }
568 _ => Err(GuiError::NotFound),
569 }
570 }
571
572 #[cfg(feature = "rich-widgets")]
573 pub fn tab_selected(&self, id: WidgetId) -> Option<usize> {
574 match self.node(id)?.kind {
575 WidgetKind::Tabs { selected, .. } => Some(selected),
576 _ => None,
577 }
578 }
579
580 #[cfg(feature = "rich-widgets")]
581 pub fn set_toast_ttl(&mut self, id: WidgetId, ttl_ms: u32) -> Result<(), GuiError> {
582 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
583 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
584 match node.kind {
585 WidgetKind::Toast {
586 ttl_ms: ref mut v, ..
587 } => {
588 *v = ttl_ms;
589 self.dirty.add(rect)?;
590 Ok(())
591 }
592 _ => Err(GuiError::NotFound),
593 }
594 }
595
596 #[cfg(feature = "rich-widgets")]
597 pub fn tick_toast(&mut self, id: WidgetId, dt_ms: u32) -> Result<(), GuiError> {
598 let ttl = match self.node(id).ok_or(GuiError::NotFound)?.kind {
599 WidgetKind::Toast { ttl_ms, .. } => ttl_ms.saturating_sub(dt_ms),
600 _ => return Err(GuiError::NotFound),
601 };
602 self.set_toast_ttl(id, ttl)
603 }
604
605 #[cfg(feature = "rich-widgets")]
606 pub fn set_meter_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
607 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
608 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
609 match node.kind {
610 WidgetKind::Meter {
611 value: ref mut v,
612 min,
613 max,
614 } => {
615 *v = value.clamp(min.min(max), min.max(max));
616 self.dirty.add(rect)?;
617 Ok(())
618 }
619 _ => Err(GuiError::NotFound),
620 }
621 }
622
623 #[cfg(feature = "rich-widgets")]
624 pub fn set_spinner_phase(&mut self, id: WidgetId, phase: f32) -> Result<(), GuiError> {
625 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
626 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
627 match node.kind {
628 WidgetKind::Spinner { phase: ref mut v } => {
629 *v = phase;
630 self.dirty.add(rect)?;
631 Ok(())
632 }
633 _ => Err(GuiError::NotFound),
634 }
635 }
636
637 #[cfg(feature = "rich-widgets")]
638 pub fn tick_spinner(
639 &mut self,
640 id: WidgetId,
641 dt_ms: u32,
642 cycles_per_sec: f32,
643 ) -> Result<(), GuiError> {
644 let phase = match self.node(id).ok_or(GuiError::NotFound)?.kind {
645 WidgetKind::Spinner { phase } => phase + (dt_ms as f32 / 1000.0) * cycles_per_sec,
646 _ => return Err(GuiError::NotFound),
647 };
648 self.set_spinner_phase(id, phase)
649 }
650
651 #[cfg(feature = "rich-widgets")]
652 pub fn set_dropdown_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
653 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
654 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
655 match node.kind {
656 WidgetKind::Dropdown {
657 items,
658 selected: ref mut current,
659 ..
660 } => {
661 *current = selected.min(items.len().saturating_sub(1));
662 self.dirty.add(rect)?;
663 Ok(())
664 }
665 _ => Err(GuiError::NotFound),
666 }
667 }
668
669 #[cfg(feature = "rich-widgets")]
670 pub fn dropdown_selected(&self, id: WidgetId) -> Option<usize> {
671 match self.node(id)?.kind {
672 WidgetKind::Dropdown { selected, .. } => Some(selected),
673 _ => None,
674 }
675 }
676
677 #[cfg(feature = "rich-widgets")]
678 pub fn set_dropdown_open(&mut self, id: WidgetId, open: bool) -> Result<(), GuiError> {
679 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
680 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
681 match node.kind {
682 WidgetKind::Dropdown {
683 open: ref mut is_open,
684 ..
685 } => {
686 if *is_open != open {
687 *is_open = open;
688 self.dirty.add(rect)?;
689 self.push_event(if open {
690 UiEvent::Opened(id)
691 } else {
692 UiEvent::Closed(id)
693 })?;
694 }
695 Ok(())
696 }
697 _ => Err(GuiError::NotFound),
698 }
699 }
700
701 #[cfg(feature = "rich-widgets")]
702 pub fn dropdown_open(&self, id: WidgetId) -> Option<bool> {
703 match self.node(id)?.kind {
704 WidgetKind::Dropdown { open, .. } => Some(open),
705 _ => None,
706 }
707 }
708
709 #[cfg(feature = "rich-widgets")]
710 pub fn set_roller_selected(&mut self, id: WidgetId, selected: usize) -> Result<(), GuiError> {
711 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
712 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
713 match node.kind {
714 WidgetKind::Roller {
715 items,
716 selected: ref mut current,
717 } => {
718 *current = selected.min(items.len().saturating_sub(1));
719 self.dirty.add(rect)?;
720 Ok(())
721 }
722 _ => Err(GuiError::NotFound),
723 }
724 }
725
726 #[cfg(feature = "rich-widgets")]
727 pub fn roller_selected(&self, id: WidgetId) -> Option<usize> {
728 match self.node(id)?.kind {
729 WidgetKind::Roller { selected, .. } => Some(selected),
730 _ => None,
731 }
732 }
733
734 #[cfg(feature = "rich-widgets")]
735 pub fn set_textarea_text(&mut self, id: WidgetId, text: &'a str) -> Result<(), GuiError> {
736 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
737 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
738 match node.kind {
739 WidgetKind::TextArea {
740 text_buf: ref mut buf,
741 text_len: ref mut len,
742 cursor: ref mut c,
743 ..
744 } => {
745 let (next_buf, next_len) = textarea_storage_from_str(text);
746 *buf = next_buf;
747 *len = next_len;
748 *c = (*c).min(textarea_text(buf, *len).chars().count());
749 self.dirty.add(rect)?;
750 Ok(())
751 }
752 _ => Err(GuiError::NotFound),
753 }
754 }
755
756 #[cfg(feature = "rich-widgets")]
757 pub fn textarea_text(&self, id: WidgetId) -> Option<&str> {
758 match &self.node(id)?.kind {
759 WidgetKind::TextArea {
760 text_buf, text_len, ..
761 } => Some(textarea_text(text_buf, *text_len)),
762 _ => None,
763 }
764 }
765
766 #[cfg(feature = "rich-widgets")]
767 pub fn set_textarea_cursor(&mut self, id: WidgetId, cursor: usize) -> Result<(), GuiError> {
768 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
769 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
770 match node.kind {
771 WidgetKind::TextArea {
772 text_buf,
773 text_len,
774 cursor: ref mut current,
775 ..
776 } => {
777 let text = textarea_text(&text_buf, text_len);
778 *current = cursor.min(text.chars().count());
779 self.dirty.add(rect)?;
780 Ok(())
781 }
782 _ => Err(GuiError::NotFound),
783 }
784 }
785
786 #[cfg(feature = "rich-widgets")]
787 pub fn move_textarea_cursor(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
788 let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
789 self.set_textarea_cursor_with_extend(id, next.max(0) as usize, false)
790 }
791
792 #[cfg(feature = "rich-widgets")]
793 pub fn move_textarea_cursor_select(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
794 let next = self.textarea_cursor(id).ok_or(GuiError::NotFound)? as i32 + delta as i32;
795 self.set_textarea_cursor_with_extend(id, next.max(0) as usize, true)
796 }
797
798 #[cfg(feature = "rich-widgets")]
799 pub fn move_textarea_cursor_word(&mut self, id: WidgetId, delta: i8) -> Result<(), GuiError> {
800 let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
801 WidgetKind::TextArea {
802 text_buf,
803 text_len,
804 cursor,
805 ..
806 } => (textarea_text(text_buf, *text_len), *cursor),
807 _ => return Err(GuiError::NotFound),
808 };
809 let next = if delta >= 0 {
810 next_word_boundary(text, cursor)
811 } else {
812 prev_word_boundary(text, cursor)
813 };
814 self.set_textarea_cursor_with_extend(id, next, false)
815 }
816
817 #[cfg(feature = "rich-widgets")]
818 pub fn move_textarea_cursor_word_select(
819 &mut self,
820 id: WidgetId,
821 delta: i8,
822 ) -> Result<(), GuiError> {
823 let (text, cursor) = match &self.node(id).ok_or(GuiError::NotFound)?.kind {
824 WidgetKind::TextArea {
825 text_buf,
826 text_len,
827 cursor,
828 ..
829 } => (textarea_text(text_buf, *text_len), *cursor),
830 _ => return Err(GuiError::NotFound),
831 };
832 let next = if delta >= 0 {
833 next_word_boundary(text, cursor)
834 } else {
835 prev_word_boundary(text, cursor)
836 };
837 self.set_textarea_cursor_with_extend(id, next, true)
838 }
839
840 #[cfg(feature = "rich-widgets")]
841 pub fn set_textarea_cursor_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
842 self.set_textarea_cursor(id, 0)
843 }
844
845 #[cfg(feature = "rich-widgets")]
846 pub fn set_textarea_cursor_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
847 let len = self
848 .textarea_text(id)
849 .map(|text| text.chars().count())
850 .ok_or(GuiError::NotFound)?;
851 self.set_textarea_cursor(id, len)
852 }
853
854 #[cfg(feature = "rich-widgets")]
855 pub fn set_textarea_cursor_line_home(&mut self, id: WidgetId) -> Result<(), GuiError> {
856 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
857 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
858 let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
859 self.set_textarea_cursor_with_extend(id, next, false)
860 }
861
862 #[cfg(feature = "rich-widgets")]
863 pub fn set_textarea_cursor_line_home_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
864 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
865 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
866 let next = textarea_cursor_from_row_col(text, row, 0, wrap_cols);
867 self.set_textarea_cursor_with_extend(id, next, true)
868 }
869
870 #[cfg(feature = "rich-widgets")]
871 pub fn set_textarea_cursor_line_end(&mut self, id: WidgetId) -> Result<(), GuiError> {
872 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
873 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
874 let row_end = textarea_row_end_col(text, row, wrap_cols);
875 let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
876 self.set_textarea_cursor_with_extend(id, next, false)
877 }
878
879 #[cfg(feature = "rich-widgets")]
880 pub fn set_textarea_cursor_line_end_select(&mut self, id: WidgetId) -> Result<(), GuiError> {
881 let (text, cursor, wrap_cols) = self.textarea_line_context(id)?;
882 let (row, _) = textarea_row_col_at_cursor(text, cursor, wrap_cols);
883 let row_end = textarea_row_end_col(text, row, wrap_cols);
884 let next = textarea_cursor_from_row_col(text, row, row_end, wrap_cols);
885 self.set_textarea_cursor_with_extend(id, next, true)
886 }
887
888 #[cfg(feature = "rich-widgets")]
889 pub fn textarea_cursor(&self, id: WidgetId) -> Option<usize> {
890 match self.node(id)?.kind {
891 WidgetKind::TextArea { cursor, .. } => Some(cursor),
892 _ => None,
893 }
894 }
895
896 #[cfg(feature = "rich-widgets")]
897 pub fn set_textarea_selection(
898 &mut self,
899 id: WidgetId,
900 start: usize,
901 end: usize,
902 ) -> Result<(), GuiError> {
903 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
904 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
905 match node.kind {
906 WidgetKind::TextArea {
907 text_buf,
908 text_len,
909 selection: ref mut current,
910 ..
911 } => {
912 let text = textarea_text(&text_buf, text_len);
913 let len = text.chars().count();
914 let start = start.min(len);
915 let end = end.min(len);
916 *current = Some((start.min(end), start.max(end)));
917 self.dirty.add(rect)?;
918 Ok(())
919 }
920 _ => Err(GuiError::NotFound),
921 }
922 }
923
924 #[cfg(feature = "rich-widgets")]
925 pub fn clear_textarea_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
926 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
927 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
928 match node.kind {
929 WidgetKind::TextArea {
930 selection: ref mut current,
931 ..
932 } => {
933 *current = None;
934 self.dirty.add(rect)?;
935 Ok(())
936 }
937 _ => Err(GuiError::NotFound),
938 }
939 }
940
941 #[cfg(feature = "rich-widgets")]
942 pub fn textarea_selection(&self, id: WidgetId) -> Option<(usize, usize)> {
943 match self.node(id)?.kind {
944 WidgetKind::TextArea { selection, .. } => selection,
945 _ => None,
946 }
947 }
948
949 #[cfg(feature = "rich-widgets")]
950 pub fn textarea_cursor_visible(&self, id: WidgetId) -> Option<bool> {
951 match self.node(id)?.kind {
952 WidgetKind::TextArea { cursor_visible, .. } => Some(cursor_visible),
953 _ => None,
954 }
955 }
956
957 #[cfg(feature = "rich-widgets")]
958 pub fn set_textarea_capabilities(
959 &mut self,
960 id: WidgetId,
961 read_only: bool,
962 single_line: bool,
963 accept_newline: bool,
964 ) -> Result<(), GuiError> {
965 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
966 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
967 match node.kind {
968 WidgetKind::TextArea {
969 read_only: ref mut ro,
970 single_line: ref mut sl,
971 accept_newline: ref mut an,
972 ..
973 } => {
974 *ro = read_only;
975 *sl = single_line;
976 *an = accept_newline && !single_line;
977 self.dirty.add(rect)?;
978 Ok(())
979 }
980 _ => Err(GuiError::NotFound),
981 }
982 }
983
984 #[cfg(feature = "rich-widgets")]
985 pub fn textarea_insert_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
986 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
987 let before = self.capture_textarea_snapshot(id)?;
988 let mut emit = false;
989 if let Some(node) = self.node_mut(id) {
990 if let WidgetKind::TextArea {
991 text_buf,
992 text_len,
993 cursor,
994 selection,
995 read_only,
996 single_line,
997 accept_newline,
998 ..
999 } = &mut node.kind
1000 {
1001 if *read_only {
1002 return Ok(());
1003 }
1004 if ch == '\n' && (*single_line || !*accept_newline) {
1005 return Ok(());
1006 }
1007 let mut chars: heapless::Vec<char, TEXTAREA_CAPACITY> = heapless::Vec::new();
1008 for c in textarea_text(text_buf, *text_len).chars() {
1009 let _ = chars.push(c);
1010 }
1011 let original_len = chars.len();
1012 let original_cursor = *cursor;
1013
1014 if ch == '\u{8}' {
1015 let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
1016 if !removed_selection && *cursor > 0 && *cursor <= chars.len() {
1017 chars.remove(*cursor - 1);
1018 *cursor -= 1;
1019 }
1020 if removed_selection
1021 || *cursor != original_cursor
1022 || chars.len() != original_len
1023 {
1024 *selection = None;
1025 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
1026 *text_buf = next_buf;
1027 *text_len = next_len;
1028 emit = true;
1029 }
1030 } else if ch == '\u{7f}' {
1031 let removed_selection = delete_selection_if_any(&mut chars, cursor, selection);
1032 if !removed_selection && *cursor < chars.len() {
1033 chars.remove(*cursor);
1034 }
1035 if removed_selection || chars.len() != original_len {
1036 *selection = None;
1037 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
1038 *text_buf = next_buf;
1039 *text_len = next_len;
1040 emit = true;
1041 }
1042 } else if ch != '\n' || *cursor < TEXTAREA_CAPACITY {
1043 if delete_selection_if_any(&mut chars, cursor, selection) {
1044 *selection = None;
1045 }
1046 if chars.len() < TEXTAREA_CAPACITY && *cursor <= chars.len() {
1047 let _ = chars.insert(*cursor, ch);
1048 *cursor += 1;
1049 *selection = None;
1050 let (next_buf, next_len) = textarea_storage_from_chars(&chars);
1051 *text_buf = next_buf;
1052 *text_len = next_len;
1053 emit = true;
1054 }
1055 }
1056 } else {
1057 return Err(GuiError::NotFound);
1058 }
1059 }
1060 if emit {
1061 self.push_textarea_undo(id, before);
1062 self.clear_textarea_redo_for(id);
1063 self.dirty.add(rect)?;
1064 self.push_event(UiEvent::TextInput { id, ch })?;
1065 self.push_event(UiEvent::ValueChanged(id))?;
1066 }
1067 Ok(())
1068 }
1069
1070 #[cfg(feature = "rich-widgets")]
1071 pub(crate) fn textarea_line_context(
1072 &self,
1073 id: WidgetId,
1074 ) -> Result<(&str, usize, usize), GuiError> {
1075 let node = self.node(id).ok_or(GuiError::NotFound)?;
1076 match &node.kind {
1077 WidgetKind::TextArea {
1078 text_buf,
1079 text_len,
1080 cursor,
1081 ..
1082 } => {
1083 let font = node.style.normal.font;
1084 let inner_w = node.rect.w.saturating_sub(2);
1085 let cols = (inner_w / font.advance()).max(1) as usize;
1086 Ok((textarea_text(text_buf, *text_len), *cursor, cols))
1087 }
1088 _ => Err(GuiError::NotFound),
1089 }
1090 }
1091
1092 #[cfg(feature = "rich-widgets")]
1093 pub(crate) fn set_textarea_cursor_with_extend(
1094 &mut self,
1095 id: WidgetId,
1096 cursor: usize,
1097 extend_selection: bool,
1098 ) -> Result<(), GuiError> {
1099 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1100 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1101 match node.kind {
1102 WidgetKind::TextArea {
1103 text_buf,
1104 text_len,
1105 cursor: ref mut current_cursor,
1106 ref mut selection,
1107 ..
1108 } => {
1109 let len = textarea_text(&text_buf, text_len).chars().count();
1110 let next = cursor.min(len);
1111 if extend_selection {
1112 let anchor = match *selection {
1113 Some((start, end)) => {
1114 if *current_cursor == start {
1115 end
1116 } else {
1117 start
1118 }
1119 }
1120 None => *current_cursor,
1121 };
1122 if anchor == next {
1123 *selection = None;
1124 } else {
1125 *selection = Some((anchor.min(next), anchor.max(next)));
1126 }
1127 } else {
1128 *selection = None;
1129 }
1130 *current_cursor = next;
1131 self.dirty.add(rect)?;
1132 Ok(())
1133 }
1134 _ => Err(GuiError::NotFound),
1135 }
1136 }
1137
1138 #[cfg(feature = "rich-widgets")]
1139 pub(crate) fn capture_textarea_snapshot(
1140 &self,
1141 id: WidgetId,
1142 ) -> Result<TextareaSnapshot, GuiError> {
1143 match self.node(id).ok_or(GuiError::NotFound)?.kind {
1144 WidgetKind::TextArea {
1145 text_buf,
1146 text_len,
1147 cursor,
1148 selection,
1149 ..
1150 } => Ok(TextareaSnapshot {
1151 text_buf,
1152 text_len,
1153 cursor,
1154 selection,
1155 }),
1156 _ => Err(GuiError::NotFound),
1157 }
1158 }
1159
1160 #[cfg(feature = "rich-widgets")]
1161 pub(crate) fn apply_textarea_snapshot(
1162 &mut self,
1163 id: WidgetId,
1164 snap: TextareaSnapshot,
1165 ) -> Result<(), GuiError> {
1166 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1167 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1168 match node.kind {
1169 WidgetKind::TextArea {
1170 text_buf: ref mut buf,
1171 text_len: ref mut len,
1172 cursor: ref mut c,
1173 selection: ref mut sel,
1174 ..
1175 } => {
1176 *buf = snap.text_buf;
1177 *len = snap.text_len;
1178 *c = snap.cursor;
1179 *sel = snap.selection;
1180 self.dirty.add(rect)?;
1181 self.push_event(UiEvent::ValueChanged(id))
1182 }
1183 _ => Err(GuiError::NotFound),
1184 }
1185 }
1186
1187 #[cfg(feature = "rich-widgets")]
1188 pub(crate) fn push_textarea_undo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
1189 if self.textarea_undo.len() == self.textarea_undo.capacity() {
1190 self.textarea_undo.remove(0);
1191 }
1192 let _ = self
1193 .textarea_undo
1194 .push(TextareaHistoryEntry { id, snapshot });
1195 }
1196
1197 #[cfg(feature = "rich-widgets")]
1198 pub(crate) fn push_textarea_redo(&mut self, id: WidgetId, snapshot: TextareaSnapshot) {
1199 if self.textarea_redo.len() == self.textarea_redo.capacity() {
1200 self.textarea_redo.remove(0);
1201 }
1202 let _ = self
1203 .textarea_redo
1204 .push(TextareaHistoryEntry { id, snapshot });
1205 }
1206
1207 #[cfg(feature = "rich-widgets")]
1208 pub(crate) fn clear_textarea_redo_for(&mut self, id: WidgetId) {
1209 let mut i = 0usize;
1210 while i < self.textarea_redo.len() {
1211 if self.textarea_redo[i].id == id {
1212 self.textarea_redo.remove(i);
1213 } else {
1214 i += 1;
1215 }
1216 }
1217 }
1218
1219 #[cfg(feature = "rich-widgets")]
1220 pub(crate) fn textarea_undo(&mut self, id: WidgetId) -> Result<(), GuiError> {
1221 let Some(pos) = self.textarea_undo.iter().rposition(|entry| entry.id == id) else {
1222 return Ok(());
1223 };
1224 let current = self.capture_textarea_snapshot(id)?;
1225 let prior = self.textarea_undo.remove(pos).snapshot;
1226 self.push_textarea_redo(id, current);
1227 self.apply_textarea_snapshot(id, prior)
1228 }
1229
1230 #[cfg(feature = "rich-widgets")]
1231 pub(crate) fn textarea_redo(&mut self, id: WidgetId) -> Result<(), GuiError> {
1232 let Some(pos) = self.textarea_redo.iter().rposition(|entry| entry.id == id) else {
1233 return Ok(());
1234 };
1235 let current = self.capture_textarea_snapshot(id)?;
1236 let next = self.textarea_redo.remove(pos).snapshot;
1237 self.push_textarea_undo(id, current);
1238 self.apply_textarea_snapshot(id, next)
1239 }
1240
1241 #[cfg(feature = "rich-widgets")]
1242 pub fn textarea_backspace(&mut self, id: WidgetId) -> Result<(), GuiError> {
1243 self.textarea_insert_char(id, '\u{8}')
1244 }
1245
1246 #[cfg(feature = "rich-widgets")]
1247 pub fn textarea_delete_forward(&mut self, id: WidgetId) -> Result<(), GuiError> {
1248 self.textarea_insert_char(id, '\u{7f}')
1249 }
1250
1251 #[cfg(feature = "rich-widgets")]
1252 pub fn keyboard_selected_key(&self, id: WidgetId) -> Option<char> {
1253 match self.node(id)?.kind {
1254 WidgetKind::Keyboard {
1255 keys,
1256 alt_keys,
1257 selected,
1258 layout,
1259 ..
1260 } => keyboard_char_for_layout(keys, alt_keys, selected, layout),
1261 _ => None,
1262 }
1263 }
1264
1265 #[cfg(feature = "rich-widgets")]
1266 pub fn keyboard_layout(&self, id: WidgetId) -> Option<KeyboardLayout> {
1267 match self.node(id)?.kind {
1268 WidgetKind::Keyboard { layout, .. } => Some(layout),
1269 _ => None,
1270 }
1271 }
1272
1273 #[cfg(feature = "rich-widgets")]
1274 pub fn set_keyboard_layout(
1275 &mut self,
1276 id: WidgetId,
1277 layout: KeyboardLayout,
1278 ) -> Result<(), GuiError> {
1279 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1280 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1281 match node.kind {
1282 WidgetKind::Keyboard {
1283 layout: ref mut current,
1284 ..
1285 } => {
1286 *current = layout;
1287 self.dirty.add(rect)?;
1288 Ok(())
1289 }
1290 _ => Err(GuiError::NotFound),
1291 }
1292 }
1293
1294 #[cfg(feature = "rich-widgets")]
1295 pub fn set_keyboard_target(
1296 &mut self,
1297 id: WidgetId,
1298 target: Option<WidgetId>,
1299 ) -> Result<(), GuiError> {
1300 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1301 match node.kind {
1302 WidgetKind::Keyboard {
1303 target: ref mut current,
1304 ..
1305 } => {
1306 *current = target;
1307 Ok(())
1308 }
1309 _ => Err(GuiError::NotFound),
1310 }
1311 }
1312
1313 #[cfg(feature = "rich-widgets")]
1314 pub fn set_gauge_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1315 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1316 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1317 match node.kind {
1318 WidgetKind::Gauge {
1319 value: ref mut v,
1320 min,
1321 max,
1322 ..
1323 }
1324 | WidgetKind::ArcGauge {
1325 value: ref mut v,
1326 min,
1327 max,
1328 ..
1329 }
1330 | WidgetKind::GaugeNeedle {
1331 value: ref mut v,
1332 min,
1333 max,
1334 ..
1335 } => {
1336 *v = value.clamp(min.min(max), min.max(max));
1337 self.dirty.add(rect)?;
1338 Ok(())
1339 }
1340 _ => Err(GuiError::NotFound),
1341 }
1342 }
1343
1344 #[cfg(feature = "rich-widgets")]
1345 pub fn set_gauge_ticks(
1346 &mut self,
1347 id: WidgetId,
1348 major_ticks: u8,
1349 minor_ticks: u8,
1350 show_value: bool,
1351 ) -> Result<(), GuiError> {
1352 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1353 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1354 match node.kind {
1355 WidgetKind::Gauge {
1356 major_ticks: ref mut major,
1357 minor_ticks: ref mut minor,
1358 show_value: ref mut show,
1359 ..
1360 }
1361 | WidgetKind::ArcGauge {
1362 major_ticks: ref mut major,
1363 minor_ticks: ref mut minor,
1364 show_value: ref mut show,
1365 ..
1366 } => {
1367 *major = major_ticks.max(1);
1368 *minor = minor_ticks.max(1);
1369 *show = show_value;
1370 self.dirty.add(rect)?;
1371 Ok(())
1372 }
1373 _ => Err(GuiError::NotFound),
1374 }
1375 }
1376
1377 pub fn set_widget_rect(&mut self, id: WidgetId, rect: Rect) -> Result<(), GuiError> {
1378 let old = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1379 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1380 node.rect = rect;
1381 self.dirty.add(old)?;
1382 self.mark_subtree_dirty(id)?;
1383 Ok(())
1384 }
1385
1386 pub fn set_widget_x(&mut self, id: WidgetId, x: i32) -> Result<(), GuiError> {
1387 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1388 rect.x = x;
1389 self.set_widget_rect(id, rect)
1390 }
1391
1392 pub fn set_widget_y(&mut self, id: WidgetId, y: i32) -> Result<(), GuiError> {
1393 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1394 rect.y = y;
1395 self.set_widget_rect(id, rect)
1396 }
1397
1398 pub fn set_widget_width(&mut self, id: WidgetId, w: u32) -> Result<(), GuiError> {
1399 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1400 rect.w = w.max(1);
1401 self.set_widget_rect(id, rect)
1402 }
1403
1404 pub fn set_widget_height(&mut self, id: WidgetId, h: u32) -> Result<(), GuiError> {
1405 let mut rect = self.node(id).ok_or(GuiError::NotFound)?.rect;
1406 rect.h = h.max(1);
1407 self.set_widget_rect(id, rect)
1408 }
1409
1410 pub fn set_widget_opacity(&mut self, id: WidgetId, opacity: u8) -> Result<(), GuiError> {
1411 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1412 node.style.normal.opacity = opacity;
1413 node.style.focused.opacity = opacity;
1414 node.style.pressed.opacity = opacity;
1415 node.style.disabled.opacity = opacity;
1416 self.mark_subtree_dirty(id)
1417 }
1418
1419 pub fn set_widget_corner_radius(&mut self, id: WidgetId, radius: u8) -> Result<(), GuiError> {
1420 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1421 node.style.normal.corner_radius = radius;
1422 node.style.focused.corner_radius = radius;
1423 node.style.pressed.corner_radius = radius;
1424 node.style.disabled.corner_radius = radius;
1425 self.mark_subtree_dirty(id)
1426 }
1427
1428 pub fn set_widget_accent(&mut self, id: WidgetId, accent: Rgb565) -> Result<(), GuiError> {
1429 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1430 node.style.normal.accent = accent;
1431 node.style.focused.accent = accent;
1432 node.style.pressed.accent = accent;
1433 node.style.disabled.accent = accent;
1434 self.mark_subtree_dirty(id)
1435 }
1436
1437 pub fn set_widget_parent(
1438 &mut self,
1439 id: WidgetId,
1440 parent: Option<WidgetId>,
1441 ) -> Result<(), GuiError> {
1442 if let Some(parent) = parent {
1443 self.node(parent).ok_or(GuiError::NotFound)?;
1444 }
1445 self.node_mut(id).ok_or(GuiError::NotFound)?.parent = parent;
1446 self.mark_subtree_dirty(id)?;
1447 Ok(())
1448 }
1449
1450 pub fn add_child(&mut self, parent: WidgetId, child: WidgetId) -> Result<(), GuiError> {
1451 self.set_widget_parent(child, Some(parent))
1452 }
1453
1454 pub fn children_of(&self, parent: WidgetId) -> impl Iterator<Item = &WidgetNode<'a>> + '_ {
1455 self.widgets
1456 .iter()
1457 .filter(move |node| node.parent == Some(parent))
1458 }
1459
1460 #[inline]
1461 pub fn absolute_rect(&self, id: WidgetId) -> Option<Rect> {
1462 let node = self.node(id)?;
1463 if node.parent.is_none() {
1464 return Some(node.rect);
1465 }
1466 let mut rect = node.rect;
1467 let mut parent = node.parent;
1468 let mut depth = 0;
1469 while let Some(parent_id) = parent {
1470 if depth >= NODES {
1471 return None;
1472 }
1473 let parent_node = self.node(parent_id)?;
1474 rect.x += parent_node.rect.x;
1475 rect.y += parent_node.rect.y;
1476 parent = parent_node.parent;
1477 depth += 1;
1478 }
1479 Some(rect)
1480 }
1481
1482 pub fn set_flag(
1483 &mut self,
1484 id: WidgetId,
1485 flag: WidgetFlags,
1486 enabled: bool,
1487 ) -> Result<(), GuiError> {
1488 let was_set = self.has_flag(id, flag)?;
1489 let before_state = self.current_visual_state(id);
1490 self.mark_subtree_dirty(id)?;
1491 self.node_mut(id)
1492 .ok_or(GuiError::NotFound)?
1493 .flags
1494 .set(flag, enabled);
1495 if flag == WidgetFlags::DISABLED
1496 && enabled
1497 && self.pressed.is_some_and(|pressed| pressed.id == id)
1498 {
1499 self.pressed = None;
1500 }
1501 self.mark_subtree_dirty(id)?;
1502 if self
1503 .focus
1504 .is_some_and(|focus| !self.effective_focusable(focus))
1505 {
1506 self.focus = None;
1507 self.ensure_focus();
1508 }
1509 if flag == WidgetFlags::DISABLED && was_set != enabled {
1510 let after_state = self.current_visual_state(id);
1511 self.start_state_transition(id, before_state, after_state);
1512 }
1513 Ok(())
1514 }
1515
1516 pub fn has_flag(&self, id: WidgetId, flag: WidgetFlags) -> Result<bool, GuiError> {
1517 Ok(self
1518 .node(id)
1519 .ok_or(GuiError::NotFound)?
1520 .flags
1521 .contains(flag))
1522 }
1523
1524 pub fn insert_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
1525 self.set_flag(id, flag, true)
1526 }
1527
1528 pub fn remove_flag(&mut self, id: WidgetId, flag: WidgetFlags) -> Result<(), GuiError> {
1529 self.set_flag(id, flag, false)
1530 }
1531
1532 pub fn set_hidden(&mut self, id: WidgetId, hidden: bool) -> Result<(), GuiError> {
1533 self.set_flag(id, WidgetFlags::HIDDEN, hidden)
1534 }
1535
1536 pub fn set_disabled(&mut self, id: WidgetId, disabled: bool) -> Result<(), GuiError> {
1537 self.set_flag(id, WidgetFlags::DISABLED, disabled)
1538 }
1539
1540 pub fn set_clickable(&mut self, id: WidgetId, clickable: bool) -> Result<(), GuiError> {
1541 self.set_flag(id, WidgetFlags::CLICKABLE, clickable)
1542 }
1543
1544 pub fn set_scrollable(&mut self, id: WidgetId, scrollable: bool) -> Result<(), GuiError> {
1545 self.set_flag(id, WidgetFlags::SCROLLABLE, scrollable)
1546 }
1547
1548 pub fn set_visible(&mut self, id: WidgetId, visible: bool) -> Result<(), GuiError> {
1549 self.set_hidden(id, !visible)
1550 }
1551
1552 pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) -> Result<(), GuiError> {
1553 self.set_disabled(id, !enabled)
1554 }
1555
1556 pub fn event_path<const M: usize>(
1557 &self,
1558 target: WidgetId,
1559 out: &mut heapless::Vec<EventContext, M>,
1560 ) -> Result<usize, GuiError> {
1561 self.node(target).ok_or(GuiError::NotFound)?;
1562 out.clear();
1563
1564 let mut chain = heapless::Vec::<WidgetId, NODES>::new();
1565 let mut current = Some(target);
1566 while let Some(id) = current {
1567 chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
1568 current = self.node(id).ok_or(GuiError::NotFound)?.parent;
1569 }
1570
1571 for id in chain.iter().rev().copied().filter(|&id| id != target) {
1572 out.push(EventContext {
1573 target,
1574 current: id,
1575 phase: EventPhase::Capture,
1576 })
1577 .map_err(|_| GuiError::EventsFull)?;
1578 }
1579
1580 out.push(EventContext {
1581 target,
1582 current: target,
1583 phase: EventPhase::Target,
1584 })
1585 .map_err(|_| GuiError::EventsFull)?;
1586
1587 for id in chain.iter().copied().skip(1) {
1588 out.push(EventContext {
1589 target,
1590 current: id,
1591 phase: EventPhase::Bubble,
1592 })
1593 .map_err(|_| GuiError::EventsFull)?;
1594 }
1595
1596 Ok(out.len())
1597 }
1598
1599 pub fn widget_event_path<const M: usize>(
1600 &self,
1601 target: WidgetId,
1602 kind: WidgetEventKind,
1603 out: &mut heapless::Vec<WidgetEvent, M>,
1604 ) -> Result<usize, GuiError> {
1605 self.node(target).ok_or(GuiError::NotFound)?;
1606 out.clear();
1607
1608 let mut chain = heapless::Vec::<WidgetId, NODES>::new();
1609 let mut current = Some(target);
1610 while let Some(id) = current {
1611 chain.push(id).map_err(|_| GuiError::WidgetsFull)?;
1612 current = self.node(id).ok_or(GuiError::NotFound)?.parent;
1613 }
1614
1615 for id in chain.iter().rev().copied().filter(|&id| id != target) {
1616 out.push(WidgetEvent {
1617 target,
1618 current: id,
1619 phase: EventPhase::Capture,
1620 kind,
1621 })
1622 .map_err(|_| GuiError::EventsFull)?;
1623 }
1624
1625 out.push(WidgetEvent {
1626 target,
1627 current: target,
1628 phase: EventPhase::Target,
1629 kind,
1630 })
1631 .map_err(|_| GuiError::EventsFull)?;
1632
1633 if self.has_flag(target, WidgetFlags::EVENT_BUBBLE)? {
1634 for id in chain.iter().copied().skip(1) {
1635 out.push(WidgetEvent {
1636 target,
1637 current: id,
1638 phase: EventPhase::Bubble,
1639 kind,
1640 })
1641 .map_err(|_| GuiError::EventsFull)?;
1642 }
1643 }
1644
1645 Ok(out.len())
1646 }
1647
1648 pub fn dispatch_widget_event<const M: usize, F>(
1649 &self,
1650 target: WidgetId,
1651 kind: WidgetEventKind,
1652 scratch: &mut heapless::Vec<WidgetEvent, M>,
1653 mut handler: F,
1654 ) -> Result<(), GuiError>
1655 where
1656 F: FnMut(WidgetEvent) -> EventPolicy,
1657 {
1658 self.widget_event_path(target, kind, scratch)?;
1659 for event in scratch.iter().copied() {
1660 let handler_policy = handler(event);
1661 if matches!(handler_policy, EventPolicy::Stop)
1662 || self.stop_due_to_builtin_widget_behavior(event)
1663 || self.stop_due_to_registered_policy(event)
1664 {
1665 break;
1666 }
1667 }
1668 Ok(())
1669 }
1670
1671 pub fn mark_subtree_dirty(&mut self, id: WidgetId) -> Result<(), GuiError> {
1672 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1673 self.dirty.add(rect)?;
1674 let child_ids: heapless::Vec<WidgetId, NODES> = self
1675 .widgets
1676 .iter()
1677 .filter(|node| node.parent == Some(id))
1678 .map(|node| node.id)
1679 .collect();
1680 for child in child_ids {
1681 self.mark_subtree_dirty(child)?;
1682 }
1683 Ok(())
1684 }
1685
1686 pub fn set_focus_group(&mut self, id: WidgetId, group: FocusGroupId) -> Result<(), GuiError> {
1687 self.node_mut(id).ok_or(GuiError::NotFound)?.focus_group = group;
1688 Ok(())
1689 }
1690
1691 pub fn set_active_focus_group(&mut self, group: Option<FocusGroupId>) {
1692 self.active_focus_group = group;
1693 if let Some(focus) = self.focus {
1694 let still_valid = self.node(focus).is_some_and(|node| {
1695 group.is_none_or(|active| node.focus_group == active)
1696 && self.effective_focusable(focus)
1697 });
1698 if !still_valid {
1699 self.focus = None;
1700 self.ensure_focus();
1701 }
1702 }
1703 }
1704
1705 pub fn apply_layout(
1706 &mut self,
1707 layout: LinearLayout,
1708 area: Rect,
1709 ids: &[WidgetId],
1710 ) -> Result<usize, GuiError> {
1711 let mut rects = [Rect::empty(); 16];
1712 let count = layout.arrange(area, ids.len().min(rects.len()), &mut rects);
1713 for (id, rect) in ids.iter().copied().zip(rects).take(count) {
1714 self.set_widget_rect(id, rect)?;
1715 }
1716 Ok(count)
1717 }
1718
1719 pub fn apply_layout_flex(
1720 &mut self,
1721 layout: LinearLayout,
1722 area: Rect,
1723 ids: &[WidgetId],
1724 items: &[LayoutItem],
1725 enable_grow: bool,
1726 enable_shrink: bool,
1727 ) -> Result<usize, GuiError> {
1728 let mut rects = [Rect::empty(); 16];
1729 let count = ids.len().min(items.len()).min(rects.len());
1730 let laid_out = layout.arrange_items_flex(
1731 area,
1732 &items[..count],
1733 &mut rects,
1734 enable_grow,
1735 enable_shrink,
1736 );
1737 for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
1738 self.set_widget_rect(id, rect)?;
1739 }
1740 Ok(laid_out)
1741 }
1742
1743 pub fn apply_layout_intrinsic(
1744 &mut self,
1745 layout: LinearLayout,
1746 area: Rect,
1747 ids: &[WidgetId],
1748 ) -> Result<usize, GuiError> {
1749 self.apply_layout_intrinsic_with_cross(layout, area, ids, false)
1750 }
1751
1752 pub fn apply_layout_intrinsic_with_cross(
1753 &mut self,
1754 layout: LinearLayout,
1755 area: Rect,
1756 ids: &[WidgetId],
1757 preserve_cross: bool,
1758 ) -> Result<usize, GuiError> {
1759 let mut specs = [LayoutItem::fill(); 16];
1760 let mut rects = [Rect::empty(); 16];
1761 let count = ids.len().min(specs.len()).min(rects.len());
1762
1763 for (idx, id) in ids.iter().copied().take(count).enumerate() {
1764 let (w, h) = self.intrinsic_size(id).ok_or(GuiError::NotFound)?;
1765 specs[idx] = match layout.axis {
1766 Axis::Horizontal => LayoutItem::length(w).with_cross(if preserve_cross {
1767 crate::layout::Constraint::Length(h)
1768 } else {
1769 crate::layout::Constraint::Fill(1)
1770 }),
1771 Axis::Vertical => LayoutItem::length(h).with_cross(if preserve_cross {
1772 crate::layout::Constraint::Length(w)
1773 } else {
1774 crate::layout::Constraint::Fill(1)
1775 }),
1776 };
1777 }
1778
1779 let laid_out = layout.arrange_items(area, &specs[..count], &mut rects);
1780 for (id, rect) in ids.iter().copied().zip(rects).take(laid_out) {
1781 self.set_widget_rect(id, rect)?;
1782 }
1783 Ok(laid_out)
1784 }
1785
1786 pub fn set_dial_value(&mut self, id: WidgetId, value: f32) -> Result<(), GuiError> {
1787 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1788 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1789 match node.kind {
1790 WidgetKind::Dial {
1791 value: ref mut v,
1792 min,
1793 max,
1794 } => {
1795 *v = value.clamp(min, max);
1796 self.dirty.add(rect)?;
1797 Ok(())
1798 }
1799 _ => Err(GuiError::NotFound),
1800 }
1801 }
1802
1803 pub fn dial_value(&self, id: WidgetId) -> Option<f32> {
1804 match self.node(id)?.kind {
1805 WidgetKind::Dial { value, .. } => Some(value),
1806 _ => None,
1807 }
1808 }
1809
1810 pub fn autocomplete_text(&self, id: WidgetId) -> Option<&str> {
1811 match &self.node(id)?.kind {
1812 WidgetKind::AutoComplete {
1813 text_buf, text_len, ..
1814 } => core::str::from_utf8(&text_buf[..*text_len as usize]).ok(),
1815 _ => None,
1816 }
1817 }
1818
1819 pub fn set_autocomplete_text(&mut self, id: WidgetId, text: &str) -> Result<(), GuiError> {
1820 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1821 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1822 if let WidgetKind::AutoComplete {
1823 text_buf,
1824 text_len,
1825 suggestions,
1826 filtered,
1827 filter_count,
1828 selected,
1829 expanded,
1830 } = &mut node.kind
1831 {
1832 let len = text.len().min(text_buf.len());
1833 text_buf[..len].copy_from_slice(&text.as_bytes()[..len]);
1834 *text_len = len as u8;
1835 *selected = None;
1836 *expanded = false;
1837 filter_suggestions(text, suggestions, filtered, filter_count);
1838 self.dirty.add(rect)?;
1839 self.push_event(UiEvent::ValueChanged(id))?;
1840 }
1841 Ok(())
1842 }
1843
1844 pub fn insert_autocomplete_char(&mut self, id: WidgetId, ch: char) -> Result<(), GuiError> {
1845 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1846 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1847 if let WidgetKind::AutoComplete {
1848 text_buf,
1849 text_len,
1850 suggestions,
1851 filtered,
1852 filter_count,
1853 selected,
1854 expanded,
1855 } = &mut node.kind
1856 {
1857 if (*text_len as usize) < text_buf.len() {
1858 text_buf[*text_len as usize] = ch as u8;
1859 *text_len += 1;
1860 *expanded = true;
1861 *selected = None;
1862 if let Ok(current_text) = core::str::from_utf8(&text_buf[..*text_len as usize]) {
1863 filter_suggestions(current_text, suggestions, filtered, filter_count);
1864 }
1865 self.dirty.add(rect)?;
1866 self.push_event(UiEvent::ValueChanged(id))?;
1867 }
1868 }
1869 Ok(())
1870 }
1871
1872 pub fn delete_autocomplete_char(&mut self, id: WidgetId) -> Result<(), GuiError> {
1873 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1874 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1875 if let WidgetKind::AutoComplete {
1876 text_buf,
1877 text_len,
1878 suggestions,
1879 filtered,
1880 filter_count,
1881 selected,
1882 expanded,
1883 } = &mut node.kind
1884 {
1885 if *text_len > 0 {
1886 *text_len -= 1;
1887 *expanded = true;
1888 *selected = None;
1889 if let Ok(current_text) = core::str::from_utf8(&text_buf[..*text_len as usize]) {
1890 filter_suggestions(current_text, suggestions, filtered, filter_count);
1891 }
1892 self.dirty.add(rect)?;
1893 self.push_event(UiEvent::ValueChanged(id))?;
1894 }
1895 }
1896 Ok(())
1897 }
1898
1899 pub fn autocomplete_confirm_selection(&mut self, id: WidgetId) -> Result<(), GuiError> {
1900 let rect = self.absolute_rect(id).ok_or(GuiError::NotFound)?;
1901 let node = self.node_mut(id).ok_or(GuiError::NotFound)?;
1902 if let WidgetKind::AutoComplete {
1903 text_buf,
1904 text_len,
1905 filtered,
1906 filter_count,
1907 selected,
1908 expanded,
1909 ..
1910 } = &mut node.kind
1911 {
1912 if *expanded {
1913 if let Some(sel_idx) = *selected {
1914 if sel_idx < *filter_count as usize {
1915 if let Some(selected_text) = filtered[sel_idx] {
1916 let len = selected_text.len().min(text_buf.len());
1917 text_buf[..len].copy_from_slice(&selected_text.as_bytes()[..len]);
1918 *text_len = len as u8;
1919 *expanded = false;
1920 *selected = None;
1921 self.dirty.add(rect)?;
1922 self.push_event(UiEvent::ValueChanged(id))?;
1923 }
1924 }
1925 }
1926 }
1927 }
1928 Ok(())
1929 }
1930}
1931
1932fn contains_ignore_ascii_case(s: &str, needle: &str) -> bool {
1933 if needle.is_empty() {
1934 return true;
1935 }
1936 let s_bytes = s.as_bytes();
1937 let n_bytes = needle.as_bytes();
1938 if n_bytes.len() > s_bytes.len() {
1939 return false;
1940 }
1941 s_bytes.windows(n_bytes.len()).any(|window| {
1942 window
1943 .iter()
1944 .zip(n_bytes.iter())
1945 .all(|(a, b)| a.eq_ignore_ascii_case(b))
1946 })
1947}
1948
1949fn filter_suggestions<'a>(
1950 text: &str,
1951 suggestions: &'a [&'a str],
1952 filtered: &mut [Option<&'a str>; 8],
1953 filter_count: &mut u8,
1954) {
1955 *filter_count = 0;
1956 for slot in filtered.iter_mut() {
1957 *slot = None;
1958 }
1959 if text.is_empty() {
1960 return;
1961 }
1962 for &s in suggestions {
1963 if contains_ignore_ascii_case(s, text) {
1964 filtered[*filter_count as usize] = Some(s);
1965 *filter_count += 1;
1966 if *filter_count == 8 {
1967 break;
1968 }
1969 }
1970 }
1971}