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