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}