1use yog_api::ui::event::keys;
7use yog_api::ui::widget::WidgetKind;
8use yog_api::ui::{
9 Action, FlexDir, LayoutNode, Pointer, Rect, StateFlags, TextBoxEvent, UiEvent, UiRoot, Units,
10 Widget, layout, surface, widget,
11};
12
13use crate::screen::{self, Request};
14use crate::{Anchor, Cx, Parts, ScreenHandle, View, ViewEvent, ViewModel};
15
16const WHEEL_STEP: f32 = 16.0;
18const FALLBACK_PAGE: u32 = 5;
20pub(crate) const LOG_CLOSE: &str = "close";
22const LOG_BACK: &str = "back";
23
24#[derive(Debug, Clone, Copy, Default, PartialEq)]
26pub struct Viewport {
27 pub offset: f32,
29 pub height: f32,
31}
32
33#[derive(Debug, Default)]
35pub struct Update {
36 pub(crate) relayout: bool,
37 pub(crate) repaint: bool,
38 pub(crate) viewports: Vec<Viewport>,
39 pub(crate) text_sets: Vec<(u16, String)>,
40 pub(crate) scroll_resets: Vec<u16>,
41 pub(crate) lists: Vec<(u16, u32, Option<u32>)>,
42}
43
44impl Update {
45 pub fn new() -> Self {
47 Self::default()
48 }
49 pub fn relayout(&mut self) {
51 self.relayout = true;
52 }
53 pub fn repaint(&mut self) {
55 self.repaint = true;
56 }
57 pub fn needs_relayout(&self) -> bool {
58 self.relayout
59 }
60 pub fn needs_repaint(&self) -> bool {
61 self.repaint || self.relayout
62 }
63 pub fn viewport(&self, scroller: u16) -> Viewport {
65 self.viewports
66 .get(scroller as usize)
67 .copied()
68 .unwrap_or_default()
69 }
70 pub fn set_text_box(&mut self, slot: u16, text: &str) {
73 self.text_sets.push((slot, text.to_owned()));
74 }
75 pub fn reset_scroll(&mut self, scroller: u16) {
77 self.scroll_resets.push(scroller);
78 self.relayout();
79 }
80 pub fn list_info(&mut self, owner: u16, len: u32, selected: Option<u32>) {
82 self.lists.push((owner, len, selected));
83 }
84}
85
86pub(crate) fn find_path(w: &Widget, id: &str) -> Option<Vec<u16>> {
88 if w.id.as_deref() == Some(id) {
89 return Some(Vec::new());
90 }
91 w.children.iter().enumerate().find_map(|(i, c)| {
92 find_path(c, id).map(|mut p| {
93 p.insert(0, i as u16);
94 p
95 })
96 })
97}
98
99fn with_root(path: &[u16]) -> Vec<u16> {
100 let mut p = Vec::with_capacity(path.len() + 1);
101 p.push(0);
102 p.extend_from_slice(path);
103 p
104}
105
106pub(crate) struct Host<W: View> {
107 pub(crate) vm: W::Vm,
108 view: W,
109 pub(crate) ui: UiRoot,
111 pub(crate) parts: Parts,
112 pub(crate) size: (f32, f32),
113 pub(crate) dirty: bool,
114 hover: Option<Vec<u16>>,
116 pressed: Option<Vec<u16>>,
117 focus: Option<Vec<u16>>,
118 focus_visible: bool,
120 outlined: Option<Vec<u16>>,
121 mouse: Option<(f32, f32)>,
122 captured: Option<Vec<u16>>,
124 hovered_surface: Option<Vec<u16>>,
126 opened: bool,
127 reveal: Option<(u16, u32)>,
128 seen_viewports: Vec<Viewport>,
130 pub(crate) handle: ScreenHandle<W::Vm>,
131 pub(crate) log: Vec<String>,
133 pub(crate) live: bool,
135 pub(crate) every_frame: bool,
138}
139
140impl<W: View> Host<W> {
141 pub(crate) fn new(vm: W::Vm, handle: ScreenHandle<W::Vm>, live: bool) -> Self {
142 let (view, content, parts) = W::build();
143 let screen = widget::panel(FlexDir::Column).child(content);
144 Self {
145 vm,
146 view,
147 ui: UiRoot::new(W::META.id, screen),
148 parts,
149 size: (W::META.w, W::META.h),
150 dirty: true,
151 hover: None,
152 pressed: None,
153 focus: None,
154 focus_visible: false,
155 outlined: None,
156 mouse: None,
157 captured: None,
158 hovered_surface: None,
159 opened: false,
160 reveal: None,
161 seen_viewports: Vec::new(),
162 handle,
163 log: Vec::new(),
164 live,
165 every_frame: false,
166 }
167 }
168
169 pub(crate) fn content(&self) -> &Widget {
170 &self.ui.root.children[0]
171 }
172
173 fn node(&self, path: &[u16]) -> Option<&LayoutNode> {
174 layout::node_at(&self.ui.layout_root, &with_root(path))
175 }
176
177 pub(crate) fn content_node(&self, path: &[u16]) -> Option<&LayoutNode> {
178 self.node(path)
179 }
180
181 fn hit(&self, x: f32, y: f32) -> Option<Vec<u16>> {
183 let p = layout::hit_path(&self.ui.layout_root, x, y)?;
184 (p.first() == Some(&0)).then(|| p[1..].to_vec())
185 }
186
187 fn with_cx<R>(&mut self, f: impl FnOnce(&mut W::Vm, &mut W, &mut Cx<'_, W::Vm>) -> R) -> R {
190 let mut reqs = Vec::new();
191 let r = {
192 let mut cx = Cx::new(&mut reqs, &self.handle);
193 f(&mut self.vm, &mut self.view, &mut cx)
194 };
195 self.dirty = true;
196 for req in reqs {
197 self.request(req);
198 }
199 r
200 }
201
202 fn request(&mut self, req: Request) {
203 let id = W::META.id;
204 match req {
205 Request::Close => {
206 self.log.push(LOG_CLOSE.into());
207 if self.live {
208 yog_api::close_ui(id);
209 }
210 }
211 Request::Navigate(to) => {
212 self.log.push(format!("navigate:{to}"));
213 if self.live {
214 screen::navigate(id, &to);
215 }
216 }
217 Request::Back => {
218 self.log.push(LOG_BACK.into());
219 if !self.live || !screen::go_back() {
220 self.request(Request::Close);
221 }
222 }
223 Request::Focus(wid) => {
224 if let Some(p) = find_path(self.content(), &wid) {
225 self.set_focus(Some(p), true);
226 }
227 }
228 Request::Invalidate => self.dirty = true,
229 }
230 }
231
232 pub(crate) fn dispatch(&mut self, ev: ViewEvent) {
233 self.with_cx(|vm, view, cx| view.dispatch(vm, cx, ev));
234 }
235
236 fn open_vm(&mut self) {
237 self.opened = true;
238 self.reset_input();
239 self.with_cx(|vm, _, cx| vm.on_open(cx));
240 }
241
242 fn close_vm(&mut self) {
243 self.opened = false;
244 self.reset_input();
245 self.with_cx(|vm, _, cx| vm.on_close(cx));
246 }
247
248 fn reset_input(&mut self) {
249 self.hover = None;
250 self.pressed = None;
251 self.focus = None;
252 self.mouse = None;
253 for t in &mut self.parts.text_boxes {
254 t.state.blur();
255 }
256 }
257
258 pub(crate) fn drain_queue(&mut self) {
259 for job in self.handle.take_queue() {
260 job(&mut self.vm);
261 self.dirty = true;
262 }
263 }
264
265 pub(crate) fn frame(&mut self, sw: f32, sh: f32) {
269 self.size = (sw, sh);
270 self.drain_queue();
271 self.dirty |= self.every_frame;
272 for _ in 0..2 {
275 if self.dirty {
276 self.run_update();
277 }
278 self.apply_parts();
279 self.apply_states();
280 self.layout();
281 if !self.do_reveal() && !self.viewports_changed() {
282 break;
283 }
284 }
285 }
286
287 fn viewports(&self) -> Vec<Viewport> {
288 self.parts
289 .scrollers
290 .iter()
291 .map(|s| Viewport {
292 offset: s.state.offset,
293 height: self.node(&s.path).map_or(0.0, |n| n.rect.h),
294 })
295 .collect()
296 }
297
298 fn viewports_changed(&mut self) -> bool {
301 let now = self.viewports();
302 let changed = self.parts.lists.iter().any(|l| {
303 let (Some(_), Some(s)) = (l.item_height, l.scroller) else {
304 return false;
305 };
306 let (a, b) = (self.seen_viewports.get(s as usize), now.get(s as usize));
307 a.map(|v| v.height) != b.map(|v| v.height)
308 });
309 if changed {
310 self.dirty = true;
311 }
312 changed
313 }
314
315 fn run_update(&mut self) {
316 let viewports = self.viewports();
317 self.seen_viewports.clone_from(&viewports);
318 let mut u = Update {
319 viewports,
320 ..Update::default()
321 };
322 let content = &mut self.ui.root.children[0];
323 self.view.update(content, &self.vm, &mut u);
324 self.vm.customize(content);
325 for (slot, text) in u.text_sets {
326 if let Some(t) = self.parts.text_boxes.get_mut(slot as usize)
327 && t.state.text() != text
328 {
329 t.state.set_text(text);
330 }
331 }
332 for s in u.scroll_resets {
333 if let Some(s) = self.parts.scrollers.get_mut(s as usize) {
334 s.state.reset();
335 }
336 }
337 for (owner, len, selected) in u.lists {
338 if let Some(l) = self.parts.lists.iter_mut().find(|l| l.owner == owner) {
339 l.len = len;
340 l.selected = selected;
341 }
342 }
343 self.dirty = false;
344 }
345
346 fn apply_parts(&mut self) {
347 let content = &mut self.ui.root.children[0];
348 for s in &self.parts.scrollers {
349 if let Some(w) = content.get_path_mut(&s.path) {
350 w.style.scroll_y = s.state.offset;
351 }
352 }
353 for t in &mut self.parts.text_boxes {
354 if let Some(w) = content.get_path_mut(&t.path) {
355 t.state.apply(w);
356 }
357 }
358 }
359
360 fn apply_states(&mut self) {
361 let content = &mut self.ui.root.children[0];
362 content.clear_state_deep(StateFlags::HOVER | StateFlags::PRESSED | StateFlags::FOCUSED);
363 if let Some(p) = self.outlined.take()
364 && let Some(w) = content.get_path_mut(&p)
365 {
366 w.focused = false;
367 }
368 for (path, flag) in [
369 (&self.hover, StateFlags::HOVER),
370 (&self.pressed, StateFlags::PRESSED),
371 (&self.focus, StateFlags::FOCUSED),
372 ] {
373 if let Some(w) = path.as_deref().and_then(|p| content.get_path_mut(p)) {
374 w.set_state_inherited(flag, true);
375 }
376 }
377 if self.focus_visible
378 && let Some(p) = &self.focus
379 && let Some(w) = content.get_path_mut(p)
380 && w.is_interactive()
381 {
382 w.focused = true;
383 self.outlined = Some(p.clone());
384 }
385 }
386
387 fn layout(&mut self) {
388 let (sw, sh) = self.size;
389 let m = W::META;
390 let root = &mut self.ui.root;
391 root.style.w = Units::Px(sw);
392 root.style.h = Units::Px(sh);
393 let c = &mut root.children[0];
394 if m.anchor == Anchor::Fill {
395 c.style.w = Units::Percent(100.0);
396 c.style.h = Units::Percent(100.0);
397 c.style.pos = Some((0.0, 0.0));
398 } else {
399 c.style.w = Units::Px(m.w);
400 c.style.h = Units::Px(m.h);
401 c.style.pos = Some(m.anchor.place(sw, sh, m.w, m.h));
402 }
403 self.ui.layout(sw, sh);
404 }
405
406 fn do_reveal(&mut self) -> bool {
409 let Some((owner, index)) = self.reveal.take() else {
410 return false;
411 };
412 let Some(list) = self.parts.lists.iter().find(|l| l.owner == owner) else {
413 return false;
414 };
415 let Some(si) = list.scroller else {
416 return false;
417 };
418 let Some(scroller) = self.parts.scrollers.get(si as usize) else {
419 return false;
420 };
421 let (Some(view), Some(items)) = (self.node(&scroller.path), self.node(&list.path)) else {
422 return false;
423 };
424 let found = items.children.iter().find(|c| {
425 c.action
426 .is_some_and(|a| a.owner == owner && a.index == index)
427 });
428 let (top, h) = match (found, list.item_height) {
429 (Some(n), _) => (n.rect.y - view.rect.y + view.scroll_y, n.rect.h),
430 (None, Some(ih)) => {
431 let w = self.content().get_path(&list.path);
432 let (gap, pad) = w.map_or((0.0, 0.0), |w| (w.style.gap, w.style.pad[0]));
433 (pad + index as f32 * (ih + gap), ih)
434 }
435 (None, None) => return false,
436 };
437 let (view_h, max) = (view.rect.h, view.max_scroll());
438 let state = &mut self.parts.scrollers[si as usize].state;
439 let before = state.offset;
440 state.scroll_into_view(top, h, view_h);
441 state.offset = state.offset.clamp(0.0, max.max(0.0));
442 if (state.offset - before).abs() > f32::EPSILON {
443 self.dirty = true;
444 true
445 } else {
446 false
447 }
448 }
449
450 pub(crate) fn draw_tooltip(&self, gfx: &yog_api::GfxContext) {
452 if let Some((x, y)) = self.mouse
453 && let Some(text) = self.ui.tooltip_at(x, y)
454 {
455 UiRoot::render_tooltip(gfx, text, x, y);
456 }
457 }
458
459 pub(crate) fn on_event(&mut self, raw: &str) -> bool {
463 self.drain_queue();
464 let ev = UiEvent::parse(raw);
465 match ev {
466 UiEvent::Open { w, h } => {
467 self.size = (w, h);
468 if self.opened {
469 self.close_vm();
470 }
471 self.open_vm();
472 false
473 }
474 UiEvent::Close => {
475 self.captured = None;
476 self.leave_surface();
477 if self.opened {
478 self.close_vm();
479 }
480 if self.live {
481 screen::screen_closed();
482 }
483 false
484 }
485 UiEvent::Move { x, y } => {
486 self.mouse = Some((x, y));
487 self.hover = self.hit(x, y);
488 let under = self.surface_at(x, y);
489 if under != self.hovered_surface {
490 self.leave_surface();
491 self.hovered_surface = under.clone();
492 }
493 under.is_some_and(|p| self.to_surface(&p, x, y, |x, y| Pointer::Move { x, y }))
494 }
495 UiEvent::Scroll { .. } => self.scroll(&ev),
496 UiEvent::Click { x, y } => self.click(x, y, &ev),
497 UiEvent::Drag { x, y } | UiEvent::Release { x, y } if self.captured.is_some() => {
498 let release = matches!(ev, UiEvent::Release { .. });
499 let path = if release {
500 self.pressed = None;
501 self.captured.take()
502 } else {
503 self.captured.clone()
504 };
505 if let Some(path) = path {
506 self.to_surface(&path, x, y, |x, y| {
507 if release {
508 Pointer::Up { x, y }
509 } else {
510 Pointer::Drag { x, y }
511 }
512 });
513 }
514 true
515 }
516 UiEvent::Drag { .. } | UiEvent::Release { .. } => {
517 if matches!(ev, UiEvent::Release { .. }) {
518 self.pressed = None;
519 }
520 let used = self.feed_scrollers(&ev, false);
521 self.feed_text_boxes(&ev).is_some() | used
522 }
523 UiEvent::Key { key, mods } => self.key(key, mods, &ev),
524 UiEvent::Char(_) => self.text_input(&ev),
525 UiEvent::Other(_) => false,
526 }
527 }
528
529 fn scroll(&mut self, ev: &UiEvent<'_>) -> bool {
530 if let (Some((x, y)), UiEvent::Scroll { dy }) = (self.mouse, ev)
532 && let Some((s, r)) = self.surface_at(x, y).and_then(|p| self.surface_box(&p))
533 && s.scroll(*dy, (r.w, r.h))
534 {
535 return true;
536 }
537 let under = self
538 .mouse
539 .and_then(|(x, y)| layout::deepest_path(&self.ui.layout_root, x, y))
540 .filter(|p| p.first() == Some(&0))
541 .map(|p| p[1..].to_vec());
542 let target = under
543 .and_then(|p| {
544 self.parts
545 .scrollers
546 .iter()
547 .enumerate()
548 .filter(|(_, s)| p.starts_with(&s.path))
549 .max_by_key(|(_, s)| s.path.len())
550 .map(|(i, _)| i)
551 })
552 .or_else(|| {
553 self.focused_list()
554 .and_then(|l| l.scroller.map(usize::from))
555 })
556 .or_else(|| (!self.parts.scrollers.is_empty()).then_some(0));
557 let Some(i) = target else {
558 return false;
559 };
560 let s = &mut self.parts.scrollers[i];
561 let Some(node) = layout::node_at(&self.ui.layout_root, &with_root(&s.path)) else {
562 return false;
563 };
564 s.state.handle_event(ev, node, WHEEL_STEP);
565 self.dirty = true;
566 true
567 }
568
569 fn click(&mut self, x: f32, y: f32, ev: &UiEvent<'_>) -> bool {
570 self.mouse = Some((x, y));
571 self.hover = self.hit(x, y);
572 if self.feed_scrollers(ev, true) {
573 return true;
574 }
575 if let Some(i) = self.feed_text_boxes(ev) {
576 self.focus = Some(self.parts.text_boxes[i].path.clone());
577 self.focus_visible = false;
578 return true;
579 }
580 if let Some(path) = self.surface_at(x, y)
581 && self.to_surface(&path, x, y, |x, y| Pointer::Down { x, y })
582 {
583 self.captured = Some(path);
584 self.focus = None;
585 return true;
586 }
587 let Some(path) = self.hit(x, y) else {
588 self.focus = None;
589 return false;
590 };
591 self.pressed = Some(path.clone());
592 self.focus = self
593 .parts
594 .lists
595 .iter()
596 .filter(|l| l.scroller.is_some() && path.starts_with(&l.path))
597 .max_by_key(|l| l.path.len())
598 .map(|l| l.path.clone());
599 self.focus_visible = false;
600 self.activate(&path);
601 true
602 }
603
604 fn feed_scrollers(&mut self, ev: &UiEvent<'_>, first: bool) -> bool {
607 let root = &self.ui.layout_root;
608 let mut used = false;
609 for s in &mut self.parts.scrollers {
610 let Some(node) = layout::node_at(root, &with_root(&s.path)) else {
611 continue;
612 };
613 if s.state.handle_event(ev, node, WHEEL_STEP) {
614 used = true;
615 if first {
616 break;
617 }
618 }
619 }
620 self.dirty |= used;
621 used
622 }
623
624 fn feed_text_boxes(&mut self, ev: &UiEvent<'_>) -> Option<usize> {
626 let root = &self.ui.layout_root;
627 let mut taker = None;
628 for (i, b) in self.parts.text_boxes.iter_mut().enumerate() {
629 let Some(node) = layout::node_at(root, &with_root(&b.path)) else {
630 continue;
631 };
632 if b.state.handle_event(ev, node).consumed() {
633 taker = Some(i);
634 }
635 }
636 taker
637 }
638
639 fn surface_at(&self, x: f32, y: f32) -> Option<Vec<u16>> {
644 let p = layout::deepest_path(&self.ui.layout_root, x, y)?;
645 if p.first() != Some(&0) {
646 return None;
647 }
648 let mut p = p[1..].to_vec();
649 loop {
650 if let Some(w) = self.content().get_path(&p)
651 && matches!(w.kind, WidgetKind::Surface(_))
652 && w.enabled
653 && w.visible
654 {
655 return Some(p);
656 }
657 p.pop()?;
658 }
659 }
660
661 fn surface_box(
663 &self,
664 path: &[u16],
665 ) -> Option<(std::sync::Arc<dyn yog_api::ui::Surface>, Rect)> {
666 let w = self.content().get_path(path)?;
667 let WidgetKind::Surface(id) = &w.kind else {
668 return None;
669 };
670 let rect = self.node(path)?.rect.inset(w.effective_style().pad);
671 Some((surface::get(id)?, rect))
672 }
673
674 fn to_surface(
676 &self,
677 path: &[u16],
678 x: f32,
679 y: f32,
680 ev: impl FnOnce(f32, f32) -> Pointer,
681 ) -> bool {
682 match self.surface_box(path) {
683 Some((s, r)) => s.pointer(ev(x - r.x, y - r.y), (r.w, r.h)),
684 None => false,
685 }
686 }
687
688 fn leave_surface(&mut self) {
690 if let Some(path) = self.hovered_surface.take()
691 && let Some((s, r)) = self.surface_box(&path)
692 {
693 s.pointer(Pointer::Leave, (r.w, r.h));
694 }
695 }
696
697 fn activate(&mut self, path: &[u16]) {
699 let Some(w) = self.content().get_path(path) else {
700 return;
701 };
702 if !w.enabled || !w.visible {
703 return;
704 }
705 if let Some(a) = w.action {
706 if a.slot == Action::SELECT {
707 self.reveal = Some((a.owner, a.index));
708 }
709 self.dispatch(ViewEvent::Action(a));
710 }
711 }
712
713 fn focused_box(&self) -> Option<usize> {
714 self.parts
715 .text_boxes
716 .iter()
717 .position(|t| t.state.is_focused())
718 }
719
720 fn focused_list(&self) -> Option<&crate::ListPart> {
721 let f = self.focus.as_ref()?;
722 self.parts.lists.iter().find(|l| &l.path == f)
723 }
724
725 fn text_input(&mut self, ev: &UiEvent<'_>) -> bool {
727 let Some(i) = self.focused_box() else {
728 return false;
729 };
730 let b = &mut self.parts.text_boxes[i];
731 let Some(node) = layout::node_at(&self.ui.layout_root, &with_root(&b.path)) else {
732 return false;
733 };
734 let r = b.state.handle_event(ev, node);
735 let text = || self.parts.text_boxes[i].state.text().to_owned();
736 match r {
737 TextBoxEvent::Changed => {
738 let text = text();
739 self.dispatch(ViewEvent::TextChanged {
740 slot: i as u16,
741 text,
742 });
743 }
744 TextBoxEvent::Submitted => {
745 let text = text();
746 self.dispatch(ViewEvent::TextSubmitted {
747 slot: i as u16,
748 text,
749 });
750 }
751 _ => {}
752 }
753 if !self.parts.text_boxes[i].state.is_focused() {
754 self.focus = None;
755 }
756 r.consumed()
757 }
758
759 fn key(&mut self, key: i32, mods: i32, ev: &UiEvent<'_>) -> bool {
760 if key == keys::TAB {
761 self.cycle_focus(mods & keys::MOD_SHIFT != 0);
762 return true;
763 }
764 if self.focused_box().is_some() {
765 return self.text_input(ev);
766 }
767 match key {
768 keys::ESCAPE => {
769 if self.with_cx(|vm, _, cx| vm.on_back(cx)) {
770 return true;
771 }
772 if self.live && screen::go_back() {
773 self.log.push(LOG_BACK.into());
774 return true;
775 }
776 false
777 }
778 keys::ENTER | keys::KP_ENTER | keys::SPACE => {
779 let Some(p) = self.focus.clone() else {
780 return false;
781 };
782 if self
783 .content()
784 .get_path(&p)
785 .is_some_and(|w| w.is_interactive())
786 {
787 self.activate(&p);
788 return true;
789 }
790 false
791 }
792 keys::UP | keys::DOWN | keys::PAGE_UP | keys::PAGE_DOWN | keys::HOME | keys::END => {
793 self.list_key(key)
794 }
795 _ => false,
796 }
797 }
798
799 fn list_key(&mut self, key: i32) -> bool {
801 let list = self
802 .focused_list()
803 .or_else(|| self.parts.lists.iter().find(|l| l.scroller.is_some()));
804 let Some(list) = list.cloned() else {
805 return false;
806 };
807 if list.len == 0 {
808 return true;
809 }
810 let last = list.len - 1;
811 let page = match (list.item_height, list.scroller) {
812 (Some(ih), Some(s)) => {
813 let view_h = self
814 .parts
815 .scrollers
816 .get(s as usize)
817 .and_then(|s| self.node(&s.path))
818 .map_or(0.0, |n| n.rect.h);
819 ((view_h / ih.max(1.0)) as u32).max(1)
820 }
821 _ => FALLBACK_PAGE,
822 };
823 let new = match (key, list.selected) {
824 (keys::HOME, _) => 0,
825 (keys::END, _) => last,
826 (_, None) => 0,
827 (keys::UP, Some(i)) => i.saturating_sub(1),
828 (keys::DOWN, Some(i)) => (i + 1).min(last),
829 (keys::PAGE_UP, Some(i)) => i.saturating_sub(page),
830 (_, Some(i)) => (i + page).min(last),
831 };
832 if list.selected != Some(new) {
833 self.dispatch(ViewEvent::Select {
834 list: list.owner,
835 index: new,
836 });
837 }
838 self.reveal = Some((list.owner, new));
839 if self.focus.is_none() {
840 self.focus = Some(list.path.clone());
841 }
842 true
843 }
844
845 fn set_focus(&mut self, path: Option<Vec<u16>>, visible: bool) {
846 for t in &mut self.parts.text_boxes {
847 t.state.blur();
848 }
849 if let Some(p) = &path
850 && let Some(t) = self.parts.text_boxes.iter_mut().find(|t| &t.path == p)
851 {
852 t.state.focus();
853 }
854 self.focus = path;
855 self.focus_visible = visible;
856 }
857
858 fn cycle_focus(&mut self, back: bool) {
860 let n = self.parts.focusables.len();
861 if n == 0 {
862 return;
863 }
864 let cur = self
865 .focus
866 .as_ref()
867 .and_then(|f| self.parts.focusables.iter().position(|p| p == f));
868 for step in 1..=n {
869 let i = match (cur, back) {
870 (None, false) => step - 1,
871 (None, true) => n - step,
872 (Some(c), false) => (c + step) % n,
873 (Some(c), true) => (c + n - step % n) % n,
874 };
875 let p = &self.parts.focusables[i];
876 if self
877 .node(p)
878 .is_some_and(|n| n.visible && n.enabled && n.rect.w > 0.0)
879 {
880 let p = p.clone();
881 self.set_focus(Some(p), true);
882 return;
883 }
884 }
885 }
886}