Skip to main content

agg_gui/widget/
app.rs

1use super::*;
2
3/// Collect all focusable widgets in paint order (DFS root → leaves).
4/// Returns their paths as `Vec<Vec<usize>>`.
5fn collect_focusable(
6    widget: &dyn Widget,
7    current_path: &mut Vec<usize>,
8    out: &mut Vec<Vec<usize>>,
9) {
10    if widget.is_focusable() {
11        out.push(current_path.clone());
12    }
13    for (i, child) in widget.children().iter().enumerate() {
14        current_path.push(i);
15        collect_focusable(child.as_ref(), current_path, out);
16        current_path.pop();
17    }
18}
19
20/// Get a mutable reference to the widget at the given path.
21fn widget_at_path<'a>(root: &'a mut Box<dyn Widget>, path: &[usize]) -> &'a mut dyn Widget {
22    if path.is_empty() {
23        return root.as_mut();
24    }
25    let idx = path[0];
26    widget_at_path(&mut root.children_mut()[idx], &path[1..])
27}
28
29fn widget_at_path_ref<'a>(root: &'a dyn Widget, path: &[usize]) -> &'a dyn Widget {
30    if path.is_empty() {
31        return root;
32    }
33    let idx = path[0];
34    widget_at_path_ref(root.children()[idx].as_ref(), &path[1..])
35}
36
37// ---------------------------------------------------------------------------
38// App — top-level owner of the widget tree
39// ---------------------------------------------------------------------------
40
41/// Owns the widget tree, handles focus, and converts OS events to Y-up coords.
42///
43/// Create with [`App::new`], call [`App::layout`] every frame before
44/// [`App::paint`], and feed OS events through the `on_*` methods.
45pub struct App {
46    root: Box<dyn Widget>,
47    /// Current focus path (indices from root into children vec).
48    /// `None` means no widget has focus.
49    focus: Option<Vec<usize>>,
50    /// Path to the widget last seen under the cursor (for hover clearing).
51    hovered: Option<Vec<usize>>,
52    /// Mouse-captured widget path. Set when a widget consumes `MouseDown`;
53    /// cleared on `MouseUp`. While set, `MouseMove` events go to the captured
54    /// widget regardless of cursor position — enabling slider drag-outside-bounds.
55    captured: Option<Vec<usize>>,
56    /// Viewport height in pixels — used for Y-down → Y-up conversion.
57    viewport_height: f64,
58    /// Viewport size in logical pixels from the most recent layout pass.
59    viewport_size: Size,
60    /// Optional legacy key handler called after widget-tree dispatch.
61    /// Returns `true` if the key was handled.
62    global_key_handler: Option<Box<dyn FnMut(Key, Modifiers) -> bool>>,
63    /// Multi-touch gesture recogniser.  Platform shells feed raw touches
64    /// through [`App::on_touch_start/move/end/cancel`]; widgets read the
65    /// per-frame aggregate via [`crate::current_multi_touch`].
66    touch_state: crate::touch_state::TouchState,
67    /// Last `async_state_epoch` `App::paint` observed.  At the top of
68    /// each paint, if the current epoch differs we explicitly mark
69    /// every widget dirty via `mark_subtree_dirty`, so a freshly-
70    /// loaded image (or any other async result that landed outside
71    /// the event-dispatch dirty-propagation path) lands in newly-
72    /// rasterised retained backbuffers, not the previous frame's
73    /// stale FBO contents.
74    last_async_state_epoch: u64,
75}
76
77impl App {
78    /// Create a new `App` with `root` as the root widget.
79    pub fn new(root: Box<dyn Widget>) -> Self {
80        Self {
81            root,
82            focus: None,
83            hovered: None,
84            captured: None,
85            viewport_height: 1.0,
86            viewport_size: Size::new(1.0, 1.0),
87            global_key_handler: None,
88            touch_state: crate::touch_state::TouchState::new(),
89            last_async_state_epoch: 0,
90        }
91    }
92
93    /// Access the root widget — used by tests and inspectors that need to
94    /// introspect the laid-out tree without re-routing events through the
95    /// full dispatch machinery.  Pair with [`find_widget_by_id`] to locate
96    /// a specific widget by its `Widget::id()` (e.g. a Window's title).
97    pub fn root(&self) -> &dyn Widget {
98        self.root.as_ref()
99    }
100
101    /// Mutable counterpart to [`root`].  Required when a test wants to
102    /// drive a specific sub-widget directly (e.g. reading ScrollView
103    /// scroll offset) after the App has routed an event.
104    pub fn root_mut(&mut self) -> &mut dyn Widget {
105        self.root.as_mut()
106    }
107
108    /// Return the type name of the currently focused widget, if any.
109    pub fn focused_widget_type_name(&self) -> Option<&'static str> {
110        self.focus
111            .as_deref()
112            .map(|path| widget_at_path_ref(self.root.as_ref(), path).type_name())
113    }
114
115    /// Register a legacy global key handler invoked only after the widget tree
116    /// has ignored the key. Prefer widget-owned key handling for new behavior.
117    ///
118    /// # Example
119    /// ```ignore
120    /// app.set_global_key_handler(|key, mods| {
121    ///     if mods.ctrl && mods.shift && key == Key::O {
122    ///         organize_windows();
123    ///         return true;
124    ///     }
125    ///     false
126    /// });
127    /// ```
128    pub fn set_global_key_handler(
129        &mut self,
130        handler: impl FnMut(Key, Modifiers) -> bool + 'static,
131    ) {
132        self.global_key_handler = Some(Box::new(handler));
133    }
134
135    /// Lay out the widget tree to fill `viewport`.  `viewport` is in **physical
136    /// pixels** (e.g. `window.inner_size()` on native, `canvas.width/height` on
137    /// wasm); this method divides by the current device scale factor so the
138    /// widget tree lays out in logical (device-independent) units.  Call once
139    /// per frame before [`paint`][Self::paint].
140    pub fn layout(&mut self, viewport: Size) {
141        let scale = crate::device_scale::device_scale().max(1e-6);
142        let logical = Size::new(viewport.width / scale, viewport.height / scale);
143        self.viewport_height = logical.height;
144        self.viewport_size = logical;
145        set_current_viewport(logical);
146        self.root
147            .set_bounds(Rect::new(0.0, 0.0, logical.width, logical.height));
148        self.root.layout(logical);
149    }
150
151    /// Paint the entire widget tree into `ctx`. Call after [`layout`][Self::layout].
152    ///
153    /// Applies a `ctx.scale(dps, dps)` transform up-front so the whole tree —
154    /// widget dimensions, font sizes, margins — is rendered at physical pixel
155    /// density on HiDPI screens without any widget having to know about DPI.
156    ///
157    /// Also clears the immediate draw flag so widgets can re-request it during
158    /// this paint if they need another frame; hosts read [`wants_draw`]
159    /// after `paint` returns to decide whether to schedule continuous draws.
160    pub fn paint(&mut self, ctx: &mut dyn DrawCtx) {
161        crate::animation::clear_draw_request();
162        // Async-state dirty walk: an image load (or other async source)
163        // that finished outside event dispatch bumped
164        // `async_state_epoch`.  Walk the whole tree and mark every
165        // widget dirty so retained backbuffers re-rasterise on this
166        // frame — without this, the freshly-decoded pixels would land
167        // inside a Window FBO whose cache check sees no other change
168        // and composites the previous frame's stale bitmap.  The
169        // explicit walk replaces a brittle "compare an extra epoch
170        // inside every cache" mechanism with a single deterministic
171        // hook at the start of paint.
172        let async_epoch = crate::animation::async_state_epoch();
173        if async_epoch != self.last_async_state_epoch {
174            tree::mark_subtree_dirty(self.root.as_mut());
175            self.last_async_state_epoch = async_epoch;
176        }
177        let viewport = self.viewport_size;
178        crate::widgets::combo_box::begin_combo_popup_frame(viewport);
179        crate::widgets::tooltip::begin_tooltip_frame();
180        // Recompute the multi-touch aggregate once per paint and publish
181        // to the thread-local — widgets read it during `on_event` or
182        // `paint` without an explicit `&App` reference.
183        self.touch_state.update_gesture();
184        crate::touch_state::set_current(self.touch_state.current());
185        let scale = crate::device_scale::device_scale();
186        if (scale - 1.0).abs() > 1e-6 {
187            ctx.save();
188            ctx.scale(scale, scale);
189            paint_subtree(self.root.as_mut(), ctx);
190            crate::widgets::combo_box::paint_global_combo_popups(ctx);
191            crate::widgets::tooltip::paint_global_tooltips(ctx, viewport);
192            paint_global_overlays(self.root.as_mut(), ctx);
193            // Modal/global overlays can contain ComboBox widgets. They submit
194            // their popups while `paint_global_overlays` runs, so drain once
195            // more to draw those popups above the modal body.
196            crate::widgets::combo_box::paint_global_combo_popups(ctx);
197            ctx.restore();
198        } else {
199            paint_subtree(self.root.as_mut(), ctx);
200            crate::widgets::combo_box::paint_global_combo_popups(ctx);
201            crate::widgets::tooltip::paint_global_tooltips(ctx, viewport);
202            paint_global_overlays(self.root.as_mut(), ctx);
203            crate::widgets::combo_box::paint_global_combo_popups(ctx);
204        }
205    }
206
207    /// After a paint pass, returns `true` if any widget requested another frame
208    /// (e.g. an in-progress hover animation).  Hosts should use this to set
209    /// their event-loop control flow to continuous polling while it's `true`.
210    ///
211    /// Combines the visibility-gated tree-walk signal ([`Widget::needs_draw`])
212    /// with the immediate draw request flag ([`crate::animation::wants_draw`]).
213    /// Widgets call `request_draw` for ordinary visual invalidation; scheduled
214    /// draw needs such as cursor blink should use `needs_draw` /
215    /// `next_draw_deadline` so hidden subtrees do not keep the loop awake.
216    pub fn wants_draw(&self) -> bool {
217        self.root.needs_draw() || crate::animation::wants_draw()
218    }
219
220    /// Earliest scheduled draw deadline across the visible widget tree.
221    /// Hosts translate `Some(t)` into `ControlFlow::WaitUntil(t)` so that
222    /// e.g. a text field's cursor blink wakes the loop exactly at the flip
223    /// boundary.  Invisible subtrees contribute nothing.
224    pub fn next_draw_deadline(&self) -> Option<web_time::Instant> {
225        self.root.next_draw_deadline()
226    }
227
228    // --- Platform event ingestion ---
229    //
230    // Hosts pass raw physical-pixel coordinates (e.g. `e.clientX * devicePixelRatio`
231    // in wasm, or `WindowEvent::CursorMoved.position` on native).  These methods
232    // divide by the current device scale factor and flip Y so widget code sees
233    // logical Y-up coordinates matching the layout pass.
234
235    /// Mouse cursor moved. `screen_y` is Y-down physical pixels.
236    pub fn on_mouse_move(&mut self, screen_x: f64, screen_y: f64) {
237        // Reset cursor so the hovered widget can set it; Default if nothing sets it.
238        crate::cursor::reset_cursor_icon();
239        let pos = self.flip_y(screen_x, screen_y);
240        set_current_mouse_world(pos);
241        if let Some(path) = active_modal_path(self.root.as_ref()) {
242            let event = Event::MouseMove { pos };
243            dispatch_event(&mut self.root, &path, &event, pos);
244            self.hovered = Some(path);
245            return;
246        }
247        self.dispatch_mouse_move(pos);
248    }
249
250    /// Mouse button pressed. `screen_y` is Y-down physical pixels.
251    pub fn on_mouse_down(
252        &mut self,
253        screen_x: f64,
254        screen_y: f64,
255        button: MouseButton,
256        mods: Modifiers,
257    ) {
258        let pos = self.flip_y(screen_x, screen_y);
259        set_current_mouse_world(pos);
260        let modal_path = active_modal_path(self.root.as_ref());
261        let event = Event::MouseDown {
262            pos,
263            button,
264            modifiers: mods,
265        };
266        if let Some(path) = modal_path {
267            self.set_focus(None);
268            if dispatch_event(&mut self.root, &path, &event, pos) == EventResult::Consumed {
269                self.captured = Some(path);
270            }
271            return;
272        }
273        let hit = self.compute_hit(pos);
274
275        // Click-to-focus: if the hit widget is focusable, give it focus.
276        if let Some(ref path) = hit {
277            let w = widget_at_path(&mut self.root, path);
278            if w.is_focusable() {
279                self.set_focus(Some(path.clone()));
280            } else {
281                self.set_focus(None);
282            }
283        } else {
284            self.set_focus(None);
285        }
286
287        if let Some(mut path) = hit {
288            let result = dispatch_event(&mut self.root, &path, &event, pos);
289            if result == EventResult::Consumed {
290                self.maybe_bring_to_front(&mut path);
291                let capture_path = self.compute_hit(pos).unwrap_or(path);
292                self.captured = Some(capture_path);
293            }
294        }
295        // NO blanket request_draw.  Mouse-down on an inert area must not
296        // cause a repaint.  Each widget that changes visual state in
297        // response to a MouseDown (button press, window raise, focus
298        // indicator on the focus-gained widget, etc.) is responsible for
299        // calling `crate::animation::request_draw` itself.
300    }
301
302    /// Mouse button released. `screen_y` is Y-down.
303    pub fn on_mouse_up(
304        &mut self,
305        screen_x: f64,
306        screen_y: f64,
307        button: MouseButton,
308        mods: Modifiers,
309    ) {
310        let pos = self.flip_y(screen_x, screen_y);
311        set_current_mouse_world(pos);
312        let event = Event::MouseUp {
313            pos,
314            button,
315            modifiers: mods,
316        };
317        if let Some(path) = active_modal_path(self.root.as_ref()) {
318            self.captured = None;
319            dispatch_event(&mut self.root, &path, &event, pos);
320            return;
321        }
322        // Deliver release to captured widget first (if any), then clear capture.
323        if let Some(path) = self.captured.take() {
324            dispatch_event(&mut self.root, &path, &event, pos);
325        } else {
326            let hit = self.compute_hit(pos);
327            if let Some(path) = hit {
328                dispatch_event(&mut self.root, &path, &event, pos);
329            }
330        }
331    }
332
333    /// Key pressed. Delivered to the focused widget first, then to the visible
334    /// widget tree as an unconsumed key if focus ignores it.
335    pub fn on_key_down(&mut self, key: Key, mods: Modifiers) {
336        if key == Key::Tab {
337            self.advance_focus(!mods.shift);
338            return;
339        }
340        let event = Event::KeyDown {
341            key: key.clone(),
342            modifiers: mods,
343        };
344        let result = if let Some(path) = active_modal_path(self.root.as_ref()) {
345            dispatch_event(&mut self.root, &path, &event, Point::ORIGIN)
346        } else if let Some(path) = self.focus.clone() {
347            dispatch_event(&mut self.root, &path, &event, Point::ORIGIN)
348        } else {
349            EventResult::Ignored
350        };
351        if result != EventResult::Consumed {
352            let result = dispatch_unconsumed_key(self.root.as_mut(), &key, mods);
353            if result != EventResult::Consumed {
354                if let Some(ref mut handler) = self.global_key_handler {
355                    handler(key, mods);
356                }
357            }
358        }
359    }
360
361    /// Key released. Delivered to the focused widget.
362    pub fn on_key_up(&mut self, key: Key, mods: Modifiers) {
363        let event = Event::KeyUp {
364            key,
365            modifiers: mods,
366        };
367        if let Some(path) = self.focus.clone() {
368            dispatch_event(&mut self.root, &path, &event, Point::ORIGIN);
369        }
370    }
371
372    /// Mouse wheel scrolled. `screen_y` is Y-down. `delta_y` positive = scroll up.
373    /// `delta_x` positive = content moves right.
374    pub fn on_mouse_wheel(&mut self, screen_x: f64, screen_y: f64, delta_y: f64) {
375        self.on_mouse_wheel_xy_mods(screen_x, screen_y, 0.0, delta_y, Modifiers::default());
376    }
377
378    /// Mouse wheel with an explicit horizontal component (trackpad pan,
379    /// shift+wheel via the platform harness).
380    pub fn on_mouse_wheel_xy(&mut self, screen_x: f64, screen_y: f64, delta_x: f64, delta_y: f64) {
381        self.on_mouse_wheel_xy_mods(screen_x, screen_y, delta_x, delta_y, Modifiers::default());
382    }
383
384    /// Mouse wheel with explicit horizontal component and modifier state.
385    pub fn on_mouse_wheel_xy_mods(
386        &mut self,
387        screen_x: f64,
388        screen_y: f64,
389        delta_x: f64,
390        delta_y: f64,
391        modifiers: Modifiers,
392    ) {
393        let pos = self.flip_y(screen_x, screen_y);
394        set_current_mouse_world(pos);
395        let hit = active_modal_path(self.root.as_ref()).or_else(|| self.compute_hit(pos));
396        let event = Event::MouseWheel {
397            pos,
398            delta_y,
399            delta_x,
400            modifiers,
401        };
402        if let Some(path) = hit {
403            dispatch_event(&mut self.root, &path, &event, pos);
404        }
405    }
406
407    /// Snapshot the entire widget tree for the inspector.
408    pub fn collect_inspector_nodes(&self) -> Vec<InspectorNode> {
409        let mut out = Vec::new();
410        collect_inspector_nodes(self.root.as_ref(), 0, Point::ORIGIN, &mut out);
411        out
412    }
413
414    /// `true` while a widget is actively capturing the pointer — i.e. the
415    /// user is mid-drag (a window edge, slider thumb, scrollbar, etc.).
416    /// Used by the demo harness to throttle expensive per-frame snapshots
417    /// (the inspector tree walk) during interactions; the snapshot can
418    /// safely defer until the user releases without changing the visible
419    /// outcome (the underlying widget tree topology doesn't change during
420    /// a drag, only the widgets' bounds).
421    pub fn has_captured_pointer(&self) -> bool {
422        self.captured.is_some()
423    }
424
425    /// Serialize the widget tree — types, bounds, depth, properties — as JSON.
426    ///
427    /// Produces a flat array of nodes in paint-order DFS.  Suitable for writing
428    /// to a file and diffing between runs to verify layout stability.  Used by
429    /// the demo harness's debug hotkey.
430    pub fn dump_tree_json(&self) -> String {
431        let nodes = self.collect_inspector_nodes();
432        let mut s = String::from("[\n");
433        for (i, n) in nodes.iter().enumerate() {
434            let props_json = n
435                .properties
436                .iter()
437                .map(|(k, v)| format!("{:?}: {:?}", k, v))
438                .collect::<Vec<_>>()
439                .join(", ");
440            s.push_str(&format!(
441                "  {{\"type\":{:?},\"depth\":{},\"x\":{:.2},\"y\":{:.2},\"w\":{:.2},\"h\":{:.2},\"props\":{{{}}}}}",
442                n.type_name, n.depth,
443                n.screen_bounds.x, n.screen_bounds.y,
444                n.screen_bounds.width, n.screen_bounds.height,
445                props_json,
446            ));
447            if i + 1 < nodes.len() {
448                s.push(',');
449            }
450            s.push('\n');
451        }
452        s.push(']');
453        s
454    }
455
456    /// Returns `true` if any widget currently holds keyboard focus.
457    /// Used by the render loop to schedule cursor-blink repaints.
458    pub fn has_focus(&self) -> bool {
459        self.focus.is_some()
460    }
461
462    /// Call when the cursor leaves the window to clear hover state.
463    pub fn on_mouse_leave(&mut self) {
464        crate::cursor::reset_cursor_icon();
465        self.dispatch_mouse_move(Point::new(-1.0, -1.0));
466    }
467
468    // --- Touch ingestion ---
469    //
470    // Raw touches go into the multi-touch gesture recogniser; widgets
471    // read `current_multi_touch()` each frame.  Platform shells ALSO
472    // route the first finger through the existing `on_mouse_*` entry
473    // points so widgets that only understand mouse input keep working
474    // without changes.  Coordinates are the same physical-pixel Y-down
475    // units the mouse entry points accept.
476    pub fn on_touch_start(
477        &mut self,
478        device: crate::touch_state::TouchDeviceId,
479        id: crate::touch_state::TouchId,
480        screen_x: f64,
481        screen_y: f64,
482        force: Option<f32>,
483    ) {
484        let pos = self.flip_y(screen_x, screen_y);
485        self.touch_state.on_start(device, id, pos, force);
486        crate::touch_state::note_touch_event();
487    }
488    pub fn on_touch_move(
489        &mut self,
490        device: crate::touch_state::TouchDeviceId,
491        id: crate::touch_state::TouchId,
492        screen_x: f64,
493        screen_y: f64,
494        force: Option<f32>,
495    ) {
496        let pos = self.flip_y(screen_x, screen_y);
497        self.touch_state.on_move(device, id, pos, force);
498        crate::touch_state::note_touch_event();
499    }
500    pub fn on_touch_end(
501        &mut self,
502        device: crate::touch_state::TouchDeviceId,
503        id: crate::touch_state::TouchId,
504    ) {
505        self.touch_state.on_end_or_cancel(device, id);
506        crate::touch_state::note_touch_event();
507    }
508    pub fn on_touch_cancel(
509        &mut self,
510        device: crate::touch_state::TouchDeviceId,
511        id: crate::touch_state::TouchId,
512    ) {
513        self.touch_state.on_end_or_cancel(device, id);
514        crate::touch_state::note_touch_event();
515    }
516    /// Current number of fingers down across all devices.  Used by
517    /// widgets that want to know the gesture has *begun* before the
518    /// first frame has had a chance to produce a delta (where
519    /// `current_multi_touch()` may still be `None`).
520    pub fn active_touch_count(&self) -> usize {
521        self.touch_state.active_count()
522    }
523
524    // --- Private helpers ---
525
526    /// If the click path passes through a `Window` widget, move that window to
527    /// the end of its parent's children list so it paints on top of siblings.
528    /// All stored paths (focus, hovered, captured, plus the clicked path itself)
529    /// are updated to reflect the new index.
530    fn maybe_bring_to_front(&mut self, clicked_path: &mut Vec<usize>) {
531        // Walk the clicked path and record the deepest Window encountered.
532        // At each step we descend into children[idx]; after descending, if the
533        // new node is a Window we record (parent_path, win_idx).  We keep
534        // scanning so a nested Window (unlikely but possible) wins.
535        let mut node: &dyn Widget = self.root.as_ref();
536        let mut window_info: Option<(Vec<usize>, usize)> = None; // (parent_path, win_idx)
537        for (depth, &idx) in clicked_path.iter().enumerate() {
538            let children = node.children();
539            if idx >= children.len() {
540                break;
541            }
542            node = &*children[idx];
543            if node.type_name() == "Window" {
544                // parent_path = clicked_path[..depth], win_idx = idx
545                window_info = Some((clicked_path[..depth].to_vec(), idx));
546            }
547        }
548
549        let (parent_path, win_idx) = match window_info {
550            Some(x) => x,
551            None => return,
552        };
553
554        // Check there's actually a sibling to leapfrog.
555        let n = {
556            let parent = widget_at_path(&mut self.root, &parent_path);
557            parent.children().len()
558        };
559        if win_idx >= n - 1 {
560            return;
561        } // already at front
562
563        // Move the window to the end of its parent's children (mutable pass).
564        {
565            let parent = widget_at_path(&mut self.root, &parent_path);
566            let child = parent.children_mut().remove(win_idx);
567            parent.children_mut().push(child);
568        }
569        let new_idx = n - 1;
570        let depth = parent_path.len(); // depth at which the window index sits
571
572        // Update any stored path whose element at `depth` was affected by the move.
573        fn shift_path(p: &mut Vec<usize>, depth: usize, old: usize, new: usize) {
574            if p.len() > depth {
575                let i = p[depth];
576                if i == old {
577                    p[depth] = new;
578                } else if i > old && i <= new {
579                    // Siblings that were after the removed window shift left by 1.
580                    p[depth] -= 1;
581                }
582            }
583        }
584        shift_path(clicked_path, depth, win_idx, new_idx);
585        if let Some(ref mut p) = self.focus {
586            shift_path(p, depth, win_idx, new_idx);
587        }
588        if let Some(ref mut p) = self.hovered {
589            shift_path(p, depth, win_idx, new_idx);
590        }
591        if let Some(ref mut p) = self.captured {
592            shift_path(p, depth, win_idx, new_idx);
593        }
594    }
595
596    #[inline]
597    /// Convert a platform-supplied physical Y-down coordinate into the
598    /// logical Y-up space the widget tree works in.  Divides by the current
599    /// device scale factor (so mouse coords line up with the scaled paint
600    /// transform) and flips Y against the cached logical viewport height.
601    fn flip_y(&self, x: f64, y_down: f64) -> Point {
602        let scale = crate::device_scale::device_scale().max(1e-6);
603        let lx = x / scale;
604        let ly_down = y_down / scale;
605        Point::new(lx, self.viewport_height - ly_down)
606    }
607
608    fn compute_hit(&self, pos: Point) -> Option<Vec<usize>> {
609        global_overlay_hit_path(self.root.as_ref(), pos)
610            .or_else(|| hit_test_subtree(self.root.as_ref(), pos))
611    }
612
613    fn dispatch_mouse_move(&mut self, pos: Point) {
614        let new_hit = self.compute_hit(pos);
615
616        // If the hovered widget changed, clear the old one — but skip the clear
617        // event when the old widget still has mouse capture (it should keep
618        // receiving real positions, not a (-1,-1) sentinel that snaps state).
619        if new_hit != self.hovered {
620            if let Some(old_path) = self.hovered.take() {
621                let is_captured = self.captured.as_ref() == Some(&old_path);
622                if !is_captured {
623                    let clear = Event::MouseMove {
624                        pos: Point::new(-1.0, -1.0),
625                    };
626                    dispatch_event(&mut self.root, &old_path, &clear, Point::new(-1.0, -1.0));
627                }
628            }
629            self.hovered = new_hit.clone();
630        }
631
632        let event = Event::MouseMove { pos };
633        if let Some(ref cap_path) = self.captured.clone() {
634            // Captured widget always receives the real position, regardless of
635            // whether the cursor is over it — this is what keeps a slider
636            // tracking the cursor when dragged outside its bounds.
637            dispatch_event(&mut self.root, cap_path, &event, pos);
638        } else if let Some(path) = new_hit {
639            dispatch_event(&mut self.root, &path, &event, pos);
640        }
641    }
642
643    /// Set focus to `new_path`, sending `FocusLost` / `FocusGained` as needed.
644    fn set_focus(&mut self, new_path: Option<Vec<usize>>) {
645        if self.focus == new_path {
646            return;
647        }
648        if let Some(old) = self.focus.take() {
649            dispatch_event(&mut self.root, &old, &Event::FocusLost, Point::ORIGIN);
650        }
651        self.focus = new_path.clone();
652        if let Some(new) = new_path {
653            dispatch_event(&mut self.root, &new, &Event::FocusGained, Point::ORIGIN);
654        }
655    }
656
657    /// Move focus to the next (or previous) focusable widget in paint order.
658    fn advance_focus(&mut self, forward: bool) {
659        let mut all: Vec<Vec<usize>> = Vec::new();
660        collect_focusable(self.root.as_ref(), &mut vec![], &mut all);
661        if all.is_empty() {
662            return;
663        }
664        let current_idx = self
665            .focus
666            .as_ref()
667            .and_then(|f| all.iter().position(|p| p == f));
668        let next_idx = match current_idx {
669            None => {
670                if forward {
671                    0
672                } else {
673                    all.len() - 1
674                }
675            }
676            Some(i) => {
677                if forward {
678                    (i + 1) % all.len()
679                } else {
680                    if i == 0 {
681                        all.len() - 1
682                    } else {
683                        i - 1
684                    }
685                }
686            }
687        };
688        let next_path = all[next_idx].clone();
689        self.set_focus(Some(next_path));
690    }
691}