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