Skip to main content

yog_kit/
host.rs

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