Skip to main content

frust_core/app/
dispatch.rs

1//! `RenderRoot`'s event pass: the contact gate, the overlay route and the
2//! dispatch into the root widget, with the capture, focus, hover and request
3//! bookkeeping the root keeps across passes.
4
5use std::any::Any;
6use std::rc::Weak;
7
8use kurbo::Point;
9
10use super::{OverlayRoute, RenderRoot, advance_focus_epoch};
11use crate::event::{
12    ContactFrame, ContactPass, EventCtx, EventOutcome, EventResult, FileDropEvent, FileDropPhase,
13    ImeState, InputEvent, Key, KeyEvent, Modifiers, NamedKey, OverlayEvent, OverlayEventKind,
14    PointerButton, PointerId, PointerPhase, RequestPass,
15};
16use crate::overlay::{OutsideTap, OverlayHit, OverlayInput, OverlayKey, OverlayPod};
17use crate::shortcuts::{DeliveryProbe, KeyForwardPass};
18use crate::tree::WidgetPod;
19use crate::view::{ChangeFlags, View};
20use crate::widget::{ChildPod, FocusGrant};
21
22/// Per-dispatch state the steps of `RenderRoot::event` share, read after the overlay route.
23struct DispatchFrame {
24    pointer_id: PointerId,
25    /// A non-claimant contact down a live capture (rule (c)).
26    secondary: bool,
27    live_focus_epoch: u64,
28    candidate_focus_epoch: u64,
29    focus_active_before: bool,
30    /// Only an uncaptured `Move` may claim hover.
31    hover_pass: bool,
32    /// A non-`secondary` `Down`/`Up`/`Cancel` ends hover.
33    hover_ends: bool,
34    hover_epoch: u64,
35    hover_was_active: bool,
36    root_identity: u64,
37    /// Any claimant `Move`, captured included, so a drag keeps its cursor outside its bounds.
38    cursor_pass: bool,
39    /// The root widget itself holds the live contacts opt-in, rather than a pod below it.
40    contacts_captor_is_root: bool,
41    /// A traversal grant pass (`traverse_focus`); `root_grant` when the root widget is its target.
42    grant: bool,
43    root_grant: bool,
44}
45
46/// What the root widget's dispatch reported, read off its `EventCtx`.
47struct RootDispatch {
48    handled: bool,
49    needs_redraw: bool,
50    captured: bool,
51    hover_claimed: bool,
52    focus_req: bool,
53    /// The claim came from a pod already on the live focus chain.
54    focus_req_from_live: bool,
55    focus_rel: bool,
56    ime: Option<ImeState>,
57    ime_published: bool,
58    /// The root widget opted into the other contacts during this dispatch.
59    root_opted_in: bool,
60}
61
62/// The root widget's stop id: no `ChildPod::inspect_id` is `0`.
63const ROOT_STOP: u64 = 0;
64
65/// One pod the focus-traversal walk can stop on.
66struct FocusStop {
67    /// The pod's `inspect_id`, or `ROOT_STOP`: the grant's target.
68    id: u64,
69    /// The floated surface holding the pod, whose owner the grant is addressed through.
70    surface: Option<OverlayKey>,
71    /// Whether the pod and every ancestor hold live focus links.
72    current: bool,
73}
74
75/// The traversal walk's state: the floated pods the last paint drew, and the stops so far.
76struct FocusWalk<'a> {
77    floated: &'a [(OverlayPod, OverlayHit)],
78    stops: Vec<FocusStop>,
79}
80
81impl FocusWalk<'_> {
82    /// Visit `pod` and its traversable subtree in pre-order; `chain` is the ancestors' live focus.
83    fn visit(&mut self, pod: &ChildPod, chain: bool, mut surface: Option<OverlayKey>) {
84        let floated = self
85            .floated
86            .iter()
87            .find(|(float, _)| std::ptr::eq(float.as_ptr(), pod));
88        if let Some((_, hit)) = floated {
89            // A transparent surface takes no input, so nothing in it is a stop either.
90            if hit.input == OverlayInput::Transparent {
91                return;
92            }
93            surface = Some(hit.key);
94        }
95        let chain = chain && pod.holds_live_focus();
96        let widget = pod.widget();
97        if widget.is_focusable() {
98            let id = pod.inspect_id().0;
99            self.stops.push(FocusStop {
100                id,
101                surface,
102                current: chain,
103            });
104        }
105        widget.visit_focus_children(&mut |child| self.visit(child, chain, surface));
106    }
107}
108
109/// `Some(backward)` for a bare Tab or Shift+Tab, the keys focus traversal answers.
110/// A plain Escape closes a modal pod; one with a modifier is a chord for the table.
111fn is_bare_escape(key: &KeyEvent) -> bool {
112    key.key == Key::Named(NamedKey::Escape) && key.modifiers == Modifiers::default()
113}
114
115fn traversal_direction(key: &KeyEvent) -> Option<bool> {
116    let mods = key.modifiers;
117    let bare = !(mods.ctrl || mods.alt || mods.meta);
118    (key.key == Key::Named(NamedKey::Tab) && bare).then_some(mods.shift)
119}
120
121/// A pointer `Down`, bare or routed to a floated surface: what ends a keyboard-origin focus.
122fn is_pointer_down(event: &InputEvent) -> bool {
123    let pointer = match event {
124        InputEvent::Pointer(pointer) => pointer,
125        InputEvent::Overlay(OverlayEvent {
126            kind: OverlayEventKind::Pointer(pointer),
127            ..
128        }) => pointer,
129        _ => return false,
130    };
131    pointer.phase == PointerPhase::Down
132}
133
134/// Contact gate, first half: unwrap a `PointerContact` into a bare `Pointer` (returned, for the
135/// caller to hold) and name its contact. A bare `Pointer` is the mouse; other events keep the
136/// enclosing pass's contact.
137fn unwrap_contact(event: &InputEvent) -> (Option<InputEvent>, PointerId) {
138    match event {
139        InputEvent::PointerContact {
140            pointer_id,
141            event: pointer,
142        } => (Some(InputEvent::Pointer(*pointer)), *pointer_id),
143        InputEvent::Pointer(_) => (None, PointerId::MOUSE),
144        _ => (None, crate::event::current_pointer_id()),
145    }
146}
147
148impl<State: 'static, V: View<State>> RenderRoot<State, V> {
149    /// Deliver an input event to the widget tree. The outcome's `handled` is the
150    /// dispatch's result; `needs_redraw` is set when a widget consumed the event
151    /// or asked for a redraw, and for a hover that ended with nothing taking it
152    /// (the one repaint no widget can request). The pass never rebuilds or
153    /// repaints: the shell runs [`RenderRoot::rebuild`] afterwards, which itself
154    /// calls this sequentially with [`InputEvent::Housekeeping`].
155    ///
156    /// Beside the outcome it resolves the cursor ([`RenderRoot::cursor`], on a
157    /// pointer `Move`), the clipboard write and paste request (every pass), the
158    /// IME state and the focus epoch. A [`FileDropPhase::Drop`] or `Cancel` is
159    /// followed in the same call by a [`FileDropPhase::Ended`] broadcast to the
160    /// whole tree; an unhandled `Key` goes on (`dispatch_declined_key`); the outcome folds all.
161    pub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome {
162        self.dispatch_pass(state, event, None).0
163    }
164
165    /// `event`'s body, as a traversal grant pass when `grant` is `Some(root_is_target)`; also
166    /// returns whether a focus claim was honoured.
167    fn dispatch_pass(
168        &mut self,
169        state: &mut State,
170        event: &InputEvent,
171        grant: Option<bool>,
172    ) -> (EventOutcome, bool) {
173        let Some(root_id) = self.root_id else {
174            return (EventOutcome::default(), false);
175        };
176        self.trap_modal_focus();
177        let (unwrapped, pointer_id) = unwrap_contact(event);
178        let event = unwrapped.as_ref().unwrap_or(event);
179        if is_pointer_down(event) {
180            self.set_keyboard_focus(false);
181        }
182        let Some(secondary) = self.gate_contact(event, pointer_id) else {
183            return (EventOutcome::default(), false);
184        };
185        // Published for the whole dispatch, file-drop follow-up included (`EventCtx::pointer_id`,
186        // the contact opt-in, `ChildPod::set_active`); restored on drop at the end of this call.
187        let contact_pass = ContactPass::enter(pointer_id, secondary);
188
189        // A pointer over a floated surface reaches nothing below, not even the blur rule.
190        let carried = match self.route_overlay(state, event) {
191            OverlayRoute::Consumed(outcome) => return (outcome, false),
192            OverlayRoute::Continue(outcome) => outcome,
193        };
194        let frame = self.open_frame(event, pointer_id, secondary, grant);
195        let Some(pod) = self.tree.pod_mut(root_id) else {
196            // Keep the outcome an outside-tap notification already produced.
197            self.publish_focus_session();
198            return (carried, false);
199        };
200        // Opened empty, so a request from a dispatch no root drove cannot leak in; closed by
201        // `resolve_requests`, before the file-drop follow-up opens its own.
202        let request_slot = RequestPass::enter();
203        let forward_slot = KeyForwardPass::enter();
204        let mut root = Self::dispatch_root(pod, &mut self.pending, &frame, state, event);
205        // Release-then-commit: only a landing grant ends the old session (`traverse_focus`).
206        if frame.grant && root.focus_req {
207            self.release_focus_session();
208        }
209        root.ime_published = root.ime.is_some();
210        self.publish_ime(event, root.focus_req, root.ime.take());
211        let focus_claim_honoured = self.commit_event(event, &frame, &root, &contact_pass);
212        self.resolve_focus_session(&frame, focus_claim_honoured);
213        self.record_key_forward(forward_slot.take());
214        drop(forward_slot);
215        self.resolve_hover(&frame, root.hover_claimed);
216        self.resolve_requests(request_slot, frame.cursor_pass);
217        let carried = if root.handled {
218            carried
219        } else {
220            self.dispatch_declined_key(state, event, carried)
221        };
222        let mut outcome = self.build_outcome(&frame, &root, carried);
223        self.file_drop_follow_up(state, event, &mut outcome);
224        (outcome, focus_claim_honoured)
225    }
226
227    /// Contact gate, second half: `Some(secondary)` for a routed pointer (see `contact_route`),
228    /// `Some(false)` for any other event, `None` to drop the event. Writes nothing.
229    fn gate_contact(&self, event: &InputEvent, pointer_id: PointerId) -> Option<bool> {
230        if matches!(event, InputEvent::Pointer(_)) {
231            // `None` is rule (b), or rule (c) for a captor that did not opt in.
232            self.contact_route(pointer_id)
233        } else {
234            Some(false)
235        }
236    }
237
238    /// Open the focus-epoch bracket (claims are stamped with a candidate epoch so older links
239    /// strand by arithmetic; `resolve_focus_session` commits it or not) and read the pass flags.
240    /// A grant's candidate skips the epoch its release would take. Writes no root field;
241    /// publishes the live focus session.
242    fn open_frame(
243        &self,
244        event: &InputEvent,
245        pointer_id: PointerId,
246        secondary: bool,
247        grant: Option<bool>,
248    ) -> DispatchFrame {
249        let live_focus_epoch = self.focus_epoch;
250        let mut candidate_focus_epoch = advance_focus_epoch(live_focus_epoch);
251        if grant.is_some() && (self.focus_active || self.ime_state.is_some()) {
252            candidate_focus_epoch = advance_focus_epoch(candidate_focus_epoch);
253        }
254        crate::widget::set_live_focus_session(
255            self.root_identity,
256            live_focus_epoch,
257            candidate_focus_epoch,
258        );
259        // `docs/LIMITATIONS.md`'s `hover-window-leave-standing` covers the ending rule.
260        let is_move = matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Move);
261        let hover_ends = !secondary
262            && matches!(
263                event,
264                InputEvent::Pointer(p)
265                    if matches!(
266                        p.phase,
267                        PointerPhase::Down | PointerPhase::Up | PointerPhase::Cancel
268                    )
269            );
270        DispatchFrame {
271            pointer_id,
272            secondary,
273            live_focus_epoch,
274            candidate_focus_epoch,
275            focus_active_before: self.focus_active,
276            hover_pass: is_move && self.capture_claimant.is_none(),
277            hover_ends,
278            hover_epoch: self.hover_epoch,
279            hover_was_active: self.hover_active,
280            root_identity: self.root_identity,
281            cursor_pass: !secondary && is_move,
282            contacts_captor_is_root: self.capture_claimant.is_some()
283                && self.capture_contacts
284                && self.contacts_captor_is_root,
285            grant: grant.is_some(),
286            root_grant: grant == Some(true),
287        }
288    }
289
290    /// Dispatch `event` into the root widget (forward-only down the active path for a
291    /// non-claimant contact) and read its context back. Writes `pending` (`LAYOUT`) when the
292    /// dispatch dirtied anything; clears the thread-local hover claimant first.
293    fn dispatch_root(
294        pod: &mut WidgetPod,
295        pending: &mut ChangeFlags,
296        frame: &DispatchFrame,
297        state: &mut State,
298        event: &InputEvent,
299    ) -> RootDispatch {
300        // The claimant pod this dispatch records, if any (see `hover_link`).
301        crate::widget::clear_hover_claimant();
302        crate::widget::clear_focus_req_from_live();
303        let state_any: &mut dyn Any = state;
304        let mut ctx = EventCtx::new(state_any, pod.origin(), pod.size());
305        // Seed the root widget's focus and hover state; deeper pods thread their own.
306        ctx.set_has_focus(frame.focus_active_before);
307        ctx.set_hovered(frame.hover_was_active);
308        ctx.set_hover_epoch(frame.hover_epoch);
309        ctx.set_hover_eligible(frame.hover_pass);
310        // Tells a dropping pod's destructor this root's link apart (see `root_identity`).
311        ctx.set_hover_root(frame.root_identity);
312        if frame.root_grant {
313            ctx.request_focus();
314        }
315        // Attributes a `capture_contacts` made by the root widget itself to the root.
316        let contact_frame = ContactFrame::enter(frame.contacts_captor_is_root);
317        let result = if frame.secondary && !frame.contacts_captor_is_root {
318            // A walk that reached no pod falls back (see `ChildPod::event_child`).
319            let widget = pod.widget_mut();
320            let carrier = crate::event::secondary_walk_carrier();
321            let (_, delivered) = crate::event::run_secondary_walk(event.clone(), || {
322                widget.event(&mut ctx, &carrier)
323            });
324            match delivered {
325                Some(result) => result,
326                None => crate::event::without_secondary_walk(|| widget.event(&mut ctx, event)),
327            }
328        } else {
329            pod.widget_mut().event(&mut ctx, event)
330        };
331        let root_opted_in = contact_frame.close().0;
332        let handled = matches!(result, EventResult::Handled);
333        // Fold every dirtying channel into `LAYOUT` (mirror of `paint_main_tree`'s
334        // fold); the root sits in no pod, so its own consumption counts too.
335        if ctx.needs_layout() || ctx.is_layout_path_dirty() || handled || ctx.needs_redraw() {
336            *pending |= ChangeFlags::LAYOUT;
337        }
338        RootDispatch {
339            handled,
340            needs_redraw: ctx.needs_redraw() || handled,
341            captured: ctx.is_pointer_captured(),
342            hover_claimed: ctx.is_hover_claimed(),
343            focus_req: ctx.is_focus_requested(),
344            // A root-widget claim (no pod) re-claims whenever a session stands.
345            focus_req_from_live: crate::widget::take_focus_req_from_live()
346                .unwrap_or(frame.focus_active_before),
347            focus_rel: ctx.is_focus_released(),
348            ime: ctx.take_ime_state(),
349            ime_published: false,
350            root_opted_in,
351        }
352    }
353
354    /// IME publish: a hit-tested or focus-routed publish needs a live or opening session; an
355    /// `Overlay` one must come from the session's owner surface (mirrors `paint_overlays`).
356    /// Writes the IME state or releases the session; runs before the focus commit.
357    fn publish_ime(&mut self, event: &InputEvent, focus_req: bool, ime: Option<ImeState>) {
358        let publish_attributable = match event {
359            InputEvent::Overlay(overlay) => focus_req || self.focus_surface == Some(overlay.key),
360            _ => self.focus_active || focus_req,
361        };
362        match ime {
363            Some(_) if !publish_attributable => {}
364            Some(ime) if !ime.active => {
365                self.release_focus_session();
366            }
367            Some(ime) => self.store_ime_state(Some(ime)),
368            None => {}
369        }
370    }
371
372    /// Per-variant commit, then the takeover check. Returns whether a focus claim was honoured
373    /// (an arm may ignore one); writes `capture_claimant`, `capture_contacts`,
374    /// `contacts_captor_is_root`, `focus_surface` and, through its writers, the focus session.
375    fn commit_event(
376        &mut self,
377        event: &InputEvent,
378        frame: &DispatchFrame,
379        root: &RootDispatch,
380        contact_pass: &ContactPass,
381    ) -> bool {
382        let mut focus_claim_honoured = false;
383        match event {
384            // A non-claimant contact (rule (c)): no capture, no blur.
385            InputEvent::Pointer(_) if frame.secondary => {
386                focus_claim_honoured = self.commit_focus_request(root);
387            }
388            // Unreachable: the contact gate unwrapped every contact.
389            InputEvent::PointerContact { .. } => {}
390            InputEvent::Pointer(pointer) => {
391                focus_claim_honoured =
392                    self.commit_pointer(pointer.phase, frame.pointer_id, root, contact_pass);
393            }
394            // Hit-tested and focus-routed input, the file-drop `Ended` broadcast, and a
395            // shortcut (a key press: its declarer may move focus, e.g. to a search field).
396            InputEvent::Scroll { .. }
397            | InputEvent::Scale(_)
398            | InputEvent::FileDrop(_)
399            | InputEvent::Key(_)
400            | InputEvent::Ime(_)
401            | InputEvent::EditCommand(_)
402            | InputEvent::Shortcut(_) => {
403                focus_claim_honoured = self.commit_focus_request(root);
404            }
405            // A broadcast opens no gesture, claims no focus and blurs nothing; only
406            // the IME publish above can move state on it.
407            InputEvent::Housekeeping => {}
408            InputEvent::Overlay(overlay) => {
409                focus_claim_honoured =
410                    self.commit_overlay(overlay, frame.pointer_id, root, contact_pass);
411            }
412        }
413        // Takeover: a container released the opted-in captor, so the opt-in goes
414        // too; the claimant latch stays until the claimant's own `Up`/`Cancel`.
415        if !frame.secondary && self.capture_claimant.is_some() && contact_pass.capture_released() {
416            self.capture_contacts = contact_pass.contacts_requested();
417        }
418        focus_claim_honoured
419    }
420
421    /// Honour an explicit focus request, then an explicit release; nothing captures or blurs.
422    /// Returns whether a claim was honoured. `focus_surface` stays: the root cannot attribute a
423    /// focus-routed claim, so the next paint re-resolves it from the pods' links.
424    fn commit_focus_request(&mut self, root: &RootDispatch) -> bool {
425        if root.focus_req {
426            self.set_focus_active(true);
427        }
428        if root.focus_rel {
429            self.release_focus_session();
430        }
431        root.focus_req
432    }
433
434    /// Commit a claimant (or uncaptured) pointer: a `Down` latches a capture and claims focus or
435    /// blurs; an `Up`/`Cancel` (only the claimant's reaches here) clears the latch. A claim whose
436    /// pass published no `ImeState` drops the previous holder's surface, unless the claimant
437    /// already held a live link (a chrome re-claim). Returns
438    /// whether a claim was honoured; writes the capture latch, `focus_surface` and focus session.
439    fn commit_pointer(
440        &mut self,
441        phase: PointerPhase,
442        pointer_id: PointerId,
443        root: &RootDispatch,
444        contact_pass: &ContactPass,
445    ) -> bool {
446        match phase {
447            PointerPhase::Down => {
448                if root.captured {
449                    // Rule (a): the capturing contact is the claimant.
450                    self.latch_capture(pointer_id, contact_pass, root.root_opted_in);
451                }
452                if root.focus_req {
453                    self.set_focus_active(true);
454                    // A hit-tested claim belongs to the main tree (see `focus_surface`).
455                    self.focus_surface = None;
456                    if !root.ime_published && !root.focus_req_from_live {
457                        self.store_ime_state(None);
458                    }
459                    true
460                } else {
461                    // Blur: no widget on the tapped path took focus.
462                    self.release_focus_session();
463                    false
464                }
465            }
466            PointerPhase::Up | PointerPhase::Cancel => {
467                self.clear_capture();
468                false
469            }
470            PointerPhase::Move => false,
471        }
472    }
473
474    /// Commit a floated surface's input: a real gesture, but never the blur branch (a toolbar tap
475    /// must not drop the selection it acts on), and `focus_surface` is not written. Returns
476    /// whether a claim was honoured; writes the capture latch and the focus session.
477    fn commit_overlay(
478        &mut self,
479        overlay: &OverlayEvent,
480        pointer_id: PointerId,
481        root: &RootDispatch,
482        contact_pass: &ContactPass,
483    ) -> bool {
484        // No container blur sweep runs for a floated press, so a session
485        // another surface owns is retired whole (`paint_overlays` does the rest).
486        if root.focus_req {
487            if self.focus_surface.is_some_and(|owner| owner != overlay.key) {
488                self.release_focus_session();
489            }
490            self.set_focus_active(true);
491        }
492        if root.focus_rel {
493            self.release_focus_session();
494        }
495        // Mirrored on any phase, so its follow-ups route down the capture path, even outside.
496        if root.captured {
497            self.latch_capture(pointer_id, contact_pass, root.root_opted_in);
498        }
499        // Released on an overlay `Up`/`Cancel` too, or the standing latch
500        // would short-circuit the overlay route for every surface.
501        if let OverlayEventKind::Pointer(pointer) = &overlay.kind
502            && matches!(pointer.phase, PointerPhase::Up | PointerPhase::Cancel)
503            && self
504                .capture_claimant
505                .is_none_or(|claimant| claimant == pointer_id)
506        {
507            self.clear_capture();
508        }
509        root.focus_req
510    }
511
512    /// Latch `pointer_id` as the capture claimant. The opt-in is read from the pass, so a captor
513    /// below a component boundary counts. Writes the three capture fields.
514    fn latch_capture(&mut self, pointer_id: PointerId, pass: &ContactPass, root_opted_in: bool) {
515        self.capture_claimant = Some(pointer_id);
516        self.capture_contacts = pass.contacts_requested();
517        self.contacts_captor_is_root = root_opted_in;
518    }
519
520    /// Clear the capture latch and its contacts opt-in. Writes the three capture fields.
521    fn clear_capture(&mut self) {
522        self.capture_claimant = None;
523        self.capture_contacts = false;
524        self.contacts_captor_is_root = false;
525    }
526
527    /// Session resolution: an ended session strands everything (past the candidate); an honoured
528    /// claim commits the candidate (outside a grant, clearing the keyboard flag); else the live
529    /// epoch stays. Writes `focus_epoch`, publishes it.
530    fn resolve_focus_session(&mut self, frame: &DispatchFrame, focus_claim_honoured: bool) {
531        if focus_claim_honoured && !frame.grant {
532            self.set_keyboard_focus(false);
533        }
534        let session_ended = frame.focus_active_before && !self.focus_active;
535        self.focus_epoch = if session_ended {
536            advance_focus_epoch(frame.candidate_focus_epoch)
537        } else if focus_claim_honoured {
538            frame.candidate_focus_epoch
539        } else {
540            frame.live_focus_epoch
541        };
542        self.publish_focus_session();
543    }
544
545    /// Hover resolution: advancing the epoch strands every stamp this pass did not renew; a
546    /// claim counts only on a hover pass. Drains the hover claimant; writes `hover_epoch`,
547    /// `hover_active`, `hover_link` and the live hover link.
548    fn resolve_hover(&mut self, frame: &DispatchFrame, hover_claimed: bool) {
549        let hover_claimant = crate::widget::take_hover_claimant();
550        if frame.hover_pass || frame.hover_ends {
551            self.hover_epoch = self.hover_epoch.wrapping_add(1);
552            self.hover_active = frame.hover_pass && hover_claimed;
553            // Link identity for the paint-epoch fold; `0` is a root-widget claim.
554            self.hover_link = self.hover_active.then(|| hover_claimant.unwrap_or(0));
555            // Republish the link a dropping pod checks (see `ChildPod`'s `Drop`);
556            // epoch `0` while nothing holds one.
557            crate::event::set_live_hover_link(
558                self.root_identity,
559                if self.hover_active {
560                    self.hover_epoch
561                } else {
562                    0
563                },
564            );
565        }
566    }
567
568    /// Request resolution: close `request_slot` and commit what it collected. The cursor commits
569    /// only on a cursor pass (last request wins, none means `Default`); writes `cursor`,
570    /// `pending_clipboard_write` and `pending_paste_request`.
571    fn resolve_requests(&mut self, request_slot: RequestPass, cursor_pass: bool) {
572        let requested = request_slot.take();
573        if cursor_pass {
574            self.cursor = requested.cursor.unwrap_or_default();
575        }
576        // The clipboard commits every pass; a paste request is raised, never
577        // lowered, so a shell that skips a drain answers late rather than never.
578        if let Some(text) = requested.clipboard_write {
579            self.pending_clipboard_write = Some(text);
580        }
581        self.pending_paste_request |= requested.paste_request;
582    }
583
584    /// Offer a `Key` the focus path declined, each delivery a full pass of its own, until one is
585    /// answered: as `OverlayEventKind::Key` to the focus session's forward target, then (an
586    /// Escape) to the topmost modal pod, then to the last paint's shortcut declarations in tier
587    /// order (`ShortcutTable::candidates`), masked to the modal pod's own, then (a bare Tab or
588    /// Shift+Tab, unless [`RenderRoot::set_tab_traversal`] is off) to `traverse_focus`. Only an
589    /// interactive pod takes keys. Folds every delivery's outcome into `carried`.
590    fn dispatch_declined_key(
591        &mut self,
592        state: &mut State,
593        event: &InputEvent,
594        mut carried: EventOutcome,
595    ) -> EventOutcome {
596        let InputEvent::Key(key) = event else {
597            return carried;
598        };
599        let interactive = |hit: &&OverlayHit| hit.input == OverlayInput::Interactive;
600        let modal = self.overlay_hits.iter().rev().find(|hit| hit.is_modal());
601        let modal = modal.map(|hit| hit.key);
602        let forward = self.live_key_forward().filter(|pod| {
603            self.overlay_hits
604                .iter()
605                .filter(interactive)
606                .any(|hit| hit.key == *pod)
607        });
608        let escape = modal.filter(|pod| is_bare_escape(key) && forward != Some(*pod));
609        for pod in forward.into_iter().chain(escape) {
610            let kind = OverlayEventKind::Key(key.clone());
611            let delivery = InputEvent::Overlay(OverlayEvent { key: pod, kind });
612            if self.deliver_declined(state, &delivery, &mut carried) {
613                return carried;
614            }
615        }
616        let top_overlay = modal.or_else(|| {
617            let top = self.overlay_hits.iter().rev().find(interactive);
618            top.map(|hit| hit.key)
619        });
620        for (id, overlay) in self.shortcuts.candidates(key, top_overlay, modal.is_some()) {
621            // A floated pod is reached only through its owner's slot, never by a bare broadcast.
622            let delivery = match overlay {
623                None => InputEvent::Shortcut(id),
624                Some(key) => InputEvent::Overlay(OverlayEvent {
625                    key,
626                    kind: OverlayEventKind::Shortcut(id),
627                }),
628            };
629            if self.deliver_declined(state, &delivery, &mut carried) {
630                return carried;
631            }
632        }
633        if let Some(backward) = traversal_direction(key).filter(|_| self.tab_traversal) {
634            self.traverse_focus(state, backward, &mut carried);
635        }
636        carried
637    }
638
639    /// Move focus to the next (`backward`: previous) stop in walk order, wrapping; with nothing
640    /// focused, to the first (last). Answers the key (and sets the keyboard flag) only when a
641    /// grant lands or the focused stop is the only one; a walk where none lands changes nothing.
642    ///
643    /// # The grant
644    ///
645    /// One pass delivers `InputEvent::Shortcut` carrying this root's private `focus_grant` id (bare
646    /// for a main-tree stop, as `OverlayEventKind::Shortcut` through a floated stop's owner) under
647    /// an open [`FocusGrant`], reaching what a shortcut reaches. The target pod (or root widget)
648    /// records the claim like `EventCtx::request_focus`; only then is the old session released
649    /// (`focus_epoch` advanced, IME surface and key forward dropped) and the claim committed, so a
650    /// landing step advances the epoch twice. A missed stop is skipped, short of the focused one.
651    fn traverse_focus(&mut self, state: &mut State, backward: bool, carried: &mut EventOutcome) {
652        let stops = self.focus_stops();
653        let len = stops.len();
654        let current = stops.iter().rposition(|stop| stop.current);
655        let next = match current {
656            _ if len == 0 => return,
657            None if backward => len - 1,
658            None => 0,
659            Some(i) if backward => (i + len - 1) % len,
660            Some(i) => (i + 1) % len,
661        };
662        let step = |n: usize| if backward { len - n } else { n };
663        let landed = current == Some(next)
664            || (0..len)
665                .map(|n| (next + step(n)) % len)
666                .take_while(|&i| Some(i) != current)
667                .any(|i| self.grant_focus(state, &stops[i]));
668        if landed {
669            carried.handled = true;
670            carried.needs_redraw = true;
671            self.set_keyboard_focus(true);
672        }
673    }
674
675    /// One grant pass to `stop` (see `traverse_focus`); whether its claim was honoured.
676    fn grant_focus(&mut self, state: &mut State, stop: &FocusStop) -> bool {
677        let carrier = self.focus_grant;
678        let delivery = match stop.surface {
679            None => InputEvent::Shortcut(carrier),
680            Some(key) => InputEvent::Overlay(OverlayEvent {
681                key,
682                kind: OverlayEventKind::Shortcut(carrier),
683            }),
684        };
685        let _grant = FocusGrant::enter(carrier, stop.id);
686        let _probe = DeliveryProbe::enter();
687        let root_is_target = stop.id == ROOT_STOP;
688        self.dispatch_pass(state, &delivery, Some(root_is_target)).1
689    }
690
691    /// The traversal walk: focusable pods in pre-order over `Widget::visit_focus_children`,
692    /// from the main root (itself a stop when focusable), or from the topmost modal pod alone
693    /// while one is registered.
694    fn focus_stops(&self) -> Vec<FocusStop> {
695        self.publish_focus_session();
696        let floated: Vec<(OverlayPod, OverlayHit)> = self
697            .overlay_pods
698            .iter()
699            .zip(&self.overlay_hits)
700            .filter_map(|(weak, hit)| Some((weak.upgrade()?, *hit)))
701            .collect();
702        let mut walk = FocusWalk {
703            floated: &floated,
704            stops: Vec::new(),
705        };
706        let chain = self.focus_active;
707        if let Some(modal) = self.overlay_hits.iter().rposition(|hit| hit.is_modal()) {
708            // `overlay_pods` runs parallel to `overlay_hits`: both come from one paint's entries.
709            if let Some(pod) = self.overlay_pods.get(modal).and_then(Weak::upgrade)
710                && let Ok(pod) = pod.try_borrow()
711            {
712                walk.visit(&pod, chain, None);
713            }
714        } else if let Some(root) = self.root_id.and_then(|id| self.tree.pod(id)) {
715            if root.widget().is_focusable() {
716                walk.stops.push(FocusStop {
717                    id: ROOT_STOP,
718                    surface: None,
719                    current: chain,
720                });
721            }
722            root.widget()
723                .visit_focus_children(&mut |pod| walk.visit(pod, chain, None));
724        }
725        walk.stops
726    }
727
728    /// One `dispatch_declined_key` delivery: run it, fold its outcome into `carried`, and
729    /// report whether a widget answered it.
730    fn deliver_declined(
731        &mut self,
732        state: &mut State,
733        delivery: &InputEvent,
734        carried: &mut EventOutcome,
735    ) -> bool {
736        let probe = DeliveryProbe::enter();
737        let outcome = self.event(state, delivery);
738        let answered = probe.handled() || outcome.handled;
739        carried.needs_redraw |= outcome.needs_redraw || answered;
740        carried.handled |= answered;
741        answered
742    }
743
744    /// The pass's outcome, folding in the overlay route's `carried` outside-tap outcome. A hover
745    /// that ended with nothing taking it is the one repaint no widget can request.
746    fn build_outcome(
747        &self,
748        frame: &DispatchFrame,
749        root: &RootDispatch,
750        carried: EventOutcome,
751    ) -> EventOutcome {
752        let needs_redraw = root.needs_redraw || (frame.hover_was_active && !self.hover_active);
753        EventOutcome {
754            handled: root.handled || carried.handled,
755            needs_redraw: needs_redraw || carried.needs_redraw,
756        }
757    }
758
759    /// File-drop follow-up: a `Drop`/`Cancel` re-enters with the `Ended` broadcast (see
760    /// `FileDropEvent`) and folds its outcome into `outcome`; `Ended` triggers none.
761    fn file_drop_follow_up(
762        &mut self,
763        state: &mut State,
764        event: &InputEvent,
765        outcome: &mut EventOutcome,
766    ) {
767        if let InputEvent::FileDrop(drop) = event
768            && matches!(drop.phase, FileDropPhase::Drop | FileDropPhase::Cancel)
769        {
770            let ended = InputEvent::FileDrop(FileDropEvent {
771                phase: FileDropPhase::Ended,
772                position: Point::ZERO,
773                paths: Vec::new(),
774            });
775            let follow_up = self.event(state, &ended);
776            outcome.handled |= follow_up.handled;
777            outcome.needs_redraw |= follow_up.needs_redraw;
778        }
779    }
780
781    /// Route one pointer contact (the root half of [`InputEvent::PointerContact`]'s
782    /// multi-contact contract): `Some(false)` as the gesture's own pointer (rule
783    /// (a)), `Some(true)` as a non-claimant down a live capture whose captor opted
784    /// in (rule (c)), `None` to drop it (rule (b), or rule (c) with no opt-in).
785    fn contact_route(&self, pointer_id: PointerId) -> Option<bool> {
786        match self.capture_claimant {
787            None if pointer_id.slot == 0 => Some(false),
788            None => None,
789            Some(claimant) if claimant == pointer_id => Some(false),
790            Some(_) if self.capture_contacts => Some(true),
791            Some(_) => None,
792        }
793    }
794
795    /// Hit-test the surfaces the last paint floated before the main tree sees an
796    /// uncaptured pointer, scroll or scale (see [`crate::overlay`]): walk
797    /// [`RenderRoot::overlay_hits`] topmost-first, skip [`OverlayInput::Transparent`],
798    /// and re-dispatch the first hit as a window-space [`InputEvent::Overlay`],
799    /// consuming the original. A missed primary `Down` notifies every
800    /// [`OutsideTap::Notify`] surface with [`OverlayEventKind::OutsideDown`] and is
801    /// consumed iff one asked. A live capture skips the route; `FileDrop`, key, IME,
802    /// edit and broadcast events pass straight through.
803    fn route_overlay(&mut self, state: &mut State, event: &InputEvent) -> OverlayRoute {
804        if self.capture_claimant.is_some() || self.overlay_hits.is_empty() {
805            return OverlayRoute::Continue(EventOutcome::default());
806        }
807        let (position, kind) = match event {
808            // The gate already unwrapped contacts; the second pattern is for totality.
809            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. } => {
810                (pointer.position, OverlayEventKind::Pointer(*pointer))
811            }
812            InputEvent::Scroll { position, delta } => (
813                *position,
814                OverlayEventKind::Scroll {
815                    position: *position,
816                    delta: *delta,
817                },
818            ),
819            InputEvent::Scale(scale) => (
820                scale.focal,
821                OverlayEventKind::Scale {
822                    focal: scale.focal,
823                    phase: scale.phase,
824                    scale_delta: scale.scale_delta,
825                    velocity: scale.velocity,
826                },
827            ),
828            InputEvent::FileDrop(_)
829            | InputEvent::Key(_)
830            | InputEvent::Ime(_)
831            | InputEvent::EditCommand(_)
832            | InputEvent::Housekeeping
833            | InputEvent::Overlay(_)
834            | InputEvent::Shortcut(_) => {
835                return OverlayRoute::Continue(EventOutcome::default());
836            }
837        };
838
839        // Reverse paint order is topmost-first. Cloned for the `&mut self`
840        // dispatches below; never on the no-overlays path.
841        let hits = self.overlay_hits.clone();
842        for hit in hits.iter().rev() {
843            // A transparent surface takes no input; the pointer passes through.
844            if hit.input == OverlayInput::Transparent {
845                continue;
846            }
847            if hit.contains(position) {
848                let routed = InputEvent::Overlay(OverlayEvent {
849                    key: hit.key,
850                    kind: kind.clone(),
851                });
852                // Shallow re-entry through the front door: the routed broadcast
853                // passes straight through the route and gets the full pass.
854                return OverlayRoute::Consumed(self.event(state, &routed));
855            }
856        }
857
858        // Only a primary press light-dismisses; a secondary press is a context
859        // gesture (`docs/CODE_STANDARDS.md`'s Interaction Semantics).
860        let dismissing = matches!(
861            event,
862            InputEvent::Pointer(pointer) | InputEvent::PointerContact { event: pointer, .. }
863                if pointer.phase == PointerPhase::Down
864                    && pointer.button == PointerButton::Primary
865        );
866        if !dismissing {
867            return OverlayRoute::Continue(EventOutcome::default());
868        }
869
870        let mut carried = EventOutcome::default();
871        let mut consumed = false;
872        for hit in hits.iter().rev() {
873            // No outside-taps for a transparent surface either.
874            if hit.input == OverlayInput::Transparent {
875                continue;
876            }
877            let OutsideTap::Notify { consume } = hit.outside_tap else {
878                continue;
879            };
880            let routed = InputEvent::Overlay(OverlayEvent {
881                key: hit.key,
882                kind: OverlayEventKind::OutsideDown,
883            });
884            let outcome = self.event(state, &routed);
885            carried.handled |= outcome.handled;
886            carried.needs_redraw |= outcome.needs_redraw;
887            // Tell every notified surface before consuming.
888            consumed |= consume;
889        }
890        if consumed {
891            OverlayRoute::Consumed(carried)
892        } else {
893            OverlayRoute::Continue(carried)
894        }
895    }
896}