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