Skip to main content

agg_gui/widget/
app.rs

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