Skip to main content

fret_ui/tree/dispatch/
window.rs

1use super::*;
2use std::collections::HashMap;
3
4use super::event_chain::pointer_cancel_event_for_capture_switch;
5
6impl<H: UiHost> UiTree<H> {
7    #[stacksafe::stacksafe]
8    pub fn dispatch_event(&mut self, app: &mut H, services: &mut dyn UiServices, event: &Event) {
9        let Some(base_root) = self
10            .base_layer
11            .and_then(|id| self.layers.get(id).map(|l| l.root))
12        else {
13            return;
14        };
15
16        let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
17        let window = self.window;
18        let frame_id = app.frame_id();
19        let kind: &'static str = match event {
20            Event::Pointer(_) | Event::PointerCancel(_) => "pointer",
21            Event::Timer { .. } => "timer",
22            _ => "other",
23        };
24
25        let ((), elapsed) = fret_perf::measure_span(
26            self.debug_enabled,
27            trace_enabled,
28            || {
29                tracing::trace_span!(
30                    "fret.ui.dispatch.event",
31                    window = ?window,
32                    frame_id = frame_id.0,
33                    kind,
34                )
35            },
36            || self.dispatch_event_inner(app, services, event, base_root),
37        );
38        if self.debug_enabled {
39            self.debug_stats.dispatch_events = self.debug_stats.dispatch_events.saturating_add(1);
40            if let Some(elapsed) = elapsed {
41                self.debug_stats.dispatch_time += elapsed;
42                match kind {
43                    "pointer" => {
44                        self.debug_stats.dispatch_pointer_events =
45                            self.debug_stats.dispatch_pointer_events.saturating_add(1);
46                        self.debug_stats.dispatch_pointer_event_time += elapsed;
47                    }
48                    "timer" => {
49                        self.debug_stats.dispatch_timer_events =
50                            self.debug_stats.dispatch_timer_events.saturating_add(1);
51                        self.debug_stats.dispatch_timer_event_time += elapsed;
52                    }
53                    _ => {
54                        self.debug_stats.dispatch_other_events =
55                            self.debug_stats.dispatch_other_events.saturating_add(1);
56                        self.debug_stats.dispatch_other_event_time += elapsed;
57                    }
58                }
59            }
60        }
61    }
62
63    #[stacksafe::stacksafe]
64    fn dispatch_event_inner(
65        &mut self,
66        app: &mut H,
67        services: &mut dyn UiServices,
68        event: &Event,
69        base_root: NodeId,
70    ) {
71        self.begin_debug_frame_if_needed(app.frame_id());
72        let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
73        #[cfg(debug_assertions)]
74        let debug_focus_scope = std::env::var_os("FRET_TEST_DEBUG_FOCUS_SCOPE").is_some();
75        #[cfg(debug_assertions)]
76        let mut debug_focus_last = self.focus;
77        #[cfg(debug_assertions)]
78        let mut debug_focus_note = |label: &str, focus: Option<NodeId>| {
79            if !debug_focus_scope {
80                return;
81            }
82            if focus != debug_focus_last {
83                eprintln!(
84                    "debug: dispatch {}: focus {:?} -> {:?}",
85                    label, debug_focus_last, focus
86                );
87                debug_focus_last = focus;
88            }
89        };
90
91        if let Some(window) = self.window {
92            let frame_id = app.frame_id();
93            let now_monotonic = app
94                .global::<fret_core::WindowFrameClockService>()
95                .and_then(|svc| svc.snapshot(window))
96                .map(|s| s.now_monotonic);
97
98            match event {
99                Event::ClipboardReadText { token, .. } => {
100                    app.with_global_mut_untracked(
101                        fret_runtime::WindowClipboardDiagnosticsStore::default,
102                        |svc, _host| {
103                            svc.record_read_ok(window, frame_id, *token);
104                        },
105                    );
106                }
107                Event::ClipboardReadFailed { token, error } => {
108                    app.with_global_mut_untracked(
109                        fret_runtime::WindowClipboardDiagnosticsStore::default,
110                        |svc, _host| {
111                            svc.record_read_unavailable(
112                                window,
113                                frame_id,
114                                *token,
115                                error.message.clone(),
116                            );
117                        },
118                    );
119                }
120                Event::ClipboardWriteCompleted { outcome, .. } => {
121                    app.with_global_mut_untracked(
122                        fret_runtime::WindowClipboardDiagnosticsStore::default,
123                        |svc, _host| match outcome {
124                            fret_core::ClipboardWriteOutcome::Succeeded => {
125                                svc.record_write_ok(window, frame_id);
126                            }
127                            fret_core::ClipboardWriteOutcome::Failed { error } => {
128                                svc.record_write_unavailable(
129                                    window,
130                                    frame_id,
131                                    error.message.clone(),
132                                );
133                            }
134                        },
135                    );
136                }
137                _ => {}
138            }
139
140            let update_pointer = |app: &mut H,
141                                  pointer_id: fret_core::PointerId,
142                                  position: Point| {
143                app.with_global_mut_untracked(
144                    crate::pointer_motion::WindowPointerMotionService::default,
145                    |svc, _host| {
146                        svc.update_position(window, pointer_id, position, frame_id, now_monotonic);
147                    },
148                );
149            };
150
151            match event {
152                Event::Pointer(pe) => match pe {
153                    PointerEvent::Move {
154                        pointer_id,
155                        position,
156                        ..
157                    }
158                    | PointerEvent::Down {
159                        pointer_id,
160                        position,
161                        ..
162                    }
163                    | PointerEvent::Up {
164                        pointer_id,
165                        position,
166                        ..
167                    }
168                    | PointerEvent::Wheel {
169                        pointer_id,
170                        position,
171                        ..
172                    }
173                    | PointerEvent::PinchGesture {
174                        pointer_id,
175                        position,
176                        ..
177                    } => {
178                        update_pointer(app, *pointer_id, *position);
179                    }
180                },
181                Event::PointerCancel(e) => {
182                    if let Some(position) = e.position {
183                        update_pointer(app, e.pointer_id, position);
184                    }
185                }
186                _ => {}
187            }
188        }
189
190        // Keep wheel routing and hover detection in sync with out-of-band scroll handle mutations
191        // (e.g. forwarded wheel handlers) by applying scroll-handle-driven invalidations before
192        // hit-testing.
193        if matches!(event, Event::Pointer(_)) {
194            let (_, elapsed) = fret_perf::measure_span(
195                self.debug_enabled,
196                trace_enabled,
197                || tracing::trace_span!("fret.ui.dispatch.scroll_handle_invalidation"),
198                || {
199                    self.invalidate_scroll_handle_bindings_for_changed_handles(
200                        app,
201                        crate::layout_pass::LayoutPassKind::Final,
202                        /* consume_deferred_scroll_to_item */ false,
203                        /* commit_scroll_handle_baselines */ false,
204                    );
205                },
206            );
207            if let Some(elapsed) = elapsed {
208                self.debug_stats.dispatch_scroll_handle_invalidation_time += elapsed;
209            }
210        }
211
212        let is_wheel = matches!(event, Event::Pointer(PointerEvent::Wheel { .. }));
213
214        let ((active_layers, barrier_root), active_layers_elapsed) = fret_perf::measure_span(
215            self.debug_enabled,
216            trace_enabled,
217            || tracing::trace_span!("fret.ui.dispatch.active_layers"),
218            || {
219                let (active_layers, barrier_root) = self.active_input_layers();
220                (active_layers, barrier_root)
221            },
222        );
223        #[cfg(debug_assertions)]
224        debug_focus_note("after active layers", self.focus);
225        if let Some(active_layers_elapsed) = active_layers_elapsed {
226            self.debug_stats.dispatch_active_layers_time += active_layers_elapsed;
227        }
228
229        let dispatch_cx = self.build_dispatch_cx(app.frame_id(), active_layers, barrier_root);
230        let active_layers: &[NodeId] = dispatch_cx.active_input_roots.as_slice();
231        let barrier_root = dispatch_cx.input_barrier_root;
232        let routing_barrier_root = dispatch_cx.barrier_root;
233
234        let hit_test_layer_roots: &[NodeId] = active_layers;
235        let pointer_chain_snapshot: &UiDispatchSnapshot = &dispatch_cx.input_snapshot;
236
237        let node_in_active_layers = |node: NodeId| dispatch_cx.node_in_active_input_layers(node);
238
239        // Focus barriers (trap scopes / modal focus arbitration) must not rely on retained parent
240        // pointers for correctness under retained/view-cache reuse. Enforce focus-barrier scope
241        // using a snapshot forest built from child edges.
242        if dispatch_cx.focus_barrier_root.is_some()
243            && self.focus.is_some()
244            && self
245                .focus
246                .is_some_and(|n| !dispatch_cx.node_in_active_focus_layers(n))
247        {
248            self.set_focus_unchecked(None, "dispatch/window: focus barrier scope");
249        }
250
251        let to_remove: Vec<fret_core::PointerId> = self
252            .captured
253            .iter()
254            .filter_map(|(p, n)| (!node_in_active_layers(*n)).then_some(*p))
255            .collect();
256        for p in to_remove {
257            self.captured.remove(&p);
258        }
259        if self.focus.is_some_and(|n| !self.node_exists(n)) {
260            self.set_focus_unchecked(None, "dispatch/window: missing focus node");
261        }
262        #[cfg(debug_assertions)]
263        debug_focus_note("after pre-dispatch cleanup", self.focus);
264
265        let focus_is_text_input = self.focus_is_text_input(app);
266        self.update_ime_composing_for_event(focus_is_text_input, event);
267        self.set_ime_allowed(app, focus_is_text_input);
268
269        let (input_ctx, input_ctx_elapsed) = fret_perf::measure_span(
270            self.debug_enabled,
271            trace_enabled,
272            || tracing::trace_span!("fret.ui.dispatch.input_context"),
273            || {
274                let is_pointer_move =
275                    matches!(event, Event::Pointer(fret_core::PointerEvent::Move { .. }));
276                let input_ctx = self.current_window_input_context(
277                    app,
278                    barrier_root.is_some(),
279                    focus_is_text_input,
280                );
281
282                if is_pointer_move {
283                    // Keep pointer-move dispatch cheap: publish the snapshot without
284                    // participating in global-change propagation.
285                    self.publish_window_input_context_snapshot_untracked(app, &input_ctx, false);
286                } else {
287                    self.publish_window_input_context_snapshot(app, &input_ctx);
288                    let next_key_contexts =
289                        self.shortcut_key_context_stack(app, routing_barrier_root);
290                    self.publish_window_key_context_stack_snapshot(app, next_key_contexts);
291                }
292                input_ctx
293            },
294        );
295        if let Some(input_ctx_elapsed) = input_ctx_elapsed {
296            self.debug_stats.dispatch_input_context_time += input_ctx_elapsed;
297        }
298
299        let mut invalidation_visited = HashMap::<NodeId, u8>::new();
300        let mut needs_redraw = false;
301
302        // ADR 0012: when a text input is focused, reserve common IME/navigation keys for the
303        // text/IME path first, and only fall back to shortcut matching if the widget doesn't
304        // consume the event.
305        let defer_keydown_shortcuts_until_after_dispatch =
306            self.pending_shortcut.keystrokes.is_empty()
307                && !self.replaying_pending_shortcut
308                && self.focus.is_some()
309                && match event {
310                    Event::KeyDown { key, modifiers, .. } => {
311                        Self::should_defer_keydown_shortcut_matching_to_text_input(
312                            *key,
313                            *modifiers,
314                            focus_is_text_input,
315                        )
316                    }
317                    _ => false,
318                };
319
320        if let Some(window) = self.window {
321            let pointer_type = match event {
322                Event::Pointer(fret_core::PointerEvent::Move { pointer_type, .. }) => {
323                    Some(*pointer_type)
324                }
325                Event::Pointer(fret_core::PointerEvent::Down { pointer_type, .. }) => {
326                    Some(*pointer_type)
327                }
328                Event::Pointer(fret_core::PointerEvent::Up { pointer_type, .. }) => {
329                    Some(*pointer_type)
330                }
331                Event::PointerCancel(fret_core::PointerCancelEvent { pointer_type, .. }) => {
332                    Some(*pointer_type)
333                }
334                _ => None,
335            };
336            if let Some(pointer_type) = pointer_type {
337                app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |rt, _app| {
338                    rt.set_window_primary_pointer_type(window, pointer_type);
339                });
340            }
341
342            let changed = crate::focus_visible::update_for_event(app, window, event);
343            if changed {
344                if let Some(focus) = self.focus {
345                    self.mark_invalidation_dedup_with_detail(
346                        focus,
347                        Invalidation::Paint,
348                        &mut invalidation_visited,
349                        UiDebugInvalidationSource::Other,
350                        UiDebugInvalidationDetail::FocusVisiblePolicy,
351                    );
352                } else {
353                    self.mark_invalidation_dedup_with_detail(
354                        base_root,
355                        Invalidation::Paint,
356                        &mut invalidation_visited,
357                        UiDebugInvalidationSource::Other,
358                        UiDebugInvalidationDetail::FocusVisiblePolicy,
359                    );
360                }
361                self.request_redraw_coalesced(app);
362            }
363
364            let changed = crate::input_modality::update_for_event(app, window, event);
365            if changed {
366                if let Some(focus) = self.focus {
367                    self.mark_invalidation_dedup_with_detail(
368                        focus,
369                        Invalidation::Paint,
370                        &mut invalidation_visited,
371                        UiDebugInvalidationSource::Other,
372                        UiDebugInvalidationDetail::InputModalityPolicy,
373                    );
374                } else {
375                    self.mark_invalidation_dedup_with_detail(
376                        base_root,
377                        Invalidation::Paint,
378                        &mut invalidation_visited,
379                        UiDebugInvalidationSource::Other,
380                        UiDebugInvalidationDetail::InputModalityPolicy,
381                    );
382                }
383                self.request_redraw_coalesced(app);
384            }
385        }
386
387        self.revalidate_pending_shortcut_for_current_routing_context(app, routing_barrier_root);
388
389        if let Event::Timer { token } = event
390            && !self.replaying_pending_shortcut
391            && !self.pending_shortcut.keystrokes.is_empty()
392            && self.pending_shortcut.timer == Some(*token)
393        {
394            let pending = std::mem::take(&mut self.pending_shortcut);
395            self.sync_pending_shortcut_overlay_state(app, None);
396            if let Some(command) = pending.fallback {
397                app.push_effect(Effect::Command {
398                    window: self.window,
399                    command,
400                });
401            } else {
402                self.replay_captured_keystrokes(app, services, &input_ctx, pending.keystrokes);
403            }
404            self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
405            return;
406        }
407        if let Event::Timer { token } = event {
408            let window = self.window;
409            let frame_id = app.frame_id();
410            let token = *token;
411            // Timer events should be dispatched to visible layers even when they are not currently
412            // hit-testable (e.g. during open/close transitions). The regular dispatch context is
413            // built from active *input* layers, which can exclude visible timer listeners and
414            // cause snapshot membership assertions during bubble dispatch.
415            let timer_dispatch_cx = {
416                let timer_layer_roots: Vec<NodeId> = self
417                    .visible_layers_in_paint_order()
418                    .filter_map(|layer_id| self.layers.get(layer_id).map(|l| l.root))
419                    .collect();
420                self.build_dispatch_cx(frame_id, timer_layer_roots, None)
421            };
422            let mut timer_target: Option<NodeId> = None;
423            let mut broadcast_rebuild_visible_layers_elapsed: Option<Duration> = None;
424            let mut broadcast_loop_elapsed: Option<Duration> = None;
425            let mut broadcast_layers_visited: u32 = 0;
426            let mut stopped = false;
427            let mut broadcast_attempted = false;
428
429            let ((), timer_elapsed) =
430                fret_perf::measure_span(
431                    self.debug_enabled,
432                    trace_enabled,
433                    || {
434                        tracing::trace_span!(
435                            "fret.ui.dispatch.timer",
436                            window = ?window,
437                            frame_id = frame_id.0,
438                            token = token.0,
439                        )
440                    },
441                    || {
442                        if let Some(window) = window {
443                            timer_target = crate::elements::timer_target(app, window, token)
444                                .and_then(|target| match target {
445                                    crate::elements::TimerTarget::Element(element) => self
446                                        .resolve_live_attached_node_for_element(
447                                            app,
448                                            Some(window),
449                                            element,
450                                        ),
451                                    crate::elements::TimerTarget::Node(node) => Some(node),
452                                });
453                        }
454                        if let Some(node) = timer_target {
455                            let (targeted_stopped, _) = fret_perf::measure_span(
456                                self.debug_enabled,
457                                trace_enabled,
458                                || {
459                                    tracing::trace_span!(
460                                        "fret.ui.dispatch.timer.targeted",
461                                        window = ?window,
462                                        frame_id = frame_id.0,
463                                        token = token.0,
464                                        node = ?node,
465                                    )
466                                },
467                                || {
468                                    self.dispatch_event_to_node_chain(
469                                        app,
470                                        services,
471                                        &timer_dispatch_cx,
472                                        &input_ctx,
473                                        node,
474                                        event,
475                                        &mut needs_redraw,
476                                        &mut invalidation_visited,
477                                    )
478                                },
479                            );
480                            stopped = targeted_stopped;
481                        }
482
483                        if !stopped {
484                            broadcast_attempted = true;
485                            let (layers, rebuild_elapsed) = fret_perf::measure_span(
486                                self.debug_enabled,
487                                trace_enabled,
488                                || {
489                                    tracing::trace_span!(
490                                        "fret.ui.dispatch.timer.broadcast.rebuild_visible_layers",
491                                        window = ?window,
492                                        frame_id = frame_id.0,
493                                        token = token.0,
494                                    )
495                                },
496                                || {
497                                    self.visible_layers_in_paint_order()
498                                        .collect::<Vec<UiLayerId>>()
499                                },
500                            );
501                            broadcast_rebuild_visible_layers_elapsed = rebuild_elapsed;
502
503                            let (broadcast_stopped, loop_elapsed) = fret_perf::measure_span(
504                                self.debug_enabled,
505                                trace_enabled,
506                                || {
507                                    tracing::trace_span!(
508                                        "fret.ui.dispatch.timer.broadcast.loop",
509                                        window = ?window,
510                                        frame_id = frame_id.0,
511                                        token = token.0,
512                                    )
513                                },
514                                || {
515                                    for layer_id in layers.into_iter().rev() {
516                                        broadcast_layers_visited =
517                                            broadcast_layers_visited.saturating_add(1);
518                                        let Some(layer) = self.layers.get(layer_id) else {
519                                            continue;
520                                        };
521                                        if !layer.wants_timer_events || !layer.visible {
522                                            continue;
523                                        }
524                                        let stopped = self.dispatch_event_to_subtree_bubble(
525                                            app,
526                                            services,
527                                            &timer_dispatch_cx,
528                                            &input_ctx,
529                                            layer.root,
530                                            event,
531                                            &mut needs_redraw,
532                                            &mut invalidation_visited,
533                                        );
534                                        if stopped {
535                                            return true;
536                                        }
537                                    }
538                                    false
539                                },
540                            );
541                            broadcast_loop_elapsed = loop_elapsed;
542                            stopped = broadcast_stopped;
543                        }
544                    },
545                );
546
547            if self.debug_enabled {
548                let is_targeted = timer_target.is_some();
549                if is_targeted {
550                    self.debug_stats.dispatch_timer_targeted_events = self
551                        .debug_stats
552                        .dispatch_timer_targeted_events
553                        .saturating_add(1);
554                } else {
555                    self.debug_stats.dispatch_timer_broadcast_events = self
556                        .debug_stats
557                        .dispatch_timer_broadcast_events
558                        .saturating_add(1);
559                }
560
561                if let Some(timer_elapsed) = timer_elapsed {
562                    if is_targeted {
563                        self.debug_stats.dispatch_timer_targeted_time += timer_elapsed;
564                    } else {
565                        self.debug_stats.dispatch_timer_broadcast_time += timer_elapsed;
566                    }
567
568                    if timer_elapsed > self.debug_stats.dispatch_timer_slowest_event_time {
569                        self.debug_stats.dispatch_timer_slowest_event_time = timer_elapsed;
570                        self.debug_stats.dispatch_timer_slowest_token = Some(token);
571                        self.debug_stats.dispatch_timer_slowest_was_broadcast = !is_targeted;
572                    }
573                }
574
575                if broadcast_attempted && timer_target.is_none() {
576                    self.debug_stats.dispatch_timer_broadcast_layers_visited = self
577                        .debug_stats
578                        .dispatch_timer_broadcast_layers_visited
579                        .saturating_add(broadcast_layers_visited);
580
581                    if let Some(rebuild_elapsed) = broadcast_rebuild_visible_layers_elapsed {
582                        self.debug_stats
583                            .dispatch_timer_broadcast_rebuild_visible_layers_time +=
584                            rebuild_elapsed;
585                    }
586                    if let Some(loop_elapsed) = broadcast_loop_elapsed {
587                        self.debug_stats.dispatch_timer_broadcast_loop_time += loop_elapsed;
588                    }
589                }
590            }
591
592            if stopped {
593                if needs_redraw {
594                    self.request_redraw_coalesced(app);
595                }
596                self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
597                return;
598            }
599        }
600
601        if matches!(
602            event,
603            Event::ClipboardReadText { .. }
604                | Event::ClipboardReadFailed { .. }
605                | Event::ClipboardWriteCompleted { .. }
606        ) {
607            for layer_id in self
608                .visible_layers_in_paint_order()
609                .collect::<Vec<_>>()
610                .into_iter()
611                .rev()
612            {
613                let Some(layer_root) = self
614                    .layers
615                    .get(layer_id)
616                    .and_then(|layer| layer.visible.then_some(layer.root))
617                else {
618                    continue;
619                };
620
621                if self.dispatch_event_to_subtree_bubble(
622                    app,
623                    services,
624                    &dispatch_cx,
625                    &input_ctx,
626                    layer_root,
627                    event,
628                    &mut needs_redraw,
629                    &mut invalidation_visited,
630                ) {
631                    break;
632                }
633            }
634            if needs_redraw {
635                self.request_redraw_coalesced(app);
636            }
637            return;
638        }
639
640        if let Event::TextInput(text) = event {
641            if !self.replaying_pending_shortcut
642                && self.pending_shortcut.capture_next_text_input_key.is_some()
643            {
644                self.pending_shortcut.capture_next_text_input_key = None;
645                if let Some(last) = self.pending_shortcut.keystrokes.last_mut() {
646                    last.text = Some(text.clone());
647                }
648                self.suppress_text_input_until_key_up = None;
649                return;
650            }
651
652            if self.suppress_text_input_until_key_up.is_some() {
653                self.suppress_text_input_until_key_up = None;
654                return;
655            }
656        }
657
658        if let Event::KeyUp { key, .. } = event {
659            if self.suppress_text_input_until_key_up == Some(*key) {
660                self.suppress_text_input_until_key_up = None;
661            }
662            if self.pending_shortcut.capture_next_text_input_key == Some(*key) {
663                self.pending_shortcut.capture_next_text_input_key = None;
664            }
665        }
666
667        if let Some(window) = self.window
668            && self.handle_alt_menu_bar_activation(app, window, focus_is_text_input, event)
669        {
670            return;
671        }
672
673        let mut cursor_choice: Option<fret_core::CursorIcon> = None;
674        let mut cursor_choice_from_query = false;
675        let mut stop_propagation_requested = false;
676        let mut stop_propagation_requested_by: Option<NodeId> = None;
677        let mut pointer_down_outside = PointerDownOutsideOutcome::default();
678        let mut suppress_touch_up_outside_dispatch = false;
679        let mut suppress_pointer_dispatch = false;
680        let is_scroll_like = Self::event_is_scroll_like(event);
681        let mut wheel_stop_node: Option<NodeId> = None;
682        let mut synth_pointer_move_prev_target: Option<NodeId> = None;
683        let mut prevented_default_actions = fret_runtime::DefaultActionSet::default();
684        let event_window_position = event_position(event);
685        let event_window_wheel_delta = match event {
686            Event::Pointer(PointerEvent::Wheel { delta, .. }) => Some(*delta),
687            _ => None,
688        };
689        let mut focus_requested = false;
690        let mut defer_escape_overlay_dismiss = false;
691
692        if let Event::KeyDown {
693            key: fret_core::KeyCode::Escape,
694            repeat: false,
695            ..
696        } = event
697            && let Some(window) = self.window
698            && {
699                let dock_drag_affects_window = app.any_drag_session(|d| {
700                    d.kind == fret_runtime::DRAG_KIND_DOCK_PANEL
701                        && (d.source_window == window || d.current_window == window)
702                });
703                if dock_drag_affects_window {
704                    // ADR 0072: Escape cancels the active dock drag session, and must not be
705                    // routed to overlays while the drag is in progress.
706                    let canceled = app.cancel_drag_sessions(|d| {
707                        d.kind == fret_runtime::DRAG_KIND_DOCK_PANEL
708                            && (d.source_window == window || d.current_window == window)
709                    });
710                    for pointer_id in canceled {
711                        self.captured.remove(&pointer_id);
712                    }
713                    true
714                } else {
715                    defer_escape_overlay_dismiss = true;
716                    false
717                }
718            }
719        {
720            self.request_redraw_coalesced(app);
721            self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
722            return;
723        }
724
725        if let Event::KeyDown {
726            key,
727            modifiers,
728            repeat,
729        } = event
730            && !defer_keydown_shortcuts_until_after_dispatch
731            && self.handle_keydown_shortcuts(
732                app,
733                services,
734                KeydownShortcutParams {
735                    input_ctx: &input_ctx,
736                    barrier_root: routing_barrier_root,
737                    focus_is_text_input,
738                    #[cfg(feature = "diagnostics")]
739                    phase: fret_runtime::ShortcutRoutingPhase::PreDispatch,
740                    #[cfg(feature = "diagnostics")]
741                    deferred: false,
742                    key: *key,
743                    modifiers: *modifiers,
744                    repeat: *repeat,
745                },
746            )
747        {
748            return;
749        }
750
751        let default_root = barrier_root.unwrap_or(base_root);
752
753        // Pointer capture only affects pointer events. Drag-and-drop style events
754        // (external/internal) must continue to follow the cursor for correct cross-window UX.
755        let event_pointer_id_for_capture: Option<fret_core::PointerId> = match event {
756            Event::Pointer(PointerEvent::Move { pointer_id, .. })
757            | Event::Pointer(PointerEvent::Down { pointer_id, .. })
758            | Event::Pointer(PointerEvent::Up { pointer_id, .. })
759            | Event::Pointer(PointerEvent::Wheel { pointer_id, .. })
760            | Event::Pointer(PointerEvent::PinchGesture { pointer_id, .. }) => Some(*pointer_id),
761            Event::PointerCancel(e) => Some(e.pointer_id),
762            _ => None,
763        };
764
765        let captured = event_pointer_id_for_capture.and_then(|p| self.captured.get(&p).copied());
766        if let Event::Pointer(PointerEvent::Move {
767            pointer_id,
768            position,
769            pointer_type: fret_core::PointerType::Touch,
770            ..
771        }) = event
772        {
773            self.update_touch_pointer_down_outside_move(*pointer_id, *position);
774        }
775        let (dock_drag_affects_window, dock_drag_capture_anchor) = self
776            .window
777            .map(|window| {
778                let affects = app.any_drag_session(|d| {
779                    d.kind == fret_runtime::DRAG_KIND_DOCK_PANEL
780                        && (d.source_window == window || d.current_window == window)
781                });
782                let anchor =
783                    crate::internal_drag::route(&*app, window, fret_runtime::DRAG_KIND_DOCK_PANEL);
784                (affects, anchor)
785            })
786            .unwrap_or((false, None));
787
788        // Internal drag overrides may need to route events to a stable "anchor" node, even if
789        // hit-testing fails or the cursor is over an unrelated widget (e.g. docking tear-off).
790        let internal_drag_target = (|| {
791            let Event::InternalDrag(e) = event else {
792                return None;
793            };
794            let window = self.window?;
795            let drag = app.drag(e.pointer_id)?;
796            if !drag.cross_window_hover {
797                return None;
798            }
799            let target = crate::internal_drag::route(app, window, drag.kind)?;
800            // Cross-window internal drags are runner-routed and rely on a stable, per-window
801            // anchor node. Do not gate the route target on the current "active layer" set:
802            // modal barriers/overlays can temporarily deactivate the base layer while a dock drag
803            // is in flight (ADR 0072), but docking still needs `InternalDrag` hover/drop events.
804            //
805            // Only require that the node still exists in the tree (mechanism-only contract).
806            self.nodes.get(target).is_some().then_some(target)
807        })();
808        if std::env::var_os("FRET_INTERNAL_DRAG_ROUTE_TRACE").is_some_and(|v| !v.is_empty())
809            && let Some(window) = self.window
810            && let Event::InternalDrag(e) = event
811            && matches!(e.kind, fret_core::InternalDragKind::Drop)
812        {
813            let (drag_kind, cross_window_hover, route, route_in_active_layer) =
814                if let Some(drag) = app.drag(e.pointer_id) {
815                    let route = crate::internal_drag::route(app, window, drag.kind);
816                    let route_in_active_layer = route.is_some_and(&node_in_active_layers);
817                    (
818                        Some(drag.kind),
819                        drag.cross_window_hover,
820                        route,
821                        route_in_active_layer,
822                    )
823                } else {
824                    (None, false, None, false)
825                };
826            tracing::info!(
827                window = ?window,
828                pointer_id = ?e.pointer_id,
829                kind = ?e.kind,
830                position = ?e.position,
831                modifiers = ?e.modifiers,
832                drag_kind = ?drag_kind,
833                cross_window_hover = cross_window_hover,
834                route = ?route,
835                route_in_active_layer = route_in_active_layer,
836                internal_drag_target = ?internal_drag_target,
837                last_internal_drag_target = ?self.last_internal_drag_target,
838                "internal drag route trace"
839            );
840        }
841
842        if let Some(window) = self.window
843            && matches!(event, Event::Pointer(_))
844            && let Some(pos) = event_position(event)
845        {
846            // Hit-testing is performance-sensitive (especially for pointer move), but must remain
847            // correct across discrete interactions like clicks where the pointer position can jump
848            // substantially between events.
849            //
850            // For now, only allow cached hit-test reuse for pointer-move events; other pointer
851            // events clear the cache and rebuild it from a full hit-test pass.
852            let hit = if matches!(event, Event::Pointer(PointerEvent::Move { .. })) {
853                self.hit_test_layers_cached(hit_test_layer_roots, pos)
854            } else {
855                self.hit_test_path_cache = None;
856                self.hit_test_layers_cached(hit_test_layer_roots, pos)
857            };
858
859            if let Event::Pointer(PointerEvent::Up {
860                pointer_id,
861                pointer_type: fret_core::PointerType::Touch,
862                ..
863            }) = event
864                && captured.is_none()
865            {
866                if dock_drag_affects_window {
867                    self.touch_pointer_down_outside_candidates
868                        .remove(pointer_id);
869                } else if let Some(candidate) = self
870                    .touch_pointer_down_outside_candidates
871                    .remove(pointer_id)
872                    && let Some(layer) = self.layers.get(candidate.layer_id)
873                {
874                    let foreign_capture_active = self.captured.iter().any(|(pid, node)| {
875                        *pid != *pointer_id
876                            && self
877                                .node_layer(*node)
878                                .is_some_and(|layer_id| layer_id != candidate.layer_id)
879                    });
880
881                    if !foreign_capture_active && !candidate.moved {
882                        let active_pointer_down_outside_layers =
883                            self.active_pointer_down_outside_layer_roots(barrier_root);
884                        let snapshot = self.build_dispatch_snapshot_for_layer_roots(
885                            app.frame_id(),
886                            active_pointer_down_outside_layers.as_slice(),
887                            barrier_root,
888                        );
889
890                        let hit_is_inside_layer = hit.is_some_and(|hit| {
891                            if snapshot.pre.get(layer.root).is_some()
892                                && snapshot.pre.get(hit).is_some()
893                            {
894                                snapshot.is_descendant(layer.root, hit)
895                            } else {
896                                self.is_reachable_from_root_via_children(layer.root, hit)
897                            }
898                        });
899                        let hit_is_inside_branch = hit.is_some_and(|hit| {
900                            layer
901                                .pointer_down_outside_branches
902                                .iter()
903                                .copied()
904                                .any(|branch| {
905                                    if snapshot.pre.get(branch).is_some()
906                                        && snapshot.pre.get(hit).is_some()
907                                    {
908                                        snapshot.is_descendant(branch, hit)
909                                    } else {
910                                        self.is_reachable_from_root_via_children(branch, hit)
911                                    }
912                                })
913                        });
914
915                        if !hit_is_inside_layer && !hit_is_inside_branch {
916                            let (window, root_element, tick_id) = if let Some(window) = self.window
917                                && let Some(root_element) =
918                                    self.nodes.get(candidate.root).and_then(|n| n.element)
919                            {
920                                let tick_id = app.tick_id();
921                                crate::elements::with_element_state(
922                                    app,
923                                    window,
924                                    root_element,
925                                    crate::action::DismissibleLastDismissRequest::default,
926                                    |st| {
927                                        st.tick_id = tick_id;
928                                        st.reason = None;
929                                        st.default_prevented = false;
930                                    },
931                                );
932                                (Some(window), Some(root_element), Some(tick_id))
933                            } else {
934                                (None, None, None)
935                            };
936                            self.dispatch_event_to_node_chain_observer(
937                                app,
938                                services,
939                                &input_ctx,
940                                candidate.root,
941                                &candidate.down_event,
942                                Some(&snapshot),
943                                &mut invalidation_visited,
944                            );
945                            let mut clear_focus = true;
946                            if let (Some(window), Some(root_element), Some(tick_id)) =
947                                (window, root_element, tick_id)
948                            {
949                                let prevented = crate::elements::with_element_state(
950                                    app,
951                                    window,
952                                    root_element,
953                                    crate::action::DismissibleLastDismissRequest::default,
954                                    |st| {
955                                        st.tick_id == tick_id
956                                            && matches!(
957                                                st.reason,
958                                                Some(
959                                                    crate::action::DismissReason::OutsidePress { .. }
960                                                )
961                                            )
962                                            && st.default_prevented
963                                    },
964                                );
965                                if prevented {
966                                    clear_focus = false;
967                                }
968                            }
969                            if clear_focus {
970                                self.set_focus(None);
971                            }
972                            needs_redraw = true;
973                            suppress_touch_up_outside_dispatch = candidate.consume;
974                        }
975                    }
976                }
977            }
978
979            // Pointer occlusion is a window-level layer substrate mechanism (policy-owned).
980            //
981            // When active, the runtime must:
982            // - suppress hover state for underlay layers (even when scroll is allowed),
983            // - optionally suppress hit-tested pointer dispatch for underlay layers depending on
984            //   the occlusion mode.
985            let mut hit_for_hover = hit;
986            let mut hit_for_hover_region = hit;
987            let mut hit_for_raw_below_barrier: Option<NodeId> = None;
988            if captured.is_none()
989                && let Some((occlusion_layer, occlusion)) =
990                    self.topmost_pointer_occlusion_layer(barrier_root)
991                && occlusion != PointerOcclusion::None
992            {
993                let occlusion_z = self
994                    .layer_order
995                    .iter()
996                    .position(|id| *id == occlusion_layer);
997                let hit_layer_z = hit
998                    .and_then(|hit| self.node_layer(hit))
999                    .and_then(|layer| self.layer_order.iter().position(|id| *id == layer));
1000
1001                let hit_is_below_occlusion = match (occlusion_z, hit_layer_z, hit) {
1002                    (Some(oz), Some(hz), Some(_)) => hz < oz,
1003                    (Some(_), None, Some(_)) => true,
1004                    (Some(_), _, None) => true,
1005                    _ => false,
1006                };
1007
1008                if hit_is_below_occlusion {
1009                    hit_for_raw_below_barrier = hit;
1010                    // Match GPUI-style "occluded hover": underlay hover/pressable detection is
1011                    // disabled while occlusion is active, even when scroll is still allowed.
1012                    hit_for_hover = None;
1013                    hit_for_hover_region = None;
1014
1015                    let blocks_pointer_dispatch = match occlusion {
1016                        PointerOcclusion::None => false,
1017                        PointerOcclusion::BlockMouse => true,
1018                        PointerOcclusion::BlockMouseExceptScroll => !is_scroll_like,
1019                    };
1020                    if blocks_pointer_dispatch {
1021                        suppress_pointer_dispatch = true;
1022                    }
1023                }
1024            }
1025
1026            if input_ctx.caps.ui.cursor_icons
1027                && cursor_choice.is_none()
1028                && matches!(event, Event::Pointer(PointerEvent::Move { .. }))
1029            {
1030                let (_, elapsed) = fret_perf::measure_span(
1031                    self.debug_enabled,
1032                    trace_enabled,
1033                    || tracing::trace_span!("fret.ui.dispatch.cursor_query"),
1034                    || {
1035                        if let Some(start) = captured.or(hit_for_hover) {
1036                            cursor_choice = self.cursor_icon_query_for_pointer_hit(
1037                                start,
1038                                &input_ctx,
1039                                event,
1040                                Some(pointer_chain_snapshot),
1041                            );
1042                            cursor_choice_from_query = cursor_choice.is_some();
1043                        }
1044                    },
1045                );
1046                if let Some(elapsed) = elapsed {
1047                    self.debug_stats.dispatch_cursor_query_time += elapsed;
1048                }
1049            }
1050
1051            if matches!(event, Event::Pointer(PointerEvent::Down { .. })) && captured.is_none() {
1052                if dock_drag_affects_window {
1053                    // ADR 0072: while a dock drag session is active, outside-press dismissal must
1054                    // not trigger. The drag owns input arbitration for the window.
1055                    //
1056                    // This is intentionally window-global (not pointer-local): a dock drag session
1057                    // is exclusive for the window, and we do not want secondary pointers to dismiss
1058                    // overlays or change focus while the drag is in progress.
1059                    //
1060                    // Note: overlay policy is expected to close/suspend non-modal overlays when a
1061                    // dock drag starts; this suppression makes the routing rule durable even if a
1062                    // layer remains mounted for a close transition.
1063                    pointer_down_outside = PointerDownOutsideOutcome::default();
1064                } else {
1065                    let active_pointer_down_outside_layers =
1066                        self.active_pointer_down_outside_layer_roots(barrier_root);
1067                    pointer_down_outside = self.dispatch_pointer_down_outside(
1068                        app,
1069                        services,
1070                        PointerDownOutsideParams {
1071                            input_ctx: &input_ctx,
1072                            active_layer_roots: &active_pointer_down_outside_layers,
1073                            barrier_root,
1074                            base_root,
1075                            hit,
1076                            event,
1077                        },
1078                        &mut invalidation_visited,
1079                    );
1080                    if pointer_down_outside.dispatched {
1081                        needs_redraw = true;
1082                    }
1083                }
1084            }
1085
1086            let hover_capable = match event {
1087                Event::Pointer(PointerEvent::Move { pointer_type, .. })
1088                | Event::Pointer(PointerEvent::Down { pointer_type, .. })
1089                | Event::Pointer(PointerEvent::Up { pointer_type, .. })
1090                | Event::Pointer(PointerEvent::Wheel { pointer_type, .. })
1091                | Event::Pointer(PointerEvent::PinchGesture { pointer_type, .. }) => {
1092                    pointer_type_supports_hover(*pointer_type)
1093                }
1094                _ => false,
1095            };
1096
1097            if hover_capable {
1098                let position = event_position(event);
1099                self.update_hover_state_from_hit(
1100                    app,
1101                    window,
1102                    barrier_root,
1103                    position,
1104                    hit_for_hover,
1105                    hit_for_hover_region,
1106                    hit_for_raw_below_barrier,
1107                    Some(pointer_chain_snapshot),
1108                    &mut invalidation_visited,
1109                    &mut needs_redraw,
1110                );
1111            }
1112        }
1113
1114        let mut pointer_hit: Option<NodeId> = None;
1115        let locked_touch_drag_target = match event {
1116            Event::Pointer(PointerEvent::Move {
1117                pointer_id,
1118                buttons,
1119                pointer_type: fret_core::PointerType::Touch,
1120                ..
1121            }) if buttons.left || buttons.right || buttons.middle => self
1122                .active_touch_drag_target
1123                .get(pointer_id)
1124                .copied()
1125                .and_then(|element| {
1126                    self.resolve_live_attached_node_for_element(app, self.window, element)
1127                })
1128                .filter(|node| node_in_active_layers(*node)),
1129            _ => None,
1130        };
1131        let touch_drag_target_override = match (captured, locked_touch_drag_target, event) {
1132            (
1133                Some(captured_node),
1134                Some(locked_node),
1135                Event::Pointer(PointerEvent::Move {
1136                    buttons,
1137                    pointer_type: fret_core::PointerType::Touch,
1138                    ..
1139                }),
1140            ) if buttons.left || buttons.right || buttons.middle => {
1141                let captured_is_pressable = self
1142                    .window
1143                    .and_then(|window| {
1144                        crate::declarative::element_record_for_node(app, window, captured_node)
1145                    })
1146                    .is_some_and(|record| {
1147                        matches!(
1148                            record.instance,
1149                            crate::declarative::ElementInstance::Pressable(_)
1150                        )
1151                    });
1152
1153                if captured_is_pressable && captured_node != locked_node {
1154                    Some(locked_node)
1155                } else {
1156                    None
1157                }
1158            }
1159            _ => None,
1160        };
1161        let touch_drag_reroute_from_pressable_capture = touch_drag_target_override.is_some();
1162        let target = if let Some(target) = touch_drag_target_override {
1163            Some(target)
1164        } else if let Some(captured) = captured {
1165            Some(captured)
1166        } else if let Some(target) = internal_drag_target {
1167            Some(target)
1168        } else if let Some(pos) = event_position(event) {
1169            // See the cached hit-test reuse note above.
1170            let hit = if matches!(event, Event::Pointer(PointerEvent::Move { .. })) {
1171                self.hit_test_layers_cached(hit_test_layer_roots, pos)
1172            } else {
1173                self.hit_test_path_cache = None;
1174                self.hit_test_layers_cached(hit_test_layer_roots, pos)
1175            };
1176
1177            let hit = if matches!(event, Event::InternalDrag(_)) {
1178                (|| {
1179                    let window = self.window?;
1180                    crate::declarative::with_window_frame(app, window, |window_frame| {
1181                        let window_frame = window_frame?;
1182                        let mut node = hit?;
1183                        loop {
1184                            if let Some(record) = window_frame.instances.get(node)
1185                                && matches!(
1186                                    record.instance,
1187                                    crate::declarative::ElementInstance::InternalDragRegion(p)
1188                                        if p.enabled
1189                                )
1190                            {
1191                                return Some(node);
1192                            }
1193                            node = pointer_chain_snapshot.parent.get(node).copied().flatten()?;
1194                        }
1195                    })
1196                })()
1197                .or(hit)
1198            } else {
1199                hit
1200            };
1201            pointer_hit = hit;
1202
1203            if let Event::Pointer(PointerEvent::Down {
1204                pointer_id,
1205                pointer_type: fret_core::PointerType::Touch,
1206                ..
1207            }) = event
1208            {
1209                let element =
1210                    hit.filter(|node| node_in_active_layers(*node))
1211                        .and_then(|mut node| {
1212                            let window = self.window?;
1213                            loop {
1214                                let record =
1215                                    crate::declarative::element_record_for_node(app, window, node);
1216                                if let Some(record) = record
1217                                    && matches!(
1218                                        record.instance,
1219                                        crate::declarative::ElementInstance::Scroll(_)
1220                                            | crate::declarative::ElementInstance::VirtualList(_)
1221                                    )
1222                                {
1223                                    return Some(record.element);
1224                                }
1225                                node = self.nodes.get(node).and_then(|n| n.parent)?;
1226                            }
1227                        });
1228                match element {
1229                    Some(element) => {
1230                        self.active_touch_drag_target.insert(*pointer_id, element);
1231                    }
1232                    None => {
1233                        self.active_touch_drag_target.remove(pointer_id);
1234                    }
1235                }
1236            }
1237
1238            if let Event::Pointer(PointerEvent::Move {
1239                buttons,
1240                pointer_id,
1241                ..
1242            }) = event
1243                && !buttons.left
1244                && !buttons.right
1245                && !buttons.middle
1246            {
1247                // When a modal barrier becomes active, the previous pointer-move hit may belong to
1248                // an underlay layer that is now inactive. Do not synthesize hover-move events into
1249                // the underlay in that case (e.g. Radix `disableOutsidePointerEvents`).
1250                let mut last_pointer_move_hit = self
1251                    .last_pointer_move_hit
1252                    .get(pointer_id)
1253                    .copied()
1254                    .flatten();
1255                if barrier_root.is_some()
1256                    && last_pointer_move_hit.is_some_and(|n| !node_in_active_layers(n))
1257                {
1258                    self.last_pointer_move_hit.remove(pointer_id);
1259                    last_pointer_move_hit = None;
1260                }
1261
1262                if hit != last_pointer_move_hit {
1263                    synth_pointer_move_prev_target = last_pointer_move_hit;
1264                    match hit {
1265                        Some(hit) => {
1266                            self.last_pointer_move_hit.insert(*pointer_id, Some(hit));
1267                        }
1268                        None => {
1269                            self.last_pointer_move_hit.remove(pointer_id);
1270                        }
1271                    }
1272                }
1273            }
1274
1275            if matches!(event, Event::InternalDrag(_)) {
1276                if let Some(node) = hit {
1277                    self.last_internal_drag_target = Some(node);
1278                } else if self
1279                    .last_internal_drag_target
1280                    .is_some_and(|n| !node_in_active_layers(n))
1281                {
1282                    self.last_internal_drag_target = None;
1283                }
1284            }
1285
1286            locked_touch_drag_target
1287                .or(hit)
1288                .or_else(|| {
1289                    matches!(event, Event::InternalDrag(_))
1290                        .then_some(self.last_internal_drag_target)?
1291                })
1292                .or(barrier_root)
1293                .or(Some(default_root))
1294        } else {
1295            match event {
1296                Event::SetTextSelection { .. } => {
1297                    let selection_node = self.window.and_then(|window| {
1298                        crate::elements::with_window_state(app, window, |window_state| {
1299                            window_state
1300                                .active_text_selection()
1301                                .map(|selection| selection.node)
1302                        })
1303                    });
1304                    selection_node.or(self.focus).or(Some(default_root))
1305                }
1306                _ => self.focus.or(Some(default_root)),
1307            }
1308        };
1309
1310        let Some(mut node_id) = target else {
1311            return;
1312        };
1313
1314        match event {
1315            Event::Pointer(PointerEvent::Up {
1316                pointer_id,
1317                pointer_type: fret_core::PointerType::Touch,
1318                ..
1319            })
1320            | Event::PointerCancel(fret_core::PointerCancelEvent {
1321                pointer_id,
1322                pointer_type: fret_core::PointerType::Touch,
1323                ..
1324            }) => {
1325                self.active_touch_drag_target.remove(pointer_id);
1326            }
1327            _ => {}
1328        }
1329
1330        if matches!(event, Event::Pointer(PointerEvent::Down { .. }))
1331            && pointer_down_outside.suppress_hit_test_dispatch
1332        {
1333            if needs_redraw {
1334                self.request_redraw_coalesced(app);
1335            }
1336            self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
1337            return;
1338        }
1339
1340        if matches!(event, Event::Pointer(PointerEvent::Up { .. }))
1341            && suppress_touch_up_outside_dispatch
1342        {
1343            if needs_redraw {
1344                self.request_redraw_coalesced(app);
1345            }
1346            self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
1347            return;
1348        }
1349
1350        if suppress_pointer_dispatch && matches!(event, Event::Pointer(_)) {
1351            if matches!(event, Event::Pointer(PointerEvent::Move { .. })) {
1352                let (_, elapsed) = fret_perf::measure_span(
1353                    self.debug_enabled,
1354                    trace_enabled,
1355                    || tracing::trace_span!("fret.ui.dispatch.pointer_move_layer_observers"),
1356                    || {
1357                        self.dispatch_pointer_move_layer_observers(
1358                            app,
1359                            services,
1360                            &input_ctx,
1361                            barrier_root,
1362                            event,
1363                            &mut needs_redraw,
1364                            &mut invalidation_visited,
1365                        );
1366                    },
1367                );
1368                if let Some(elapsed) = elapsed {
1369                    self.debug_stats.dispatch_pointer_move_layer_observers_time += elapsed;
1370                }
1371            }
1372            if needs_redraw {
1373                self.request_redraw_coalesced(app);
1374            }
1375            self.publish_post_dispatch_runtime_snapshots_for_event(app, event);
1376            return;
1377        }
1378
1379        if cursor_choice.is_none()
1380            && input_ctx.caps.ui.cursor_icons
1381            && matches!(event, Event::Pointer(_))
1382            && let Some(hit) = pointer_hit
1383        {
1384            cursor_choice = self.cursor_icon_query_for_pointer_hit(
1385                hit,
1386                &input_ctx,
1387                event,
1388                Some(pointer_chain_snapshot),
1389            );
1390            cursor_choice_from_query = cursor_choice.is_some();
1391        }
1392
1393        if !suppress_pointer_dispatch
1394            && matches!(
1395                event,
1396                Event::Pointer(_)
1397                    | Event::PointerCancel(_)
1398                    | Event::ExternalDrag(_)
1399                    | Event::InternalDrag(_)
1400            )
1401        {
1402            let (chain, chain_elapsed) = fret_perf::measure_span(
1403                self.debug_enabled,
1404                trace_enabled,
1405                || tracing::trace_span!("fret.ui.dispatch.event_chain_build"),
1406                || {
1407                    if event_position(event).is_some() {
1408                        self.build_mapped_event_chain(node_id, event, Some(pointer_chain_snapshot))
1409                    } else {
1410                        self.build_unmapped_event_chain(
1411                            node_id,
1412                            event,
1413                            Some(&dispatch_cx.focus_snapshot),
1414                        )
1415                    }
1416                },
1417            );
1418            if let Some(chain_elapsed) = chain_elapsed {
1419                self.debug_stats.dispatch_event_chain_build_time += chain_elapsed;
1420            }
1421            let pointer_hit_is_text_input =
1422                if matches!(event, Event::Pointer(PointerEvent::Down { .. }))
1423                    && let Some(window) = self.window
1424                {
1425                    chain.iter().any(|(node_id, _)| {
1426                        crate::declarative::element_record_for_node(app, window, *node_id)
1427                            .is_some_and(|record| {
1428                                matches!(
1429                                    &record.instance,
1430                                    crate::declarative::ElementInstance::TextInput(_)
1431                                        | crate::declarative::ElementInstance::TextArea(_)
1432                                        | crate::declarative::ElementInstance::TextInputRegion(_)
1433                                )
1434                            })
1435                    })
1436                } else {
1437                    false
1438                };
1439            let pointer_hit_is_pressable =
1440                if matches!(event, Event::Pointer(PointerEvent::Down { .. }))
1441                    && let Some(window) = self.window
1442                {
1443                    chain.iter().any(|(node_id, _)| {
1444                        crate::declarative::element_record_for_node(app, window, *node_id)
1445                            .is_some_and(|record| {
1446                                matches!(
1447                                    &record.instance,
1448                                    crate::declarative::ElementInstance::Pressable(_)
1449                                )
1450                            })
1451                    })
1452                } else {
1453                    false
1454                };
1455            let pointer_hit_pressable =
1456                if matches!(event, Event::Pointer(PointerEvent::Down { .. }))
1457                    && let Some(window) = self.window
1458                {
1459                    chain
1460                        .iter()
1461                        .enumerate()
1462                        .find_map(|(chain_index, (node_id, _))| {
1463                            crate::declarative::element_record_for_node(app, window, *node_id)
1464                                .and_then(|record| {
1465                                    matches!(
1466                                        &record.instance,
1467                                        crate::declarative::ElementInstance::Pressable(_)
1468                                    )
1469                                    .then_some((record.element, chain_index))
1470                                })
1471                        })
1472                } else {
1473                    None
1474                };
1475            let should_run_capture_phase = match event {
1476                Event::Pointer(PointerEvent::Down { .. })
1477                | Event::Pointer(PointerEvent::Up { .. })
1478                | Event::Pointer(PointerEvent::Wheel { .. })
1479                | Event::Pointer(PointerEvent::PinchGesture { .. })
1480                | Event::PointerCancel(..) => true,
1481                Event::Pointer(PointerEvent::Move { buttons, .. }) => {
1482                    !touch_drag_reroute_from_pressable_capture
1483                        && (captured.is_some() || buttons.left || buttons.right || buttons.middle)
1484                }
1485                _ => false,
1486            };
1487            let mut stopped_in_capture = false;
1488            if should_run_capture_phase {
1489                let mut capture_ctx = input_ctx.clone();
1490                capture_ctx.dispatch_phase = InputDispatchPhase::Capture;
1491
1492                let (_, capture_elapsed) = fret_perf::measure_span(
1493                    self.debug_enabled,
1494                    trace_enabled,
1495                    || tracing::trace_span!("fret.ui.dispatch.widget_capture"),
1496                    || {
1497                        for (chain_index, (node_id, event_for_node)) in
1498                            chain.iter().enumerate().rev()
1499                        {
1500                            let node_id = *node_id;
1501                            let (
1502                                pointer_hit_pressable_target,
1503                                pointer_hit_pressable_target_in_descendant_subtree,
1504                            ) = pointer_hit_pressable.map_or(
1505                                (None, false),
1506                                |(target, pressable_index)| {
1507                                    (Some(target), pressable_index < chain_index)
1508                                },
1509                            );
1510                            let (
1511                                invalidations,
1512                                scroll_handle_invalidations,
1513                                scroll_target_invalidations,
1514                                requested_focus,
1515                                requested_focus_target,
1516                                requested_capture,
1517                                requested_cursor,
1518                                notify_requested,
1519                                notify_requested_location,
1520                                stop_propagation,
1521                            ) = self.with_widget_mut(node_id, |widget, tree| {
1522                                let (children, bounds) = tree
1523                                    .nodes
1524                                    .get(node_id)
1525                                    .map(|n| (n.children.as_slice(), n.bounds))
1526                                    .unwrap_or((&[][..], Rect::default()));
1527                                let mut cx = EventCx {
1528                                    app,
1529                                    services: &mut *services,
1530                                    node: node_id,
1531                                    layer_root: tree.node_root(node_id),
1532                                    window: tree.window,
1533                                    pointer_id: event_pointer_id_for_capture,
1534                                    scale_factor: tree.last_layout_scale_factor.unwrap_or(1.0),
1535                                    event_window_position,
1536                                    event_window_wheel_delta,
1537                                    input_ctx: capture_ctx.clone(),
1538                                    pointer_hit_is_text_input,
1539                                    pointer_hit_is_pressable,
1540                                    pointer_hit_pressable_target,
1541                                    pointer_hit_pressable_target_in_descendant_subtree,
1542                                    prevented_default_actions: &mut prevented_default_actions,
1543                                    children,
1544                                    focus: tree.focus,
1545                                    captured: event_pointer_id_for_capture
1546                                        .and_then(|p| tree.captured.get(&p).copied()),
1547                                    bounds,
1548                                    invalidations: Vec::new(),
1549                                    scroll_handle_invalidations: Vec::new(),
1550                                    scroll_target_invalidations: Vec::new(),
1551                                    requested_focus: None,
1552                                    requested_focus_target: None,
1553                                    requested_capture: None,
1554                                    requested_cursor: None,
1555                                    notify_requested: false,
1556                                    notify_requested_location: None,
1557                                    stop_propagation: false,
1558                                };
1559                                widget.event_capture(&mut cx, event_for_node);
1560                                (
1561                                    cx.invalidations,
1562                                    cx.scroll_handle_invalidations,
1563                                    cx.scroll_target_invalidations,
1564                                    cx.requested_focus,
1565                                    cx.requested_focus_target,
1566                                    cx.requested_capture,
1567                                    cx.requested_cursor,
1568                                    cx.notify_requested,
1569                                    cx.notify_requested_location,
1570                                    cx.stop_propagation,
1571                                )
1572                            });
1573
1574                            if !invalidations.is_empty()
1575                                || !scroll_handle_invalidations.is_empty()
1576                                || !scroll_target_invalidations.is_empty()
1577                                || requested_focus.is_some()
1578                                || requested_focus_target.is_some()
1579                                || requested_capture.is_some()
1580                                || notify_requested
1581                            {
1582                                needs_redraw = true;
1583                            }
1584
1585                            for (id, inv) in invalidations {
1586                                self.mark_invalidation(id, inv);
1587                            }
1588                            let mut resolved_scroll_handle_invalidations = Vec::new();
1589                            self.extend_live_bound_scroll_handle_invalidations(
1590                                app,
1591                                &scroll_handle_invalidations,
1592                                &mut resolved_scroll_handle_invalidations,
1593                            );
1594                            for (id, inv) in resolved_scroll_handle_invalidations {
1595                                self.mark_invalidation(id, inv);
1596                            }
1597                            let mut resolved_scroll_target_invalidations = Vec::new();
1598                            self.extend_live_scroll_target_invalidations(
1599                                app,
1600                                &scroll_target_invalidations,
1601                                &mut resolved_scroll_target_invalidations,
1602                            );
1603                            for (id, inv) in resolved_scroll_target_invalidations {
1604                                self.mark_invalidation(id, inv);
1605                            }
1606                            if notify_requested {
1607                                self.debug_record_notify_request(
1608                                    app.frame_id(),
1609                                    node_id,
1610                                    notify_requested_location,
1611                                );
1612                                self.mark_invalidation_with_source(
1613                                    node_id,
1614                                    Invalidation::Paint,
1615                                    UiDebugInvalidationSource::Notify,
1616                                );
1617                            }
1618
1619                            let focus_requested_now =
1620                                requested_focus.is_some() || requested_focus_target.is_some();
1621                            if let Some(focus) = self.resolve_requested_focus(
1622                                app,
1623                                requested_focus,
1624                                requested_focus_target,
1625                            ) && self.focus_request_is_allowed(
1626                                app,
1627                                self.window,
1628                                dispatch_cx.active_focus_roots.as_slice(),
1629                                focus,
1630                                Some(&dispatch_cx.focus_snapshot),
1631                            ) {
1632                                focus_requested = true;
1633                                if let Some(prev) = self.focus {
1634                                    self.mark_invalidation(prev, Invalidation::Paint);
1635                                }
1636                                self.focus = Some(focus);
1637                                self.mark_invalidation(focus, Invalidation::Paint);
1638                                // Avoid scrolling during pointer-driven focus changes:
1639                                // programmatic scroll-to-focus can move content under a stationary cursor,
1640                                // causing pointer activation to miss/cancel (especially for nested pressables).
1641                                //
1642                                // Keyboard traversal still scrolls focused nodes into view.
1643                                if !matches!(event, Event::Pointer(_) | Event::PointerCancel(_)) {
1644                                    self.scroll_node_into_view(app, focus);
1645                                }
1646                            } else if focus_requested_now {
1647                                focus_requested = true;
1648                            }
1649
1650                            if let Some(capture) = requested_capture
1651                                && let Some(pointer_id) = event_pointer_id_for_capture
1652                            {
1653                                match capture {
1654                                    Some(node) => {
1655                                        let allow = !dock_drag_affects_window
1656                                            || dock_drag_capture_anchor == Some(node);
1657                                        if allow {
1658                                            if !matches!(event, Event::PointerCancel(_))
1659                                                && let Some(old_capture) =
1660                                                    self.captured.get(&pointer_id).copied()
1661                                                && old_capture != node
1662                                                && node_in_active_layers(old_capture)
1663                                            {
1664                                                let mut cancel_ctx = input_ctx.clone();
1665                                                cancel_ctx.dispatch_phase =
1666                                                    InputDispatchPhase::Bubble;
1667                                                let cancel_event =
1668                                                    pointer_cancel_event_for_capture_switch(
1669                                                        event, pointer_id,
1670                                                    );
1671                                                let _ = self.dispatch_event_to_node_chain(
1672                                                    app,
1673                                                    services,
1674                                                    &dispatch_cx,
1675                                                    &cancel_ctx,
1676                                                    old_capture,
1677                                                    &cancel_event,
1678                                                    &mut needs_redraw,
1679                                                    &mut invalidation_visited,
1680                                                );
1681                                            }
1682                                            self.captured.insert(pointer_id, node);
1683                                        }
1684                                    }
1685                                    None => {
1686                                        self.captured.remove(&pointer_id);
1687                                    }
1688                                }
1689                            }
1690
1691                            if let Some(requested_cursor) = requested_cursor
1692                                && (cursor_choice.is_none() || cursor_choice_from_query)
1693                            {
1694                                cursor_choice = Some(requested_cursor);
1695                                cursor_choice_from_query = false;
1696                            }
1697
1698                            if stop_propagation {
1699                                stop_propagation_requested = true;
1700                                if stop_propagation_requested_by.is_none() {
1701                                    stop_propagation_requested_by = Some(node_id);
1702                                }
1703                                if is_wheel && wheel_stop_node.is_none() {
1704                                    wheel_stop_node = Some(node_id);
1705                                }
1706                                stopped_in_capture = true;
1707                                break;
1708                            }
1709                        }
1710                    },
1711                );
1712                if let Some(capture_elapsed) = capture_elapsed {
1713                    self.debug_stats.dispatch_widget_capture_time += capture_elapsed;
1714                }
1715            }
1716
1717            if !stopped_in_capture {
1718                let mut bubble_ctx = input_ctx.clone();
1719                bubble_ctx.dispatch_phase = InputDispatchPhase::Bubble;
1720
1721                let (_, bubble_elapsed) = fret_perf::measure_span(
1722                    self.debug_enabled,
1723                    trace_enabled,
1724                    || tracing::trace_span!("fret.ui.dispatch.widget_bubble"),
1725                    || {
1726                        for (chain_index, (node_id, event_for_node)) in chain.iter().enumerate() {
1727                            let node_id = *node_id;
1728                            let (
1729                                pointer_hit_pressable_target,
1730                                pointer_hit_pressable_target_in_descendant_subtree,
1731                            ) = pointer_hit_pressable.map_or(
1732                                (None, false),
1733                                |(target, pressable_index)| {
1734                                    (Some(target), pressable_index < chain_index)
1735                                },
1736                            );
1737                            let (
1738                                invalidations,
1739                                scroll_handle_invalidations,
1740                                scroll_target_invalidations,
1741                                requested_focus,
1742                                requested_focus_target,
1743                                requested_capture,
1744                                requested_cursor,
1745                                notify_requested,
1746                                notify_requested_location,
1747                                stop_propagation,
1748                            ) = self.with_widget_mut(node_id, |widget, tree| {
1749                                let (children, bounds) = tree
1750                                    .nodes
1751                                    .get(node_id)
1752                                    .map(|n| (n.children.as_slice(), n.bounds))
1753                                    .unwrap_or((&[][..], Rect::default()));
1754                                let mut cx = EventCx {
1755                                    app,
1756                                    services: &mut *services,
1757                                    node: node_id,
1758                                    layer_root: tree.node_root(node_id),
1759                                    window: tree.window,
1760                                    pointer_id: event_pointer_id_for_capture,
1761                                    scale_factor: tree.last_layout_scale_factor.unwrap_or(1.0),
1762                                    event_window_position,
1763                                    event_window_wheel_delta,
1764                                    input_ctx: bubble_ctx.clone(),
1765                                    pointer_hit_is_text_input,
1766                                    pointer_hit_is_pressable,
1767                                    pointer_hit_pressable_target,
1768                                    pointer_hit_pressable_target_in_descendant_subtree,
1769                                    prevented_default_actions: &mut prevented_default_actions,
1770                                    children,
1771                                    focus: tree.focus,
1772                                    captured: event_pointer_id_for_capture
1773                                        .and_then(|p| tree.captured.get(&p).copied()),
1774                                    bounds,
1775                                    invalidations: Vec::new(),
1776                                    scroll_handle_invalidations: Vec::new(),
1777                                    scroll_target_invalidations: Vec::new(),
1778                                    requested_focus: None,
1779                                    requested_focus_target: None,
1780                                    requested_capture: None,
1781                                    requested_cursor: None,
1782                                    notify_requested: false,
1783                                    notify_requested_location: None,
1784                                    stop_propagation: false,
1785                                };
1786                                widget.event(&mut cx, event_for_node);
1787                                if cx.requested_cursor.is_none()
1788                                    && matches!(event_for_node, Event::Pointer(_))
1789                                    && cx.input_ctx.caps.ui.cursor_icons
1790                                    && let Some(position) = event_position(event_for_node)
1791                                {
1792                                    cx.requested_cursor =
1793                                        widget.cursor_icon_at(bounds, position, &cx.input_ctx);
1794                                }
1795                                (
1796                                    cx.invalidations,
1797                                    cx.scroll_handle_invalidations,
1798                                    cx.scroll_target_invalidations,
1799                                    cx.requested_focus,
1800                                    cx.requested_focus_target,
1801                                    cx.requested_capture,
1802                                    cx.requested_cursor,
1803                                    cx.notify_requested,
1804                                    cx.notify_requested_location,
1805                                    cx.stop_propagation,
1806                                )
1807                            });
1808
1809                            if !invalidations.is_empty()
1810                                || !scroll_handle_invalidations.is_empty()
1811                                || !scroll_target_invalidations.is_empty()
1812                                || requested_focus.is_some()
1813                                || requested_focus_target.is_some()
1814                                || requested_capture.is_some()
1815                                || notify_requested
1816                            {
1817                                needs_redraw = true;
1818                            }
1819
1820                            for (id, inv) in invalidations {
1821                                self.mark_invalidation(id, inv);
1822                            }
1823                            let mut resolved_scroll_handle_invalidations = Vec::new();
1824                            self.extend_live_bound_scroll_handle_invalidations(
1825                                app,
1826                                &scroll_handle_invalidations,
1827                                &mut resolved_scroll_handle_invalidations,
1828                            );
1829                            for (id, inv) in resolved_scroll_handle_invalidations {
1830                                self.mark_invalidation(id, inv);
1831                            }
1832                            let mut resolved_scroll_target_invalidations = Vec::new();
1833                            self.extend_live_scroll_target_invalidations(
1834                                app,
1835                                &scroll_target_invalidations,
1836                                &mut resolved_scroll_target_invalidations,
1837                            );
1838                            for (id, inv) in resolved_scroll_target_invalidations {
1839                                self.mark_invalidation(id, inv);
1840                            }
1841                            if notify_requested {
1842                                self.debug_record_notify_request(
1843                                    app.frame_id(),
1844                                    node_id,
1845                                    notify_requested_location,
1846                                );
1847                                self.mark_invalidation_with_source(
1848                                    node_id,
1849                                    Invalidation::Paint,
1850                                    UiDebugInvalidationSource::Notify,
1851                                );
1852                            }
1853
1854                            let focus_requested_now =
1855                                requested_focus.is_some() || requested_focus_target.is_some();
1856                            if let Some(focus) = self.resolve_requested_focus(
1857                                app,
1858                                requested_focus,
1859                                requested_focus_target,
1860                            ) && self.focus_request_is_allowed(
1861                                app,
1862                                self.window,
1863                                dispatch_cx.active_focus_roots.as_slice(),
1864                                focus,
1865                                Some(&dispatch_cx.focus_snapshot),
1866                            ) {
1867                                focus_requested = true;
1868                                if let Some(prev) = self.focus {
1869                                    self.mark_invalidation(prev, Invalidation::Paint);
1870                                }
1871                                self.focus = Some(focus);
1872                                self.mark_invalidation(focus, Invalidation::Paint);
1873                                // Avoid scrolling during pointer-driven focus changes:
1874                                // programmatic scroll-to-focus can move content under a stationary cursor,
1875                                // causing pointer activation to miss/cancel (especially for nested pressables).
1876                                //
1877                                // Keyboard traversal still scrolls focused nodes into view.
1878                                if !matches!(
1879                                    event_for_node,
1880                                    Event::Pointer(_) | Event::PointerCancel(_)
1881                                ) {
1882                                    self.scroll_node_into_view(app, focus);
1883                                }
1884                            } else if focus_requested_now {
1885                                focus_requested = true;
1886                            }
1887
1888                            if let Some(capture) = requested_capture
1889                                && let Some(pointer_id) = event_pointer_id_for_capture
1890                            {
1891                                match capture {
1892                                    Some(node) => {
1893                                        let allow = !dock_drag_affects_window
1894                                            || dock_drag_capture_anchor == Some(node);
1895                                        if allow {
1896                                            if !matches!(event, Event::PointerCancel(_))
1897                                                && let Some(old_capture) =
1898                                                    self.captured.get(&pointer_id).copied()
1899                                                && old_capture != node
1900                                                && node_in_active_layers(old_capture)
1901                                            {
1902                                                let mut cancel_ctx = input_ctx.clone();
1903                                                cancel_ctx.dispatch_phase =
1904                                                    InputDispatchPhase::Bubble;
1905                                                let cancel_event =
1906                                                    pointer_cancel_event_for_capture_switch(
1907                                                        event, pointer_id,
1908                                                    );
1909                                                let _ = self.dispatch_event_to_node_chain(
1910                                                    app,
1911                                                    services,
1912                                                    &dispatch_cx,
1913                                                    &cancel_ctx,
1914                                                    old_capture,
1915                                                    &cancel_event,
1916                                                    &mut needs_redraw,
1917                                                    &mut invalidation_visited,
1918                                                );
1919                                            }
1920                                            self.captured.insert(pointer_id, node);
1921                                        }
1922                                    }
1923                                    None => {
1924                                        self.captured.remove(&pointer_id);
1925                                    }
1926                                }
1927                            }
1928
1929                            if let Some(requested_cursor) = requested_cursor
1930                                && (cursor_choice.is_none() || cursor_choice_from_query)
1931                            {
1932                                cursor_choice = Some(requested_cursor);
1933                                cursor_choice_from_query = false;
1934                            }
1935
1936                            if stop_propagation {
1937                                stop_propagation_requested = true;
1938                                if stop_propagation_requested_by.is_none() {
1939                                    stop_propagation_requested_by = Some(node_id);
1940                                }
1941                                if is_wheel && wheel_stop_node.is_none() {
1942                                    wheel_stop_node = Some(node_id);
1943                                }
1944                            }
1945
1946                            let captured_now = event_pointer_id_for_capture
1947                                .and_then(|p| self.captured.get(&p).copied());
1948                            if (captured_now.is_some()
1949                                && !touch_drag_reroute_from_pressable_capture)
1950                                || stop_propagation
1951                            {
1952                                break;
1953                            }
1954                        }
1955                    },
1956                );
1957                if let Some(bubble_elapsed) = bubble_elapsed {
1958                    self.debug_stats.dispatch_widget_bubble_time += bubble_elapsed;
1959                }
1960            }
1961        } else if matches!(event, Event::KeyDown { .. } | Event::KeyUp { .. }) {
1962            // Key events must be scoped to the active focus layers (including focus barriers that
1963            // do not block pointer input). When no focused node is available, default to the
1964            // combined barrier root instead of the underlay base root.
1965            let key_start = self
1966                .focus
1967                .filter(|&n| dispatch_cx.node_in_active_focus_layers(n))
1968                .or(dispatch_cx.barrier_root)
1969                .unwrap_or(node_id);
1970
1971            let mut chain: Vec<NodeId> = Vec::new();
1972            let mut cur = Some(key_start);
1973            while let Some(id) = cur {
1974                chain.push(id);
1975                if dispatch_cx.focus_snapshot.pre.get(id).is_none() {
1976                    debug_assert!(
1977                        false,
1978                        "dispatch/window: key chain node missing from focus snapshot (node={id:?}, frame_id={:?}, window={:?})",
1979                        dispatch_cx.focus_snapshot.frame_id, dispatch_cx.focus_snapshot.window
1980                    );
1981                    break;
1982                }
1983                cur = dispatch_cx.focus_snapshot.parent.get(id).copied().flatten();
1984            }
1985
1986            let mut stopped_in_capture = false;
1987            {
1988                let mut capture_ctx = input_ctx.clone();
1989                capture_ctx.dispatch_phase = InputDispatchPhase::Capture;
1990
1991                let (_, capture_elapsed) = fret_perf::measure_span(
1992                    self.debug_enabled,
1993                    trace_enabled,
1994                    || tracing::trace_span!("fret.ui.dispatch.widget_capture", kind = "key"),
1995                    || {
1996                        for &node_id in chain.iter().rev() {
1997                            let (
1998                                invalidations,
1999                                scroll_handle_invalidations,
2000                                scroll_target_invalidations,
2001                                requested_focus,
2002                                requested_focus_target,
2003                                requested_capture,
2004                                requested_cursor,
2005                                notify_requested,
2006                                notify_requested_location,
2007                                stop_propagation,
2008                            ) = self.with_widget_mut(node_id, |widget, tree| {
2009                                let (children, bounds) = tree
2010                                    .nodes
2011                                    .get(node_id)
2012                                    .map(|n| (n.children.as_slice(), n.bounds))
2013                                    .unwrap_or((&[][..], Rect::default()));
2014                                let mut cx = EventCx {
2015                                    app,
2016                                    services: &mut *services,
2017                                    node: node_id,
2018                                    layer_root: tree.node_root(node_id),
2019                                    window: tree.window,
2020                                    pointer_id: event_pointer_id_for_capture,
2021                                    scale_factor: tree.last_layout_scale_factor.unwrap_or(1.0),
2022                                    event_window_position,
2023                                    event_window_wheel_delta,
2024                                    input_ctx: capture_ctx.clone(),
2025                                    pointer_hit_is_text_input: false,
2026                                    pointer_hit_is_pressable: false,
2027                                    pointer_hit_pressable_target: None,
2028                                    pointer_hit_pressable_target_in_descendant_subtree: false,
2029                                    prevented_default_actions: &mut prevented_default_actions,
2030                                    children,
2031                                    focus: tree.focus,
2032                                    captured: event_pointer_id_for_capture
2033                                        .and_then(|p| tree.captured.get(&p).copied()),
2034                                    bounds,
2035                                    invalidations: Vec::new(),
2036                                    scroll_handle_invalidations: Vec::new(),
2037                                    scroll_target_invalidations: Vec::new(),
2038                                    requested_focus: None,
2039                                    requested_focus_target: None,
2040                                    requested_capture: None,
2041                                    requested_cursor: None,
2042                                    notify_requested: false,
2043                                    notify_requested_location: None,
2044                                    stop_propagation: false,
2045                                };
2046                                widget.event_capture(&mut cx, event);
2047                                (
2048                                    cx.invalidations,
2049                                    cx.scroll_handle_invalidations,
2050                                    cx.scroll_target_invalidations,
2051                                    cx.requested_focus,
2052                                    cx.requested_focus_target,
2053                                    cx.requested_capture,
2054                                    cx.requested_cursor,
2055                                    cx.notify_requested,
2056                                    cx.notify_requested_location,
2057                                    cx.stop_propagation,
2058                                )
2059                            });
2060
2061                            if !invalidations.is_empty()
2062                                || !scroll_handle_invalidations.is_empty()
2063                                || !scroll_target_invalidations.is_empty()
2064                                || requested_focus.is_some()
2065                                || requested_focus_target.is_some()
2066                                || requested_capture.is_some()
2067                                || notify_requested
2068                            {
2069                                needs_redraw = true;
2070                            }
2071
2072                            for (id, inv) in invalidations {
2073                                self.mark_invalidation(id, inv);
2074                            }
2075                            let mut resolved_scroll_handle_invalidations = Vec::new();
2076                            self.extend_live_bound_scroll_handle_invalidations(
2077                                app,
2078                                &scroll_handle_invalidations,
2079                                &mut resolved_scroll_handle_invalidations,
2080                            );
2081                            for (id, inv) in resolved_scroll_handle_invalidations {
2082                                self.mark_invalidation(id, inv);
2083                            }
2084                            let mut resolved_scroll_target_invalidations = Vec::new();
2085                            self.extend_live_scroll_target_invalidations(
2086                                app,
2087                                &scroll_target_invalidations,
2088                                &mut resolved_scroll_target_invalidations,
2089                            );
2090                            for (id, inv) in resolved_scroll_target_invalidations {
2091                                self.mark_invalidation(id, inv);
2092                            }
2093                            if notify_requested {
2094                                self.debug_record_notify_request(
2095                                    app.frame_id(),
2096                                    node_id,
2097                                    notify_requested_location,
2098                                );
2099                                self.mark_invalidation_with_source(
2100                                    node_id,
2101                                    Invalidation::Paint,
2102                                    UiDebugInvalidationSource::Notify,
2103                                );
2104                            }
2105
2106                            let focus_requested_now =
2107                                requested_focus.is_some() || requested_focus_target.is_some();
2108                            if let Some(focus) = self.resolve_requested_focus(
2109                                app,
2110                                requested_focus,
2111                                requested_focus_target,
2112                            ) && self.focus_request_is_allowed(
2113                                app,
2114                                self.window,
2115                                dispatch_cx.active_focus_roots.as_slice(),
2116                                focus,
2117                                Some(&dispatch_cx.focus_snapshot),
2118                            ) {
2119                                focus_requested = true;
2120                                if let Some(prev) = self.focus {
2121                                    self.mark_invalidation(prev, Invalidation::Paint);
2122                                }
2123                                self.focus = Some(focus);
2124                                self.mark_invalidation(focus, Invalidation::Paint);
2125                                self.scroll_node_into_view(app, focus);
2126                            } else if focus_requested_now {
2127                                focus_requested = true;
2128                            }
2129
2130                            if let Some(capture) = requested_capture
2131                                && let Some(pointer_id) = event_pointer_id_for_capture
2132                            {
2133                                match capture {
2134                                    Some(node) => {
2135                                        let allow = !dock_drag_affects_window
2136                                            || dock_drag_capture_anchor == Some(node);
2137                                        if allow {
2138                                            self.captured.insert(pointer_id, node);
2139                                        }
2140                                    }
2141                                    None => {
2142                                        self.captured.remove(&pointer_id);
2143                                    }
2144                                }
2145                            }
2146
2147                            if requested_cursor.is_some() && cursor_choice.is_none() {
2148                                cursor_choice = requested_cursor;
2149                            }
2150
2151                            if stop_propagation {
2152                                stop_propagation_requested = true;
2153                                if stop_propagation_requested_by.is_none() {
2154                                    stop_propagation_requested_by = Some(node_id);
2155                                }
2156                                stopped_in_capture = true;
2157                                break;
2158                            }
2159                        }
2160                    },
2161                );
2162                if let Some(capture_elapsed) = capture_elapsed {
2163                    self.debug_stats.dispatch_widget_capture_time += capture_elapsed;
2164                }
2165            }
2166            if !stopped_in_capture {
2167                let mut bubble_ctx = input_ctx.clone();
2168                bubble_ctx.dispatch_phase = InputDispatchPhase::Bubble;
2169
2170                let (_, bubble_elapsed) = fret_perf::measure_span(
2171                    self.debug_enabled,
2172                    trace_enabled,
2173                    || tracing::trace_span!("fret.ui.dispatch.widget_bubble", kind = "key"),
2174                    || {
2175                        for node_id in chain {
2176                            let (
2177                                invalidations,
2178                                scroll_handle_invalidations,
2179                                scroll_target_invalidations,
2180                                requested_focus,
2181                                requested_focus_target,
2182                                requested_capture,
2183                                requested_cursor,
2184                                notify_requested,
2185                                notify_requested_location,
2186                                stop_propagation,
2187                            ) = self.with_widget_mut(node_id, |widget, tree| {
2188                                let (children, bounds) = tree
2189                                    .nodes
2190                                    .get(node_id)
2191                                    .map(|n| (n.children.as_slice(), n.bounds))
2192                                    .unwrap_or((&[][..], Rect::default()));
2193                                let mut cx = EventCx {
2194                                    app,
2195                                    services: &mut *services,
2196                                    node: node_id,
2197                                    layer_root: tree.node_root(node_id),
2198                                    window: tree.window,
2199                                    pointer_id: event_pointer_id_for_capture,
2200                                    scale_factor: tree.last_layout_scale_factor.unwrap_or(1.0),
2201                                    event_window_position,
2202                                    event_window_wheel_delta,
2203                                    input_ctx: bubble_ctx.clone(),
2204                                    pointer_hit_is_text_input: false,
2205                                    pointer_hit_is_pressable: false,
2206                                    pointer_hit_pressable_target: None,
2207                                    pointer_hit_pressable_target_in_descendant_subtree: false,
2208                                    prevented_default_actions: &mut prevented_default_actions,
2209                                    children,
2210                                    focus: tree.focus,
2211                                    captured: event_pointer_id_for_capture
2212                                        .and_then(|p| tree.captured.get(&p).copied()),
2213                                    bounds,
2214                                    invalidations: Vec::new(),
2215                                    scroll_handle_invalidations: Vec::new(),
2216                                    scroll_target_invalidations: Vec::new(),
2217                                    requested_focus: None,
2218                                    requested_focus_target: None,
2219                                    requested_capture: None,
2220                                    requested_cursor: None,
2221                                    notify_requested: false,
2222                                    notify_requested_location: None,
2223                                    stop_propagation: false,
2224                                };
2225                                widget.event(&mut cx, event);
2226                                (
2227                                    cx.invalidations,
2228                                    cx.scroll_handle_invalidations,
2229                                    cx.scroll_target_invalidations,
2230                                    cx.requested_focus,
2231                                    cx.requested_focus_target,
2232                                    cx.requested_capture,
2233                                    cx.requested_cursor,
2234                                    cx.notify_requested,
2235                                    cx.notify_requested_location,
2236                                    cx.stop_propagation,
2237                                )
2238                            });
2239
2240                            if !invalidations.is_empty()
2241                                || !scroll_handle_invalidations.is_empty()
2242                                || !scroll_target_invalidations.is_empty()
2243                                || requested_focus.is_some()
2244                                || requested_focus_target.is_some()
2245                                || requested_capture.is_some()
2246                                || notify_requested
2247                            {
2248                                needs_redraw = true;
2249                            }
2250
2251                            for (id, inv) in invalidations {
2252                                self.mark_invalidation(id, inv);
2253                            }
2254                            let mut resolved_scroll_handle_invalidations = Vec::new();
2255                            self.extend_live_bound_scroll_handle_invalidations(
2256                                app,
2257                                &scroll_handle_invalidations,
2258                                &mut resolved_scroll_handle_invalidations,
2259                            );
2260                            for (id, inv) in resolved_scroll_handle_invalidations {
2261                                self.mark_invalidation(id, inv);
2262                            }
2263                            let mut resolved_scroll_target_invalidations = Vec::new();
2264                            self.extend_live_scroll_target_invalidations(
2265                                app,
2266                                &scroll_target_invalidations,
2267                                &mut resolved_scroll_target_invalidations,
2268                            );
2269                            for (id, inv) in resolved_scroll_target_invalidations {
2270                                self.mark_invalidation(id, inv);
2271                            }
2272                            if notify_requested {
2273                                self.debug_record_notify_request(
2274                                    app.frame_id(),
2275                                    node_id,
2276                                    notify_requested_location,
2277                                );
2278                                self.mark_invalidation_with_source(
2279                                    node_id,
2280                                    Invalidation::Paint,
2281                                    UiDebugInvalidationSource::Notify,
2282                                );
2283                            }
2284
2285                            let focus_requested_now =
2286                                requested_focus.is_some() || requested_focus_target.is_some();
2287                            if let Some(focus) = self.resolve_requested_focus(
2288                                app,
2289                                requested_focus,
2290                                requested_focus_target,
2291                            ) && self.focus_request_is_allowed(
2292                                app,
2293                                self.window,
2294                                dispatch_cx.active_focus_roots.as_slice(),
2295                                focus,
2296                                Some(&dispatch_cx.focus_snapshot),
2297                            ) {
2298                                focus_requested = true;
2299                                if let Some(prev) = self.focus {
2300                                    self.mark_invalidation(prev, Invalidation::Paint);
2301                                }
2302                                self.focus = Some(focus);
2303                                self.mark_invalidation(focus, Invalidation::Paint);
2304                                self.scroll_node_into_view(app, focus);
2305                            } else if focus_requested_now {
2306                                focus_requested = true;
2307                            }
2308
2309                            if let Some(capture) = requested_capture
2310                                && let Some(pointer_id) = event_pointer_id_for_capture
2311                            {
2312                                match capture {
2313                                    Some(node) => {
2314                                        let allow = !dock_drag_affects_window
2315                                            || dock_drag_capture_anchor == Some(node);
2316                                        if allow {
2317                                            self.captured.insert(pointer_id, node);
2318                                        }
2319                                    }
2320                                    None => {
2321                                        self.captured.remove(&pointer_id);
2322                                    }
2323                                }
2324                            }
2325
2326                            if requested_cursor.is_some() && cursor_choice.is_none() {
2327                                cursor_choice = requested_cursor;
2328                            }
2329
2330                            if stop_propagation {
2331                                stop_propagation_requested = true;
2332                                if stop_propagation_requested_by.is_none() {
2333                                    stop_propagation_requested_by = Some(node_id);
2334                                }
2335                                break;
2336                            }
2337                        }
2338                    },
2339                );
2340                if let Some(bubble_elapsed) = bubble_elapsed {
2341                    self.debug_stats.dispatch_widget_bubble_time += bubble_elapsed;
2342                }
2343            }
2344
2345            let stopped_by_dismissible_root_hook = stop_propagation_requested
2346                && self.window.is_some_and(|window| {
2347                    stop_propagation_requested_by
2348                        .and_then(|node| self.nodes.get(node).and_then(|n| n.element))
2349                        .and_then(|element| {
2350                            crate::elements::with_element_state(
2351                                app,
2352                                window,
2353                                element,
2354                                crate::action::DismissibleActionHooks::default,
2355                                |hooks| hooks.on_dismiss_request.clone(),
2356                            )
2357                        })
2358                        .is_some()
2359                });
2360
2361            if defer_escape_overlay_dismiss
2362                && !stopped_by_dismissible_root_hook
2363                && (!stop_propagation_requested || !focus_requested)
2364                && let Event::KeyDown {
2365                    key: fret_core::KeyCode::Escape,
2366                    repeat: false,
2367                    ..
2368                } = event
2369                && let Some(window) = self.window
2370                && self.dismiss_topmost_overlay_on_escape(app, window, base_root, barrier_root)
2371            {
2372                self.request_redraw_coalesced(app);
2373                return;
2374            }
2375        } else {
2376            loop {
2377                let (
2378                    invalidations,
2379                    scroll_handle_invalidations,
2380                    scroll_target_invalidations,
2381                    requested_focus,
2382                    requested_focus_target,
2383                    requested_capture,
2384                    requested_cursor,
2385                    notify_requested,
2386                    notify_requested_location,
2387                    stop_propagation,
2388                ) = self.with_widget_mut(node_id, |widget, tree| {
2389                    let (children, bounds) = tree
2390                        .nodes
2391                        .get(node_id)
2392                        .map(|n| (n.children.as_slice(), n.bounds))
2393                        .unwrap_or((&[][..], Rect::default()));
2394                    let mut cx = EventCx {
2395                        app,
2396                        services: &mut *services,
2397                        node: node_id,
2398                        layer_root: tree.node_root(node_id),
2399                        window: tree.window,
2400                        pointer_id: event_pointer_id_for_capture,
2401                        scale_factor: tree.last_layout_scale_factor.unwrap_or(1.0),
2402                        event_window_position,
2403                        event_window_wheel_delta,
2404                        input_ctx: input_ctx.clone(),
2405                        pointer_hit_is_text_input: false,
2406                        pointer_hit_is_pressable: false,
2407                        pointer_hit_pressable_target: None,
2408                        pointer_hit_pressable_target_in_descendant_subtree: false,
2409                        prevented_default_actions: &mut prevented_default_actions,
2410                        children,
2411                        focus: tree.focus,
2412                        captured: event_pointer_id_for_capture
2413                            .and_then(|p| tree.captured.get(&p).copied()),
2414                        bounds,
2415                        invalidations: Vec::new(),
2416                        scroll_handle_invalidations: Vec::new(),
2417                        scroll_target_invalidations: Vec::new(),
2418                        requested_focus: None,
2419                        requested_focus_target: None,
2420                        requested_capture: None,
2421                        requested_cursor: None,
2422                        notify_requested: false,
2423                        notify_requested_location: None,
2424                        stop_propagation: false,
2425                    };
2426                    widget.event(&mut cx, event);
2427                    (
2428                        cx.invalidations,
2429                        cx.scroll_handle_invalidations,
2430                        cx.scroll_target_invalidations,
2431                        cx.requested_focus,
2432                        cx.requested_focus_target,
2433                        cx.requested_capture,
2434                        cx.requested_cursor,
2435                        cx.notify_requested,
2436                        cx.notify_requested_location,
2437                        cx.stop_propagation,
2438                    )
2439                });
2440                if !invalidations.is_empty()
2441                    || !scroll_handle_invalidations.is_empty()
2442                    || !scroll_target_invalidations.is_empty()
2443                    || requested_focus.is_some()
2444                    || requested_focus_target.is_some()
2445                    || requested_capture.is_some()
2446                    || notify_requested
2447                {
2448                    needs_redraw = true;
2449                }
2450
2451                for (id, inv) in invalidations {
2452                    self.mark_invalidation(id, inv);
2453                }
2454                let mut resolved_scroll_handle_invalidations = Vec::new();
2455                self.extend_live_bound_scroll_handle_invalidations(
2456                    app,
2457                    &scroll_handle_invalidations,
2458                    &mut resolved_scroll_handle_invalidations,
2459                );
2460                for (id, inv) in resolved_scroll_handle_invalidations {
2461                    self.mark_invalidation(id, inv);
2462                }
2463                let mut resolved_scroll_target_invalidations = Vec::new();
2464                self.extend_live_scroll_target_invalidations(
2465                    app,
2466                    &scroll_target_invalidations,
2467                    &mut resolved_scroll_target_invalidations,
2468                );
2469                for (id, inv) in resolved_scroll_target_invalidations {
2470                    self.mark_invalidation(id, inv);
2471                }
2472                if notify_requested {
2473                    self.debug_record_notify_request(
2474                        app.frame_id(),
2475                        node_id,
2476                        notify_requested_location,
2477                    );
2478                    self.mark_invalidation_with_source(
2479                        node_id,
2480                        Invalidation::Paint,
2481                        UiDebugInvalidationSource::Notify,
2482                    );
2483                }
2484
2485                let focus_requested_now =
2486                    requested_focus.is_some() || requested_focus_target.is_some();
2487                if let Some(focus) =
2488                    self.resolve_requested_focus(app, requested_focus, requested_focus_target)
2489                    && self.focus_request_is_allowed(
2490                        app,
2491                        self.window,
2492                        dispatch_cx.active_focus_roots.as_slice(),
2493                        focus,
2494                        Some(&dispatch_cx.focus_snapshot),
2495                    )
2496                {
2497                    focus_requested = true;
2498                    if let Some(prev) = self.focus {
2499                        self.mark_invalidation(prev, Invalidation::Paint);
2500                    }
2501                    self.focus = Some(focus);
2502                    self.mark_invalidation(focus, Invalidation::Paint);
2503                    // Avoid scrolling during pointer-driven focus changes:
2504                    // programmatic scroll-to-focus can move content under a stationary cursor,
2505                    // causing pointer activation to miss/cancel (especially for nested pressables).
2506                    //
2507                    // Keyboard traversal still scrolls focused nodes into view.
2508                    if !matches!(event, Event::Pointer(_) | Event::PointerCancel(_)) {
2509                        self.scroll_node_into_view(app, focus);
2510                    }
2511                } else if focus_requested_now {
2512                    focus_requested = true;
2513                }
2514
2515                if let Some(capture) = requested_capture
2516                    && let Some(pointer_id) = event_pointer_id_for_capture
2517                {
2518                    match capture {
2519                        Some(node) => {
2520                            let allow =
2521                                !dock_drag_affects_window || dock_drag_capture_anchor == Some(node);
2522                            if allow {
2523                                self.captured.insert(pointer_id, node);
2524                            }
2525                        }
2526                        None => {
2527                            self.captured.remove(&pointer_id);
2528                        }
2529                    }
2530                };
2531
2532                if requested_cursor.is_some() && cursor_choice.is_none() {
2533                    cursor_choice = requested_cursor;
2534                }
2535
2536                if stop_propagation {
2537                    stop_propagation_requested = true;
2538                    if stop_propagation_requested_by.is_none() {
2539                        stop_propagation_requested_by = Some(node_id);
2540                    }
2541                    if is_wheel && wheel_stop_node.is_none() {
2542                        wheel_stop_node = Some(node_id);
2543                    }
2544                }
2545
2546                let captured_now =
2547                    event_pointer_id_for_capture.and_then(|p| self.captured.get(&p).copied());
2548                if captured_now.is_some() || stop_propagation {
2549                    break;
2550                }
2551
2552                if dispatch_cx.focus_snapshot.pre.get(node_id).is_none() {
2553                    tracing::warn!(
2554                        node = ?node_id,
2555                        frame_id = ?dispatch_cx.focus_snapshot.frame_id,
2556                        window = ?dispatch_cx.focus_snapshot.window,
2557                        "dispatch/window: bubble chain node missing from focus snapshot"
2558                    );
2559                    break;
2560                }
2561                node_id = match dispatch_cx
2562                    .focus_snapshot
2563                    .parent
2564                    .get(node_id)
2565                    .copied()
2566                    .flatten()
2567                {
2568                    Some(parent) => parent,
2569                    None => break,
2570                };
2571            }
2572        }
2573
2574        if let Event::Pointer(PointerEvent::Down {
2575            button,
2576            pointer_type,
2577            ..
2578        }) = event
2579            && *button == fret_core::MouseButton::Left
2580            && !focus_requested
2581            && !prevented_default_actions.contains(fret_runtime::DefaultAction::FocusOnPointerDown)
2582            && captured.is_none()
2583            && internal_drag_target.is_none()
2584            && let Some(window) = self.window
2585            && let Some(hit) = pointer_hit
2586        {
2587            let candidate = self.first_focusable_ancestor_including_declarative(app, window, hit);
2588            if let Some(focus) = candidate
2589                && self.focus_request_is_allowed(
2590                    app,
2591                    self.window,
2592                    dispatch_cx.active_focus_roots.as_slice(),
2593                    focus,
2594                    Some(&dispatch_cx.focus_snapshot),
2595                )
2596            {
2597                if let Some(prev) = self.focus {
2598                    self.mark_invalidation(prev, Invalidation::Paint);
2599                }
2600                self.focus = Some(focus);
2601                self.mark_invalidation(focus, Invalidation::Paint);
2602
2603                // Mobile-friendly best-effort: if touch input focused a text-editing widget,
2604                // request the virtual keyboard within the same input turn so platforms that
2605                // require user activation can comply (ADR 0261).
2606                if *pointer_type == fret_core::PointerType::Touch && self.focus_is_text_input(app) {
2607                    app.push_effect(Effect::ImeRequestVirtualKeyboard {
2608                        window,
2609                        visible: true,
2610                    });
2611                }
2612
2613                // Pointer-driven focus should not scroll: the user is already interacting at the
2614                // pointer location, and scrolling here can move content under the cursor between
2615                // pointer-down and pointer-up.
2616                needs_redraw = true;
2617            }
2618        }
2619
2620        if is_wheel
2621            && let Some(scroll_target) = wheel_stop_node
2622            && let Some(window) = self.window
2623        {
2624            let is_scroll_target = declarative::with_window_frame(app, window, |window_frame| {
2625                let window_frame = window_frame?;
2626                let record = window_frame.instances.get(scroll_target)?;
2627                Some(matches!(
2628                    record.instance,
2629                    declarative::ElementInstance::Scroll(_)
2630                        | declarative::ElementInstance::VirtualList(_)
2631                        | declarative::ElementInstance::WheelRegion(_)
2632                        | declarative::ElementInstance::Scrollbar(_)
2633                ))
2634            })
2635            .unwrap_or(false);
2636
2637            if is_scroll_target {
2638                struct ScrollDismissHookHost<'a, H: crate::UiHost> {
2639                    app: &'a mut H,
2640                    window: AppWindowId,
2641                    element: crate::GlobalElementId,
2642                }
2643
2644                impl<H: crate::UiHost> crate::action::UiActionHost for ScrollDismissHookHost<'_, H> {
2645                    fn models_mut(&mut self) -> &mut fret_runtime::ModelStore {
2646                        self.app.models_mut()
2647                    }
2648
2649                    fn push_effect(&mut self, effect: Effect) {
2650                        match effect {
2651                            Effect::SetTimer {
2652                                window: Some(window),
2653                                token,
2654                                ..
2655                            } if window == self.window => {
2656                                crate::elements::record_timer_target(
2657                                    &mut *self.app,
2658                                    window,
2659                                    token,
2660                                    self.element,
2661                                );
2662                            }
2663                            Effect::CancelTimer { token } => {
2664                                crate::elements::clear_timer_target(
2665                                    &mut *self.app,
2666                                    self.window,
2667                                    token,
2668                                );
2669                            }
2670                            _ => {}
2671                        }
2672                        self.app.push_effect(effect);
2673                    }
2674
2675                    fn request_redraw(&mut self, window: AppWindowId) {
2676                        self.app.request_redraw(window);
2677                    }
2678
2679                    fn next_timer_token(&mut self) -> fret_runtime::TimerToken {
2680                        self.app.next_timer_token()
2681                    }
2682
2683                    fn next_clipboard_token(&mut self) -> fret_runtime::ClipboardToken {
2684                        self.app.next_clipboard_token()
2685                    }
2686
2687                    fn next_share_sheet_token(&mut self) -> fret_runtime::ShareSheetToken {
2688                        self.app.next_share_sheet_token()
2689                    }
2690                }
2691
2692                let mut dismissed_any = false;
2693                for layer_id in self.visible_layers_in_paint_order() {
2694                    let Some(layer) = self.layers.get(layer_id) else {
2695                        continue;
2696                    };
2697                    if layer.scroll_dismiss_elements.is_empty() {
2698                        continue;
2699                    }
2700                    let should_dismiss = layer
2701                        .scroll_dismiss_elements
2702                        .iter()
2703                        .copied()
2704                        .filter_map(|element| {
2705                            self.resolve_live_attached_node_for_element(app, Some(window), element)
2706                        })
2707                        .any(|node| self.is_descendant(scroll_target, node));
2708                    if !should_dismiss {
2709                        continue;
2710                    }
2711                    let Some(root_element) = self.nodes.get(layer.root).and_then(|n| n.element)
2712                    else {
2713                        continue;
2714                    };
2715                    let hook = crate::elements::with_element_state(
2716                        app,
2717                        window,
2718                        root_element,
2719                        crate::action::DismissibleActionHooks::default,
2720                        |hooks| hooks.on_dismiss_request.clone(),
2721                    );
2722                    let Some(hook) = hook else {
2723                        continue;
2724                    };
2725                    let mut host = ScrollDismissHookHost {
2726                        app,
2727                        window,
2728                        element: root_element,
2729                    };
2730                    let mut req =
2731                        crate::action::DismissRequestCx::new(crate::action::DismissReason::Scroll);
2732                    hook(
2733                        &mut host,
2734                        crate::action::ActionCx {
2735                            window,
2736                            target: root_element,
2737                        },
2738                        &mut req,
2739                    );
2740                    dismissed_any = true;
2741                }
2742
2743                if dismissed_any {
2744                    needs_redraw = true;
2745                }
2746            }
2747        }
2748
2749        if matches!(event, Event::PointerCancel(_))
2750            && let Some(pointer_id) = event_pointer_id_for_capture
2751        {
2752            self.captured.remove(&pointer_id);
2753        }
2754
2755        if let Event::PointerCancel(e) = event
2756            && let Some(window) = self.window
2757            && pointer_type_supports_hover(e.pointer_type)
2758        {
2759            let (prev_element, prev_node, _next_element, _next_node) =
2760                crate::elements::update_hovered_pressable(app, window, None);
2761            if prev_node.is_some() {
2762                needs_redraw = true;
2763                self.debug_record_hover_edge_pressable();
2764                if let Some(node) = prev_node {
2765                    self.mark_invalidation_dedup_with_source(
2766                        node,
2767                        Invalidation::Paint,
2768                        &mut invalidation_visited,
2769                        UiDebugInvalidationSource::Hover,
2770                    );
2771                }
2772            }
2773
2774            if let Some(element) = prev_element
2775                && prev_node.is_some()
2776            {
2777                Self::run_pressable_hover_hook(app, window, element, false);
2778            }
2779
2780            let (_prev_element, prev_node, _next_element, _next_node) =
2781                crate::elements::update_hovered_hover_region(app, window, None);
2782            if prev_node.is_some() {
2783                needs_redraw = true;
2784                self.debug_record_hover_edge_hover_region();
2785                if let Some(node) = prev_node {
2786                    self.mark_invalidation_dedup_with_source(
2787                        node,
2788                        Invalidation::Paint,
2789                        &mut invalidation_visited,
2790                        UiDebugInvalidationSource::Hover,
2791                    );
2792                }
2793            }
2794        }
2795
2796        if let Event::PointerCancel(e) = event {
2797            self.touch_pointer_down_outside_candidates
2798                .remove(&e.pointer_id);
2799        }
2800
2801        #[cfg(feature = "diagnostics")]
2802        if defer_keydown_shortcuts_until_after_dispatch
2803            && stop_propagation_requested
2804            && let Some(window) = self.window
2805            && let Event::KeyDown {
2806                key,
2807                modifiers,
2808                repeat,
2809            } = event
2810        {
2811            let focus_is_text_input = self.focus_is_text_input(app);
2812            let key_contexts = if !self.pending_shortcut.keystrokes.is_empty() {
2813                self.pending_shortcut.key_contexts.clone()
2814            } else {
2815                self.shortcut_key_context_stack(app, routing_barrier_root)
2816            };
2817            app.with_global_mut_untracked(
2818                fret_runtime::WindowShortcutRoutingDiagnosticsStore::default,
2819                |store, app| {
2820                    store.record(
2821                        window,
2822                        fret_runtime::ShortcutRoutingDecision {
2823                            seq: 0,
2824                            frame_id: app.frame_id(),
2825                            phase: fret_runtime::ShortcutRoutingPhase::PostDispatch,
2826                            key: *key,
2827                            modifiers: *modifiers,
2828                            repeat: *repeat,
2829                            deferred: true,
2830                            focus_is_text_input,
2831                            ime_composing: self.ime_composing,
2832                            pending_sequence_len: self
2833                                .pending_shortcut
2834                                .keystrokes
2835                                .len()
2836                                .min(u32::MAX as usize)
2837                                as u32,
2838                            outcome: fret_runtime::ShortcutRoutingOutcome::ConsumedByWidget,
2839                            command: None,
2840                            command_enabled: None,
2841                            key_contexts,
2842                        },
2843                    );
2844                },
2845            );
2846        }
2847
2848        if defer_keydown_shortcuts_until_after_dispatch
2849            && !stop_propagation_requested
2850            && let Event::KeyDown {
2851                key,
2852                modifiers,
2853                repeat,
2854            } = event
2855        {
2856            let focus_is_text_input = self.focus_is_text_input(app);
2857            let input_ctx_for_shortcuts = InputContext {
2858                focus_is_text_input,
2859                ..input_ctx.clone()
2860            };
2861
2862            let ime_reserved = self.ime_composing
2863                && Self::should_defer_keydown_shortcut_matching_to_text_input(
2864                    *key,
2865                    *modifiers,
2866                    focus_is_text_input,
2867                );
2868
2869            #[cfg(feature = "diagnostics")]
2870            if let Some(window) = self.window
2871                && ime_reserved
2872            {
2873                let key_contexts = if !self.pending_shortcut.keystrokes.is_empty() {
2874                    self.pending_shortcut.key_contexts.clone()
2875                } else {
2876                    self.shortcut_key_context_stack(app, routing_barrier_root)
2877                };
2878                app.with_global_mut_untracked(
2879                    fret_runtime::WindowShortcutRoutingDiagnosticsStore::default,
2880                    |store, app| {
2881                        store.record(
2882                            window,
2883                            fret_runtime::ShortcutRoutingDecision {
2884                                seq: 0,
2885                                frame_id: app.frame_id(),
2886                                phase: fret_runtime::ShortcutRoutingPhase::PostDispatch,
2887                                key: *key,
2888                                modifiers: *modifiers,
2889                                repeat: *repeat,
2890                                deferred: true,
2891                                focus_is_text_input,
2892                                ime_composing: self.ime_composing,
2893                                pending_sequence_len: self
2894                                    .pending_shortcut
2895                                    .keystrokes
2896                                    .len()
2897                                    .min(u32::MAX as usize)
2898                                    as u32,
2899                                outcome: fret_runtime::ShortcutRoutingOutcome::ReservedForIme,
2900                                command: None,
2901                                command_enabled: None,
2902                                key_contexts,
2903                            },
2904                        );
2905                    },
2906                );
2907            }
2908
2909            if !ime_reserved
2910                && self.handle_keydown_shortcuts(
2911                    app,
2912                    services,
2913                    KeydownShortcutParams {
2914                        input_ctx: &input_ctx_for_shortcuts,
2915                        barrier_root: routing_barrier_root,
2916                        focus_is_text_input,
2917                        #[cfg(feature = "diagnostics")]
2918                        phase: fret_runtime::ShortcutRoutingPhase::PostDispatch,
2919                        #[cfg(feature = "diagnostics")]
2920                        deferred: true,
2921                        key: *key,
2922                        modifiers: *modifiers,
2923                        repeat: *repeat,
2924                    },
2925                )
2926            {
2927                if needs_redraw {
2928                    self.request_redraw_coalesced(app);
2929                }
2930                return;
2931            }
2932        }
2933
2934        if let Event::Pointer(PointerEvent::Move { .. }) = event
2935            && let Some(prev) = synth_pointer_move_prev_target
2936            && captured.is_none()
2937            && node_in_active_layers(prev)
2938        {
2939            // Forward a synthetic hover-move to the previously hovered target so retained
2940            // widgets can clear hover state when the pointer crosses between siblings.
2941            //
2942            // We intentionally use observer dispatch to avoid allowing the previous target to
2943            // mutate focus/capture/cursor routing on the transition frame.
2944            let (_, elapsed) = fret_perf::measure_span(
2945                self.debug_enabled,
2946                trace_enabled,
2947                || tracing::trace_span!("fret.ui.dispatch.synth_hover_observer", node = ?prev),
2948                || {
2949                    self.dispatch_event_to_node_chain_observer(
2950                        app,
2951                        services,
2952                        &input_ctx,
2953                        prev,
2954                        event,
2955                        Some(&dispatch_cx.input_snapshot),
2956                        &mut invalidation_visited,
2957                    );
2958                    needs_redraw = true;
2959                },
2960            );
2961            if let Some(elapsed) = elapsed {
2962                self.debug_stats.dispatch_synth_hover_observer_time += elapsed;
2963            }
2964        }
2965
2966        if is_wheel
2967            && wheel_stop_node.is_some()
2968            && captured.is_none()
2969            && let Some(window) = self.window
2970            && let Event::Pointer(PointerEvent::Wheel {
2971                position,
2972                pointer_type,
2973                ..
2974            }) = event
2975            && pointer_type_supports_hover(*pointer_type)
2976        {
2977            // Capture scroll-handle-driven invalidations triggered by this wheel event, including
2978            // out-of-band handle mutations that were not routed through a `Scroll` widget.
2979            self.invalidate_scroll_handle_bindings_for_changed_handles(
2980                app,
2981                crate::layout_pass::LayoutPassKind::Final,
2982                /* consume_deferred_scroll_to_item */ false,
2983                /* commit_scroll_handle_baselines */ false,
2984            );
2985
2986            self.hit_test_path_cache = None;
2987            let hit = self.hit_test_layers_cached(hit_test_layer_roots, *position);
2988
2989            let mut hit_for_hover = hit;
2990            let mut hit_for_hover_region = hit;
2991            let mut hit_for_raw_below_barrier: Option<NodeId> = None;
2992            if let Some((occlusion_layer, occlusion)) =
2993                self.topmost_pointer_occlusion_layer(barrier_root)
2994                && occlusion != PointerOcclusion::None
2995            {
2996                let occlusion_z = self
2997                    .layer_order
2998                    .iter()
2999                    .position(|id| *id == occlusion_layer);
3000                let hit_layer_z = hit
3001                    .and_then(|hit| self.node_layer(hit))
3002                    .and_then(|layer| self.layer_order.iter().position(|id| *id == layer));
3003                let hit_is_below_occlusion = match (occlusion_z, hit_layer_z, hit) {
3004                    (Some(oz), Some(hz), Some(_)) => hz < oz,
3005                    (Some(_), None, Some(_)) => true,
3006                    (Some(_), _, None) => true,
3007                    _ => false,
3008                };
3009                if hit_is_below_occlusion {
3010                    hit_for_raw_below_barrier = hit;
3011                    hit_for_hover = None;
3012                    hit_for_hover_region = None;
3013                }
3014            }
3015
3016            let (_, elapsed) = fret_perf::measure_span(
3017                self.debug_enabled,
3018                trace_enabled,
3019                || tracing::trace_span!("fret.ui.dispatch.hover_update"),
3020                || {
3021                    self.update_hover_state_from_hit(
3022                        app,
3023                        window,
3024                        barrier_root,
3025                        Some(*position),
3026                        hit_for_hover,
3027                        hit_for_hover_region,
3028                        hit_for_raw_below_barrier,
3029                        Some(pointer_chain_snapshot),
3030                        &mut invalidation_visited,
3031                        &mut needs_redraw,
3032                    );
3033                },
3034            );
3035            if let Some(elapsed) = elapsed {
3036                self.debug_stats.dispatch_hover_update_time += elapsed;
3037            }
3038        }
3039
3040        if input_ctx.caps.ui.cursor_icons
3041            && let Some(window) = self.window
3042            && matches!(event, Event::Pointer(_))
3043        {
3044            let icon = cursor_choice.unwrap_or(fret_core::CursorIcon::Default);
3045            let (_, elapsed) = fret_perf::measure_span(
3046                self.debug_enabled,
3047                trace_enabled,
3048                || {
3049                    tracing::trace_span!(
3050                        "fret.ui.dispatch.cursor_effect",
3051                        window = ?window,
3052                        icon = ?icon
3053                    )
3054                },
3055                || app.push_effect(Effect::CursorSetIcon { window, icon }),
3056            );
3057            if let Some(elapsed) = elapsed {
3058                self.debug_stats.dispatch_cursor_effect_time += elapsed;
3059            }
3060        }
3061
3062        if needs_redraw {
3063            self.request_redraw_coalesced(app);
3064        }
3065        let (_, elapsed) = fret_perf::measure_span(
3066            self.debug_enabled,
3067            trace_enabled,
3068            || tracing::trace_span!("fret.ui.dispatch.pointer_move_layer_observers"),
3069            || {
3070                self.dispatch_pointer_move_layer_observers(
3071                    app,
3072                    services,
3073                    &input_ctx,
3074                    barrier_root,
3075                    event,
3076                    &mut needs_redraw,
3077                    &mut invalidation_visited,
3078                );
3079            },
3080        );
3081        if let Some(elapsed) = elapsed {
3082            self.debug_stats.dispatch_pointer_move_layer_observers_time += elapsed;
3083        }
3084        if needs_redraw {
3085            self.request_redraw_coalesced(app);
3086        }
3087
3088        // Publish a post-dispatch snapshot so runner-level integration surfaces (e.g. OS menubars)
3089        // see the latest focus/modal state without waiting for the next paint pass.
3090        let (_, elapsed) = fret_perf::measure_span(
3091            self.debug_enabled,
3092            trace_enabled,
3093            || tracing::trace_span!("fret.ui.dispatch.post_dispatch_snapshot"),
3094            || self.publish_post_dispatch_runtime_snapshots_for_event(app, event),
3095        );
3096        if let Some(elapsed) = elapsed {
3097            self.debug_stats.dispatch_post_dispatch_snapshot_time += elapsed;
3098        }
3099    }
3100}