Skip to main content

agg_gui/widget/
app.rs

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