Skip to main content

agg_gui/widget/
app.rs

1use super::*;
2
3mod gesture;
4mod keyboard;
5mod pointer;
6mod touch;
7mod tree_paths;
8use tree_paths::{collect_focusable, widget_at_path, widget_at_path_ref};
9
10// ---------------------------------------------------------------------------
11// App — top-level owner of the widget tree
12// ---------------------------------------------------------------------------
13
14/// Owns the widget tree, handles focus, and converts OS events to Y-up coords.
15///
16/// Create with [`App::new`], call [`App::layout`] every frame before
17/// [`App::paint`], and feed OS events through the `on_*` methods.
18pub struct App {
19    root: Box<dyn Widget>,
20    /// Current focus path (indices from root into children vec).
21    /// `None` means no widget has focus.
22    focus: Option<Vec<usize>>,
23    /// Path to the widget last seen under the cursor (for hover clearing).
24    hovered: Option<Vec<usize>>,
25    /// Mouse-captured widget path. Set when a widget consumes `MouseDown`;
26    /// cleared on `MouseUp`. While set, `MouseMove` events go to the captured
27    /// widget regardless of cursor position — enabling slider drag-outside-bounds.
28    captured: Option<Vec<usize>>,
29    /// Gesture-captured widget path. Set on the frame a multi-touch gesture
30    /// begins if the widget under the initial centroid consumes
31    /// `Event::MultiTouch`; the whole gesture then routes there (capture
32    /// semantics) even as the centroid drifts. Cleared when the aggregate
33    /// returns to `None`. See [`super::app::gesture`].
34    gesture_captured: Option<Vec<usize>>,
35    /// Whether last frame's aggregate was `Some` — lets the gesture router
36    /// detect the `None`→`Some` start edge (when it hit-tests) versus an
37    /// ongoing gesture (when it delivers to the captured path only).
38    gesture_in_progress: bool,
39    /// Viewport height in pixels — used for Y-down → Y-up conversion.
40    viewport_height: f64,
41    /// Viewport size in logical pixels from the most recent layout pass.
42    viewport_size: Size,
43    /// Optional legacy key handler called after widget-tree dispatch.
44    /// Returns `true` if the key was handled.
45    global_key_handler: Option<Box<dyn FnMut(Key, Modifiers) -> bool>>,
46    /// Multi-touch gesture recogniser.  Platform shells feed raw touches
47    /// through [`App::on_touch_start/move/end/cancel`]; widgets read the
48    /// per-frame aggregate via [`crate::current_multi_touch`].
49    touch_state: crate::touch_state::TouchState,
50    /// Primary-finger mouse emulation (tap = left click, drag =
51    /// middle-drag pan).  Fed by the same `on_touch_*` entry points;
52    /// its commands are replayed through `on_mouse_*` so shells only
53    /// ever forward raw touches.  See [`crate::touch_emulation`].
54    touch_mouse_emu: crate::touch_emulation::TouchMouseEmu,
55    /// Last `async_state_epoch` `App::paint` observed.  At the top of
56    /// each paint, if the current epoch differs we explicitly mark
57    /// every widget dirty via `mark_subtree_dirty`, so a freshly-
58    /// loaded image (or any other async result that landed outside
59    /// the event-dispatch dirty-propagation path) lands in newly-
60    /// rasterised retained backbuffers, not the previous frame's
61    /// stale FBO contents.
62    last_async_state_epoch: u64,
63    /// `async_state_epoch` snapshot taken at the start of the most recent
64    /// [`App::layout`] (`None` until the first layout).  When
65    /// [`App::paint`] observes a newer epoch, the async change landed after
66    /// this frame's layout, so paint requests one follow-up frame to
67    /// re-lay-out against it.
68    layout_async_state_epoch: Option<u64>,
69}
70
71impl App {
72    /// Create a new `App` with `root` as the root widget.
73    pub fn new(root: Box<dyn Widget>) -> Self {
74        Self {
75            root,
76            focus: None,
77            hovered: None,
78            captured: None,
79            gesture_captured: None,
80            gesture_in_progress: false,
81            viewport_height: 1.0,
82            viewport_size: Size::new(1.0, 1.0),
83            global_key_handler: None,
84            touch_state: crate::touch_state::TouchState::new(),
85            touch_mouse_emu: crate::touch_emulation::TouchMouseEmu::new(),
86            last_async_state_epoch: 0,
87            layout_async_state_epoch: None,
88        }
89    }
90
91    /// Access the root widget — used by tests and inspectors that need to
92    /// introspect the laid-out tree without re-routing events through the
93    /// full dispatch machinery.  Pair with [`find_widget_by_id`] to locate
94    /// a specific widget by its `Widget::id()` (e.g. a Window's title).
95    pub fn root(&self) -> &dyn Widget {
96        self.root.as_ref()
97    }
98
99    /// Mutable counterpart to [`root`].  Required when a test wants to
100    /// drive a specific sub-widget directly (e.g. reading ScrollView
101    /// scroll offset) after the App has routed an event.
102    pub fn root_mut(&mut self) -> &mut dyn Widget {
103        self.root.as_mut()
104    }
105
106    /// Return the type name of the currently focused widget, if any.
107    pub fn focused_widget_type_name(&self) -> Option<&'static str> {
108        self.focus
109            .as_deref()
110            .map(|path| widget_at_path_ref(self.root.as_ref(), path).type_name())
111    }
112
113    /// Whether the focused widget accepts typed text (a `TextField`, `TextArea`,
114    /// `RichTextEdit`, or a `DragValue` in its edit mode).
115    ///
116    /// Web shells use this to decide when to focus the hidden DOM `<textarea>`
117    /// that backs IME composition *and* clipboard events: without it focused,
118    /// the browser never delivers `copy` / `cut` / `paste` events to a
119    /// canvas-only app, so the in-canvas editors get no clipboard bridge.
120    ///
121    /// Delegates straight to [`Widget::accepts_text_input`] on the focused
122    /// widget rather than matching type-name strings, so any new editor that
123    /// overrides the trait is enrolled automatically (a hardcoded name list
124    /// would silently miss it and reintroduce exactly the wasm clipboard bug).
125    pub fn focused_is_text_input(&self) -> bool {
126        self.focus
127            .as_deref()
128            .is_some_and(|path| widget_at_path_ref(self.root.as_ref(), path).accepts_text_input())
129    }
130
131    /// Register a legacy global key handler invoked only after the widget tree
132    /// has ignored the key. Prefer widget-owned key handling for new behavior.
133    ///
134    /// # Example
135    /// ```ignore
136    /// app.set_global_key_handler(|key, mods| {
137    ///     if mods.ctrl && mods.shift && key == Key::O {
138    ///         organize_windows();
139    ///         return true;
140    ///     }
141    ///     false
142    /// });
143    /// ```
144    pub fn set_global_key_handler(
145        &mut self,
146        handler: impl FnMut(Key, Modifiers) -> bool + 'static,
147    ) {
148        self.global_key_handler = Some(Box::new(handler));
149    }
150
151    /// Lay out the widget tree to fill `viewport`.  `viewport` is in **physical
152    /// pixels** (e.g. `window.inner_size()` on native, `canvas.width/height` on
153    /// wasm); this method divides by the current device scale factor so the
154    /// widget tree lays out in logical (device-independent) units.  Call once
155    /// per frame before [`paint`][Self::paint].
156    pub fn layout(&mut self, viewport: Size) {
157        // Snapshot BEFORE laying out: a signal arriving mid-layout is then
158        // treated as post-layout and earns a follow-up frame (see `paint`).
159        self.layout_async_state_epoch = Some(crate::animation::async_state_epoch());
160        // Effective scale combines hardware DPR with the UX zoom
161        // factor — mobile platforms set ux_scale ≈ 1.7 so widgets at
162        // their natural logical size read comfortably at arm's length.
163        let scale = crate::ux_scale::effective_scale().max(1e-6);
164        let logical = Size::new(viewport.width / scale, viewport.height / scale);
165        self.viewport_height = logical.height;
166        self.viewport_size = logical;
167        set_current_viewport(logical);
168        // Fresh safe-area for this frame. The on-screen keyboard is the
169        // one library-owned edge obstruction, so it reserves its strip
170        // here; app chrome (rails, trays) reserves via
171        // `widgets::ReserveInset` during the tree layout below.
172        crate::overlay_insets::begin_frame();
173        if crate::widgets::on_screen_keyboard::is_visible() {
174            crate::overlay_insets::reserve(crate::layout_props::Insets {
175                bottom: crate::widgets::on_screen_keyboard::target_panel_height(logical.width),
176                ..crate::layout_props::Insets::default()
177            });
178        }
179        self.root
180            .set_bounds(Rect::new(0.0, 0.0, logical.width, logical.height));
181        self.root.layout(logical);
182        self.apply_pending_focus();
183        // Re-evaluate the keyboard-avoidance lift against FRESH bounds
184        // (see `keyboard_scroll::relift_after_layout` for the why).
185        crate::widget::keyboard_scroll::relift_after_layout(
186            self.focus.as_deref(),
187            logical.width,
188            self.root.as_mut(),
189        );
190    }
191
192    /// Service a pending programmatic focus request
193    /// ([`crate::focus::request_focus`]). Runs at the end of [`layout`] so the
194    /// tree (and thus the set of focusable widgets) reflects any visibility
195    /// change made in the same handler that requested focus. Moves focus to
196    /// the focusable widget whose [`Widget::focus_id`] matches; no-op when
197    /// there's no request or no match.
198    fn apply_pending_focus(&mut self) {
199        let Some(id) = crate::focus::take_focus_request() else {
200            return;
201        };
202        let mut all: Vec<Vec<usize>> = Vec::new();
203        collect_focusable(self.root.as_ref(), &mut Vec::new(), &mut all);
204        let target = all
205            .into_iter()
206            .find(|p| widget_at_path_ref(self.root.as_ref(), p).focus_id() == Some(id));
207        if let Some(path) = target {
208            self.set_focus(Some(path));
209        }
210    }
211
212    /// Paint the entire widget tree into `ctx`. Call after [`layout`][Self::layout].
213    ///
214    /// Applies a `ctx.scale(dps, dps)` transform up-front so the whole tree —
215    /// widget dimensions, font sizes, margins — is rendered at physical pixel
216    /// density on HiDPI screens without any widget having to know about DPI.
217    ///
218    /// Also clears the immediate draw flag so widgets can re-request it during
219    /// this paint if they need another frame; hosts read [`wants_draw`]
220    /// after `paint` returns to decide whether to schedule continuous draws.
221    pub fn paint(&mut self, ctx: &mut dyn DrawCtx) {
222        crate::animation::clear_draw_request();
223        // Async-state dirty walk: an image load (or other async source)
224        // that finished outside event dispatch bumped
225        // `async_state_epoch`.  Walk the whole tree and mark every
226        // widget dirty so retained backbuffers re-rasterise on this
227        // frame — without this, the freshly-decoded pixels would land
228        // inside a Window FBO whose cache check sees no other change
229        // and composites the previous frame's stale bitmap.  The
230        // explicit walk replaces a brittle "compare an extra epoch
231        // inside every cache" mechanism with a single deterministic
232        // hook at the start of paint.
233        let async_epoch = crate::animation::async_state_epoch();
234        if async_epoch != self.last_async_state_epoch {
235            tree::mark_subtree_dirty(self.root.as_mut());
236            self.last_async_state_epoch = async_epoch;
237        }
238        // The dirty walk fixes pixels, not layout.  If the async change
239        // landed after this frame's layout (worker signal between the host's
240        // layout and this paint), ask for one more frame so the next layout
241        // sees it.  Advancing the snapshot makes this once-per-signal even
242        // for a host that repaints without re-laying-out — no redraw loop.
243        if let Some(laid_out) = self.layout_async_state_epoch {
244            if laid_out != async_epoch {
245                self.layout_async_state_epoch = Some(async_epoch);
246                crate::animation::request_draw_tagged("app.async_after_layout");
247            }
248        }
249        let viewport = self.viewport_size;
250        crate::widgets::combo_box::begin_combo_popup_frame(viewport);
251        crate::widgets::tooltip::begin_tooltip_frame(viewport);
252        // Central tooltip pass: find the deepest hovered widget carrying a
253        // `with_tooltip` string and drive the app-wide tip state machine. Any
254        // resulting tip is submitted to the tooltip queue and painted below in
255        // the global-overlay drain, above all clips.
256        self.drive_tooltip_controller();
257        // Recompute the multi-touch aggregate once per paint and publish
258        // to the thread-local — widgets read it during `on_event` or
259        // `paint` without an explicit `&App` reference.
260        self.touch_state.update_gesture();
261        crate::touch_state::set_current(self.touch_state.current());
262        // Route the aggregate as a captured `Event::MultiTouch` before the
263        // paint traversal runs: a consuming widget marks its cached window
264        // subtree dirty (via the normal Consumed → request_draw → epoch-bump
265        // → mark_dirty chain), so the fold it applied here re-rasters this
266        // same frame.  See `super::app::gesture`.
267        self.dispatch_gesture();
268        // Tick the keyboard-driven lift once per paint.  Translates
269        // the widget tree (and its global overlays) upward by `lift`
270        // pixels so a focused field doesn't disappear behind the
271        // soft-keyboard panel; the panel itself paints unlifted so
272        // it always sits at the bottom of the viewport.
273        let lift = super::keyboard_scroll::tick_lift();
274        // Use the combined device-DPR × UX-zoom scale so widgets at
275        // their natural logical size render at the right physical pixel
276        // count *and* at a comfortable on-screen footprint.
277        let scale = crate::ux_scale::effective_scale();
278        if (scale - 1.0).abs() > 1e-6 {
279            ctx.save();
280            ctx.scale(scale, scale);
281            super::keyboard_scroll::paint_lifted_tree(self.root.as_mut(), ctx, viewport, lift);
282            crate::widgets::on_screen_keyboard::paint_software_keyboard(ctx, viewport);
283            ctx.restore();
284        } else {
285            super::keyboard_scroll::paint_lifted_tree(self.root.as_mut(), ctx, viewport, lift);
286            crate::widgets::on_screen_keyboard::paint_software_keyboard(ctx, viewport);
287        }
288    }
289
290    /// After a paint pass, returns `true` if any widget requested another frame
291    /// (e.g. an in-progress hover animation).  Hosts should use this to set
292    /// their event-loop control flow to continuous polling while it's `true`.
293    ///
294    /// Combines the visibility-gated tree-walk signal ([`Widget::needs_draw`])
295    /// with the immediate draw request flag ([`crate::animation::wants_draw`]).
296    /// Widgets call `request_draw` for ordinary visual invalidation; scheduled
297    /// draw needs such as cursor blink should use `needs_draw` /
298    /// `next_draw_deadline` so hidden subtrees do not keep the loop awake.
299    pub fn wants_draw(&self) -> bool {
300        self.root.needs_draw()
301            || crate::animation::wants_draw()
302            || crate::widgets::on_screen_keyboard::needs_draw()
303            || super::keyboard_scroll::is_lift_animating()
304    }
305
306    /// Pump pending synthetic keys back through [`Self::on_key_down`]
307    /// AND apply any pending dismiss request — the close key on the
308    /// keyboard panel clears focus, which then drops the
309    /// keyboard-aware screen lift via `notify_focus_change`.
310    fn drain_keyboard_synthetic_keys(&mut self) {
311        let pending = crate::widgets::on_screen_keyboard::drain_synthetic_keys();
312        for (key, mods) in pending {
313            self.on_key_down(key, mods);
314        }
315        if crate::widgets::on_screen_keyboard::take_dismiss_request() {
316            self.set_focus(None);
317        }
318    }
319
320    /// Test-only mirror of the end-of-event-loop drain.
321    #[cfg(test)]
322    pub fn drain_keyboard_events_for_test(&mut self) {
323        self.drain_keyboard_synthetic_keys();
324    }
325
326    /// Earliest scheduled draw deadline across the visible widget tree.
327    /// Hosts translate `Some(t)` into `ControlFlow::WaitUntil(t)` so that
328    /// e.g. a text field's cursor blink wakes the loop exactly at the flip
329    /// boundary.  Invisible subtrees contribute nothing.
330    pub fn next_draw_deadline(&self) -> Option<web_time::Instant> {
331        // Two schedule channels feed the host's WaitUntil: per-widget
332        // deadlines (cursor blink) from the tree walk, and the global
333        // `animation::request_draw_after` thread-local. Both are read
334        // non-destructively so a host can re-arm `WaitUntil` idempotently
335        // every idle iteration — an intervening non-repainting event can no
336        // longer strand the scheduled wake. A due global deadline surfaces
337        // separately through `animation::wants_draw`. Serve the earliest.
338        let widget = self.root.next_draw_deadline();
339        let scheduled = crate::animation::peek_next_draw_deadline();
340        match (widget, scheduled) {
341            (Some(a), Some(b)) => Some(a.min(b)),
342            (deadline, None) | (None, deadline) => deadline,
343        }
344    }
345
346    // --- Platform event ingestion ---
347    //
348    // Hosts pass raw physical-pixel coordinates (e.g. `e.clientX * devicePixelRatio`
349    // in wasm, or `WindowEvent::CursorMoved.position` on native).  These methods
350    // divide by the current device scale factor and flip Y so widget code sees
351    // logical Y-up coordinates matching the layout pass.
352
353    /// Mouse wheel scrolled. `screen_y` is Y-down. Convention matches
354    /// `winit` / `WheelEvent`: positive `delta_y` = wheel rotated
355    /// forward = user wants to see content ABOVE the current view.
356    /// Scroll containers DECREASE their offset when `delta_y` is
357    /// positive. Positive `delta_x` = see content to the LEFT.
358    pub fn on_mouse_wheel(&mut self, screen_x: f64, screen_y: f64, delta_y: f64) {
359        self.on_mouse_wheel_xy_mods(screen_x, screen_y, 0.0, delta_y, Modifiers::default());
360    }
361
362    /// Mouse wheel with an explicit horizontal component (trackpad pan,
363    /// shift+wheel via the platform harness).
364    pub fn on_mouse_wheel_xy(&mut self, screen_x: f64, screen_y: f64, delta_x: f64, delta_y: f64) {
365        self.on_mouse_wheel_xy_mods(screen_x, screen_y, delta_x, delta_y, Modifiers::default());
366    }
367
368    /// Mouse wheel with explicit horizontal component and modifier state.
369    pub fn on_mouse_wheel_xy_mods(
370        &mut self,
371        screen_x: f64,
372        screen_y: f64,
373        delta_x: f64,
374        delta_y: f64,
375        modifiers: Modifiers,
376    ) {
377        let pos = super::keyboard_scroll::lift_to_world(self.flip_y(screen_x, screen_y));
378        set_current_mouse_world(pos);
379        let hit = active_modal_path(self.root.as_ref())
380            .map(|path| self.extend_modal_path(&path, pos))
381            .or_else(|| self.compute_hit(pos));
382        let event = Event::MouseWheel {
383            pos,
384            delta_y,
385            delta_x,
386            modifiers,
387        };
388        if let Some(path) = hit {
389            dispatch_event(&mut self.root, &path, &event, pos);
390        }
391    }
392
393    /// Snapshot the entire widget tree for the inspector.
394    pub fn collect_inspector_nodes(&self) -> Vec<InspectorNode> {
395        let mut out = Vec::new();
396        collect_inspector_nodes(self.root.as_ref(), 0, Point::ORIGIN, &mut out);
397        out
398    }
399
400    /// `true` while a widget is actively capturing the pointer — i.e. the
401    /// user is mid-drag (a window edge, slider thumb, scrollbar, etc.).
402    /// Used by the demo harness to throttle expensive per-frame snapshots
403    /// (the inspector tree walk) during interactions; the snapshot can
404    /// safely defer until the user releases without changing the visible
405    /// outcome (the underlying widget tree topology doesn't change during
406    /// a drag, only the widgets' bounds).
407    pub fn has_captured_pointer(&self) -> bool {
408        self.captured.is_some()
409    }
410
411    /// Serialize the widget tree — types, bounds, depth, properties — as JSON.
412    ///
413    /// Produces a flat array of nodes in paint-order DFS.  Suitable for writing
414    /// to a file and diffing between runs to verify layout stability.  Used by
415    /// the demo harness's debug hotkey.
416    pub fn dump_tree_json(&self) -> String {
417        let nodes = self.collect_inspector_nodes();
418        let mut s = String::from("[\n");
419        for (i, n) in nodes.iter().enumerate() {
420            let props_json = n
421                .properties
422                .iter()
423                .map(|(k, v)| format!("{:?}: {:?}", k, v))
424                .collect::<Vec<_>>()
425                .join(", ");
426            s.push_str(&format!(
427                "  {{\"type\":{:?},\"depth\":{},\"x\":{:.2},\"y\":{:.2},\"w\":{:.2},\"h\":{:.2},\"props\":{{{}}}}}",
428                n.type_name, n.depth,
429                n.screen_bounds.x, n.screen_bounds.y,
430                n.screen_bounds.width, n.screen_bounds.height,
431                props_json,
432            ));
433            if i + 1 < nodes.len() {
434                s.push(',');
435            }
436            s.push('\n');
437        }
438        s.push(']');
439        s
440    }
441
442    /// Returns `true` if any widget currently holds keyboard focus.
443    /// Used by the render loop to schedule cursor-blink repaints.
444    pub fn has_focus(&self) -> bool {
445        self.focus.is_some()
446    }
447
448    /// Call when the cursor leaves the window to clear hover state.
449    pub fn on_mouse_leave(&mut self) {
450        crate::cursor::reset_cursor_icon();
451        self.dispatch_mouse_move(Point::new(-1.0, -1.0));
452    }
453
454    /// Native drag-and-drop landed `paths` on the window at the given
455    /// screen position. Dispatches an [`Event::FileDropped`] to the
456    /// widget under the cursor (same hit-test path as `on_mouse_down`),
457    /// so a widget can opt in by handling the event in `on_event`.
458    ///
459    /// Native shells typically receive one path per `DroppedFile` event
460    /// from winit; they may forward each separately, or batch a single
461    /// drag gesture into one call. The widget receives `paths` as-is.
462    pub fn on_file_dropped(
463        &mut self,
464        screen_x: f64,
465        screen_y: f64,
466        paths: Vec<std::path::PathBuf>,
467    ) {
468        if paths.is_empty() {
469            return;
470        }
471        let pos = super::keyboard_scroll::lift_to_world(self.flip_y(screen_x, screen_y));
472        let event = Event::FileDropped { pos, paths };
473        let hit = self.compute_hit(pos);
474        let consumed = match hit {
475            Some(path) => dispatch_event(&mut self.root, &path, &event, pos),
476            // No hit target: dispatch to the root anyway so app-level
477            // handlers (e.g. "open the dropped .atmr project") can run
478            // even when the user drops on chrome rather than canvas.
479            None => dispatch_event(&mut self.root, &[], &event, pos),
480        }
481        .is_consumed();
482        if !consumed {
483            // The widget under the drop point ignored the files. Offer
484            // the event to the rest of the tree before giving up — the
485            // reported position is often wrong through no fault of the
486            // user (winit's Windows backend discards the OLE drop point
487            // and emits no CursorMoved during the drag, so shells fall
488            // back to the last pre-drag cursor position). A drop must
489            // find the app's file handler even when it "lands" on
490            // chrome or a sibling pane.
491            super::tree::dispatch_event_broadcast(&mut self.root, &event, pos);
492        }
493        crate::animation::request_draw();
494    }
495
496    // --- Touch ingestion ---
497    //
498    // Raw touches go into the multi-touch gesture recogniser; widgets
499    // read `current_multi_touch()` each frame.  The primary finger is
500    // ALSO replayed through the `on_mouse_*` entry points by the
501    // core-owned `TouchMouseEmu` (see `crate::touch_emulation`), so
502    // widgets that only understand mouse input keep working and
503    // platform shells forward raw touches only.  Coordinates are the
504    // same physical-pixel Y-down 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    /// Feed the central tooltip controller from the current hover path: the
599    /// deepest hovered widget whose [`Widget::tooltip_text`] is `Some` supplies
600    /// the tip (its index-path identity + text), anchored at the pointer.
601    fn drive_tooltip_controller(&self) {
602        let target = self
603            .hovered
604            .as_deref()
605            .and_then(|path| deepest_tipped(self.root.as_ref(), path));
606        crate::widgets::tooltip::controller::drive(target, current_mouse_world());
607    }
608
609    /// Test hook: run the central tooltip pass without a full paint (no
610    /// `DrawCtx` needed). Mirrors what [`paint`](Self::paint) does after
611    /// `begin_tooltip_frame`, so a test can drive hover + clock + this and then
612    /// assert controller state.
613    #[cfg(test)]
614    pub fn update_tooltips_for_test(&self) {
615        crate::widgets::tooltip::begin_tooltip_frame(self.viewport_size);
616        self.drive_tooltip_controller();
617    }
618
619    fn dispatch_mouse_move(&mut self, pos: Point) {
620        let new_hit = self.compute_hit(pos);
621
622        // If the hovered widget changed, clear the old one — but skip the clear
623        // event when the old widget still has mouse capture (it should keep
624        // receiving real positions, not a (-1,-1) sentinel that snaps state).
625        if new_hit != self.hovered {
626            if let Some(old_path) = self.hovered.take() {
627                let is_captured = self.captured.as_ref() == Some(&old_path);
628                if !is_captured {
629                    let clear = Event::MouseMove {
630                        pos: Point::new(-1.0, -1.0),
631                    };
632                    dispatch_event(&mut self.root, &old_path, &clear, Point::new(-1.0, -1.0));
633                }
634            }
635            self.hovered = new_hit.clone();
636        }
637
638        let event = Event::MouseMove { pos };
639        if let Some(ref cap_path) = self.captured.clone() {
640            // Captured widget always receives the real position, regardless of
641            // whether the cursor is over it — this is what keeps a slider
642            // tracking the cursor when dragged outside its bounds.
643            dispatch_event(&mut self.root, cap_path, &event, pos);
644        } else if let Some(path) = new_hit {
645            dispatch_event(&mut self.root, &path, &event, pos);
646        }
647    }
648
649    /// Set focus to `new_path`, sending `FocusLost` / `FocusGained` as needed.
650    fn set_focus(&mut self, new_path: Option<Vec<usize>>) {
651        if self.focus == new_path {
652            return;
653        }
654        if let Some(old) = self.focus.take() {
655            dispatch_event(&mut self.root, &old, &Event::FocusLost, Point::ORIGIN);
656        }
657        self.focus = new_path.clone();
658        if let Some(new) = new_path.clone() {
659            dispatch_event(&mut self.root, &new, &Event::FocusGained, Point::ORIGIN);
660        }
661        super::keyboard_scroll::notify_focus_change(
662            new_path.as_deref(),
663            self.viewport_size.width,
664            self.root.as_mut(),
665        );
666    }
667
668    /// Lift the focused widget above the on-screen keyboard panel so
669    /// typing never disappears behind it.  No-op when already visible.
670    pub fn ensure_focused_visible_above_keyboard(&mut self) {
671        super::keyboard_scroll::ensure_focused_visible_above_keyboard(
672            self.focus.as_deref(),
673            self.viewport_size.width,
674            self.root.as_mut(),
675        );
676    }
677
678    /// Focus the first focusable widget in paint order.
679    ///
680    /// Apps whose root is a game/canvas widget need this at startup:
681    /// [`on_key_up`](Self::on_key_up) only routes to the focused widget
682    /// (there is no unconsumed-key fallback for releases), so held-key
683    /// state tracking breaks until the user clicks or tabs. Calling this
684    /// right after building the app gives that widget key delivery from
685    /// the first frame. No-op when nothing is focusable.
686    pub fn focus_first(&mut self) {
687        if self.focus.is_none() {
688            self.advance_focus(true);
689        }
690    }
691
692    /// Move focus to the next (or previous) focusable widget in paint order.
693    fn advance_focus(&mut self, forward: bool) {
694        let mut all: Vec<Vec<usize>> = Vec::new();
695        collect_focusable(self.root.as_ref(), &mut vec![], &mut all);
696        if all.is_empty() {
697            return;
698        }
699        let current_idx = self
700            .focus
701            .as_ref()
702            .and_then(|f| all.iter().position(|p| p == f));
703        let next_idx = match current_idx {
704            None => {
705                if forward {
706                    0
707                } else {
708                    all.len() - 1
709                }
710            }
711            Some(i) => {
712                if forward {
713                    (i + 1) % all.len()
714                } else {
715                    if i == 0 {
716                        all.len() - 1
717                    } else {
718                        i - 1
719                    }
720                }
721            }
722        };
723        let next_path = all[next_idx].clone();
724        self.set_focus(Some(next_path));
725    }
726}
727
728/// Walk the hover `path` from `root` and return the **deepest** widget along it
729/// whose [`Widget::tooltip_text`] is `Some`, as `(identity_path, text)`. The
730/// identity is the path prefix down to that widget, which the controller uses
731/// to detect target changes (moving to a different tipped control ⇒ reshow).
732/// A shallower tipped ancestor is used only when no deeper descendant has a tip.
733fn deepest_tipped(root: &dyn Widget, path: &[usize]) -> Option<(Vec<usize>, String)> {
734    let mut widget: &dyn Widget = root;
735    let mut best: Option<(usize, String)> = None;
736    if let Some(t) = widget.tooltip_text() {
737        best = Some((0, t.to_string()));
738    }
739    for (i, &idx) in path.iter().enumerate() {
740        let Some(child) = widget.children().get(idx) else {
741            break;
742        };
743        widget = child.as_ref();
744        if let Some(t) = widget.tooltip_text() {
745            best = Some((i + 1, t.to_string()));
746        }
747    }
748    best.map(|(depth, text)| (path[..depth].to_vec(), text))
749}