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