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