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    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
16/// Pixels per mouse-wheel notch.
17const WHEEL_STEP: f32 = 16.0;
18/// Rows PgUp/PgDn move in a list whose row height or viewport is unknown.
19const FALLBACK_PAGE: u32 = 5;
20/// [`Host::log`] entries for a close / back request.
21pub(crate) const LOG_CLOSE: &str = "close";
22const LOG_BACK: &str = "back";
23
24/// A scroller's visible window during an update.
25#[derive(Debug, Clone, Copy, Default, PartialEq)]
26pub struct Viewport {
27    /// Scroll offset (0 = top).
28    pub offset: f32,
29    /// Height of the scroller in the last layout (0 before the first).
30    pub height: f32,
31}
32
33/// What a [`View::update`] changed, plus what it can ask of the host.
34#[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    /// A standalone update (no scrollers) — for tests of generated views.
46    pub fn new() -> Self {
47        Self::default()
48    }
49    /// A change that may move or resize widgets.
50    pub fn relayout(&mut self) {
51        self.relayout = true;
52    }
53    /// A paint-only change.
54    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    /// The visible window of scroller `i`.
64    pub fn viewport(&self, scroller: u16) -> Viewport {
65        self.viewports
66            .get(scroller as usize)
67            .copied()
68            .unwrap_or_default()
69    }
70    /// Sets text box `slot`'s text (kept if it's already that, so the caret
71    /// doesn't jump while typing).
72    pub fn set_text_box(&mut self, slot: u16, text: &str) {
73        self.text_sets.push((slot, text.to_owned()));
74    }
75    /// Scrolls scroller `i` back to the top.
76    pub fn reset_scroll(&mut self, scroller: u16) {
77        self.scroll_resets.push(scroller);
78        self.relayout();
79    }
80    /// Records list `owner`'s item count and selected index.
81    pub fn list_info(&mut self, owner: u16, len: u32, selected: Option<u32>) {
82        self.lists.push((owner, len, selected));
83    }
84}
85
86/// Child-index path of the first widget tagged `id`.
87pub(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    /// Root = a full-screen panel whose only child is the view's content.
110    pub(crate) ui: UiRoot,
111    pub(crate) parts: Parts,
112    pub(crate) size: (f32, f32),
113    pub(crate) dirty: bool,
114    /// Content-relative paths.
115    hover: Option<Vec<u16>>,
116    pressed: Option<Vec<u16>>,
117    focus: Option<Vec<u16>>,
118    /// The focus came from the keyboard (draw its outline).
119    focus_visible: bool,
120    outlined: Option<Vec<u16>>,
121    mouse: Option<(f32, f32)>,
122    /// The surface a button went down on (it gets the drags and the release).
123    captured: Option<Vec<u16>>,
124    /// The surface under the cursor (it gets `Move`s, then a `Leave`).
125    hovered_surface: Option<Vec<u16>>,
126    opened: bool,
127    reveal: Option<(u16, u32)>,
128    /// Viewports the last update saw.
129    seen_viewports: Vec<Viewport>,
130    pub(crate) handle: ScreenHandle<W::Vm>,
131    /// What the screen asked for (close, navigate, ...), for tests.
132    pub(crate) log: Vec<String>,
133    /// Calls `yog_api` for close/navigation (off in tests).
134    pub(crate) live: bool,
135    /// Re-evaluate the bindings every frame: an inventory screen's slots
136    /// change by drag-and-drop, which runs no command.
137    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    /// Content path of the clickable widget at `(x, y)`.
182    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    // ── view model calls ────────────────────────────────────────────────────
188
189    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    // ── frame ───────────────────────────────────────────────────────────────
266
267    /// Brings the tree up to date and lays it out for a `sw` × `sh` GUI.
268    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        // A keyboard reveal can scroll a virtualized list, which needs a
273        // second update for the rows that came into view.
274        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    /// A virtualized list's scroller changed size since the last update
299    /// (e.g. the first layout) — its window must be recomputed.
300    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    /// Scrolls a list so the item asked for is visible; `true` when the
407    /// offset changed.
408    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    /// The tooltip of what's under the cursor, drawn on top.
451    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    // ── input ───────────────────────────────────────────────────────────────
460
461    /// Handles one raw UI event; `true` = consumed.
462    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        // A surface under the cursor gets the wheel first.
531        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    /// Feeds a pointer event to the scrollers (stopping at the first taker
605    /// when `first`); true if any used it.
606    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    /// Feeds a pointer event to every text box; the last one that took it.
625    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    // ── surfaces ────────────────────────────────────────────────────────────
640
641    /// Content path of the enabled surface under `(x, y)`: the deepest node
642    /// there or its nearest surface ancestor.
643    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    /// The surface at `path` and its content box (rect less padding).
662    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    /// Hands the surface at `path` a pointer event at screen `(x, y)`.
675    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    /// Tells the hovered surface the cursor left it.
689    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    /// Runs the action of the widget at `path`.
698    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    /// Feeds a key/char event to the focused text box.
726    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    /// Up/Down/PgUp/PgDn/Home/End in the focused list (else the first).
800    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    /// Tab / Shift+Tab through the visible, enabled focusables.
859    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}