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