Skip to main content

teksilo_core/widget_tree/
pointer_router.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Pointer, keyboard and accessibility event routing: the dispatch
5//! pipeline, hit-testing, the preview/bubble handler passes and the
6//! context-menu entry points.
7
8use super::*;
9
10use crate::event::ButtonMask;
11use crate::gesture::{GestureEvent, RawPointerEvent, TapEvent};
12
13/// Fire an `EventResponse`-returning handler from BOTH the external
14/// and own slots (in that order). Returns `Handled` if either did,
15/// `Ignored` otherwise. `None` slots are skipped.
16fn fire_event_handler_both(
17    external: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
18    own: &mut Option<Box<dyn FnMut(&WidgetEvent, &mut EventContext) -> EventResponse>>,
19    event: &WidgetEvent,
20    ctx: &mut EventContext,
21) -> EventResponse {
22    let r1 = external
23        .as_mut()
24        .map(|h| h(event, ctx))
25        .unwrap_or(EventResponse::Ignored);
26    let r2 = own
27        .as_mut()
28        .map(|h| h(event, ctx))
29        .unwrap_or(EventResponse::Ignored);
30    if r1 == EventResponse::Handled || r2 == EventResponse::Handled {
31        EventResponse::Handled
32    } else {
33        EventResponse::Ignored
34    }
35}
36
37/// A dispatch deferred because another was in flight.
38///
39/// Carries everything needed to replay it faithfully: the event, and the input
40/// snapshot that says which pointer produced it. `ops` is not carried — the
41/// drain runs inside the same top-level call, so the caller's sink is still in
42/// hand.
43pub(super) enum QueuedDispatch {
44    /// A nested `dispatch_*` call, replayed verbatim — event *and* input
45    /// snapshot — once the outer sample completes.
46    ///
47    /// The snapshot is boxed: it carries the packet's coalesced positions, so
48    /// it is by some way the largest thing either variant holds, and a queue
49    /// entry that is mostly padding would be paid for on the cancel path too.
50    Event {
51        event: WidgetEvent,
52        snapshot: Box<crate::pointer::InputSnapshot>,
53    },
54    /// A revocation raised through
55    /// [`WidgetTree::cancel_pointer`](crate::WidgetTree::cancel_pointer).
56    ///
57    /// It rides this queue rather than one of its own so that a cancel and the
58    /// dispatch that provoked it cannot be reordered relative to each other:
59    /// one queue is one order. It carries the reason rather than a
60    /// pre-built `PointerCancel`, because the event's position and
61    /// `PointerInfo` must be read from the table at *drain* time — by then the
62    /// pointer may have moved, or ceased to exist, and a snapshot taken at
63    /// queue time would describe a state the widget is no longer in.
64    Cancel {
65        pointer: crate::pointer::PointerId,
66        reason: crate::pointer::CancelReason,
67        /// Who to tell, when the producer knows better than the table does.
68        ///
69        /// The funnel normally addresses the cancel to whoever holds the
70        /// capture. A producer that has *already* taken the capture back as
71        /// part of its own teardown — the OS-drag escalation hands the pointer
72        /// to the platform before it raises the cancel — would leave the
73        /// funnel with nobody to tell, so it names the widget itself.
74        recipient: Option<WidgetId>,
75    },
76}
77
78/// The window-logical position a pointer event happened at, if it carries one.
79///
80/// The pointer table is fed from here: every event with a position refreshes
81/// its pointer's entry, and everything else (keys, IME, focus, AT actions)
82/// leaves the table alone.
83fn pointer_event_position(event: &WidgetEvent) -> Option<Point> {
84    match event {
85        WidgetEvent::PointerDown { position, .. }
86        | WidgetEvent::PointerUp { position, .. }
87        | WidgetEvent::PointerMove { position, .. } => Some(*position),
88        // Deliberately not `PointerCancel`. A revocation must never *create*
89        // a pointer: admitting one here would resurrect a contact the funnel
90        // is in the middle of forgetting, and leave its entry behind for good.
91        _ => None,
92    }
93}
94
95/// What the bubble pass is allowed to run on one node.
96///
97/// Two independent gates, kept together because they answer the same
98/// question — "how much of this node takes part in *this* dispatch".
99#[derive(Copy, Clone)]
100struct BubbleGates {
101    /// Gates the pre-gesture `on_pointer_event` intercept. `true` for the
102    /// bubble target (the widget the event was dispatched at) and `false`
103    /// for every ancestor, because ancestors already fired their
104    /// `on_pointer_event` during the preview pass — firing it again in
105    /// bubble was the source of double-toggle / double-select bugs when a
106    /// wrapper widget (e.g. `ListItemWrapper`) held the handler and a child
107    /// leaf was the hit target.
108    fire_on_pointer_event: bool,
109    /// This node lost the pointer's arbitration, so its **recognizers** stay
110    /// out of the event and it bubbles on as if it carried none. Its own
111    /// handlers still run: losing an arbitration is not the same as being
112    /// removed from the tree. See `WidgetTree::sequence_blocks_arena`.
113    arena_blocked: bool,
114}
115
116impl WidgetTree {
117    /// Hops from `focus` up to `scope_id` (0 when equal), or `None` when
118    /// `scope_id` is not an ancestor-or-self of `focus`. Fewer hops means
119    /// the scope sits closer to focus — i.e. a more specific binding.
120    fn scope_distance_from_focus(&self, focus: WidgetId, scope_id: WidgetId) -> Option<usize> {
121        let mut hops = 0usize;
122        let mut current = Some(focus);
123        while let Some(c) = current {
124            if c == scope_id {
125                return Some(hops);
126            }
127            current = self.arena.parent(c);
128            hops += 1;
129        }
130        None
131    }
132
133    /// From every same-chord shortcut candidate, choose the one whose
134    /// scope applies to the current focus, preferring the most specific
135    /// scope: a `Scoped` binding whose subtree contains focus beats a
136    /// `Global` one, and among nested applicable scopes the one closest
137    /// to focus (fewest hops) wins. Equal-specificity ties keep the
138    /// deterministic `(category, id)` order `candidates` arrives in (the
139    /// first such candidate wins). Returns `None` when no candidate
140    /// applies — every match is a scoped binding outside the focused
141    /// subtree — so the caller falls through to normal KeyDown dispatch.
142    fn select_shortcut_for_focus(
143        &self,
144        candidates: &[(&'static str, crate::shortcut::ShortcutScope, bool)],
145    ) -> Option<(&'static str, crate::shortcut::ShortcutScope, bool)> {
146        use crate::shortcut::ShortcutScope;
147        let mut best: Option<(usize, (&'static str, ShortcutScope, bool))> = None;
148        for &(id, scope, propagate) in candidates {
149            // Specificity score, higher = more specific. Global is the
150            // least-specific fallback (0); any applicable scoped binding
151            // outranks it (`usize::MAX - hops`, so fewer hops = deeper
152            // scope = higher score). Tree depth is tiny, so no overflow.
153            let score = match scope {
154                ShortcutScope::Global => Some(0usize),
155                ShortcutScope::Scoped(scope_id) => self
156                    .focused
157                    .and_then(|f| self.scope_distance_from_focus(f, scope_id))
158                    .map(|hops| usize::MAX - hops),
159            };
160            let Some(score) = score else { continue };
161            // Strictly-greater keeps the first candidate on a tie, so the
162            // existing `(category, id)` precedence holds within a scope.
163            if best.as_ref().is_none_or(|(b, _)| score > *b) {
164                best = Some((score, (id, scope, propagate)));
165            }
166        }
167        best.map(|(_, c)| c)
168    }
169
170    /// Dispatch an event into the widget tree.
171    ///
172    /// Routing rules:
173    /// - Pointer events -> hit testing against layout tree
174    /// - Keyboard/IME events -> focused widget
175    /// - AccessKit actions -> target widget directly
176    /// - Scroll events -> hit testing (scroll target under pointer)
177    ///
178    /// Dispatch an event with the caller-supplied app-level
179    /// [`WindowOps`](crate::window::WindowOps) sink. `teksilo-app` calls
180    /// this variant; handlers can reach the multi-window API
181    /// synchronously (`open_window` creates the winit window inside
182    /// the same call before returning).
183    pub fn dispatch_event_with_ops(
184        &mut self,
185        event: WidgetEvent,
186        ops: &mut dyn crate::window::WindowOps,
187    ) {
188        // A legacy event names no pointer, so it is the mouse at the epoch —
189        // which is exactly what it has always meant.
190        let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
191        // A legacy `WidgetEvent` carries no timestamp of its own, so stamp it
192        // from the tree clock. Without this the gesture layer would see every
193        // hand-built event at the epoch and no interval — a double tap, a long
194        // press and a swipe would all be undecidable.
195        if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
196            snapshot.pointer.time = self.input_now();
197        }
198        self.dispatch_with_input_snapshot(event, snapshot, ops)
199    }
200
201    /// Dispatch an event on a standalone tree (tests, headless
202    /// scenarios). Handler code that calls `ctx.open_window(...)`
203    /// from within this dispatch will panic — by design. See
204    /// [`dispatch_event_with_ops`](Self::dispatch_event_with_ops)
205    /// for the app-facing variant.
206    pub fn dispatch_event(&mut self, event: WidgetEvent) {
207        let mut noop = crate::window::NoopWindowOps;
208        self.dispatch_event_with_ops(event, &mut noop);
209    }
210
211    // The three ingress doors
212
213    /// Deliver one pointer sample.
214    ///
215    /// This and [`dispatch_scroll`](Self::dispatch_scroll) are the real input
216    /// doors: a backend produces [`PointerSample`](crate::pointer::PointerSample)s
217    /// and [`ScrollSample`](crate::pointer::ScrollSample)s, and everything
218    /// Teksilo knows about *who* is pointing — identity, kind, pressure,
219    /// timestamp, coalesced history — reaches the tree through them.
220    ///
221    /// For now a sample is **lowered** onto the legacy `WidgetEvent` it
222    /// describes and takes the existing route, so a mouse behaves bit for bit
223    /// as it did before the doors existed. What changes here is only that the
224    /// door exists and that the sample's
225    /// [`PointerInfo`](crate::pointer::PointerInfo) is visible to handlers
226    /// through [`EventContext::pointer`](crate::widget::EventContext::pointer).
227    ///
228    /// A `Down` and an `Up` sample at the same point form a tap, whatever
229    /// timestamps a test gives them. For a whole gesture in one call a test
230    /// has [`click`](Self::click) (the centre of a widget) and
231    /// [`tap_with`](Self::tap_with) (a point, for any pointer kind).
232    pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
233        let mut noop = crate::window::NoopWindowOps;
234        self.dispatch_pointer_with_ops(sample, &mut noop);
235    }
236
237    /// [`dispatch_pointer`](Self::dispatch_pointer) with the caller's
238    /// app-level [`WindowOps`](crate::window::WindowOps) sink, so handlers can
239    /// reach the multi-window API synchronously.
240    pub fn dispatch_pointer_with_ops(
241        &mut self,
242        sample: crate::pointer::PointerSample,
243        ops: &mut dyn crate::window::WindowOps,
244    ) {
245        use crate::pointer::PointerPhase;
246
247        crate::trace_input!(
248            Samples,
249            "{:?} {:?} at {:?} buttons={:?} t={:?}",
250            sample.phase,
251            sample.pointer.id,
252            sample.position,
253            sample.pointer.buttons,
254            sample.pointer.time
255        );
256
257        // The button a Down/Up is *about*. A direct-pointer contact reports no
258        // button at all, and the widget layer has always been told
259        // `Primary` for a press — that is what a tap is.
260        let button = sample
261            .button
262            .unwrap_or(crate::event::PointerButton::Primary);
263        let event = match sample.phase {
264            PointerPhase::Down => WidgetEvent::PointerDown {
265                position: sample.position,
266                button,
267                modifiers: sample.modifiers,
268                pointer: sample.pointer,
269            },
270            PointerPhase::Move => WidgetEvent::PointerMove {
271                position: sample.position,
272                modifiers: sample.modifiers,
273                pointer: sample.pointer,
274            },
275            PointerPhase::Up => WidgetEvent::PointerUp {
276                position: sample.position,
277                button,
278                modifiers: sample.modifiers,
279                pointer: sample.pointer,
280            },
281            // The platform revoked the contact (a `wl_touch.cancel`, a
282            // `WM_POINTERCAPTURECHANGED`, a compositor grab). It takes the
283            // cancel funnel directly rather than being lowered onto an event:
284            // the funnel owns the teardown order, and lowering would first
285            // *admit* the pointer the sample is revoking.
286            PointerPhase::Cancel => {
287                // The dismissal this contact had armed goes with it. The funnel
288                // below returns early for a pointer with nothing revocable, and
289                // a suppressed arming `Down` leaves exactly that shape — no
290                // sequence, no capture — so the abort cannot ride on it.
291                self.overlay_manager.abort_dismiss(sample.pointer.id);
292                self.cancel_pointer(
293                    sample.pointer.id,
294                    crate::pointer::CancelReason::Platform,
295                    ops,
296                );
297                // A revoked contact ceases to exist, exactly as a lifted one
298                // does — the `ends_pointer` rule below, which this arm returns
299                // before reaching. The funnel ends the entry itself *when it
300                // runs*; for a pointer that had no press to revoke it returns
301                // first, and the entry would outlive the finger. Idempotent, so
302                // the ordinary path is unaffected.
303                if !sample.pointer.kind.hovers() {
304                    self.pointers.end(sample.pointer.id);
305                }
306                return;
307            }
308        };
309
310        // Admit the pointer before anything is dispatched. A palm, or an
311        // eleventh simultaneous contact, is refused here and produces no event
312        // at all — the alternative (evicting a live contact to make room) turns
313        // a pinch into a fling, and letting a resting palm through turns a hand
314        // on a tablet into a stream of taps.
315        if !self.pointers.would_admit(&sample.pointer) {
316            return;
317        }
318        // A contact ceases to exist when it lifts; a hovering-capable pointer
319        // does not — a mouse that releases a button is still there, still
320        // hovering, and its entry is what every legacy singular accessor reads.
321        let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
322            && !sample.pointer.kind.hovers();
323        let pointer_id = sample.pointer.id;
324
325        let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
326        self.dispatch_with_input_snapshot(event, snapshot, ops);
327
328        if ends_pointer {
329            self.pointers.end(pointer_id);
330        }
331    }
332
333    /// Deliver one scroll sample.
334    ///
335    /// Routing follows [`ScrollSample::position`](crate::pointer::ScrollSample::position):
336    /// `Some` hit-tests it, `None` falls back to the hovered (else focused)
337    /// widget, which is what every scroll did before. A mouse wheel carries no
338    /// position, so this is a no-op for a mouse; a pan synthesised from a
339    /// direct pointer must carry one, because a contact never writes hover.
340    pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
341        let mut noop = crate::window::NoopWindowOps;
342        self.dispatch_scroll_with_ops(sample, &mut noop);
343    }
344
345    /// [`dispatch_scroll`](Self::dispatch_scroll) with the caller's app-level
346    /// [`WindowOps`](crate::window::WindowOps) sink.
347    pub fn dispatch_scroll_with_ops(
348        &mut self,
349        sample: crate::pointer::ScrollSample,
350        ops: &mut dyn crate::window::WindowOps,
351    ) {
352        crate::trace_input!(
353            Samples,
354            "scroll {:?} {:?}/{:?} at {:?}",
355            sample.delta,
356            sample.phase,
357            sample.source,
358            sample.position
359        );
360
361        let event = WidgetEvent::Scroll {
362            delta: sample.delta,
363            modifiers: sample.modifiers,
364            window_position: sample.position,
365            phase: sample.phase,
366            pointer: sample.pointer,
367        };
368        let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
369        self.dispatch_with_input_snapshot(event, snapshot, ops);
370    }
371
372    /// The pointer left the window.
373    ///
374    /// The third ingress door, and the only one that carries no sample: the OS
375    /// says the cursor crossed the window boundary and nothing else. It exists
376    /// because hover is otherwise cleared *only* by a move that lands
377    /// elsewhere — so a mouse that leaves through an edge would leave the last
378    /// widget hovered for as long as it stays away, with its hover chrome
379    /// painted, its `hover_within` signal true and its tooltip still counting
380    /// down.
381    ///
382    /// Clears hover the way a move to an empty spot does: a `PointerLeave` to
383    /// the hovered widget, the tooltip dwell cancelled, the `hover_within`
384    /// chain updated. It touches nothing else — no pointer is cancelled, no
385    /// capture released, no table entry ended. A mouse that leaves the window
386    /// is still a mouse, and a *captured* pointer is deliberately exempt: a
387    /// drag whose pointer wanders off the window keeps its target, which is
388    /// what makes a drag past the edge (and the OS-drag escalation built on
389    /// it) work at all.
390    ///
391    /// There is no matching `pointer_entered_window`, and that is not an
392    /// omission: the enter carries no position either, and a position only
393    /// ever arrives with a `CursorMoved` — which re-arms hover through the
394    /// ordinary path. A door that could only say "somewhere" would have
395    /// nothing to hit-test.
396    pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
397        // The pointer that owns hover is the one that just left; if it holds a
398        // capture, the interaction it is in the middle of outranks the
399        // boundary crossing.
400        if self
401            .pointers
402            .hover_owner_id()
403            .and_then(|id| self.captured_by(id))
404            .is_some()
405        {
406            return;
407        }
408        let Some(hovered) = self.hovered_id() else {
409            return;
410        };
411        // The hover owner is the pointer that just left — read from the table
412        // because this door carries no sample of its own.
413        let leave = WidgetEvent::PointerLeave {
414            pointer: self.hover_transition_pointer(),
415        };
416        self.dispatch_to_widget(hovered, &leave, &mut *ops);
417        self.tooltip_pointer_leave(hovered, &mut *ops);
418        self.set_hovered(None);
419        self.update_hover_within_signals(Some(hovered), None);
420    }
421
422    /// The pointer a hover transition is credited to.
423    ///
424    /// [`PointerEnter`](WidgetEvent::PointerEnter) /
425    /// [`PointerLeave`](WidgetEvent::PointerLeave) are hover-owner events by
426    /// construction — a contact never writes hover — so the answer is the hover
427    /// owner's own [`PointerInfo`](crate::pointer::PointerInfo), read from the
428    /// table rather than from the in-flight sample: a transition can be raised
429    /// by something that is not a pointer sample at all (a relayout that moves
430    /// a widget out from under the cursor, the window-leave door), and a
431    /// hover-incapable pointer must never be credited with hover.
432    ///
433    /// Falls back to a mouse at the current tree time when the table has no
434    /// hover owner, which is the state a synthesized `dispatch_event` leaves it
435    /// in. Either way this is the tree's own view rather than a producer's — see
436    /// the rule stated on
437    /// [`handle_pointer_move`](Self::handle_pointer_move).
438    pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
439        self.pointers
440            .hover_owner()
441            .map(|entry| entry.info)
442            .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
443    }
444
445    /// Run one dispatch with `snapshot` installed as the tree's view of the
446    /// in-flight sample, restoring the previous value afterwards.
447    ///
448    /// Save-and-restore rather than reset-to-default so a nested dispatch (a
449    /// synthetic click queued by a handler, a scroll-into-view walk) leaves the
450    /// outer sample's snapshot intact for the rest of the outer dispatch.
451    fn dispatch_with_input_snapshot(
452        &mut self,
453        event: WidgetEvent,
454        snapshot: crate::pointer::InputSnapshot,
455        ops: &mut dyn crate::window::WindowOps,
456    ) {
457        // A dispatch reached from inside a dispatch — a handler's synthetic
458        // click, an assistive-technology action re-entering the door — is
459        // **queued**, not run inline. Running it inline would let it unwind the
460        // pointer state the outer sample is still standing on: the outer
461        // handler would return to a tree whose hover, capture and table entries
462        // had all moved under it. Queued, the outer dispatch finishes on the
463        // state it started with and the nested one replays immediately
464        // afterwards, so from a caller's side nothing changed — the queue is
465        // empty again before the top-level call returns.
466        self.pending_dispatch.push_back(QueuedDispatch::Event {
467            event,
468            snapshot: Box::new(snapshot),
469        });
470        if self.dispatch_depth > 0 {
471            return;
472        }
473        self.drain_pending_dispatch(&mut *ops);
474    }
475
476    /// Run everything the queue holds, in order, each at depth zero.
477    ///
478    /// `pop_front` in a loop rather than `drain`: a replayed dispatch — or a
479    /// cancel's own `PointerCancel` handler — may queue another entry, and
480    /// each must in turn run at depth zero.
481    pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
482        while let Some(queued) = self.pending_dispatch.pop_front() {
483            match queued {
484                QueuedDispatch::Event { event, snapshot } => {
485                    self.run_one_dispatch(event, *snapshot, &mut *ops);
486                }
487                QueuedDispatch::Cancel {
488                    pointer,
489                    reason,
490                    recipient,
491                } => {
492                    self.run_one_cancel(pointer, reason, recipient, &mut *ops);
493                }
494            }
495        }
496    }
497
498    /// One dispatch at depth zero, with `snapshot` installed as the tree's view
499    /// of the in-flight sample and the previous value restored afterwards.
500    ///
501    /// Save-and-restore rather than reset-to-default: the restore is what stops
502    /// one queued dispatch's snapshot standing as the tree's view of the world
503    /// once it has returned, while the drain replays the next entry.
504    fn run_one_dispatch(
505        &mut self,
506        event: WidgetEvent,
507        snapshot: crate::pointer::InputSnapshot,
508        ops: &mut dyn crate::window::WindowOps,
509    ) {
510        let previous = std::mem::replace(&mut self.current_input, snapshot);
511        self.dispatch_depth += 1;
512        self.dispatch_event_impl(event, ops);
513        self.dispatch_depth -= 1;
514        self.current_input = previous;
515    }
516
517    fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
518        // Admit the pointer this event belongs to into the table before
519        // anything routes. A legacy `WidgetEvent` names no pointer, so
520        // `current_input` reports the mouse and this creates (or refreshes) the
521        // one mouse entry — which is what every singular accessor then reads,
522        // so a mouse-only tree behaves exactly as it did before the table.
523        if let Some(position) = pointer_event_position(&event) {
524            let is_down = matches!(event, WidgetEvent::PointerDown { .. });
525            let is_move = matches!(event, WidgetEvent::PointerMove { .. });
526            if !self.admit_current_pointer(position, is_down, is_move) {
527                return;
528            }
529            // A hovering-capable pointer takes the hover-owner role by pointing:
530            // the later sample wins, and whoever held it is sent a leave. A
531            // contact is refused the role outright — it has no hover to give.
532            if self.current_input.pointer.kind.hovers() {
533                self.claim_hover_owner_for_current(&mut *ops);
534            }
535        }
536
537        // Track input modality for `:focus-visible`: keyboard input reveals
538        // focus rings, pointer input hides them. Updated at the dispatch root so
539        // every handler (and the next paint) observes the current modality.
540        match &event {
541            WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
542                self.focus_visible.set(true);
543            }
544            WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
545                self.focus_visible.set(false);
546            }
547            _ => {}
548        }
549
550        // The "back toward the parent overlay" key closes the top nested
551        // overlay (e.g. an open submenu over its parent menu). It is the
552        // inline-start arrow: ArrowLeft under LTR, ArrowRight under RTL.
553        // Without the RTL flip, ArrowLeft would navigate *into* a submenu
554        // in RTL menus yet still dismiss it here.
555        let overlay_back_key = match self.layout_direction {
556            crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
557            crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
558        };
559        if let WidgetEvent::KeyDown { key, .. } = &event
560            && *key == overlay_back_key
561        {
562            // Count menu-level (non-host) overlays. A revealed collapsible
563            // `MenuBar` is itself a *host* overlay (Role::MenuBar), so a
564            // single open top-level menu sitting over it must NOT be treated
565            // as a nested submenu — otherwise the back key would close the
566            // menu instead of letting the menubar navigate to the previous
567            // one. Only when ≥2 non-host overlays are stacked (a submenu over
568            // its parent menu) does the back key dismiss the top overlay.
569            //
570            // **Menus only**, which is what the band says. Every mounted text
571            // editor keeps one full-viewport affordance host alive in the
572            // [`TextAffordance`](crate::overlay::OverlayBand::TextAffordance)
573            // band for its selection handles, so counting bands alike made two
574            // editors on one page read as a menu cascade: the back key then
575            // tore down an affordance host and returned, and ArrowLeft stopped
576            // reaching *any* editor in that window for as long as a second one
577            // was mounted. A text affordance is not a cascade level, the same
578            // reason `OverlayBand::dismissed_by_outside_press` already excludes
579            // it from press dismissal.
580            //
581            // An inert overlay (a slider's value readout, say) is no level
582            // either: it takes no input, so it is neither counted nor what the
583            // key closes. Counted, a readout under one open menu made that menu
584            // read as a submenu and closed it; on top of the stack, it was the
585            // overlay the key closed, and the key was spent on it.
586            //
587            // The topmost *non-inert* overlay is the menu this key means: the
588            // stack is band-ordered (`OverlayManager::show_with_auto_dismiss`
589            // inserts, it does not push), so a `Standard` overlay always sits
590            // above every `TextAffordance` one.
591            let nested_menu_overlays = {
592                let ids: Vec<_> = self
593                    .overlay_manager
594                    .stack
595                    .iter()
596                    .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
597                    .map(|o| o.id)
598                    .collect();
599                ids.into_iter()
600                    .filter(|&id| !self.overlay_is_host_surface(id))
601                    .count()
602            };
603            // The back key only navigates *menu* cascades; it must never close a
604            // dialog / alert / modal that happens to sit on top. Each modal is a
605            // scrim+panel overlay pair and the (non-host) scrims inflate the count
606            // above, so also require the topmost non-inert overlay to be
607            // back-navigable — i.e. a non-host (menu) surface — before dismissing
608            // it.
609            let top_id = self
610                .overlay_manager
611                .stack
612                .iter()
613                .rev()
614                .find(|o| !o.inert)
615                .map(|o| o.id);
616            let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
617            if nested_menu_overlays > 1
618                && top_is_back_navigable
619                && let Some(top_id) = top_id
620            {
621                let (content_ids, focus_restore) =
622                    self.overlay_manager.dismiss_with_focus_restore_because(
623                        top_id,
624                        crate::overlay::DismissReason::Escape,
625                    );
626                self.dormant_dismissed_content(&content_ids, &mut *ops);
627                if let Some(restore_id) = focus_restore
628                    && self.arena.is_active(restore_id)
629                {
630                    self.focus_ops(restore_id, &mut *ops);
631                }
632                return;
633            }
634        }
635
636        // Escape retires any shown tooltip first, and does **not** stop there —
637        // see `tooltip_escape_pressed`. Ordered before the stack walk below so
638        // that walk can no longer pick a tooltip as the thing to dismiss, which
639        // is what used to spend the key on a tip nobody was reading while the
640        // editor / menu / dialog the user meant stayed open.
641        if let WidgetEvent::KeyDown {
642            key: Key::Escape, ..
643        } = &event
644        {
645            self.tooltip_escape_pressed();
646            // The same rule for every overlay nothing can be done in: it goes,
647            // and the key goes on. See `inert_overlays_escape_pressed`.
648            self.inert_overlays_escape_pressed(&mut *ops);
649        }
650
651        if let WidgetEvent::KeyDown {
652            key: Key::Escape, ..
653        } = &event
654            && !self.overlay_manager.is_empty()
655            && let Some((_id, content_ids, focus_restore)) =
656                self.overlay_manager.try_dismiss_top_on_escape()
657        {
658            self.dormant_dismissed_content(&content_ids, &mut *ops);
659            if let Some(restore_id) = focus_restore
660                && self.arena.is_active(restore_id)
661            {
662                self.focus_ops(restore_id, &mut *ops);
663            }
664            return;
665        }
666
667        // Outside-press overlay dismissal. Two shapes, chosen by the pointer:
668        // an indirect one dismisses on the press and falls through, exactly as
669        // it always has; a direct one *arms* on the press and commits on the
670        // release. See `arm_outside_press_dismissal`.
671        match &event {
672            WidgetEvent::PointerDown {
673                position, button, ..
674            } => {
675                if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
676                    return;
677                }
678            }
679            WidgetEvent::PointerUp { position, .. } => {
680                if self.commit_outside_press_dismissal(*position, &mut *ops) {
681                    return;
682                }
683            }
684            WidgetEvent::PointerCancel { pointer, .. } => {
685                // A revoked press dismisses nothing. The arming Down was never
686                // delivered beneath either, so the whole gesture leaves no
687                // trace — which is the point of deferring to the release.
688                self.overlay_manager.abort_dismiss(pointer.id);
689            }
690            _ => {}
691        }
692
693        // Key-capture mode: if a callback is armed (via
694        // `WidgetTree::begin_key_capture`), the next KeyDown bypasses
695        // shortcut resolution entirely and runs the callback with
696        // mutable access to the registry AND an `EventContext` so
697        // rebind handlers can also emit commands, send intents,
698        // dismiss overlays, etc. The capture is one-shot; its slot
699        // is emptied before the callback runs so a re-entrant
700        // `begin_key_capture` call from inside the callback arms a
701        // fresh session (rather than competing with the in-flight
702        // one).
703        if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
704            && let Some(callback) = self.take_key_capture()
705        {
706            let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
707            let mut cap_ctx = self.make_event_context(&mut *ops);
708            callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
709            // Route side effects of the callback through the
710            // focused widget (or an arbitrary root if no focus).
711            let anchor = self.focused.or_else(|| self.arena.roots().first().copied());
712            if let Some(anchor_id) = anchor {
713                self.collect_from_ctx(cap_ctx, anchor_id);
714                self.drain_pending_intents(&mut *ops);
715            }
716            return;
717        }
718
719        // Keyboard-capture surfaces (a terminal, a game viewport) opt out
720        // of shortcut resolution entirely while focused: they want every
721        // keystroke delivered raw so a host-app `Ctrl+C` shortcut can't
722        // steal the SIGINT the child process needs. The Escape / overlay
723        // back-navigation handled above still runs first, so an open
724        // overlay is still dismissable. Only a KeyDown is affected; KeyUp
725        // and IME already bypass the shortcut path.
726        let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
727            && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
728
729        // Shortcut → intent → action dispatch. A KeyDown whose chord
730        // matches a registered enabled `Shortcut` whose scope contains
731        // the focused widget is consumed here: the shortcut's
732        // `on_activate` runs (producing an `Intent`), its ctx side
733        // effects are collected, and the intent walks source-widget →
734        // root firing any matching `Action`. Otherwise the focused
735        // widget sees the raw KeyDown below.
736        //
737        // Two-phase: the registry is inspected first (immutable read)
738        // to resolve `id / scope / propagate_when_disabled`. Only if
739        // scope matches the current focus do we take a mutable borrow
740        // to invoke `on_activate` — this way a scope mismatch cannot
741        // drop side effects the closure put into its ctx, because
742        // the closure never runs.
743        if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
744            let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
745            // Gather every same-chord candidate (owned fields) before any
746            // mutable borrow of the registry, then pick the one whose scope
747            // actually applies to the current focus. `find_by_keystroke`
748            // alone yields only the first by `(category, id)` order, which
749            // can be a `Scoped` binding outside focus shadowing an
750            // applicable `Global` one — or a `Global` binding that should
751            // yield to an in-focus `Scoped` one. Selection needs focus +
752            // the tree, so it happens here, not in the registry.
753            let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
754                .shortcut_registry
755                .matches_by_keystroke(keystroke)
756                .map(|eff| {
757                    (
758                        eff.shortcut.id,
759                        eff.shortcut.scope,
760                        eff.shortcut.propagate_when_disabled,
761                    )
762                })
763                .collect();
764            let lookup = self.select_shortcut_for_focus(&candidates);
765            if let Some((id, scope, propagate_when_disabled)) = lookup {
766                let anchor = match scope {
767                    // Global shortcuts fire regardless of focus. If no
768                    // widget is currently focused, anchor the intent
769                    // walk at an arbitrary root so actions registered
770                    // at the top of the tree still see the intent.
771                    crate::shortcut::ShortcutScope::Global => {
772                        self.focused.or_else(|| self.arena.roots().first().copied())
773                    }
774                    crate::shortcut::ShortcutScope::Scoped(scope_id) => {
775                        self.focused.filter(|f| self.is_descendant_of(*f, scope_id))
776                    }
777                };
778                if let Some(anchor_id) = anchor {
779                    let mut act_ctx = self.make_event_context(&mut *ops);
780                    if let Some(intent) =
781                        self.shortcut_registry
782                            .invoke_on_activate(id, keystroke, &mut act_ctx)
783                    {
784                        self.collect_from_ctx(act_ctx, anchor_id);
785                        // Intents `on_activate` sent through its ctx go
786                        // first, as they did when everything shared one
787                        // queue.
788                        self.drain_pending_intents(&mut *ops);
789                        // Tag shortcut origin so analytics can
790                        // distinguish keyboard-driven activations from
791                        // button / menu / programmatic ones.
792                        let intent = intent.with_source(crate::telemetry::IntentSource::Shortcut);
793                        let consumed = self.dispatch_intent(
794                            anchor_id,
795                            intent,
796                            propagate_when_disabled,
797                            &mut *ops,
798                        );
799                        self.drain_pending_intents(&mut *ops);
800                        if consumed {
801                            return;
802                        }
803                        // No action took the intent: the key is the focused
804                        // widget's after all, delivered below as if no
805                        // shortcut had matched. The standard edit shortcuts
806                        // depend on this — Ctrl+C is bound in every tree, and
807                        // a widget that copies from its own `on_key` must
808                        // still see it.
809                    }
810                }
811                // Chosen candidate had no anchor after all (e.g. a Global
812                // match while nothing is focused and the tree has no
813                // roots) — fall through to normal KeyDown dispatch.
814                // `on_activate` was never called, so nothing to clean up.
815            }
816            // `lookup` is `None` when every same-chord candidate was a
817            // scoped binding outside the focused subtree — fall through.
818        }
819
820        // Escape during an OS drag we escalated. There is no `active_drag`
821        // any more — `try_escalate_to_os_drag` took it when the platform
822        // accepted the hand-off — so this cannot live in the block below, but
823        // it is the same user gesture and belongs on the same path rather than
824        // being special-cased in the event loop of whichever backend needs it.
825        if self.outbound_drag_source.is_some()
826            && let WidgetEvent::KeyDown {
827                key: Key::Escape, ..
828            } = &event
829        {
830            ops.cancel_os_drag();
831            // Deliberately no `return`: the backend answers asynchronously with
832            // a terminal `DragEnded`, which is what actually tears the session
833            // down via `handle_os_drag_ended`. Swallowing the key here would
834            // also stop Escape from closing whatever else is open.
835        }
836
837        // --- Active drag session handling ---
838        if self.active_drag.is_some() {
839            match &event {
840                WidgetEvent::PointerMove { position, .. } => {
841                    self.handle_drag_move(*position, &mut *ops);
842                    return;
843                }
844                WidgetEvent::PointerUp { position, .. } => {
845                    self.handle_drag_drop(*position, &mut *ops);
846                    return;
847                }
848                WidgetEvent::KeyDown {
849                    key: Key::Escape, ..
850                } => {
851                    self.cancel_active_drag(&mut *ops);
852                    return;
853                }
854                WidgetEvent::Scroll { .. } => {
855                    // Route the wheel to the current drop target so users
856                    // can scroll the list/tree beneath the drag. Then
857                    // synthesise a hover at the stationary pointer so
858                    // feedback, drop-index math and the preview overlay
859                    // all reflect the new scroll offset.
860                    let target_and_pos = self
861                        .active_drag
862                        .as_ref()
863                        .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
864                    if let Some((target, _pos)) = target_and_pos {
865                        self.dispatch_to_widget(target, &event, &mut *ops);
866                    }
867                    if let Some((_, pos)) = target_and_pos
868                        && self.active_drag.is_some()
869                    {
870                        self.handle_drag_move(pos, &mut *ops);
871                    }
872                    return;
873                }
874                _ => {}
875            }
876        }
877
878        // A keyboard route to the context menu, reserved at the dispatcher so
879        // every widget with a `.context_menu(..)` gets one without opting in.
880        //
881        // It has to be here rather than in a widget, and it cannot be a
882        // `Shortcut`: shortcut resolution runs above this point, so a global
883        // binding would fire while the user was typing in a modal. Sitting
884        // below it means an application that deliberately binds Shift+F10 to
885        // something else still wins.
886        if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
887            && is_context_menu_chord(*key, *modifiers)
888            && self.open_context_menu_from_keyboard(&mut *ops)
889        {
890            return;
891        }
892
893        match &event {
894            WidgetEvent::PointerMove { position, .. } => {
895                // Every sample: re-check the sequence's members against the
896                // arena, and record where the pointer now is so each threshold
897                // reads one number.
898                self.note_sequence_position(*position);
899                self.revalidate_sequence(&mut *ops);
900                // Timers before positional thresholds, and before the move
901                // reaches any recognizer — see `tick_sequence_timers`.
902                self.tick_sequence_timers();
903                // The multi-contact and palm layers see every sample, decided
904                // or not: a pinch is arbitrated by contact count rather than by
905                // the press arbitration, and the palm watch has to know whether
906                // this contact ever moved.
907                self.note_palm_sample(*position);
908                self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
909                if let Some(captured) = self.current_pointer_capture() {
910                    self.dispatch_to_widget(captured, &event, &mut *ops);
911                    // Advance the arbitration so an ancestor drag can still
912                    // begin while a descendant tap holds the capture. Once a
913                    // drag latches, `active_drag` takes over and the capture
914                    // branch above is bypassed.
915                    if self.active_drag.is_none() {
916                        self.advance_sequence(&event, &mut *ops);
917                    }
918                } else {
919                    self.handle_pointer_move(&event, *position, &mut *ops);
920                    // No capture: for a mouse there is nothing enrolled (a
921                    // gesture member is only enrolled *through* a capture), so
922                    // this is a no-op. A contact panning from empty space has
923                    // its pan claimants here.
924                    if self.active_drag.is_none() {
925                        self.advance_sequence(&event, &mut *ops);
926                    }
927                }
928                // After the arbitration, so a claim taken on *this* sample
929                // already delivers its own movement rather than waiting a frame.
930                self.advance_pan(*position, &mut *ops);
931                // …and after that, so the press visual answers to a claim taken
932                // on this very sample rather than surviving it by one move.
933                self.update_press(*position);
934                // A tree-owned hold survives only while the contact holds
935                // still; past the tap boundary this is a pan or a drag.
936                self.touch_route_moved(self.current_pointer_id());
937                // Hover-owner-only, exactly as `handle_pointer_move` is. The
938                // `DismissBehavior::PointerLeave` grace is a *hover* dismissal:
939                // it asks "has the pointer left this overlay and its trigger",
940                // a question only a pointer that hovers is entitled to answer.
941                // Ungated, any contact's move anywhere started the 150 ms grace
942                // on every such overlay — so a submenu a finger had just tapped
943                // open was closed by the frame pass with no further input, and
944                // no mouse test could see it because a mouse *is* the hover
945                // owner.
946                if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
947                    self.update_pointer_leave_overlays(*position, &mut *ops);
948                }
949            }
950            WidgetEvent::PointerDown {
951                position, button, ..
952            } => {
953                // A new press is a new interaction: whatever took the previous
954                // one away has nothing to say about this one.
955                let pressed = self.current_pointer_id();
956                self.cancelled_pointers.retain(|p| *p != pressed);
957                // The user has acted — a tooltip that has not yet appeared is
958                // now answering a question nobody is asking any more, and one
959                // already up is covering the thing being clicked. Cancel the
960                // pending dwell and retire any shown non-sticky tip, the way
961                // Windows and GTK both do. Runs before hit-testing so it fires
962                // even for a press that lands on nothing.
963                self.tooltip_pointer_press(Some(*position));
964                // Routed for the pointer that is actually pressing: a finger
965                // gets its grip outsets and its miss-only slop, a mouse gets the
966                // exact test it has always had.
967                let pressing = self.current_input.pointer;
968                if let Some(target) = self.hit_test_for(*position, &pressing) {
969                    if *button == PointerButton::Secondary
970                        && self.show_context_menu_for(
971                            target,
972                            *position,
973                            crate::widget_builder::ContextMenuTrigger::Pointer,
974                            &mut *ops,
975                        )
976                    {
977                        return;
978                    }
979                    // Open the arbitration BEFORE any handler runs: the frozen
980                    // `TouchAction` has to be readable from `ctx.touch_action()`
981                    // inside the press handler, and an explicit
982                    // `capture_pointer()` made there needs a sequence to enrol
983                    // into.
984                    self.begin_sequence(target, *position);
985                    // Straight after the sequence, so the frozen `TouchAction`
986                    // and the enrolled pan members are already in hand — and so
987                    // a press on a coasting list catches it before anything
988                    // else runs.
989                    let modifiers = match &event {
990                        WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
991                        _ => crate::event::Modifiers::NONE,
992                    };
993                    self.begin_pan(target, *position, modifiers);
994                    self.begin_palm_watch(*position);
995                    self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
996                    // Open the press record before the dispatch, so a handler
997                    // asking `ctx.press_pending()` on its own `PointerDown`
998                    // gets the answer the router already knows.
999                    let focusable = self.find_focusable_at_or_above(target);
1000                    self.begin_press(focusable);
1001                    // An indirect pointer focuses on press, as it always has.
1002                    // A direct one waits for the release: a finger that lands
1003                    // on a control and slides away has not chosen it, and
1004                    // moving focus at touch-down would leave the ring — and
1005                    // the caret — on a control the user never activated. See
1006                    // `focus_on_release`.
1007                    if !pressing.kind.is_direct()
1008                        && let Some(focusable) = focusable
1009                    {
1010                        self.focus_with_origin_ops(
1011                            focusable,
1012                            crate::focus::FocusOrigin::Pointer(pressing.kind),
1013                            &mut *ops,
1014                        );
1015                    }
1016                    self.dispatch_to_widget(target, &event, &mut *ops);
1017                    // Enrol the competitors that only become knowable once the
1018                    // press has been dispatched: the drag-capable ancestors of
1019                    // whoever took the capture (tap-vs-drag across the hit
1020                    // path).
1021                    if self.active_drag.is_none() {
1022                        self.enrol_sequence_members(&event, &mut *ops);
1023                    }
1024                    // The arena has now claimed the press, so the node whose
1025                    // visual this record drives is known — and whether the
1026                    // button that opened it is one that node can act on.
1027                    self.adopt_press_owner(*button);
1028                    // Last: the tree-owned hold. It has to see the enrolment
1029                    // (a deferred grab spends the hold) and it has to see the
1030                    // handlers a press-time build may have installed, so it is
1031                    // resolved after both. See `super::touch_route`.
1032                    self.arm_touch_route(target, *position);
1033                }
1034            }
1035            WidgetEvent::PointerUp { position, .. } => {
1036                // A `PointerCancel` is terminal. If this pointer's press was
1037                // revoked, the `Up` that follows completes nothing — the widget
1038                // has already been told to let go, and delivering the release
1039                // would hand it back an interaction the system took away. Drop
1040                // it, and forget the cancel: the pointer is free again.
1041                let released = self.current_pointer_id();
1042                // The hold is over whatever else this release does, and it is
1043                // cleared before any of it so a handler that runs below cannot
1044                // see a route that will never fire.
1045                self.cancel_touch_route(released);
1046                if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1047                    self.cancelled_pointers.swap_remove(index);
1048                    crate::trace_input!(
1049                        Samples,
1050                        "swallowing the Up for {released:?}: its press was cancelled"
1051                    );
1052                    return;
1053                }
1054                // The palm verdict, before anything else acts on the release:
1055                // a contact the heuristic rejects must fire no tap at all, and
1056                // the only way to guarantee that is to take the cancel funnel
1057                // instead of the release path. Judged on the `Up` and never
1058                // earlier — a contact is not revoked while the user might still
1059                // be doing something with it.
1060                self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1061                if self.take_palm_verdict(released) {
1062                    crate::trace_input!(
1063                        Samples,
1064                        "{released:?} released as a palm: large, and it never moved"
1065                    );
1066                    self.cancel_pointer(
1067                        released,
1068                        crate::pointer::CancelReason::PalmRejected,
1069                        &mut *ops,
1070                    );
1071                    return;
1072                }
1073                // A pan hands its release velocity to the fling driver here,
1074                // and closes its session either way.
1075                self.end_pan(*position, &mut *ops);
1076                // The pointer sequence ends here — the release sweep feeds the
1077                // `Up` to every member still following the press so its
1078                // recognizer clears the press origin it recorded. Without this,
1079                // a press that an interactive descendant captured (a card's
1080                // editor, a row's button) leaves the ancestor's DragRecognizer
1081                // armed, and the next hover move starts a phantom drag.
1082                self.note_sequence_position(*position);
1083                self.end_sequence(&event, &mut *ops);
1084                // A direct pointer's focus lands here, before the release is
1085                // dispatched, so a handler activating on the `Up` runs with the
1086                // focus its own press earned.
1087                self.focus_on_release(*position, &mut *ops);
1088                if let Some(captured) = self.current_pointer_capture() {
1089                    self.dispatch_to_widget(captured, &event, &mut *ops);
1090                    // Per pointer: this Up releases *this* pointer's capture and
1091                    // leaves every other contact's alone.
1092                    self.set_current_pointer_capture(None);
1093                } else {
1094                    let releasing = self.current_input.pointer;
1095                    if let Some(target) = self.hit_test_for(*position, &releasing) {
1096                        self.dispatch_to_widget(target, &event, &mut *ops);
1097                    }
1098                }
1099                // The press is over. Any arena still following this contact saw
1100                // its `Down` but not its `Up` — see `release_arenas_following`.
1101                let released = self.current_pointer_id();
1102                self.release_arenas_following(released);
1103                // The visual goes with it. After the dispatch, so a release
1104                // handler reading `ctx.is_pressed()` still sees the press it is
1105                // completing.
1106                self.end_press(released);
1107            }
1108            WidgetEvent::PointerCancel {
1109                reason, pointer, ..
1110            } => {
1111                // A hand-built `PointerCancel` — a caller reaching the legacy
1112                // door with one, a test — means the same thing a producer does,
1113                // so it takes the same funnel rather than a second teardown of
1114                // its own. Queued behind this dispatch, like every cancel.
1115                let (reason, pointer) = (*reason, pointer.id);
1116                self.cancel_pointer(pointer, reason, &mut *ops);
1117            }
1118            WidgetEvent::Scroll {
1119                window_position: position,
1120                ..
1121            } => {
1122                use super::pan_arbiter::ScrollDelivery;
1123                match ScrollDelivery::for_source(self.current_input.scroll_source) {
1124                    // A synthesised pan (and the coast that follows it) walks
1125                    // the pan claimants and nothing else — see
1126                    // `widget_tree::pan_arbiter` for why the generic bubble
1127                    // would be wrong here.
1128                    ScrollDelivery::ClaimantChain => {
1129                        let position = *position;
1130                        self.route_scroll_along_chain(&event, position, &mut *ops);
1131                    }
1132                    // A positioned scroll routes by hit test; a positionless one
1133                    // keeps the historical hover-then-focus fallback. A mouse wheel
1134                    // is positionless, so this is a no-op for it — the change
1135                    // exists for a pan synthesised from a direct pointer, which
1136                    // never writes hover and would otherwise route nowhere.
1137                    ScrollDelivery::Bubble => {
1138                        let scrolling = self.current_input.pointer;
1139                        let target = match position {
1140                            Some(p) => self.hit_test_for(*p, &scrolling),
1141                            None => self.hovered_id().or(self.focused),
1142                        };
1143                        if let Some(target) = target {
1144                            self.dispatch_to_widget(target, &event, &mut *ops);
1145                        }
1146                    }
1147                }
1148            }
1149            WidgetEvent::KeyDown { key, modifiers, .. } => {
1150                if *key == Key::Tab {
1151                    // Ctrl+Tab / Ctrl+Shift+Tab always leave a keyboard-capture
1152                    // surface (WCAG 2.1.2). A capture node exists precisely to
1153                    // swallow every keystroke — a terminal encodes Tab as `\t`
1154                    // and Shift+Tab as CSI Z — so the ordinary "dispatch first,
1155                    // cycle only when unhandled" rule below can never move focus
1156                    // out of one. Reserving this one chord at the dispatcher, not
1157                    // in each capture widget, is what makes the escape a property
1158                    // of `keyboard_capture` itself rather than a promise every
1159                    // future capture-surface author has to remember to keep.
1160                    //
1161                    // Literal `ctrl()`, not `command()`: Ctrl+Tab is Ctrl+Tab on
1162                    // macOS too — ⌘⇥ is the application switcher and never
1163                    // reaches an app at all. Same reading as `TableView`'s
1164                    // cell-grid escape and `RichTextEditor`'s `tab_escape`.
1165                    let captured_focus = self
1166                        .focused
1167                        .is_some_and(|focused| self.is_keyboard_capture(focused));
1168                    if captured_focus && modifiers.ctrl() {
1169                        self.cycle_focus(modifiers.shift(), &mut *ops);
1170                        return;
1171                    }
1172                    // Dispatch Tab to the focused widget first so
1173                    // ancestors (e.g. an open overlay that wants to
1174                    // close instead of moving focus out through its
1175                    // content) get a chance to intercept. Fall back to
1176                    // built-in focus cycling only when no handler
1177                    // returns `EventResponse::Handled`.
1178                    let handled = self
1179                        .focused
1180                        .map(|focused| {
1181                            self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1182                        })
1183                        .unwrap_or(false);
1184                    if !handled {
1185                        self.cycle_focus(modifiers.shift(), &mut *ops);
1186                    }
1187                } else if let Some(focused) = self.focused {
1188                    self.dispatch_to_widget(focused, &event, &mut *ops);
1189                }
1190            }
1191            WidgetEvent::KeyUp { .. }
1192            | WidgetEvent::ImeComposition { .. }
1193            | WidgetEvent::ImeCommit { .. } => {
1194                if let Some(focused) = self.focused {
1195                    self.dispatch_to_widget(focused, &event, &mut *ops);
1196                }
1197            }
1198            WidgetEvent::AccessAction { target, action, .. } => {
1199                // An AT action (e.g. VoiceOver's VO+Space → `Action::Click`)
1200                // always names the node it targets — the element under the
1201                // assistive-technology cursor. It must be delivered to THAT
1202                // node, never to whatever happens to hold keyboard focus.
1203                // Falling back to `self.focused` would make VO+Space fire the
1204                // focused control instead of the cursored one, and would mask
1205                // a stale/inactive target by silently activating something
1206                // else. If the target is missing or no longer active, drop the
1207                // action rather than redirecting it.
1208                //
1209                // Drop it too when a modal is up and the target is behind it
1210                // (`is_behind_modal`). The scrim keeps the pointer off the page
1211                // and the Tab cycle stays in the modal, but the page stays in
1212                // the tree a screen reader walks, and its request names a node
1213                // there directly. Delivered, a click behind "Save changes?"
1214                // opened a second box over it, and a focus request put the
1215                // keyboard on a control the box covers.
1216                if let Some(id) =
1217                    target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1218                {
1219                    if *action == accesskit::Action::Focus {
1220                        // Land where the keys go. A composite publishes one AT
1221                        // node on a root that is not itself focusable (a
1222                        // `ComboBox` or a `DateEdit` keeps focus on an inner
1223                        // leaf), and `ctx.request_focus` has always walked
1224                        // into the subtree for exactly that reason. The AT path
1225                        // must too: focusing the root parks `self.focused` on a
1226                        // node that takes no keystrokes, and because
1227                        // `on_key_preview` fires only on *strict* ancestors of
1228                        // the focused node, it also disarms the composite's own
1229                        // stepping keys. `first_focusable_descendant` returns the
1230                        // node itself when it is focusable, so every leaf control
1231                        // is unchanged.
1232                        //
1233                        // The walk is gated on the node actually offering
1234                        // `Action::Focus`, which is what makes the sentence
1235                        // above true of composites and only of them. Walking
1236                        // from *any* non-focusable node meant an AT `Focus` on a
1237                        // `Panel`, a `GroupBox`, a landmark or a label moved the
1238                        // keyboard onto the first control inside it — a node the
1239                        // assistive technology could have named itself and did
1240                        // not — and reported success. A node that offers no
1241                        // `Focus` now reports the action unhandled instead,
1242                        // which is the honest answer.
1243                        //
1244                        // A node that stands for a focusable composite (an
1245                        // editor's text body, see `Widget::accessibility_proxy`)
1246                        // is the composite to the assistive technology, so the
1247                        // keyboard goes to the composite.
1248                        if self.advertises_focus_action(id) {
1249                            let target = self
1250                                .focusable_composite_behind(id)
1251                                .or_else(|| self.first_focusable_descendant(id))
1252                                .unwrap_or(id);
1253                            // An assistive move, not a scripted one: the user is
1254                            // navigating, so the focus ring appears exactly as it
1255                            // would for a Tab. `Programmatic` — what this used to
1256                            // pass — declares no modality and left a screen-reader
1257                            // user with an invisible focus after any click.
1258                            self.focus_with_origin_ops(
1259                                target,
1260                                crate::focus::FocusOrigin::Accessibility,
1261                                &mut *ops,
1262                            );
1263                            // Focus is serviced here rather than by the widget,
1264                            // so "handled" means the focus actually landed.
1265                            self.access_action_handled = self.focused == Some(target);
1266                        } else {
1267                            self.access_action_handled = false;
1268                        }
1269                    } else if *action == accesskit::Action::ShowContextMenu {
1270                        // A "show context menu" AT action — a screen reader's
1271                        // menu key, or an automation `right_click` /
1272                        // `invoke_action(node, "show_context_menu")` — first
1273                        // offers itself to the node's own `on_access_action`
1274                        // handlers. If none consume it, fall through to the very
1275                        // same machinery a Secondary `PointerDown` drives, so a
1276                        // widget's `.context_menu(..)` factory opens without the
1277                        // caller having to synthesise a right-click. The AT
1278                        // action carries no point, so anchor the menu at the
1279                        // node's centre. Without this, the AT action was a silent
1280                        // no-op for every widget that wires its menu through the
1281                        // factory (i.e. all of them) — see `show_context_menu_for`.
1282                        // Handled = the widget consumed it, or the factory
1283                        // fallback actually opened a menu. A node with neither
1284                        // reports unhandled rather than a silent success.
1285                        self.access_action_handled =
1286                            if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1287                                true
1288                            } else {
1289                                let position = self.arena.bounds(id).center();
1290                                self.show_context_menu_for(
1291                                    id,
1292                                    position,
1293                                    crate::widget_builder::ContextMenuTrigger::Accessibility,
1294                                    &mut *ops,
1295                                )
1296                            };
1297                    } else {
1298                        self.access_action_handled =
1299                            self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1300                    }
1301                }
1302            }
1303            WidgetEvent::Gesture { .. } => {
1304                if let Some(target) = self.hovered_id().or(self.focused) {
1305                    self.dispatch_to_widget(target, &event, &mut *ops);
1306                }
1307            }
1308            WidgetEvent::ScrollIntoView { .. }
1309            | WidgetEvent::PointerEnter { .. }
1310            | WidgetEvent::PointerLeave { .. }
1311            | WidgetEvent::FocusGained { .. }
1312            | WidgetEvent::FocusLost => {}
1313        }
1314        // Any intents queued by handlers via `ctx.send_intent(...)`
1315        // are dispatched after the raw event has been handled but
1316        // before commands are flushed, so commands emitted from
1317        // action handlers land on the same tick.
1318        self.drain_pending_intents(&mut *ops);
1319    }
1320
1321    /// Open the context menu the keyboard just asked for, and report whether
1322    /// one appeared.
1323    ///
1324    /// Targets the focused widget, or whatever its
1325    /// [`context_menu_key_target`](crate::widget::Widget::context_menu_key_target)
1326    /// nominates instead — for a data view, the selected row. Anchors the menu
1327    /// at the target's own bounds rather than at the last pointer position,
1328    /// which may be anywhere on screen or nowhere at all.
1329    ///
1330    /// Returns `false` when nothing on the ancestor chain owns a factory, so
1331    /// the key falls through to normal dispatch and a widget that wants to
1332    /// handle it itself still can.
1333    fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1334        let Some(focused) = self.focused else {
1335            return false;
1336        };
1337        let target = self
1338            .arena
1339            .get(focused)
1340            .and_then(|node| node.widget.context_menu_key_target())
1341            .filter(|id| self.arena.is_active(*id))
1342            .unwrap_or(focused);
1343
1344        // The menu belongs where the thing it is about is. A keyboard user has
1345        // no pointer position, and the stale one is worse than useless: it
1346        // would put the menu over an unrelated part of the window.
1347        let bounds = self.bounds(target);
1348        let anchor = Point {
1349            x: bounds.x + bounds.width / 2.0,
1350            y: bounds.y + bounds.height / 2.0,
1351        };
1352        self.show_context_menu_for(
1353            target,
1354            anchor,
1355            crate::widget_builder::ContextMenuTrigger::Keyboard,
1356            ops,
1357        )
1358    }
1359
1360    /// Ask the factories from `target` up for a menu, and mount the first one
1361    /// offered at `position`.
1362    ///
1363    /// `trigger` reaches each factory through
1364    /// [`EventContext::context_menu_trigger`](crate::widget::EventContext::context_menu_trigger):
1365    /// only a pointer's `position` is a place the user chose, and a factory
1366    /// that moves a caret to it on a keyboard request writes the menu's Paste
1367    /// into the middle of the text.
1368    pub(super) fn show_context_menu_for(
1369        &mut self,
1370        target: WidgetId,
1371        position: Point,
1372        trigger: crate::widget_builder::ContextMenuTrigger,
1373        ops: &mut dyn crate::window::WindowOps,
1374    ) -> bool {
1375        // Walks up the parent chain calling each factory in turn. A
1376        // factory returning `Some(menu)` claims the click and mounts;
1377        // a factory returning `None` declines and the walk continues.
1378        // No factory anywhere on the chain → fall through to whatever
1379        // the caller does with the unconsumed PointerDown.
1380        let mut ctx = self.make_event_context(&mut *ops);
1381        ctx.context_menu_trigger = Some(trigger);
1382        let mut walker = Some(target);
1383        let menu_decision: Option<(WidgetId, Box<dyn Widget>)> = loop {
1384            // Walk to the next ancestor (including `walker` itself)
1385            // that owns a factory.
1386            let owner_id = {
1387                let mut probe = walker;
1388                loop {
1389                    match probe {
1390                        None => break None,
1391                        Some(id) => {
1392                            if self
1393                                .arena
1394                                .get(id)
1395                                .is_some_and(|node| node.context_menu_factory.is_some())
1396                            {
1397                                break Some(id);
1398                            }
1399                            probe = self.arena.get(id).and_then(|node| node.parent);
1400                        }
1401                    }
1402                }
1403            };
1404            let Some(owner_id) = owner_id else {
1405                break None;
1406            };
1407            // Invoke the factory with the click position and a real
1408            // EventContext. The factory is `Fn` (not FnMut), so we
1409            // can call it through an immutable borrow on the node.
1410            // `ctx` is a local — its `&mut WindowOps` lifetime is
1411            // disjoint from `self.arena`, so the immutable arena
1412            // borrow doesn't conflict with the mutable ctx borrow.
1413            let outcome: Option<Box<dyn Widget>> = {
1414                let node = self
1415                    .arena
1416                    .get(owner_id)
1417                    .expect("owner_id from active arena walk");
1418                let factory = node
1419                    .context_menu_factory
1420                    .as_ref()
1421                    .expect("owner_id only set when factory present");
1422                factory(position, &mut ctx)
1423            };
1424            match outcome {
1425                Some(menu) => break Some((owner_id, menu)),
1426                None => {
1427                    // Decline → keep walking up from the parent.
1428                    walker = self.arena.get(owner_id).and_then(|n| n.parent);
1429                }
1430            }
1431        };
1432
1433        // Drain ctx side effects regardless of whether a menu showed —
1434        // a factory that returns `None` may still have queued intents,
1435        // updated signals, or requested a frame.
1436        let drain_anchor = menu_decision
1437            .as_ref()
1438            .map(|(id, _)| *id)
1439            .or_else(|| self.arena.roots().first().copied())
1440            .unwrap_or(target);
1441        self.collect_from_ctx(ctx, drain_anchor);
1442
1443        let Some((owner_id, menu_widget)) = menu_decision else {
1444            return false;
1445        };
1446
1447        // Clear stale transient overlays (other menus / popovers) before mounting
1448        // the new menu, but KEEP any overlay that *contains* the right-clicked
1449        // widget — otherwise a right-click inside a modal editor would tear down
1450        // the modal it lives in (dismiss_all did exactly that). The context menu
1451        // then mounts on top of its host overlay.
1452        let keep: std::collections::HashSet<WidgetId> = self
1453            .overlay_manager
1454            .stack
1455            .iter()
1456            .map(|o| o.content_id)
1457            .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
1458            .collect();
1459        let dismissed = self.overlay_manager.dismiss_except(&keep);
1460        self.dormant_dismissed_content(&dismissed, &mut *ops);
1461
1462        let content_id = self.add_boxed(menu_widget);
1463        let prev_focus = self.focused;
1464        // One branch, every menu: a coarse pointer gets a placement that keeps
1465        // clear of its own contact patch, everything else the historical
1466        // `AtPointer`. A point-anchored panel that puts its own corner under
1467        // the finger is the defect this exists to fix, and it is invisible from
1468        // a mouse — which is why the decision lives in
1469        // `OverlayPlacement::at_pointer_for` rather than at each call site.
1470        let placement =
1471            crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
1472        self.overlay_manager
1473            .show(crate::overlay::OverlayRequest::new(
1474                content_id,
1475                owner_id,
1476                placement,
1477                crate::overlay::DismissBehavior::EscapeOrClickOutside,
1478            ));
1479        if let Some(focus_id) = prev_focus {
1480            self.overlay_manager.set_top_focus_restore(focus_id);
1481        }
1482        self.focus_ops(content_id, &mut *ops);
1483        // Flush intents the factory queued so they take effect on the
1484        // same dispatch tick as the menu mount. The caller's
1485        // PointerDown handler returns after we return `true`, skipping
1486        // its own drain — fire ours here.
1487        self.drain_pending_intents(&mut *ops);
1488        true
1489    }
1490
1491    // -----------------------------------------------------------------
1492    // Outside-press overlay dismissal
1493    // -----------------------------------------------------------------
1494
1495    /// Handle a press that lands outside one or more dismissable overlays.
1496    ///
1497    /// Returns `true` when the press is consumed and must not reach the tree.
1498    ///
1499    /// **An indirect pointer is unchanged.** It dismisses on the press and
1500    /// falls through, so one click still closes a menu and actuates the control
1501    /// beneath — deliberate behaviour for a cursor, which names one pixel that
1502    /// the user could see the whole time they were aiming at it.
1503    ///
1504    /// **A direct pointer arms instead.** A finger covers what it is about to
1505    /// actuate: the menu is the only thing the user was looking at, and the
1506    /// control underneath is one they never saw. So the `Down` is withheld from
1507    /// the tree, the dismissal waits for the release, and a press that is
1508    /// cancelled — or slid onto the very overlay it would have closed — leaves
1509    /// nothing behind at all. Both the arming `Down` and the committing `Up`
1510    /// are consumed, so nothing beneath ever sees half a press.
1511    fn arm_outside_press_dismissal(
1512        &mut self,
1513        position: Point,
1514        button: PointerButton,
1515        ops: &mut dyn crate::window::WindowOps,
1516    ) -> bool {
1517        self.prune_stale_dismiss_arms();
1518        let pressing = self.current_input.pointer;
1519        let busy = self.busy_press_points(pressing.id);
1520
1521        if pressing.kind.is_direct() {
1522            return self
1523                .overlay_manager
1524                .arm_dismiss(pressing.id, position, &busy)
1525                .suppress_beneath;
1526        }
1527
1528        let (dismissed, focus_restore, toggle_anchors) =
1529            self.overlay_manager.dismiss_outside_press(position, &busy);
1530        if dismissed.is_empty() {
1531            return false;
1532        }
1533        self.dormant_dismissed_content(&dismissed, &mut *ops);
1534        if let Some(restore_id) = focus_restore
1535            && self.arena.is_active(restore_id)
1536        {
1537            self.focus_ops(restore_id, &mut *ops);
1538        }
1539        // The press dismissed one or more overlays. By default it
1540        // now ALSO falls through to the widget under the cursor,
1541        // so a single click both closes the menu/popover and
1542        // activates the control beneath. The one case swallowed: a
1543        // press on the anchor of a click-opened overlay, with a
1544        // button the anchor's tap answers to, because that tap
1545        // would otherwise reopen the overlay this very press just
1546        // dismissed (click-the-trigger-to-close). A primary press
1547        // is always taken for the toggle, as it always was; any
1548        // other button only where the trigger opens on it, so a
1549        // right click on a left-click trigger still dismisses and
1550        // falls through to the trigger's context menu.
1551        toggle_anchors.iter().any(|&anchor| {
1552            self.arena.is_active(anchor)
1553                && self.arena.bounds(anchor).contains(position)
1554                && (button == PointerButton::Primary
1555                    || self
1556                        .anchor_tap_buttons(anchor, position, &pressing)
1557                        .contains(button))
1558        })
1559    }
1560
1561    /// The buttons a tap at `position` would be recognized for on its way up
1562    /// to `anchor`: the union of the tap masks of every node from the one hit
1563    /// at `position` up to and including `anchor`.
1564    ///
1565    /// An anchor is often not the node carrying the tap. A popover anchors on
1566    /// its own wrapper, and the opener sits on the trigger inside it, or on a
1567    /// control inside that; any of them answering to the button means the
1568    /// press would reach an opener. [`ButtonMask::NONE`] when the node hit is
1569    /// not inside `anchor` at all.
1570    fn anchor_tap_buttons(
1571        &self,
1572        anchor: WidgetId,
1573        position: Point,
1574        pointer: &crate::pointer::PointerInfo,
1575    ) -> ButtonMask {
1576        let mut mask = ButtonMask::NONE;
1577        let mut node = self.hit_test_for(position, pointer);
1578        while let Some(id) = node {
1579            mask = mask.union(self.tap_buttons(id));
1580            if id == anchor {
1581                return mask;
1582            }
1583            node = self.arena.parent(id);
1584        }
1585        ButtonMask::NONE
1586    }
1587
1588    /// Complete a direct pointer's armed dismissal on its release.
1589    ///
1590    /// Returns `true` when this pointer held an arm — in which case the `Up` is
1591    /// consumed whether or not anything actually closed. Nothing beneath saw
1592    /// the `Down`, so delivering the `Up` alone would hand a widget the second
1593    /// half of a press it never started.
1594    fn commit_outside_press_dismissal(
1595        &mut self,
1596        position: Point,
1597        ops: &mut dyn crate::window::WindowOps,
1598    ) -> bool {
1599        let pointer = self.current_pointer_id();
1600        if !self.overlay_manager.has_armed_dismiss(pointer) {
1601            return false;
1602        }
1603        let (dismissed, focus_restore, _anchors) =
1604            self.overlay_manager.commit_dismiss(pointer, position);
1605        if !dismissed.is_empty() {
1606            self.dormant_dismissed_content(&dismissed, &mut *ops);
1607            if let Some(restore_id) = focus_restore
1608                && self.arena.is_active(restore_id)
1609            {
1610                self.focus_ops(restore_id, &mut *ops);
1611            }
1612        }
1613        true
1614    }
1615
1616    /// Where every *other* live pointer is holding a press.
1617    ///
1618    /// A press is only "outside" relative to the overlays nobody else is
1619    /// working in — see
1620    /// [`OverlayManager::dismiss_outside_press`](crate::overlay::OverlayManager::dismiss_outside_press).
1621    /// Liveness is judged with
1622    /// [`press_is_revocable`](Self::press_is_revocable), the predicate the
1623    /// cancel funnel already uses, and for the same reason: a pointer with
1624    /// neither a sequence nor a capture has no interaction that could be taken
1625    /// away, so it has none to protect either. That also exempts a pointer
1626    /// inside its terminal `Up` — its sequence is still installed but already
1627    /// terminating — which is what lets a menu item's own handler close its
1628    /// menu while a second contact rests elsewhere.
1629    ///
1630    /// The *press* position, not the current one: a contact that grabbed a
1631    /// menu and dragged past its edge is still manipulating that menu.
1632    fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
1633        self.pointers
1634            .iter()
1635            .filter(|entry| entry.info.id != exclude)
1636            .filter(|entry| self.press_is_revocable(entry.info.id))
1637            .map(|entry| entry.down_position)
1638            .collect()
1639    }
1640
1641    /// Drop arms whose contact is gone.
1642    ///
1643    /// An arm is normally retired by its own `Up` or `Cancel`. A contact that
1644    /// disappears without either — a table sweep, a window losing its input —
1645    /// would otherwise leave one behind, and a later contact minted with the
1646    /// same id would inherit a dismissal it never asked for.
1647    fn prune_stale_dismiss_arms(&mut self) {
1648        let stale: Vec<crate::pointer::PointerId> = self
1649            .overlay_manager
1650            .armed_pointers()
1651            .into_iter()
1652            .filter(|id| self.pointers.get(*id).is_none())
1653            .collect();
1654        for id in stale {
1655            self.overlay_manager.abort_dismiss(id);
1656        }
1657    }
1658
1659    // -----------------------------------------------------------------
1660    // Press state
1661    // -----------------------------------------------------------------
1662
1663    /// Open the press record for the contact being dispatched.
1664    ///
1665    /// Called from the `PointerDown` arm before the press is dispatched, with
1666    /// the focusable the press landed on (which a direct pointer will hold
1667    /// until its release). The node whose visual the record drives is not
1668    /// known yet — the arena claims the press during the dispatch — so
1669    /// [`adopt_press_owner`](Self::adopt_press_owner) finishes the record
1670    /// afterwards.
1671    ///
1672    /// The press-feedback delay applies **only inside a pan claimant**: a
1673    /// finger resting on a list row must not flash the row before the pan has
1674    /// been ruled out, while a button that nothing can scroll has no ambiguity
1675    /// to wait out and highlights at once. `begin_pan` has already decided
1676    /// whether this press is inside one — a session exists only when a claimant
1677    /// along the hit path accepts this pointer kind — so this reads its answer
1678    /// rather than re-deriving it.
1679    fn begin_press(&mut self, focusable: Option<WidgetId>) {
1680        let pointer = self.current_pointer_id();
1681        let now = self.sequence_now();
1682        let delay = self
1683            .pan_session_open(pointer)
1684            .then(|| self.current_profile().press_feedback_delay)
1685            .filter(|d| !d.is_zero());
1686        self.presses.press(pointer, focusable, now, delay);
1687    }
1688
1689    /// Record the node whose gesture arena took this press, and publish its
1690    /// signal.
1691    ///
1692    /// The owner is the sequence's `pressed_owner` — the node holding the
1693    /// pointer capture once the `Down` has been dispatched, which is exactly
1694    /// the node whose `on_tap` would fire. A press no arena took owns no
1695    /// visual, and its record stays for the focus deferral alone.
1696    ///
1697    /// So does a press on a **button the owner cannot act on**. A press visual
1698    /// says "release here and this control acts", so it has to answer to the
1699    /// same buttons the activation does: the router opens a record for every
1700    /// button, but only [`press_buttons`](Self::press_buttons) — the union of
1701    /// the owner's own click-style [`ButtonMask`]s,
1702    /// `PRIMARY` unless the widget widened it — decides which of them may light
1703    /// the control up. Without the gate a middle-click, or a right-click on a
1704    /// node with no context menu, would raise a press that can never complete;
1705    /// with it, the visual and the activation agree by construction, exactly as
1706    /// they do at the tap boundary where one predicate fails the tap, fires
1707    /// `cancel_taps` and clears the visual.
1708    ///
1709    /// The record itself is untouched either way, so the focus a direct
1710    /// pointer deferred to its release is still there to be assigned.
1711    fn adopt_press_owner(&mut self, button: PointerButton) {
1712        let pointer = self.current_pointer_id();
1713        let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
1714            return;
1715        };
1716        if !self.press_buttons(owner).contains(button) {
1717            crate::trace_input!(
1718                Samples,
1719                "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
1720            );
1721            return;
1722        }
1723        self.presses.set_owner(pointer, owner);
1724        self.publish_pressed(owner);
1725    }
1726
1727    /// Re-evaluate the press being dispatched against `position`.
1728    ///
1729    /// Three ways a press visual changes without a release:
1730    ///
1731    /// * the pointer left the press's [`TapBoundary`](crate::gesture::TapBoundary)
1732    ///   — the same predicate that fails the tap and fires `cancel_taps`, so
1733    ///   the visual and the activation are abandoned together;
1734    /// * it came back inside, which restores the visual: WCAG 2.2 SC 2.5.2's
1735    ///   abort gesture is reversible right up to the release;
1736    /// * the arbitration decided for somebody else — a pan claimant, an
1737    ///   ancestor drag — and the pressed control has lost the press without
1738    ///   ever seeing a release.
1739    fn update_press(&mut self, position: Point) {
1740        let pointer = self.current_pointer_id();
1741        if self.presses.get(pointer).is_none() {
1742            return;
1743        }
1744        // A peer claim ends the press outright: the node was never told, and
1745        // leaving its visual up would advertise an interaction it has lost.
1746        let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
1747            sequence
1748                .winner()
1749                .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
1750        });
1751        if claimed_elsewhere {
1752            self.end_press(pointer);
1753            return;
1754        }
1755        let profile = self.current_profile();
1756        let origin = self.current_sequence().map(|s| s.press_origin());
1757        let owner = self.presses.get(pointer).and_then(|p| p.owner);
1758        let inside = match (origin, owner) {
1759            (Some(origin), Some(owner)) => {
1760                let bounds = self
1761                    .arena
1762                    .is_active(owner)
1763                    .then(|| self.arena.bounds(owner));
1764                !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
1765                    .left(origin, position, bounds, &profile)
1766            }
1767            // No sequence to measure from, or no owner to measure against:
1768            // there is no visual either way, so the flag is moot.
1769            _ => true,
1770        };
1771        let Some(press) = self.presses.get_mut(pointer) else {
1772            return;
1773        };
1774        if press.inside == inside {
1775            return;
1776        }
1777        press.inside = inside;
1778        if let Some(owner) = owner {
1779            self.publish_pressed(owner);
1780        }
1781    }
1782
1783    /// Close the press held by `pointer` and republish the node it drove.
1784    ///
1785    /// The one exit: a release, a cancel, and a peer claim all come through
1786    /// here, so a node can never be left painted as pressed by a path that
1787    /// forgot to clear it.
1788    pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
1789        if let Some(owner) = self.presses.release(pointer) {
1790            self.publish_pressed(owner);
1791        }
1792    }
1793
1794    /// Resolve every elapsed press-feedback delay and publish the visuals that
1795    /// just appeared. Driven by the same tick that advances the long press.
1796    pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
1797        if self.presses.is_empty() {
1798            return;
1799        }
1800        for id in self.presses.resolve_delays(now) {
1801            self.publish_pressed(id);
1802            self.arena.mark_needs_paint(id);
1803        }
1804    }
1805
1806    /// The earliest instant a pending press wants the event loop back, so a
1807    /// finger that lands and does not move still gets its highlight.
1808    pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
1809        self.presses.next_deadline().map(|t| self.instant_for(t))
1810    }
1811
1812    /// Assign the focus a direct pointer's press deferred, if the release
1813    /// earned it.
1814    ///
1815    /// The guard is "the release landed on the same focusable as the press".
1816    /// A finger that presses a button, slides onto its neighbour and lifts has
1817    /// activated nothing and must move focus nowhere — the same rule the tap
1818    /// recognizer applies to activation, applied to focus so the two cannot
1819    /// disagree. A press that found no focusable defers nothing.
1820    ///
1821    /// A no-op for an indirect pointer, which focused at press.
1822    fn focus_on_release(&mut self, position: Point, ops: &mut dyn crate::window::WindowOps) {
1823        let releasing = self.current_input.pointer;
1824        if !releasing.kind.is_direct() {
1825            return;
1826        }
1827        let pointer = self.current_pointer_id();
1828        let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
1829            return;
1830        };
1831        let released_on = self
1832            .hit_test_for(position, &releasing)
1833            .and_then(|target| self.find_focusable_at_or_above(target));
1834        if released_on == Some(pressed) {
1835            self.focus_with_origin_ops(
1836                pressed,
1837                crate::focus::FocusOrigin::Pointer(releasing.kind),
1838                &mut *ops,
1839            );
1840        }
1841    }
1842
1843    /// The hover walk for one move: hit-test, run the enter/leave transitions,
1844    /// then deliver `event` to whatever the pointer is now over.
1845    ///
1846    /// `event` is the `PointerMove` being dispatched and is forwarded
1847    /// **verbatim** — its `modifiers` (a Shift or Ctrl pressed mid-drag) and its
1848    /// `pointer` are the producer's own, not a copy assembled here. `position`
1849    /// is that event's position, passed separately only because every step below
1850    /// needs it.
1851    ///
1852    /// Which is the rule for the whole router: a producer's event reaches the
1853    /// widget carrying what the producer said, while an event the *tree*
1854    /// synthesizes — the enter/leave pair below — carries the tree's own view of
1855    /// who is pointing, read from the pointer table. The two differ only for a
1856    /// hand-built legacy event, which names no time and is stamped from the tree
1857    /// clock into the snapshot [`EventContext::pointer`] reports (see
1858    /// [`dispatch_event_with_ops`](Self::dispatch_event_with_ops)) without that
1859    /// stamp being written back onto the event.
1860    ///
1861    /// [`EventContext::pointer`]: crate::widget::EventContext::pointer
1862    fn handle_pointer_move(
1863        &mut self,
1864        event: &WidgetEvent,
1865        position: Point,
1866        ops: &mut dyn crate::window::WindowOps,
1867    ) {
1868        // A contact routes its move by hit test but writes **no hover**: a
1869        // finger has no hover state, so a second finger arriving beside a
1870        // hovering mouse must leave enter/leave, the cursor, tooltip dwell and
1871        // every `on_hover` handler exactly where they were.
1872        let moving = self.current_input.pointer;
1873        if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
1874            if let Some(target) = self.hit_test_for(position, &moving) {
1875                self.dispatch_to_widget(target, event, &mut *ops);
1876            }
1877            return;
1878        }
1879        let target = self.hit_test_for(position, &moving);
1880
1881        if target != self.hovered_id() {
1882            // Past the gate above, so the mover *is* the hover owner.
1883            let hover = self.hover_transition_pointer();
1884            let previously_hovered = self.hovered_id();
1885            if let Some(old) = previously_hovered {
1886                self.dispatch_to_widget(
1887                    old,
1888                    &WidgetEvent::PointerLeave { pointer: hover },
1889                    &mut *ops,
1890                );
1891                self.tooltip_pointer_leave(old, &mut *ops);
1892            }
1893            if let Some(new) = target {
1894                self.dispatch_to_widget(
1895                    new,
1896                    &WidgetEvent::PointerEnter { pointer: hover },
1897                    &mut *ops,
1898                );
1899                self.tooltip_pointer_enter(new);
1900            }
1901            self.set_hovered(target);
1902            self.update_hover_within_signals(previously_hovered, target);
1903        } else if let Some(target) = target {
1904            // Same hover target — restart pending tooltip timers if the
1905            // pointer is still moving beyond the stationary slop.
1906            self.tooltip_pointer_moved(target, position);
1907        }
1908
1909        if let Some(target) = target {
1910            self.dispatch_to_widget(target, event, &mut *ops);
1911        }
1912    }
1913
1914    pub(super) fn dispatch_to_widget(
1915        &mut self,
1916        target: WidgetId,
1917        event: &WidgetEvent,
1918        ops: &mut dyn crate::window::WindowOps,
1919    ) {
1920        self.dispatch_to_widget_returning_handled(target, event, ops);
1921    }
1922
1923    /// Rebuild `event` with any pointer position converted into `id`'s
1924    /// **widget-local** space. Returns `None` for events that carry no
1925    /// position, so the caller keeps the original event.
1926    ///
1927    /// This is the single point where the framework localizes pointer
1928    /// coordinates. It runs once per node in both the preview and bubble
1929    /// passes, and because both `on_pointer_event` and the gesture arena
1930    /// read the position out of `event`, localizing it here makes
1931    /// `on_tap` / `on_double_tap` / `on_long_press` / `on_drag` and
1932    /// `on_pointer_event` all receive widget-local coordinates uniformly.
1933    /// See [`WidgetArena::local_pointer_position`]. `frame` is the node's,
1934    /// from [`WidgetArena::local_frame`], computed once per node so every
1935    /// position in the event is converted by the same walk — and the same
1936    /// frame is handed to the node's handlers as
1937    /// [`EventContext::to_local`](crate::widget::EventContext::to_local).
1938    pub(super) fn localize_event(
1939        frame: &crate::arena::LocalFrame,
1940        event: &WidgetEvent,
1941    ) -> Option<WidgetEvent> {
1942        match event {
1943            WidgetEvent::PointerDown {
1944                position,
1945                button,
1946                modifiers,
1947                pointer,
1948            } => Some(WidgetEvent::PointerDown {
1949                position: frame.to_local(*position),
1950                button: *button,
1951                modifiers: *modifiers,
1952                pointer: *pointer,
1953            }),
1954            WidgetEvent::PointerUp {
1955                position,
1956                button,
1957                modifiers,
1958                pointer,
1959            } => Some(WidgetEvent::PointerUp {
1960                position: frame.to_local(*position),
1961                button: *button,
1962                modifiers: *modifiers,
1963                pointer: *pointer,
1964            }),
1965            WidgetEvent::PointerMove {
1966                position,
1967                modifiers,
1968                pointer,
1969            } => Some(WidgetEvent::PointerMove {
1970                position: frame.to_local(*position),
1971                modifiers: *modifiers,
1972                pointer: *pointer,
1973            }),
1974            WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
1975                gesture: Self::localize_gesture(frame, gesture),
1976            }),
1977            // Deliberately no `Scroll` or `PointerCancel` arm. Both name their
1978            // position `window_position` precisely because it stays in window
1979            // space — see the fields' own docs for why routing and velocity
1980            // need it there — and adding an arm here would silently change the
1981            // frame every reader of those two fields works in.
1982            _ => None,
1983        }
1984    }
1985
1986    /// Convert every position / center field of a pre-recognized
1987    /// [`GestureEvent`] into `id`'s widget-local space (`DragMoved.delta`
1988    /// is relative and left untouched). Used for the platform gesture
1989    /// path; arena-recognized gestures are already local because the
1990    /// `RawPointerEvent` feeding the arena was localized by
1991    /// [`Self::localize_event`].
1992    fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
1993        let loc = |p: teksilo_canvas::Point| frame.to_local(p);
1994        let tap = |t: &TapEvent| {
1995            TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
1996        };
1997        match gesture {
1998            GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
1999            GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2000            GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2001            GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2002            GestureEvent::DragStarted {
2003                position,
2004                button,
2005                pointer,
2006            } => GestureEvent::DragStarted {
2007                position: loc(*position),
2008                button: *button,
2009                pointer: *pointer,
2010            },
2011            GestureEvent::DragMoved {
2012                position,
2013                delta,
2014                pointer,
2015            } => GestureEvent::DragMoved {
2016                position: loc(*position),
2017                delta: *delta,
2018                pointer: *pointer,
2019            },
2020            GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2021                position: loc(*position),
2022                pointer: *pointer,
2023            },
2024            GestureEvent::DragCancelled {
2025                position,
2026                pointer,
2027                reason,
2028            } => GestureEvent::DragCancelled {
2029                position: loc(*position),
2030                pointer: *pointer,
2031                reason: *reason,
2032            },
2033            GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2034                center: loc(*center),
2035            },
2036            GestureEvent::PinchChanged {
2037                center,
2038                scale,
2039                rotation,
2040            } => GestureEvent::PinchChanged {
2041                center: loc(*center),
2042                scale: *scale,
2043                rotation: *rotation,
2044            },
2045            GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2046            GestureEvent::PinchCancelled { reason } => {
2047                GestureEvent::PinchCancelled { reason: *reason }
2048            }
2049            GestureEvent::Swipe {
2050                direction,
2051                velocity,
2052            } => GestureEvent::Swipe {
2053                direction: *direction,
2054                velocity: *velocity,
2055            },
2056        }
2057    }
2058
2059    /// Same as `dispatch_to_widget` but returns `true` when any
2060    /// preview or bubble handler consumed the event. Used for keyboard
2061    /// events the framework wants to consume by default (Tab focus
2062    /// navigation): callers can dispatch first, then fall back to
2063    /// built-in behavior only when no widget claimed it.
2064    pub(super) fn dispatch_to_widget_returning_handled(
2065        &mut self,
2066        target: WidgetId,
2067        event: &WidgetEvent,
2068        ops: &mut dyn crate::window::WindowOps,
2069    ) -> bool {
2070        if !self.arena.is_enabled(target) {
2071            return false;
2072        }
2073
2074        let mut ancestors = Vec::new();
2075        let mut current = self.arena.parent(target);
2076        while let Some(id) = current {
2077            ancestors.push(id);
2078            current = self.arena.parent(id);
2079        }
2080        ancestors.reverse();
2081
2082        // For a pointer press, find the innermost tap-owning node at-or-above
2083        // the hit target (a chevron / checkbox / inline button). A row or
2084        // container that selects on press consults
2085        // `ctx.press_claimed_by_interactive_child()` to skip selecting when this
2086        // owner is a strict descendant of it — the press belongs to the inner
2087        // control, not the row. Tap-like handlers only; drag/swipe are excluded
2088        // so a draggable row still selects itself on press.
2089        //
2090        // **`on_tap` / `on_long_press` only — never `on_double_tap` alone.**
2091        // The question this answers is "does a descendant own *this press*",
2092        // and a widget that wired only a multi-tap handler does not: the first
2093        // click of a double-click is not its business. Counting it meant a
2094        // table cell could not carry double-click-to-edit without also
2095        // silently stopping its row from selecting on a plain click — while
2096        // every file manager selects a row on the first click of the
2097        // double-click that opens it. A node that wants the press still has
2098        // `on_tap` (a real `Button`, a checkbox), and those are unaffected.
2099        let tap_owner: Option<WidgetId> = if matches!(
2100            event,
2101            WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2102        ) {
2103            let mut owner = None;
2104            let mut cur = Some(target);
2105            while let Some(id) = cur {
2106                if self.arena.get(id).is_some_and(|n| {
2107                    n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2108                }) {
2109                    owner = Some(id);
2110                    break;
2111                }
2112                cur = self.arena.parent(id);
2113            }
2114            owner
2115        } else {
2116            None
2117        };
2118
2119        for &id in &ancestors {
2120            let frame = self.arena.local_frame(id);
2121            let mut ctx = self
2122                .make_event_context(&mut *ops)
2123                .with_dispatch_node(id)
2124                .with_dispatch_target(target)
2125                .with_local_frame(frame);
2126            ctx.press_claimed_by_interactive_child =
2127                tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2128            // Convert any pointer position into this node's widget-local
2129            // space before its handlers see it (see `localize_event`).
2130            let localized = Self::localize_event(&frame, event);
2131            let event = localized.as_ref().unwrap_or(event);
2132            let response = if let Some(node) = self.arena.get_mut(id) {
2133                Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2134            } else {
2135                EventResponse::Ignored
2136            };
2137            self.collect_from_ctx(ctx, id);
2138            if response == EventResponse::Handled {
2139                self.arena.mark_needs_paint(id);
2140                // Step 1 of the decision procedure: the raw-preview pass runs
2141                // FIRST and keeps its root-first order, and the first `Handled`
2142                // claims the press. Deliberately not folded into the
2143                // innermost-first member order — `rich_text/mouse.rs` documents
2144                // relying on an outer wrapper seeing a press before an inner
2145                // one, and reordering it would silently invert a precedence
2146                // real widgets depend on.
2147                if matches!(event, WidgetEvent::PointerDown { .. }) {
2148                    self.note_preview_claim(id);
2149                }
2150                return true;
2151            }
2152        }
2153
2154        let needs_layout_on_handle = matches!(
2155            event,
2156            WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2157        );
2158        let mut current = Some(target);
2159        let mut is_target = true;
2160        while let Some(id) = current {
2161            let frame = self.arena.local_frame(id);
2162            let mut ctx = self
2163                .make_event_context(&mut *ops)
2164                .with_dispatch_node(id)
2165                .with_dispatch_target(target)
2166                .with_local_frame(frame);
2167            ctx.press_claimed_by_interactive_child =
2168                tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2169            // Convert any pointer position into this node's widget-local
2170            // space before its handlers (and its gesture arena) see it.
2171            let localized = Self::localize_event(&frame, event);
2172            let gesture_cx = self.recognizer_context(id);
2173            // A member that lost the arbitration keeps its handlers and loses
2174            // only its recognizers — see `sequence_blocks_arena`.
2175            let arena_blocked = self.sequence_blocks_arena(id);
2176            let WidgetTree {
2177                arena,
2178                gesture_owners,
2179                ..
2180            } = self;
2181            let event = localized.as_ref().unwrap_or(event);
2182            let response = if let Some(node) = arena.get_mut(id) {
2183                Self::try_handler_bubble(
2184                    node,
2185                    event,
2186                    &mut ctx,
2187                    BubbleGates {
2188                        fire_on_pointer_event: is_target,
2189                        arena_blocked,
2190                    },
2191                    id,
2192                    gesture_owners,
2193                    gesture_cx,
2194                )
2195                .unwrap_or(EventResponse::Ignored)
2196            } else {
2197                EventResponse::Ignored
2198            };
2199            self.collect_from_ctx(ctx, id);
2200            if response == EventResponse::Handled {
2201                if needs_layout_on_handle {
2202                    self.arena.mark_needs_layout(id);
2203                } else {
2204                    self.arena.mark_needs_paint(id);
2205                }
2206                self.note_pointer_acceptance(id, event);
2207                // **Hover transitions are notifications, and every ancestor is
2208                // entitled to one.** Stopping the bubble here left a container
2209                // stuck hovered whenever the pointer left it *through* an
2210                // interactive child: the child's own `on_hover` handled the
2211                // `PointerLeave`, the bubble stopped, and the row went on believing
2212                // the pointer was still over it. A search result whose controls
2213                // appear on hover then kept them after the pointer had gone.
2214                //
2215                // The preview pass already refuses to let an ancestor swallow a
2216                // descendant's Enter/Leave; this is that rule in the other
2217                // direction, and it is what makes a container's hover mean "the
2218                // pointer is somewhere inside me" rather than "the pointer is on my
2219                // own background". Every other event still stops at its handler,
2220                // which is what makes handling one mean anything.
2221                if !matches!(
2222                    event,
2223                    WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
2224                ) {
2225                    return true;
2226                }
2227            }
2228            is_target = false;
2229            current = self.arena.parent(id);
2230        }
2231        false
2232    }
2233
2234    pub(super) fn dispatch_to_widget_direct(
2235        &mut self,
2236        target: WidgetId,
2237        event: &WidgetEvent,
2238        ops: &mut dyn crate::window::WindowOps,
2239    ) {
2240        self.dispatch_to_widget_direct_returning_handled(target, event, ops);
2241    }
2242
2243    /// [`dispatch_to_widget_direct`](Self::dispatch_to_widget_direct), reporting
2244    /// whether the node consumed the event.
2245    ///
2246    /// The claimant chain needs the answer: `Handled` means the container
2247    /// absorbed some of the delta and the walk stops, `Ignored` means it is at
2248    /// a boundary and the same whole event goes to the next container outward.
2249    /// Addressed rather than bubbled, which is exactly what a claimant chain is
2250    /// — a list of named recipients, like the one the cancel funnel delivers
2251    /// to.
2252    pub(super) fn dispatch_to_widget_direct_returning_handled(
2253        &mut self,
2254        target: WidgetId,
2255        event: &WidgetEvent,
2256        ops: &mut dyn crate::window::WindowOps,
2257    ) -> bool {
2258        if !self.arena.is_enabled(target) {
2259            return false;
2260        }
2261
2262        let frame = self.arena.local_frame(target);
2263        let mut ctx = self
2264            .make_event_context(&mut *ops)
2265            .with_dispatch_node(target)
2266            .with_dispatch_target(target)
2267            .with_local_frame(frame);
2268        let gesture_cx = self.recognizer_context(target);
2269        let arena_blocked = self.sequence_blocks_arena(target);
2270        let WidgetTree {
2271            arena,
2272            gesture_owners,
2273            ..
2274        } = self;
2275        let response = if let Some(node) = arena.get_mut(target) {
2276            Self::try_handler_bubble(
2277                node,
2278                event,
2279                &mut ctx,
2280                BubbleGates {
2281                    fire_on_pointer_event: true,
2282                    arena_blocked,
2283                },
2284                target,
2285                gesture_owners,
2286                gesture_cx,
2287            )
2288            .unwrap_or(EventResponse::Ignored)
2289        } else {
2290            EventResponse::Ignored
2291        };
2292        self.collect_from_ctx(ctx, target);
2293
2294        if response == EventResponse::Handled {
2295            // A scroll changes geometry, so it earns a layout pass rather than
2296            // a repaint — the same distinction the bubble path makes.
2297            if matches!(
2298                event,
2299                WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2300            ) {
2301                self.arena.mark_needs_layout(target);
2302            } else {
2303                self.arena.mark_needs_paint(target);
2304            }
2305            self.note_pointer_acceptance(target, event);
2306        }
2307        response == EventResponse::Handled
2308    }
2309
2310    /// Remember that `target` answered `Handled` to one of the current
2311    /// pointer's positional events.
2312    ///
2313    /// Read only by the cancel funnel, as the recipient of last resort when a
2314    /// revoked pointer holds no capture. Restricted to the three positional
2315    /// phases: a key, an accessibility action or a focus change is not "an
2316    /// event from this pointer", and letting one of those set the anchor would
2317    /// address the cancel to a widget the pointer never touched.
2318    pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
2319        if !matches!(
2320            event,
2321            WidgetEvent::PointerDown { .. }
2322                | WidgetEvent::PointerMove { .. }
2323                | WidgetEvent::PointerUp { .. }
2324        ) {
2325            return;
2326        }
2327        let pointer = self.current_pointer_id();
2328        if let Some(entry) = self.pointers.get_mut(pointer) {
2329            entry.last_accepted = Some(target);
2330        }
2331    }
2332
2333    fn try_handler_preview(
2334        node: &mut crate::arena::WidgetNode,
2335        event: &WidgetEvent,
2336        ctx: &mut EventContext,
2337    ) -> Option<EventResponse> {
2338        match event {
2339            // Key + IME events fire `on_key_preview` on each strict
2340            // ancestor of the focused widget (root → parent-of-target).
2341            // Mirrors how `on_pointer_event` previews on the pointer
2342            // side; the focused widget itself does NOT see its own
2343            // `on_key_preview` (the dispatch loop builds an ancestors
2344            // list that excludes the target, so this is enforced by
2345            // the caller, not here).
2346            WidgetEvent::KeyDown { .. }
2347            | WidgetEvent::KeyUp { .. }
2348            | WidgetEvent::ImeComposition { .. }
2349            | WidgetEvent::ImeCommit { .. } => {
2350                let has = node.external_handlers.on_key_preview.is_some()
2351                    || node.handlers.on_key_preview.is_some();
2352                if !has {
2353                    return None;
2354                }
2355                Some(fire_event_handler_both(
2356                    &mut node.external_handlers.on_key_preview,
2357                    &mut node.handlers.on_key_preview,
2358                    event,
2359                    ctx,
2360                ))
2361            }
2362            // `PointerEnter` / `PointerLeave` are per-node hover transitions
2363            // synthesized by `handle_pointer_move`, not part of the raw pointer
2364            // stream. Running them through the ancestor preview pass would let
2365            // a drag-detecting ancestor whose `on_pointer_event` returns
2366            // `Handled` silently swallow a descendant's hover (its cursor and
2367            // `on_hover` would never fire). They are delivered to their target
2368            // directly via the bubble pass (where Enter/Leave fire `on_hover`),
2369            // so they have no business in preview. `PointerMove`/`Down`/`Up`
2370            // and `Scroll` still preview through the catch-all below — the
2371            // tab-bar wheel-remap (`tab_widget/bar.rs`) and the split-view /
2372            // rich-text drag guards depend on that.
2373            WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
2374            _ => {
2375                let has = node.external_handlers.on_pointer_event.is_some()
2376                    || node.handlers.on_pointer_event.is_some();
2377                if !has {
2378                    return None;
2379                }
2380                Some(fire_event_handler_both(
2381                    &mut node.external_handlers.on_pointer_event,
2382                    &mut node.handlers.on_pointer_event,
2383                    event,
2384                    ctx,
2385                ))
2386            }
2387        }
2388    }
2389
2390    fn try_handler_bubble(
2391        node: &mut crate::arena::WidgetNode,
2392        event: &WidgetEvent,
2393        ctx: &mut EventContext,
2394        gates: BubbleGates,
2395        node_id: WidgetId,
2396        gesture_owners: &mut std::collections::HashSet<WidgetId>,
2397        gesture_cx: crate::gesture::RecognizerContext<'_>,
2398    ) -> Option<EventResponse> {
2399        let BubbleGates {
2400            fire_on_pointer_event,
2401            arena_blocked,
2402        } = gates;
2403        match event {
2404            WidgetEvent::PointerEnter { .. } => {
2405                if let Some(cursor) = node.node_cursor {
2406                    // The declared channel, not `set_cursor`: a handler
2407                    // overriding the cursor in the same dispatch must not
2408                    // erase the tree's record of what the node asked for —
2409                    // that record is what `release_cursor` hands back to.
2410                    ctx.declared_cursor_request = Some(cursor);
2411                }
2412                let mut fired = false;
2413                if let Some(h) = node.external_handlers.on_hover.as_mut() {
2414                    h(true, ctx);
2415                    fired = true;
2416                }
2417                if let Some(h) = node.handlers.on_hover.as_mut() {
2418                    h(true, ctx);
2419                    fired = true;
2420                }
2421                if fired {
2422                    Some(EventResponse::Handled)
2423                } else {
2424                    node.node_cursor.map(|_| EventResponse::Handled)
2425                }
2426            }
2427            WidgetEvent::PointerLeave { .. } => {
2428                if node.node_cursor.is_some() {
2429                    ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
2430                }
2431                let mut fired = false;
2432                if let Some(h) = node.external_handlers.on_hover.as_mut() {
2433                    h(false, ctx);
2434                    fired = true;
2435                }
2436                if let Some(h) = node.handlers.on_hover.as_mut() {
2437                    h(false, ctx);
2438                    fired = true;
2439                }
2440                if fired {
2441                    Some(EventResponse::Handled)
2442                } else {
2443                    node.node_cursor.map(|_| EventResponse::Handled)
2444                }
2445            }
2446            WidgetEvent::FocusGained { origin } => {
2447                // The handler signature is a bare `bool`; the origin rides on
2448                // the context so a widget can tell a rebuild's restore from a
2449                // real arrival (`EventContext::focus_origin`).
2450                let previous_origin = ctx.focus_origin.replace(*origin);
2451                let mut fired = false;
2452                if let Some(h) = node.external_handlers.on_focus.as_mut() {
2453                    h(true, ctx);
2454                    fired = true;
2455                }
2456                if let Some(h) = node.handlers.on_focus.as_mut() {
2457                    h(true, ctx);
2458                    fired = true;
2459                }
2460                ctx.focus_origin = previous_origin;
2461                fired.then_some(EventResponse::Handled)
2462            }
2463            WidgetEvent::FocusLost => {
2464                let mut fired = false;
2465                if let Some(h) = node.external_handlers.on_focus.as_mut() {
2466                    h(false, ctx);
2467                    fired = true;
2468                }
2469                if let Some(h) = node.handlers.on_focus.as_mut() {
2470                    h(false, ctx);
2471                    fired = true;
2472                }
2473                fired.then_some(EventResponse::Handled)
2474            }
2475            WidgetEvent::KeyDown { .. }
2476            | WidgetEvent::KeyUp { .. }
2477            | WidgetEvent::ImeComposition { .. }
2478            | WidgetEvent::ImeCommit { .. } => {
2479                if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2480                    Some(fire_event_handler_both(
2481                        &mut node.external_handlers.on_key,
2482                        &mut node.handlers.on_key,
2483                        event,
2484                        ctx,
2485                    ))
2486                } else {
2487                    None
2488                }
2489            }
2490            WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2491                if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2492                    Some(fire_event_handler_both(
2493                        &mut node.external_handlers.on_scroll,
2494                        &mut node.handlers.on_scroll,
2495                        event,
2496                        ctx,
2497                    ))
2498                } else {
2499                    None
2500                }
2501            }
2502            WidgetEvent::AccessAction {
2503                action,
2504                target_node,
2505                data,
2506                ..
2507            } => {
2508                // Every installed slot fires — both payload shapes, and
2509                // within each shape both the external (app-installed
2510                // `.on_access_action*`) and the widget's own. Button (own)
2511                // and Dialog (external) layered together rely on that for a
2512                // single accesskit click.
2513                //
2514                // The two shapes are layered, not alternatives, because they
2515                // have different owners: `on_access_action_request` is what a
2516                // widget reaches for when it needs `target_node` or `data`
2517                // (`Slider`, `SpinBox`, `TextInputField`, `CodeEditor`,
2518                // `TabBar`), while `.on_access_action(..)` is the app's
2519                // builder-level hook. Preferring the payload shape when it was
2520                // set therefore did not choose between two handlers for the
2521                // same job — it silently disabled the app's handler on exactly
2522                // the widgets that had migrated, with nothing at the call site
2523                // to say so.
2524                //
2525                // Assistive-tech action paths run under the `Accessibility`
2526                // source label. Restored after the block.
2527                let saved_a11y_source = ctx
2528                    .current_source
2529                    .replace(crate::telemetry::IntentSource::Accessibility);
2530                let mut any_slot = false;
2531                let mut any_handled = false;
2532                if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2533                    any_slot = true;
2534                    any_handled |=
2535                        h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2536                }
2537                if let Some(h) = node.handlers.on_access_action_request.as_mut() {
2538                    any_slot = true;
2539                    any_handled |=
2540                        h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2541                }
2542                if let Some(h) = node.external_handlers.on_access_action.as_mut() {
2543                    any_slot = true;
2544                    any_handled |= h(*action, ctx) == EventResponse::Handled;
2545                }
2546                if let Some(h) = node.handlers.on_access_action.as_mut() {
2547                    any_slot = true;
2548                    any_handled |= h(*action, ctx) == EventResponse::Handled;
2549                }
2550                let user_handled = any_slot.then_some(if any_handled {
2551                    EventResponse::Handled
2552                } else {
2553                    EventResponse::Ignored
2554                });
2555
2556                // Builder-level access_action / access_custom_action
2557                // callbacks. These layer on top of any user-installed
2558                // on_access_action / on_access_action_request — both
2559                // fire for the same dispatched event. Drives the
2560                // SwiftUI `.accessibilityAction(...)` parity.
2561                let mut override_handled = false;
2562                if let Some(ov) = node.access_overrides.as_deref_mut() {
2563                    if matches!(action, accesskit::Action::CustomAction) {
2564                        if let Some(accesskit::ActionData::CustomAction(idx)) = data
2565                            && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2566                        {
2567                            cb(ctx);
2568                            override_handled = true;
2569                        }
2570                    } else {
2571                        for (a, cb) in ov.actions.iter_mut() {
2572                            if *a == *action {
2573                                cb(ctx);
2574                                override_handled = true;
2575                            }
2576                        }
2577                    }
2578                }
2579
2580                ctx.current_source = saved_a11y_source;
2581                match (user_handled, override_handled) {
2582                    (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2583                    (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2584                    (None, false) => None,
2585                }
2586            }
2587            WidgetEvent::Gesture { gesture } => {
2588                // Pre-recognized gestures from the platform (OS trackpad
2589                // pinch/rotation, double-tap, …) bypass the gesture arena
2590                // and go straight to the matching handler. See §10.
2591                let matched = matches!(
2592                    gesture,
2593                    GestureEvent::PinchStarted { .. }
2594                        | GestureEvent::PinchChanged { .. }
2595                        | GestureEvent::PinchEnded
2596                        | GestureEvent::Swipe { .. }
2597                        | GestureEvent::DoubleTap { .. }
2598                        | GestureEvent::TripleTap { .. }
2599                ) && {
2600                    let has_handler = match gesture {
2601                        GestureEvent::PinchStarted { .. }
2602                        | GestureEvent::PinchChanged { .. }
2603                        | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2604                        GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2605                        GestureEvent::DoubleTap { .. } => {
2606                            node.any_handler(|h| h.on_double_tap.is_some())
2607                        }
2608                        GestureEvent::TripleTap { .. } => {
2609                            node.any_handler(|h| h.on_triple_tap.is_some())
2610                        }
2611                        _ => false,
2612                    };
2613                    if has_handler {
2614                        Self::dispatch_recognized_gesture(node, *gesture, ctx);
2615                    }
2616                    has_handler
2617                };
2618                if matched {
2619                    Some(EventResponse::Handled)
2620                } else {
2621                    None
2622                }
2623            }
2624            WidgetEvent::PointerDown {
2625                position,
2626                button,
2627                modifiers,
2628                ..
2629            } => {
2630                // Raw pointer handler runs first so widgets can intercept
2631                // events that the gesture recognizers won't catch (e.g.
2632                // right-click → context menu). If it returns Handled the
2633                // gesture arena is skipped; otherwise we fall through.
2634                // Only fire for the target — ancestors already fired
2635                // on_pointer_event during the preview pass.
2636                if fire_on_pointer_event {
2637                    let r = fire_event_handler_both(
2638                        &mut node.external_handlers.on_pointer_event,
2639                        &mut node.handlers.on_pointer_event,
2640                        event,
2641                        ctx,
2642                    );
2643                    if r == EventResponse::Handled {
2644                        return Some(EventResponse::Handled);
2645                    }
2646                }
2647                if arena_blocked {
2648                    // This node lost the arbitration for the press: its
2649                    // recognizers stay out of it, and the event goes on
2650                    // bubbling as if the node carried none.
2651                    return None;
2652                }
2653                Self::ensure_gesture_arena(node, node_id, gesture_owners);
2654                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2655                    let cx = gesture_cx;
2656                    // Implicit capture for the Down..Up sequence so that
2657                    // moves leaving the widget bounds still reach the
2658                    // arena. Without this, a drag that starts inside the
2659                    // widget but crosses its edge before the recognizer
2660                    // latches would be hit-tested to another widget and
2661                    // the press-origin arena would never see a `Move`.
2662                    // Released unconditionally by the `PointerUp` branch
2663                    // in `dispatch_event`.
2664                    //
2665                    // **Implicit**: this is plumbing, not a claim. Routing it
2666                    // through the public `capture_pointer` would enrol every
2667                    // arena-bearing node as a `RawDrag` competitor and decide
2668                    // every mouse sequence at press. See
2669                    // `EventContext::capture_pointer_implicit`.
2670                    ctx.capture_pointer_implicit();
2671                    let result = arena.process(
2672                        &RawPointerEvent::Down {
2673                            position: *position,
2674                            button: *button,
2675                            modifiers: *modifiers,
2676                            pointer: cx.pointer,
2677                            time: cx.now,
2678                        },
2679                        &cx,
2680                    );
2681                    if let Some(gesture) = result {
2682                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2683                    }
2684                    return Some(EventResponse::Handled);
2685                }
2686                None
2687            }
2688            WidgetEvent::PointerUp {
2689                position,
2690                button,
2691                modifiers,
2692                ..
2693            } => {
2694                if fire_on_pointer_event {
2695                    let r = fire_event_handler_both(
2696                        &mut node.external_handlers.on_pointer_event,
2697                        &mut node.handlers.on_pointer_event,
2698                        event,
2699                        ctx,
2700                    );
2701                    if r == EventResponse::Handled {
2702                        return Some(EventResponse::Handled);
2703                    }
2704                }
2705                if arena_blocked {
2706                    return None;
2707                }
2708                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2709                    let cx = gesture_cx;
2710                    let result = arena.process(
2711                        &RawPointerEvent::Up {
2712                            position: *position,
2713                            button: *button,
2714                            modifiers: *modifiers,
2715                            pointer: cx.pointer,
2716                            time: cx.now,
2717                        },
2718                        &cx,
2719                    );
2720                    if let Some(gesture) = result {
2721                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2722                    }
2723                    return Some(EventResponse::Handled);
2724                }
2725                None
2726            }
2727            WidgetEvent::PointerMove { position, .. } => {
2728                if fire_on_pointer_event {
2729                    let r = fire_event_handler_both(
2730                        &mut node.external_handlers.on_pointer_event,
2731                        &mut node.handlers.on_pointer_event,
2732                        event,
2733                        ctx,
2734                    );
2735                    if r == EventResponse::Handled {
2736                        return Some(EventResponse::Handled);
2737                    }
2738                }
2739                if arena_blocked {
2740                    return None;
2741                }
2742                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2743                    let cx = gesture_cx;
2744                    let result = arena.process(
2745                        &RawPointerEvent::Move {
2746                            position: *position,
2747                            pointer: cx.pointer,
2748                            time: cx.now,
2749                        },
2750                        &cx,
2751                    );
2752                    if let Some(gesture) = result {
2753                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2754                        // A recognized gesture (DragStarted / DragMoved / …)
2755                        // almost always changes visible state — return
2756                        // `Handled` so the bubble loop marks this widget
2757                        // `needs_paint`, which in turn makes
2758                        // `WidgetTree::needs_redraw()` return true and
2759                        // triggers a `request_redraw` for the next frame.
2760                        // Without this, state updates via bound signals are
2761                        // only observed on the *next* layout/render pass,
2762                        // which in turn is never scheduled because
2763                        // `teksilo-app::update_control_flow` only wakes up when
2764                        // `needs_redraw()` is true.
2765                        return Some(EventResponse::Handled);
2766                    }
2767                    return Some(EventResponse::Ignored);
2768                }
2769                None
2770            }
2771            WidgetEvent::PointerCancel {
2772                reason, pointer, ..
2773            } => {
2774                // Two hooks, and the dedicated one always runs. `on_pointer_cancel`
2775                // is a notification, not a route: a widget releasing what its
2776                // press latched has nothing to consume, and letting it report
2777                // `Handled` would make releasing state look like claiming the
2778                // event. The raw `on_pointer_event` hook keeps its ordinary
2779                // consuming semantics for widgets that drive the whole pointer
2780                // stream themselves.
2781                for slot in [
2782                    &mut node.external_handlers.on_pointer_cancel,
2783                    &mut node.handlers.on_pointer_cancel,
2784                ] {
2785                    if let Some(handler) = slot.as_mut() {
2786                        handler(pointer, *reason, ctx);
2787                    }
2788                }
2789                if fire_on_pointer_event {
2790                    let r = fire_event_handler_both(
2791                        &mut node.external_handlers.on_pointer_event,
2792                        &mut node.handlers.on_pointer_event,
2793                        event,
2794                        ctx,
2795                    );
2796                    if r == EventResponse::Handled {
2797                        return Some(EventResponse::Handled);
2798                    }
2799                }
2800                None
2801            }
2802        }
2803    }
2804
2805    pub(super) fn collect_from_ctx<'ops>(
2806        &mut self,
2807        mut ctx: EventContext<'ops>,
2808        source_widget: WidgetId,
2809    ) {
2810        // Take the ops handle out of ctx up front so we can freely
2811        // reborrow it inside the method without fighting the 'ops
2812        // lifetime propagation when other fields of `ctx` are moved.
2813        // When no ops is set (standalone trees / tests), fall back to
2814        // a stack NoopWindowOps.
2815        let local_ops = ctx.window_ops.take();
2816        let mut noop = crate::window::NoopWindowOps;
2817        let ops: &mut dyn crate::window::WindowOps = match local_ops {
2818            Some(o) => o,
2819            None => &mut noop,
2820        };
2821        if ctx.frame_requested {
2822            self.request_frame();
2823        }
2824        // Declared first, handler second — the order the two used to occur in
2825        // when both wrote the same slot, so a handler that speaks during an
2826        // enter still outranks the node it entered.
2827        if let Some(declared) = ctx.declared_cursor_request {
2828            self.node_declared_cursor = declared;
2829            self.current_cursor = declared;
2830        }
2831        match ctx.cursor_request {
2832            Some(crate::widget::CursorRequest::Set(cursor)) => {
2833                self.current_cursor = cursor;
2834            }
2835            // A withdrawal restores the node-declared cursor rather than
2836            // resetting to `Default`: the handler is stepping back, not
2837            // claiming the cursor is nothing.
2838            Some(crate::widget::CursorRequest::Release) => {
2839                self.current_cursor = self.node_declared_cursor;
2840            }
2841            None => {}
2842        }
2843        // Intents queued through `ctx.send_intent` are anchored at
2844        // the originating widget. Programmatic sends default to
2845        // `propagate_when_disabled = true` — there is no shortcut to
2846        // consult, and propagation is the safe, least-surprising
2847        // default.
2848        for intent in ctx.pending_intents {
2849            self.enqueue_intent(source_widget, intent, true);
2850        }
2851        // Key capture: process cancel before arm, matching the
2852        // handler's call order (the handler sets `cancel_key_capture`
2853        // when it calls `ctx.cancel_key_capture()`, and separately
2854        // stores `pending_key_capture` when it calls
2855        // `ctx.begin_key_capture(...)`). If the handler did both,
2856        // arm wins (whichever was called last on the ctx has
2857        // already overwritten the other field's effect via the
2858        // setter logic).
2859        if ctx.cancel_key_capture {
2860            self.cancel_key_capture();
2861        }
2862        if let Some(slot) = ctx.pending_key_capture {
2863            self.key_capture = Some(slot);
2864        }
2865        // Registry mutations queued by settings-UI buttons.
2866        for mutation in ctx.pending_shortcut_mutations {
2867            match mutation {
2868                crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2869                    self.shortcut_registry.rebind_primary(id, keystroke);
2870                }
2871                crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2872                    self.shortcut_registry.rebind_secondary(id, keystroke);
2873                }
2874                crate::widget::ShortcutMutation::ClearOverride { id } => {
2875                    self.shortcut_registry.clear_override(&id);
2876                }
2877            }
2878        }
2879        if ctx.close_window_requested {
2880            self.close_window_requested = true;
2881        }
2882        if ctx.force_close_requested {
2883            self.force_close_requested = true;
2884        }
2885        self.pending_modal_requests
2886            .extend(ctx.modal_requests.into_iter().map(|request| {
2887                crate::modal::QueuedModalRequest {
2888                    source_widget,
2889                    request,
2890                }
2891            }));
2892        if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2893            self.pending_modal_dismissal = true;
2894        }
2895        // After the dismissal above, which hands focus back to whatever the
2896        // modal was opened over: `send_intent_from_focus` means that widget.
2897        if !ctx.pending_focus_intents.is_empty() {
2898            let from = self
2899                .focused
2900                .filter(|id| self.arena.is_active(*id))
2901                .unwrap_or(source_widget);
2902            for intent in ctx.pending_focus_intents {
2903                self.enqueue_intent(from, intent, true);
2904            }
2905        }
2906        for callback in ctx.idle_callbacks {
2907            self.idle_queue.push_boxed(callback);
2908        }
2909        match ctx.dismiss_scope {
2910            Some(crate::widget::DismissScope::All) => {
2911                let dismissed = self.overlay_manager.dismiss_all();
2912                self.dormant_dismissed_content(&dismissed, &mut *ops);
2913            }
2914            Some(crate::widget::DismissScope::AllExceptHosts) => {
2915                self.dismiss_all_overlays_except_hosts(&mut *ops);
2916            }
2917            Some(crate::widget::DismissScope::SelfChain) => {
2918                self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2919            }
2920            Some(crate::widget::DismissScope::Top) => {
2921                if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2922                {
2923                    self.dormant_dismissed_content(&content_ids, &mut *ops);
2924                    if let Some(restore_id) = focus_restore
2925                        && self.arena.is_active(restore_id)
2926                    {
2927                        self.focus_ops(restore_id, &mut *ops);
2928                    }
2929                }
2930            }
2931            None => {
2932                for id in ctx.overlay_dismissals {
2933                    let dismissed = self.overlay_manager.dismiss(id);
2934                    self.dormant_dismissed_content(&dismissed, &mut *ops);
2935                }
2936            }
2937        }
2938        // Content-keyed dismissals (`dismiss_overlay_by_content`). Drained
2939        // unconditionally — independent of `dismiss_scope` and of the
2940        // pending delayed-overlay list — so a handler can retract a shown
2941        // reusable overlay it identifies only by content. Resolving the
2942        // id here (not at call time) is what lets the caller skip
2943        // tracking the `OverlayId`.
2944        for content_id in ctx.overlay_content_dismissals {
2945            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2946                let dismissed = self.overlay_manager.dismiss(overlay_id);
2947                self.dormant_dismissed_content(&dismissed, &mut *ops);
2948            }
2949        }
2950        // Apply pause/resume queue (ToastHost hover-pause). Drained
2951        // here so the handler-side `ctx.pause_overlay_auto_dismiss(id)`
2952        // is order-independent with `dismiss_overlay(id)` and the
2953        // scope-based dismissals: pause/resume on an overlay that
2954        // was concurrently dismissed is silently dropped (the find
2955        // inside the OverlayManager methods misses on the gone id).
2956        for (id, pause) in ctx.overlay_pause_requests {
2957            if pause {
2958                self.overlay_manager.pause_auto_dismiss(id);
2959            } else {
2960                self.overlay_manager.resume_auto_dismiss(id);
2961            }
2962        }
2963        for preserve_content in ctx.dismiss_descendant_overlays {
2964            self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2965        }
2966        let deferred_row_activations =
2967            self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2968        if ctx.request_a11y_update {
2969            self.a11y_dirty = true;
2970        }
2971        if let Some(visible) = ctx.soft_keyboard_request.take() {
2972            self.request_soft_keyboard(visible);
2973        }
2974        // Handed to the tree's own live regions, which schedule the two
2975        // accessibility syncs each message needs. See `crate::announcer`.
2976        for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2977            self.announce_with(message, politeness);
2978        }
2979        for mut req in ctx.overlay_requests {
2980            if req.parent_overlay.is_none() {
2981                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2982            }
2983            if self
2984                .overlay_manager
2985                .find_by_content(req.content_id)
2986                .is_some()
2987            {
2988                continue;
2989            }
2990            let current_focus = self.focused;
2991            self.overlay_manager.show(req);
2992            // Overlay show changes the AT tree shape — mirror the
2993            // `WidgetTree::show_overlay` path. The dismissal sibling
2994            // (`dismiss_overlay_with_ops`) already flips this.
2995            self.a11y_dirty = true;
2996            if let Some(focus_id) = current_focus {
2997                self.overlay_manager.set_top_focus_restore(focus_id);
2998            }
2999        }
3000        for (mut req, band) in ctx.overlay_band_requests {
3001            if req.parent_overlay.is_none() {
3002                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3003            }
3004            if self
3005                .overlay_manager
3006                .find_by_content(req.content_id)
3007                .is_some()
3008            {
3009                continue;
3010            }
3011            let content_id = req.content_id;
3012            self.overlay_manager.show_in_band(req, band);
3013            self.arena.activate(content_id);
3014            self.a11y_dirty = true;
3015            // Deliberately no `set_top_focus_restore`: the text-affordance band
3016            // never takes focus from the anchor, so there is nothing to give
3017            // back when it goes.
3018        }
3019        // After the shows, so a handler may raise an overlay and place it in
3020        // the same dispatch.
3021        for (content_id, placement) in ctx.overlay_placement_updates {
3022            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3023                self.overlay_manager.update_placement(overlay_id, placement);
3024            }
3025        }
3026        for (content_id, source) in ctx.overlay_placement_sources {
3027            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3028                self.overlay_manager
3029                    .set_placement_source(overlay_id, source);
3030            }
3031        }
3032        for (mut req, duration) in ctx.timed_overlay_requests {
3033            if req.parent_overlay.is_none() {
3034                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3035            }
3036            if self
3037                .overlay_manager
3038                .find_by_content(req.content_id)
3039                .is_some()
3040            {
3041                continue;
3042            }
3043            let current_focus = self.focused;
3044            let overlay_id = self.overlay_manager.show_for(req, duration);
3045            self.overlay_manager
3046                .set_shown_at_sim(overlay_id, self.sim_clock);
3047            self.a11y_dirty = true;
3048            if let Some(focus_id) = current_focus {
3049                self.overlay_manager.set_top_focus_restore(focus_id);
3050            }
3051        }
3052        for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3053            if req.parent_overlay.is_none() {
3054                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3055            }
3056            if self
3057                .overlay_manager
3058                .find_by_content(req.content_id)
3059                .is_some()
3060            {
3061                continue;
3062            }
3063            let content_id = req.content_id;
3064            let current_focus = self.focused;
3065            let overlay_id = self.overlay_manager.show(req);
3066            self.overlay_manager
3067                .set_shown_at_sim(overlay_id, self.sim_clock);
3068            self.a11y_dirty = true;
3069            if let Some(focus_id) = current_focus {
3070                self.overlay_manager.set_top_focus_restore(focus_id);
3071            }
3072            // Drive the caller's progress signal 0 → 1, and register it
3073            // as the overlay's fade-state signal so every dismiss path
3074            // tweens it 1 → 0 and defers removal until it completes — the
3075            // same deferral machinery as `with_fade`, minus `set_opacity`
3076            // (the caller owns how `progress` paints).
3077            self.register_animated_signal(&progress, content_id);
3078            let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3079                target: 1.0,
3080                duration,
3081                easing: teksilo_tokens::Easing::EaseOut,
3082                frame_interval: None,
3083                looping: false,
3084                epsilon: 0.0,
3085                max_duration: None,
3086            });
3087            self.overlay_manager
3088                .attach_fade(overlay_id, progress, duration);
3089        }
3090        if let Some((pointer, capture)) = ctx.pointer_capture {
3091            // Per pointer, and by default the pointer whose sample the handler
3092            // was serving — so a mouse call site means exactly what it meant
3093            // before, and two contacts on two widgets hold two captures.
3094            let named = pointer;
3095            let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3096            self.set_pointer_capture(pointer, capture.then_some(source_widget));
3097            // An explicit `capture_pointer()` from a handler is an arbitration
3098            // act; the arena's and the drag pipeline's own captures are
3099            // plumbing and route through `capture_pointer_implicit`.
3100            if capture && ctx.explicit_capture && named.is_none() {
3101                self.note_explicit_capture(source_widget);
3102            }
3103        }
3104        if let Some(activation) = ctx.drag_activation_override.take() {
3105            // A press handler chose this node's drag activation for this press.
3106            // Onto the sequence, where it dies with the press — see
3107            // `EventContext::set_drag_activation`.
3108            self.note_drag_activation_override(source_widget, activation);
3109        }
3110        if ctx.recognized_owning_gesture {
3111            // A drag or a swipe recognized on this node owns the rest of the
3112            // press, however the recognizer was reached.
3113            self.note_gesture_recognized(source_widget);
3114        }
3115        if !ctx.gesture_acts.is_empty() {
3116            let acts = std::mem::take(&mut ctx.gesture_acts);
3117            self.apply_gesture_acts(&acts, source_widget);
3118        }
3119        if let Some(reason) = ctx.cancel_pointer_request {
3120            let pointer = self.current_pointer_id();
3121            self.cancel_pointer(pointer, reason, &mut *ops);
3122        }
3123        for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3124            if request.parent_overlay.is_none() {
3125                request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3126            }
3127            if self
3128                .overlay_manager
3129                .find_by_content(request.content_id)
3130                .is_some()
3131            {
3132                continue;
3133            }
3134            let content_id = request.content_id;
3135            self.pending_delayed_overlays
3136                .retain(|pending| pending.request.content_id != content_id);
3137            self.pending_delayed_overlays.push(PendingDelayedOverlay {
3138                request,
3139                delay,
3140                focus_target,
3141                replace_siblings,
3142                real_requested_at: std::time::Instant::now(),
3143                sim_requested_at: self.sim_clock,
3144            });
3145            self.arena.mark_needs_paint(source_widget);
3146        }
3147        for content_id in ctx.cancel_delayed_overlays {
3148            self.pending_delayed_overlays
3149                .retain(|pending| pending.request.content_id != content_id);
3150        }
3151        // Apex = the last sample that was still over the anchor, which
3152        // for the intended caller (the anchor's own hover-leave) is the
3153        // point the diagonal starts from. Ops are applied per node as
3154        // its handler returns, so this lands before the next widget's
3155        // hover-enter and before the move's own pointer-leave
3156        // bookkeeping.
3157        for content_id in ctx.safe_region_arm_requests {
3158            if let Some(apex) = self
3159                .previous_pointer_position
3160                .or_else(|| self.hover_owner_position())
3161            {
3162                self.overlay_manager.arm_safe_region(
3163                    content_id,
3164                    apex,
3165                    std::time::Instant::now(),
3166                    self.sim_clock,
3167                );
3168            }
3169        }
3170        for id in ctx.repaint_requests {
3171            self.arena.mark_needs_paint(id);
3172        }
3173        for id in ctx.synthetic_clicks {
3174            // Over the caller's ops, never a standalone dispatch: the
3175            // tapped widget's own handler runs inside this nested
3176            // dispatch, so a standalone one would deny it the
3177            // multi-window API this dispatch already has in hand.
3178            self.synthesise_tap_with_ops(id, &mut *ops);
3179        }
3180        // `clear_focus` first: it emptied the two request lists as it was
3181        // queued, so anything still in them was requested after it.
3182        if ctx.clear_focus_requested {
3183            self.clear_focus_ops(&mut *ops);
3184        }
3185        // Both request kinds go through `request_focus_with_ops`, which holds
3186        // a request for a dormant node (a page this same handler is showing)
3187        // until the next layout pass has woken it, rather than focusing a node
3188        // the pass's revalidation would take focus straight back from.
3189        if let Some(&id) = ctx.focus_requests.last() {
3190            // If the requested widget is itself not focusable (e.g. a
3191            // composite like `TextInput` whose focus-handling lives on
3192            // an inner leaf), walk into the subtree and land on the
3193            // first focusable descendant in document order. This makes
3194            // `ctx.request_focus(some_composite)` Do The Right Thing
3195            // without every caller having to reach into private inner
3196            // ids. `first_focusable_descendant` returns the node itself
3197            // when it's focusable, so the usual leaf-target case is
3198            // still a no-op lookup.
3199            self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3200        }
3201        if let Some(&id) = ctx.focus_into_requests.last() {
3202            // "Focus into" semantics: land on the first focusable descendant
3203            // and — unlike `focus_requests` above — do NOT fall back to the
3204            // container itself. A region with no focusable content (and not
3205            // focusable in its own right) leaves focus untouched rather than
3206            // trapping it on a non-interactive node. Drives Enter-on-a-tab →
3207            // into the tab panel.
3208            self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3209        }
3210
3211        // Rect-based "scroll this into view" requests (`ctx.ensure_visible`).
3212        // Walk outward from the widget whose handler queued the request and
3213        // reveal the rect inside every enclosing scroll container. Run after
3214        // focus so that if the same handler also moved focus, both follows
3215        // settle against the same (pre-relayout) bounds; each dispatch is
3216        // gated on the container not already showing the rect, so ordering is
3217        // harmless. The source widget itself is excluded from the walk — it
3218        // owns revealing an interior rect inside its own viewport.
3219        for req in ctx.scroll_into_view_requests {
3220            self.scroll_rect_into_view(
3221                // Whoever the rect belongs to — the source widget unless the caller
3222                // named another. See `EventContext::ensure_visible_from`.
3223                req.from.unwrap_or(source_widget),
3224                req.rect,
3225                req.margin,
3226                req.align,
3227                req.motion,
3228                &mut *ops,
3229            );
3230        }
3231        // Id-based `ctx.ensure_widget_visible`: resolve to the target's current
3232        // absolute bounds and walk *its* ancestors (skip if it was destroyed
3233        // before the drain). Walking from the target — not `source_widget` —
3234        // means the request reveals that widget wherever it sits, even when the
3235        // handler runs on a different node (a group's roving-key handler
3236        // revealing the child tile it just selected).
3237        for (id, margin) in ctx.scroll_widget_into_view_requests {
3238            if self.arena.get(id).is_some() {
3239                let bounds = self.arena.bounds(id);
3240                self.scroll_rect_into_view(
3241                    id,
3242                    bounds,
3243                    margin,
3244                    crate::event::ScrollAlign::Minimal,
3245                    crate::event::ScrollMotion::Instant,
3246                    &mut *ops,
3247                );
3248            }
3249        }
3250
3251        // Keyboard-highlight tooltip: surface the highlighted (menu) item's
3252        // tooltip immediately and dismiss the previously-highlighted one. Keyed
3253        // on the item id, NOT real focus (which stays on the menu panel for key
3254        // handling). Only the last request per handler is honoured.
3255        if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3256            self.show_highlight_tooltip(id, &mut *ops);
3257        }
3258
3259        // --- Drag and drop ---
3260        if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3261            let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3262                // `add_boxed` — NOT `arena.insert` — runs the widget's
3263                // `build()` so composite previews (our `DragPreview`
3264                // wrapper in teksilo-widgets, or anything a user supplies)
3265                // actually instantiate their child subtree. Plain
3266                // `arena.insert` stops at the root node, leaves build
3267                // un-fired, and the overlay renders an empty widget.
3268                let content_id = self.add_boxed(preview);
3269                let overlay_id = self
3270                    .overlay_manager
3271                    .show(crate::overlay::OverlayRequest::new(
3272                        content_id,
3273                        source_widget,
3274                        crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3275                        crate::overlay::DismissBehavior::Manual,
3276                    ));
3277                // Force the next layout pass to run `position_overlays`
3278                // and `set_content_bounds` — otherwise the preview sits
3279                // at its initial (0, 0) placement forever.
3280                self.arena.mark_needs_layout(content_id);
3281                (Some(content_id), Some(overlay_id))
3282            } else {
3283                (None, None)
3284            };
3285            self.active_drag = Some(crate::drag_state::DragSession {
3286                payload,
3287                // The pointer that armed the drag. `start_drag` is always
3288                // reached from a handler serving a real sample, which is the
3289                // only place this is knowable — from here on the drag runs
3290                // through layout ticks and platform threads that have no
3291                // sample of their own. See `DragSession::pointer`.
3292                pointer: self.current_input.pointer,
3293                source_widget: Some(source_widget),
3294                is_external: false,
3295                current_position: teksilo_canvas::Point::ZERO,
3296                current_target: None,
3297                feedback: crate::drag_state::DropFeedback::NoFeedback,
3298                preview_content_id,
3299                preview_overlay_id,
3300            });
3301            self.set_current_pointer_capture(Some(source_widget));
3302            // Grabbing-hand cursor while the drag is in flight. Reset on
3303            // drop / cancel / source-destroyed below.
3304            self.current_cursor = crate::widget::CursorIcon::Grabbing;
3305        }
3306        if ctx.cancel_drag {
3307            self.cancel_active_drag(&mut *ops);
3308        }
3309
3310        // --- Environment changes (architecture §9.5) ---
3311        if let Some(theme) = ctx.theme_request {
3312            // Stored, not applied: the app layer routes this through
3313            // `WindowManager::set_theme` so every window re-themes, matching
3314            // the app-wide `set_locale` path below. Applying
3315            // `WidgetTree::set_theme` inline would re-theme only the
3316            // originating window.
3317            self.pending_theme_request = Some(theme);
3318        }
3319        if ctx.follow_system_request {
3320            // Stored, not applied: the app layer switches to
3321            // `ThemeMode::Native` and recomputes the theme from the current
3322            // OS colours, fanning it to every window.
3323            self.pending_follow_system_request = true;
3324        }
3325        if let Some(locale) = ctx.locale_request {
3326            // Stored, not applied: the app layer must route this through
3327            // `WindowManager::set_locale` so the `I18nManager`'s active
3328            // locale and direction stay in sync. Applying via
3329            // `WidgetTree::set_locale` alone would leave `tr!` bindings
3330            // reading the old translations.
3331            self.pending_locale_request = Some(locale);
3332        }
3333        if let Some(scale) = ctx.text_scale_request {
3334            // Stored, not applied: the app layer routes this through
3335            // `WindowManager::set_text_scale` so every window re-scales its
3336            // text. Applying `WidgetTree::set_user_text_scale` inline would
3337            // grow only the originating window.
3338            self.pending_text_scale_request = Some(scale);
3339        }
3340        // Last, and with a context of their own: `Space` on a data view's
3341        // focused row runs the row's published toggle, and a checkbox's toggle
3342        // fires the app's `on_change`, which may send an intent or open a
3343        // window. Running them here rather than inside the mutation drain is
3344        // what gives them an `EventContext`; the drain resolved which action to
3345        // run against the live tree and handed it back.
3346        if !deferred_row_activations.is_empty() {
3347            self.run_with_event_context(&mut *ops, move |ctx| {
3348                for action in deferred_row_activations {
3349                    action(ctx);
3350                }
3351            });
3352        }
3353    }
3354
3355    /// Returns the row activations it resolved but could not run: they need an
3356    /// [`EventContext`], and this method has no `ops` to build one from. The
3357    /// caller runs them once the drain is finished, the way
3358    /// [`WidgetTree::run_mount_actions`](crate::WidgetTree::run_mount_actions)
3359    /// does.
3360    #[must_use]
3361    fn apply_tree_mutations(
3362        &mut self,
3363        mutations: Vec<crate::widget::TreeMutation>,
3364    ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3365        let mut deferred_row_activations = Vec::new();
3366        use crate::binding::BindingLevel;
3367        use crate::widget::TreeMutation;
3368
3369        for mutation in mutations {
3370            match mutation {
3371                TreeMutation::SetDormant(id) => {
3372                    self.park_subtree(id);
3373                }
3374                TreeMutation::Activate(id) => self.arena.activate(id),
3375                TreeMutation::Destroy(id) => {
3376                    // Route through `destroy_subtree`, NOT the bare
3377                    // `arena.destroy`: the latter only unlinks nodes from
3378                    // the slotmap and leaks everything the widget owned —
3379                    // animation-scheduler entries (which hold strong
3380                    // `Signal<f32>` clones, so the widget keeps animating
3381                    // after it's gone), animated-quad slots, event-source
3382                    // subscriptions, registered shortcuts, bindings, and
3383                    // gesture ownership — and leaves `focused`/`hovered`
3384                    // dangling at a removed id. This mirrors the build-time
3385                    // `BuildContext::destroy_subtree`, including dismissing
3386                    // any overlay that still references the subtree so the
3387                    // manager doesn't retain a stale content reference.
3388                    if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3389                        self.dismiss_overlay(overlay_id);
3390                    }
3391                    self.destroy_subtree(id);
3392                }
3393                // Build now, not next frame: the same handler is about to show
3394                // an overlay over this node and move focus into it, and both
3395                // read the subtree. See `EventContext::materialize_now`.
3396                TreeMutation::MaterializeNow(id) => {
3397                    if self.arena.get(id).is_some() {
3398                        self.rebuild_single_widget(id);
3399                    }
3400                }
3401                TreeMutation::RowSpaceActivate { row, fallback } => {
3402                    // Resolve against the *live* tree: a data view rebuilds its
3403                    // rows as they realize, so the row that was focused when
3404                    // the key arrived may have been rebuilt since.
3405                    //
3406                    // Resolve here, run later. The action carries an
3407                    // `EventContext` so a row's checkbox fires its `on_change`
3408                    // on this path exactly as it does under the pointer; this
3409                    // method has no `ops` to build one from, so the caller runs
3410                    // it after the drain.
3411                    deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3412                }
3413                TreeMutation::WithWidgetMut { id, dirty, apply } => {
3414                    // Run the typed mutation while `&mut arena` is live, then
3415                    // drop the borrow before dirty-marking (the `mark_*` calls
3416                    // re-borrow the arena). Only dirty-mark a live node so we
3417                    // never call `mark_ancestors_need_layout` on a destroyed id.
3418                    let existed = if let Some(any) =
3419                        self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3420                    {
3421                        apply(any);
3422                        true
3423                    } else {
3424                        false
3425                    };
3426                    if existed {
3427                        match dirty {
3428                            BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3429                            BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3430                            BindingLevel::Relayout => {
3431                                self.arena.mark_needs_layout(id);
3432                                self.arena.mark_ancestors_need_layout(id);
3433                            }
3434                            BindingLevel::Rebuild => {
3435                                self.arena.mark_needs_rebuild(id);
3436                                self.arena.mark_ancestors_need_layout(id);
3437                            }
3438                            BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3439                        }
3440                    }
3441                }
3442            }
3443        }
3444        deferred_row_activations
3445    }
3446
3447    /// Hit-test at a point for the **mouse, exactly** — the meaning this door
3448    /// has always had, and keeps.
3449    ///
3450    /// A mouse cursor's hot-spot is exact, so neither hit-targeting mechanism
3451    /// applies to it: the outset pre-pass sees zero insets and the miss-only
3452    /// slop pass short-circuits on a zero radius. A caller that holds a pointer
3453    /// should use [`hit_test_for`](Self::hit_test_for) instead, which is the
3454    /// same test for a mouse and the widened one for a finger or a stylus.
3455    pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3456        self.hit_test_excluding_overlay_and_widget(point, None, None)
3457    }
3458
3459    /// Hit-test at a point on behalf of a named pointer.
3460    ///
3461    /// Runs the exact pass with that pointer's `Widget::hit_outset`, then — only
3462    /// if the exact pass found nothing eligible — the miss-only slop pass. For
3463    /// [`PointerKind::Mouse`](teksilo_tokens::PointerKind::Mouse) this is
3464    /// exactly [`hit_test`](Self::hit_test).
3465    ///
3466    /// Candidates are restricted to the **topmost overlay layer the exact pass
3467    /// entered**: a press inside an open menu can be re-attributed to a menu
3468    /// row, never to a control on the page behind it.
3469    pub fn hit_test_for(
3470        &self,
3471        point: Point,
3472        pointer: &crate::pointer::PointerInfo,
3473    ) -> Option<WidgetId> {
3474        self.hit_test_for_excluding(point, pointer, None, None)
3475    }
3476
3477    /// [`hit_test_for`](Self::hit_test_for) with the drag-and-drop exclusions of
3478    /// [`hit_test_excluding_overlay_and_widget`](Self::hit_test_excluding_overlay_and_widget).
3479    pub fn hit_test_for_excluding(
3480        &self,
3481        point: Point,
3482        pointer: &crate::pointer::PointerInfo,
3483        exclude_overlay: Option<crate::overlay::OverlayId>,
3484        exclude_widget: Option<WidgetId>,
3485    ) -> Option<WidgetId> {
3486        let surfaces = self.text_surfaces();
3487        let read_only = |id: WidgetId| surfaces.is_read_only(id);
3488        let hit =
3489            crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3490                .direction(self.layout_direction)
3491                .read_only_probe(&read_only);
3492        self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3493    }
3494
3495    /// Hit-test at a point, excluding a specific overlay and widget from consideration.
3496    /// Used during drag-and-drop to exclude the preview overlay and its content widget,
3497    /// so they don't block hit-testing of the actual drop targets underneath.
3498    ///
3499    /// **Mouse, exact** — the pointer-aware twin is
3500    /// [`hit_test_for_excluding`](Self::hit_test_for_excluding).
3501    pub fn hit_test_excluding_overlay_and_widget(
3502        &self,
3503        point: Point,
3504        exclude_overlay: Option<crate::overlay::OverlayId>,
3505        exclude_widget: Option<WidgetId>,
3506    ) -> Option<WidgetId> {
3507        self.hit_test_with(
3508            point,
3509            exclude_overlay,
3510            exclude_widget,
3511            &crate::pointer::hit_slop::HitContext::mouse(),
3512        )
3513    }
3514
3515    /// The one hit-test body: overlay first, then the arena, under whichever
3516    /// [`HitContext`](crate::pointer::hit_slop::HitContext) the caller built.
3517    pub(crate) fn hit_test_with(
3518        &self,
3519        point: Point,
3520        exclude_overlay: Option<crate::overlay::OverlayId>,
3521        exclude_widget: Option<WidgetId>,
3522        hit: &crate::pointer::hit_slop::HitContext<'_>,
3523    ) -> Option<WidgetId> {
3524        if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3525            if Some(overlay_id) == exclude_overlay {
3526                // Skip this excluded overlay, fall through to widget tree
3527            } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3528                // Scoped to the overlay's content: this is what restricts the
3529                // slop pass's candidates to the topmost layer the exact pass
3530                // entered.
3531                let content_id = overlay.content_id;
3532                if let Some(found) =
3533                    self.arena
3534                        .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3535                {
3536                    return Some(found);
3537                }
3538                // The overlay was chosen by its **bounds** and its content
3539                // claimed nothing at this point. For every ordinary overlay
3540                // that is the end of the search — answering `None` is what
3541                // keeps the slop pass inside the one layer the exact pass
3542                // entered, so a near-miss on a menu row cannot be beaten by a
3543                // control behind the menu.
3544                //
3545                // An overlay whose content root declares `event_pass_through`
3546                // is the exception, because that flag already means "what I did
3547                // not claim belongs to whatever is behind me": the tree's
3548                // walker honours it for an ordinary node *after* its children
3549                // miss, and an overlay root is not an exception to it. Without
3550                // this, a viewport-sized pass-through layer — the placement the
3551                // text-affordance band was written for — stops the surface
3552                // under it taking presses at all. The widening is confined to
3553                // that flag: an overlay that does not set it still returns
3554                // here, so no ordinary overlay's candidate set changes.
3555                if !self
3556                    .arena
3557                    .get(content_id)
3558                    .is_some_and(|node| node.event_pass_through)
3559                {
3560                    return None;
3561                }
3562            }
3563        }
3564
3565        if self.overlay_manager.topmost_modal().is_some() {
3566            return None;
3567        }
3568
3569        // The walk of the arena's roots honours event_pass_through and
3570        // clips_children. An overlay's content is one of those roots, and an
3571        // inert one is left out: the stack has stepped over it above, and a
3572        // point over it with nothing beneath it on the stack would otherwise
3573        // land in it here, wherever its root falls in the walk.
3574        let mut roots = self.arena.roots();
3575        let inert: Vec<WidgetId> = self
3576            .overlay_manager
3577            .stack
3578            .iter()
3579            .filter(|overlay| overlay.inert)
3580            .map(|overlay| overlay.content_id)
3581            .collect();
3582        if !inert.is_empty() {
3583            roots.retain(|root| !inert.contains(root));
3584        }
3585        self.arena
3586            .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3587    }
3588}
3589
3590/// Whether this keystroke is one of the chords that ask for a context menu.
3591///
3592/// Three routes, because no single one exists on every platform:
3593///
3594/// * **The dedicated key.** `VK_APPS` on Windows, `keysyms::Menu` on X11 and
3595///   Wayland. `winit-0.30.13`'s AppKit backend references
3596///   `NamedKey::ContextMenu` zero times, so macOS never produces it.
3597/// * **Shift+F10.** The convention Windows, GTK and Qt all honour, and the one
3598///   thing a Windows or Linux keyboard without a Menu key can still reach.
3599/// * **Ctrl+Shift+M on macOS.** Neither of the above is available there: Mac
3600///   keyboards have no Menu key, and F10 is a media key under the default
3601///   "Use F1, F2 etc. as standard function keys = off" setting, so Shift+F10
3602///   may never arrive as F10 at all. Kept off the other platforms, where
3603///   Ctrl+Shift+M is a plausible application binding.
3604///
3605/// Modifiers are matched exactly. Shift+F10 with Ctrl held is a different
3606/// gesture and must reach the application unchanged.
3607fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3608    match key {
3609        Key::ContextMenu => modifiers == Modifiers::NONE,
3610        Key::F10 => modifiers == Modifiers::SHIFT,
3611        #[cfg(target_os = "macos")]
3612        Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3613        _ => false,
3614    }
3615}
3616
3617#[cfg(test)]
3618mod tests {
3619    use super::*;
3620    use crate::test_widgets::FillWidget;
3621    use crate::widget::CursorIcon;
3622    use crate::widget_builder::WidgetBuilder;
3623
3624    #[test]
3625    fn pointer_enter_leave_synthesized() {
3626        let mut tree = WidgetTree::new();
3627        let widget = tree.add(FillWidget::new());
3628        tree.layout(SizeProposal::exact(100.0, 50.0));
3629        tree.pointer_move(Point::new(50.0, 25.0));
3630        assert_eq!(tree.hovered(), Some(widget));
3631        tree.pointer_move(Point::new(200.0, 200.0));
3632        assert_eq!(tree.hovered(), None);
3633    }
3634
3635    #[test]
3636    fn pointer_hover_updates_current_cursor() {
3637        let mut tree = WidgetTree::new();
3638        tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3639        tree.layout(SizeProposal::exact(100.0, 50.0));
3640
3641        tree.pointer_move(Point::new(50.0, 25.0));
3642        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3643
3644        tree.pointer_move(Point::new(200.0, 200.0));
3645        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3646    }
3647
3648    /// A leaf that declares its cursor from `build()` through
3649    /// `apply_self_handlers`, the route a composing widget takes.
3650    #[derive(Debug)]
3651    struct SelfDeclared(CursorIcon);
3652
3653    impl crate::widget::Widget for SelfDeclared {
3654        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3655            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3656            Vec::new()
3657        }
3658
3659        fn layout_response(
3660            &self,
3661            proposal: SizeProposal,
3662            _ctx: &crate::widget::LayoutContext,
3663        ) -> crate::widget::LayoutResponse {
3664            proposal.resolve(100.0, 50.0).into()
3665        }
3666    }
3667
3668    /// A test reads the cursor two ways: `current_cursor` is the window's
3669    /// running answer and follows the hover, `declared_cursor` is what a node
3670    /// asks for and does not care where the pointer is. Both declaration
3671    /// routes (`.cursor(..)` and `apply_self_handlers`) land in the latter,
3672    /// and "declares nothing" is `None`, distinct from declaring `Default`.
3673    #[test]
3674    fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3675        let over = Point::new(50.0, 25.0);
3676        let away = Point::new(200.0, 200.0);
3677
3678        // `.cursor(..)` through the builder.
3679        let mut tree = WidgetTree::new();
3680        let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3681        tree.layout(SizeProposal::exact(100.0, 50.0));
3682        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3683        assert_eq!(
3684            tree.current_cursor(),
3685            CursorIcon::Default,
3686            "not hovered yet"
3687        );
3688        tree.pointer_move(over);
3689        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3690        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3691        tree.pointer_move(away);
3692        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3693        assert_eq!(
3694            tree.declared_cursor(built),
3695            Some(CursorIcon::ColResize),
3696            "the declaration does not depend on the pointer",
3697        );
3698
3699        // `apply_self_handlers` in `build()`.
3700        let mut tree = WidgetTree::new();
3701        let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3702        tree.layout(SizeProposal::exact(100.0, 50.0));
3703        assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3704        tree.pointer_move(over);
3705        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3706        tree.pointer_move(away);
3707        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3708
3709        // Nothing declared, and an explicit `Default`.
3710        let mut tree = WidgetTree::new();
3711        let plain = tree.add(FillWidget::new());
3712        tree.layout(SizeProposal::exact(100.0, 50.0));
3713        assert_eq!(tree.declared_cursor(plain), None);
3714        let mut tree = WidgetTree::new();
3715        let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3716        tree.layout(SizeProposal::exact(100.0, 50.0));
3717        assert_eq!(
3718            tree.declared_cursor(explicit),
3719            Some(CursorIcon::Default),
3720            "declaring the default cursor is not declaring nothing",
3721        );
3722    }
3723
3724    /// A handler's `set_cursor` outlives its dispatch, so a handler that
3725    /// re-decides the cursor on every move needs a way to stop deciding.
3726    ///
3727    /// Going quiet does not do it: the cursor moves only when something writes
3728    /// to it, and the node-declared cursor is written on `PointerEnter` /
3729    /// `PointerLeave` alone — so while the pointer stays inside one node there
3730    /// is no second writer, and the handler's last word simply stands. That is
3731    /// what `release_cursor` withdraws, and it withdraws *to the node's own
3732    /// declaration*, not to `Default`.
3733    #[test]
3734    fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3735        // Overrides itself to `Crosshair` on the left third of the widget and
3736        // withdraws everywhere else — the shape of any handler that arbitrates
3737        // its own affordance against the node it sits on.
3738        let mut tree = WidgetTree::new();
3739        tree.add(
3740            FillWidget::new()
3741                .cursor(CursorIcon::ColResize)
3742                .on_pointer_event(|event, ctx| {
3743                    if let WidgetEvent::PointerMove { position, .. } = event {
3744                        if position.x < 30.0 {
3745                            ctx.set_cursor(CursorIcon::Crosshair);
3746                        } else {
3747                            ctx.release_cursor();
3748                        }
3749                    }
3750                    EventResponse::Ignored
3751                }),
3752        );
3753        tree.layout(SizeProposal::exact(100.0, 50.0));
3754
3755        tree.pointer_move(Point::new(10.0, 25.0));
3756        assert_eq!(
3757            tree.current_cursor(),
3758            CursorIcon::Crosshair,
3759            "the handler outranks the node it is attached to",
3760        );
3761        tree.pointer_move(Point::new(60.0, 25.0));
3762        assert_eq!(
3763            tree.current_cursor(),
3764            CursorIcon::ColResize,
3765            "and hands it back to the node, not to Default — no enter/leave \
3766             fires on a move within one node, so nothing else could",
3767        );
3768        tree.pointer_move(Point::new(10.0, 25.0));
3769        assert_eq!(
3770            tree.current_cursor(),
3771            CursorIcon::Crosshair,
3772            "and can take it again"
3773        );
3774        tree.pointer_move(Point::new(200.0, 200.0));
3775        assert_eq!(
3776            tree.current_cursor(),
3777            CursorIcon::Default,
3778            "leaving the node clears both the override and the declaration",
3779        );
3780    }
3781
3782    /// The withdrawal is a no-op for a handler that never spoke, and resolves
3783    /// to `Default` when the chain declares nothing — so a handler may call it
3784    /// unconditionally without having to remember whether it once set a cursor.
3785    #[test]
3786    fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3787        let mut tree = WidgetTree::new();
3788        tree.add(
3789            FillWidget::new()
3790                .cursor(CursorIcon::ColResize)
3791                .on_pointer_event(|event, ctx| {
3792                    if matches!(event, WidgetEvent::PointerMove { .. }) {
3793                        ctx.release_cursor();
3794                    }
3795                    EventResponse::Ignored
3796                }),
3797        );
3798        tree.layout(SizeProposal::exact(100.0, 50.0));
3799        tree.pointer_move(Point::new(50.0, 25.0));
3800        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3801
3802        // Same handler over a node that declares nothing.
3803        let mut tree = WidgetTree::new();
3804        tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3805            if let WidgetEvent::PointerMove { position, .. } = event {
3806                if position.x < 30.0 {
3807                    ctx.set_cursor(CursorIcon::Crosshair);
3808                } else {
3809                    ctx.release_cursor();
3810                }
3811            }
3812            EventResponse::Ignored
3813        }));
3814        tree.layout(SizeProposal::exact(100.0, 50.0));
3815        tree.pointer_move(Point::new(10.0, 25.0));
3816        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3817        tree.pointer_move(Point::new(60.0, 25.0));
3818        assert_eq!(
3819            tree.current_cursor(),
3820            CursorIcon::Default,
3821            "nothing declared a cursor for this chain, so the hand-back \
3822             resolves to Default",
3823        );
3824    }
3825
3826    /// A handler that speaks during the `PointerEnter` itself still outranks
3827    /// the node it entered — and the node's declaration is remembered anyway,
3828    /// so the hand-back has somewhere to go.
3829    ///
3830    /// The two used to share one slot, where the handler's later write erased
3831    /// the declaration outright; they are separate channels now precisely so
3832    /// this case keeps both.
3833    #[test]
3834    fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3835        let mut tree = WidgetTree::new();
3836        tree.add(
3837            FillWidget::new()
3838                .cursor(CursorIcon::ColResize)
3839                .on_hover(|entered, ctx| {
3840                    if entered {
3841                        ctx.set_cursor(CursorIcon::Crosshair);
3842                    }
3843                })
3844                .on_pointer_event(|event, ctx| {
3845                    if let WidgetEvent::PointerMove { position, .. } = event
3846                        && position.x >= 30.0
3847                    {
3848                        ctx.release_cursor();
3849                    }
3850                    EventResponse::Ignored
3851                }),
3852        );
3853        tree.layout(SizeProposal::exact(100.0, 50.0));
3854
3855        tree.pointer_move(Point::new(10.0, 25.0));
3856        assert_eq!(
3857            tree.current_cursor(),
3858            CursorIcon::Crosshair,
3859            "on_hover runs after the node cursor is applied, so it wins",
3860        );
3861        tree.pointer_move(Point::new(60.0, 25.0));
3862        assert_eq!(
3863            tree.current_cursor(),
3864            CursorIcon::ColResize,
3865            "and the node's declaration survived being overridden",
3866        );
3867    }
3868
3869    // A leaf that opts into typed introspection, so `with_widget_mut` /
3870    // `widget_as_any(_mut)` can reach it (the default `as_any_mut` is `None`).
3871    #[derive(Debug)]
3872    struct Bumpable {
3873        value: i32,
3874    }
3875
3876    impl crate::widget::Widget for Bumpable {
3877        fn layout_response(
3878            &self,
3879            proposal: SizeProposal,
3880            _ctx: &crate::widget::LayoutContext,
3881        ) -> crate::widget::LayoutResponse {
3882            proposal.resolve(10.0, 10.0).into()
3883        }
3884        fn as_any(&self) -> Option<&dyn std::any::Any> {
3885            Some(self)
3886        }
3887        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3888            Some(self)
3889        }
3890    }
3891
3892    #[test]
3893    fn with_widget_mut_applies_and_dirty_marks() {
3894        let mut tree = WidgetTree::new();
3895        let id = tree.add(Bumpable { value: 0 });
3896        tree.layout(SizeProposal::exact(100.0, 100.0));
3897
3898        let mut ctx = EventContext::new();
3899        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3900            b.value = 42;
3901        });
3902        tree.collect_from_ctx(ctx, id);
3903
3904        let value = tree
3905            .widget_as_any(id)
3906            .and_then(|a| a.downcast_ref::<Bumpable>())
3907            .map(|b| b.value);
3908        assert_eq!(
3909            value,
3910            Some(42),
3911            "the deferred closure must mutate the live widget"
3912        );
3913        assert!(
3914            tree.needs_layout(),
3915            "Relayout dirty level must mark the tree for relayout"
3916        );
3917    }
3918
3919    #[test]
3920    #[cfg(debug_assertions)]
3921    #[should_panic(expected = "not the requested type")]
3922    fn with_widget_mut_wrong_type_panics_in_debug() {
3923        struct Other;
3924        let mut tree = WidgetTree::new();
3925        let id = tree.add(Bumpable { value: 0 });
3926        let mut ctx = EventContext::new();
3927        ctx.with_widget_mut::<Other>(
3928            id,
3929            crate::binding::BindingLevel::RepaintOnly,
3930            |_o: &mut Other| {},
3931        );
3932        // Bumpable opts into as_any_mut, so the closure runs and the
3933        // wrong-type downcast trips the debug_assert.
3934        tree.collect_from_ctx(ctx, id);
3935    }
3936
3937    #[test]
3938    fn with_widget_mut_closure_may_fire_observed_signals() {
3939        // Reentrancy guard. The closure runs inside `apply_tree_mutations`
3940        // while the target arena node is mutably borrowed. If it fires a
3941        // `Signal` whose observer sets *another* signal — the exact
3942        // `SceneView` shape (`item_change_signal` → bump `reconcile_dirty`) —
3943        // nothing may double-borrow the arena. The arena borrow is scoped to
3944        // the closure call and dropped before dirty-marking; signal/observer
3945        // work touches the binding registry, not the arena.
3946        use crate::signal::Signal;
3947        let mut tree = WidgetTree::new();
3948        let id = tree.add(Bumpable { value: 0 });
3949        tree.layout(SizeProposal::exact(100.0, 100.0));
3950
3951        let trigger = Signal::new(0_u64);
3952        let echo = Signal::new(0_u64);
3953        let echo_for_obs = echo.clone();
3954        let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3955
3956        let trigger_in = trigger.clone();
3957        let mut ctx = EventContext::new();
3958        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3959            b.value = 7;
3960            // Fires `_obs` synchronously, mid-deferred-apply.
3961            trigger_in.set(99);
3962        });
3963        tree.collect_from_ctx(ctx, id); // must not panic / double-borrow
3964
3965        assert_eq!(
3966            echo.get(),
3967            99,
3968            "the observer ran during the deferred mutation"
3969        );
3970        let value = tree
3971            .widget_as_any(id)
3972            .and_then(|a| a.downcast_ref::<Bumpable>())
3973            .map(|b| b.value);
3974        assert_eq!(value, Some(7));
3975    }
3976
3977    #[test]
3978    fn request_accessibility_update_forces_rewalk() {
3979        let mut tree = WidgetTree::new();
3980        let id = tree.add(Bumpable { value: 0 });
3981        tree.layout(SizeProposal::exact(100.0, 100.0));
3982        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
3983        assert!(
3984            !tree.a11y_dirty,
3985            "sync_accessibility should clear the dirty flag"
3986        );
3987
3988        let mut ctx = EventContext::new();
3989        ctx.request_accessibility_update();
3990        tree.collect_from_ctx(ctx, id);
3991        assert!(
3992            tree.a11y_dirty,
3993            "request_accessibility_update must force an AT re-walk"
3994        );
3995    }
3996
3997    #[test]
3998    fn rebuild_dirties_accessibility_tree() {
3999        // Regression for audit Blocker G1: every `BindingLevel::Rebuild`
4000        // consumer (ListView / TreeView / TableView / ComboBox / Calendar /
4001        // DockingLayout / ...) tears down and re-creates its subtree on an
4002        // ordinary model change, allocating fresh WidgetIds and changing the
4003        // AccessKit tree shape. That pass must dirty the cached AT snapshot,
4004        // or screen readers keep reading the pre-mutation tree indefinitely.
4005        let mut tree = WidgetTree::new();
4006        let id = tree.add(Bumpable { value: 0 });
4007        tree.layout(SizeProposal::exact(100.0, 100.0));
4008        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4009        assert!(
4010            !tree.a11y_dirty,
4011            "sync_accessibility should clear the dirty flag"
4012        );
4013
4014        // Marking for rebuild is exactly what a Rebuild-level binding does;
4015        // the following layout pass drains pending rebuilds.
4016        tree.arena_mark_needs_rebuild_for_testing(id);
4017        tree.layout(SizeProposal::exact(100.0, 100.0));
4018        assert!(
4019            tree.a11y_dirty,
4020            "a rebuild must dirty the AT tree so the next sync re-walks"
4021        );
4022    }
4023
4024    #[test]
4025    fn bound_access_label_change_dirties_accessibility_tree() {
4026        use crate::signal::Signal;
4027        use crate::test_widgets::FillWidget;
4028        use crate::widget_builder::WidgetBuilder;
4029
4030        // Regression for audit G15: a reactive `.access_label(signal)` (and
4031        // likewise description / value) must register at AccessibilityOnly so
4032        // changing the signal re-walks the AT tree and re-resolves the
4033        // announced name. Previously only `access_hidden` was registered, so
4034        // label / description / value updates were invisible to screen readers.
4035        let label = Signal::new("first".to_string());
4036        let mut tree = WidgetTree::new();
4037        let _id = tree.add(FillWidget::new().access_label(label.clone()));
4038        tree.layout(SizeProposal::exact(100.0, 100.0));
4039        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4040        assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4041
4042        label.set("second".to_string());
4043        tree.layout(SizeProposal::exact(100.0, 100.0));
4044        assert!(
4045            tree.a11y_dirty,
4046            "changing a bound access_label must dirty the AT tree"
4047        );
4048    }
4049
4050    #[test]
4051    fn disabled_ancestor_blocks_event_to_descendant() {
4052        use crate::signal::Signal;
4053        use crate::test_widgets::StackWidget;
4054        use std::cell::Cell;
4055        use std::rc::Rc;
4056
4057        let tapped = Rc::new(Cell::new(false));
4058        let flag = tapped.clone();
4059        let enabled = Signal::new(true);
4060
4061        let mut tree = WidgetTree::new();
4062        let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4063            flag.set(true);
4064        }));
4065        let parent = tree.add(StackWidget::new().child(child));
4066        tree.enabled_when(parent, enabled.clone());
4067        tree.layout(SizeProposal::exact(100.0, 50.0));
4068
4069        enabled.set(false);
4070        tree.click(child);
4071        assert!(
4072            !tapped.get(),
4073            "disabled ancestor should block descendant tap"
4074        );
4075
4076        enabled.set(true);
4077        tree.click(child);
4078        assert!(tapped.get(), "re-enabling should restore dispatch");
4079    }
4080
4081    #[test]
4082    fn pointer_positions_are_widget_local_at_nonzero_origin() {
4083        use crate::event::{Modifiers, PointerButton};
4084        use crate::test_widgets::InsetWidget;
4085        use std::cell::Cell;
4086        use std::rc::Rc;
4087
4088        // A 20px inset places the child at window origin (20, 20).
4089        let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4090        let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4091        let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4092        let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4093
4094        let mut tree = WidgetTree::new();
4095        let child = tree.add(
4096            FillWidget::new()
4097                .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4098                .on_pointer_event(move |ev, _ctx| {
4099                    if let WidgetEvent::PointerDown { position, .. } = ev {
4100                        dp.set(Some(*position));
4101                    }
4102                    crate::event::EventResponse::Ignored
4103                })
4104                .on_drag(move |phase, _ctx| {
4105                    use crate::gesture::DragPhase;
4106                    match phase {
4107                        DragPhase::Started { position, .. }
4108                        | DragPhase::Moved { position, .. }
4109                        | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4110                        _ => {}
4111                    }
4112                }),
4113        );
4114        let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4115        let _ = inset;
4116        tree.layout(SizeProposal::exact(200.0, 200.0));
4117        assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4118
4119        // A tap at window (50, 40) must reach the handler as local (30, 20).
4120        tree.dispatch_event(WidgetEvent::pointer_down(
4121            Point::new(50.0, 40.0),
4122            PointerButton::Primary,
4123            Modifiers::NONE,
4124        ));
4125        assert_eq!(
4126            down_pos.get(),
4127            Some(Point::new(30.0, 20.0)),
4128            "on_pointer_event PointerDown must be widget-local"
4129        );
4130        tree.dispatch_event(WidgetEvent::pointer_up(
4131            Point::new(50.0, 40.0),
4132            PointerButton::Primary,
4133            Modifiers::NONE,
4134        ));
4135        assert_eq!(
4136            tap_pos.get(),
4137            Some(Point::new(30.0, 20.0)),
4138            "on_tap position must be widget-local"
4139        );
4140
4141        // A drag (down then a move past the recognizer threshold) must
4142        // also deliver widget-local coordinates.
4143        tree.dispatch_event(WidgetEvent::pointer_down(
4144            Point::new(50.0, 40.0),
4145            PointerButton::Primary,
4146            Modifiers::NONE,
4147        ));
4148        // First move crosses the recognizer threshold (DragStarted);
4149        // the second reports a known DragMoved position.
4150        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4151        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4152        assert_eq!(
4153            drag_pos.get(),
4154            Some(Point::new(70.0, 50.0)),
4155            "on_drag position must be widget-local"
4156        );
4157    }
4158
4159    #[test]
4160    fn dormant_widget_not_hit_tested() {
4161        let mut tree = WidgetTree::new();
4162        let widget = tree.add(FillWidget::new());
4163        tree.layout(SizeProposal::exact(100.0, 50.0));
4164
4165        tree.pointer_move(Point::new(50.0, 25.0));
4166        assert_eq!(tree.hovered(), Some(widget));
4167
4168        tree.set_dormant(widget);
4169        tree.pointer_move(Point::new(200.0, 200.0));
4170        tree.pointer_move(Point::new(50.0, 25.0));
4171        assert_eq!(tree.hovered(), None);
4172    }
4173
4174    #[test]
4175    fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4176        use crate::event::EventResponse;
4177        use crate::test_widgets::StackWidget;
4178        use std::cell::Cell;
4179        use std::rc::Rc;
4180
4181        // The child reports its own hover transitions via `on_hover`.
4182        let hovered = Rc::new(Cell::new(false));
4183        let h = hovered.clone();
4184
4185        let mut tree = WidgetTree::new();
4186        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4187        // An ancestor whose `on_pointer_event` greedily claims everything it
4188        // previews — exactly the "drag-detecting ancestor" footgun. Before the
4189        // fix it consumed the descendant's `PointerEnter`/`Leave` in the
4190        // preview pass and the child's hover never fired.
4191        tree.add(
4192            StackWidget::new()
4193                .child(child)
4194                .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4195        );
4196        tree.layout(SizeProposal::exact(100.0, 50.0));
4197
4198        tree.pointer_move(Point::new(50.0, 25.0));
4199        assert!(
4200            hovered.get(),
4201            "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4202        );
4203
4204        tree.pointer_move(Point::new(500.0, 500.0));
4205        assert!(
4206            !hovered.get(),
4207            "PointerLeave must likewise reach the child despite the ancestor"
4208        );
4209    }
4210
4211    /// **The other direction: a child must not swallow its ancestor's hover.**
4212    ///
4213    /// A row that reveals controls on hover puts interactive children inside
4214    /// itself, and the pointer leaves the row *through* one of them. The child's
4215    /// own `on_hover` used to handle the `PointerLeave` and stop the bubble there,
4216    /// so the row went on believing the pointer was still over it and kept its
4217    /// controls showing after the pointer had gone.
4218    #[test]
4219    fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4220        use crate::test_widgets::StackWidget;
4221        use std::cell::Cell;
4222        use std::rc::Rc;
4223
4224        let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4225        let (r, b) = (row.clone(), button.clone());
4226
4227        let mut tree = WidgetTree::new();
4228        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4229        tree.add(
4230            StackWidget::new()
4231                .child(child)
4232                .on_hover(move |entered, _ctx| r.set(entered)),
4233        );
4234        tree.layout(SizeProposal::exact(100.0, 50.0));
4235
4236        tree.pointer_move(Point::new(50.0, 25.0));
4237        assert!(button.get(), "the child is hovered");
4238        assert!(row.get(), "and so is the row it is inside");
4239
4240        tree.pointer_move(Point::new(500.0, 500.0));
4241        assert!(!button.get(), "the child heard the leave");
4242        assert!(
4243            !row.get(),
4244            "and so did the row — a container is not still hovered because the \
4245             pointer left it through a button"
4246        );
4247    }
4248
4249    // NOTE: legacy `shortcut_intercepts_before_widget` test removed with
4250    // the ShortcutMap dispatch path. The new shortcut→intent interception
4251    // is built on top of `ShortcutRegistry` + `Action`.
4252
4253    // ── on_key_preview ──────────────────────────────────────────
4254
4255    #[test]
4256    fn key_preview_consumes_before_focused_on_key() {
4257        // root → mid → leaf (focused). Root consumes Enter via
4258        // on_key_preview; the leaf's on_key must NOT fire.
4259        use crate::event::EventResponse;
4260        use crate::test_widgets::StackWidget;
4261        use std::cell::Cell;
4262        use std::rc::Rc;
4263
4264        let leaf_fired = Rc::new(Cell::new(false));
4265        let leaf_flag = leaf_fired.clone();
4266        let preview_fired = Rc::new(Cell::new(false));
4267        let preview_flag = preview_fired.clone();
4268
4269        let mut tree = WidgetTree::new();
4270        let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4271            // Only count KeyDown so the trailing KeyUp from
4272            // press_key doesn't trigger us spuriously.
4273            if matches!(event, WidgetEvent::KeyDown { .. }) {
4274                leaf_flag.set(true);
4275            }
4276            EventResponse::Handled
4277        }));
4278        let mid = tree.add(StackWidget::new().child(leaf));
4279        let _root = tree.add(
4280            StackWidget::new()
4281                .child(mid)
4282                .on_key_preview(move |event, _c| match event {
4283                    WidgetEvent::KeyDown {
4284                        key: Key::Enter, ..
4285                    } => {
4286                        preview_flag.set(true);
4287                        EventResponse::Handled
4288                    }
4289                    _ => EventResponse::Ignored,
4290                }),
4291        );
4292
4293        tree.layout(SizeProposal::exact(100.0, 50.0));
4294        tree.focus(leaf);
4295        tree.press_key(Key::Enter, Modifiers::NONE);
4296
4297        assert!(
4298            preview_fired.get(),
4299            "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4300        );
4301        assert!(
4302            !leaf_fired.get(),
4303            "consuming the event in preview must prevent the focused widget's on_key from running"
4304        );
4305    }
4306
4307    #[test]
4308    fn key_preview_falls_through_when_returning_ignored() {
4309        // Same shape; this time the preview returns Ignored, so
4310        // the leaf's on_key must still fire.
4311        use crate::event::EventResponse;
4312        use crate::test_widgets::StackWidget;
4313        use std::cell::Cell;
4314        use std::rc::Rc;
4315
4316        let leaf_fired = Rc::new(Cell::new(false));
4317        let leaf_flag = leaf_fired.clone();
4318        let preview_fired = Rc::new(Cell::new(false));
4319        let preview_flag = preview_fired.clone();
4320
4321        let mut tree = WidgetTree::new();
4322        let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4323            leaf_flag.set(true);
4324            EventResponse::Handled
4325        }));
4326        let mid = tree.add(StackWidget::new().child(leaf));
4327        let _root = tree.add(
4328            StackWidget::new()
4329                .child(mid)
4330                .on_key_preview(move |_event, _c| {
4331                    preview_flag.set(true);
4332                    EventResponse::Ignored
4333                }),
4334        );
4335
4336        tree.layout(SizeProposal::exact(100.0, 50.0));
4337        tree.focus(leaf);
4338        tree.press_key(Key::Enter, Modifiers::NONE);
4339
4340        assert!(preview_fired.get(), "preview must always be invoked");
4341        assert!(
4342            leaf_fired.get(),
4343            "preview returning Ignored must not block the focused widget's on_key"
4344        );
4345    }
4346
4347    #[test]
4348    fn key_preview_excludes_focused_target_itself() {
4349        // Strict-ancestors-only: the focused widget's own
4350        // on_key_preview must NOT fire — the preview pass walks
4351        // strict ancestors only.
4352        use crate::event::EventResponse;
4353        use std::cell::Cell;
4354        use std::rc::Rc;
4355
4356        let preview_on_target = Rc::new(Cell::new(false));
4357        let pf = preview_on_target.clone();
4358
4359        let mut tree = WidgetTree::new();
4360        let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4361            pf.set(true);
4362            EventResponse::Handled
4363        }));
4364        tree.layout(SizeProposal::exact(100.0, 50.0));
4365        tree.focus(leaf);
4366        tree.press_key(Key::Enter, Modifiers::NONE);
4367
4368        assert!(
4369            !preview_on_target.get(),
4370            "the focused widget itself must not see its own on_key_preview"
4371        );
4372    }
4373
4374    #[test]
4375    fn key_preview_root_to_target_order() {
4376        // Two ancestors with on_key_preview attached. The outer
4377        // (root-side) one must fire first; the closer one (still
4378        // ancestor of the focused leaf) fires second.
4379        use crate::event::EventResponse;
4380        use crate::test_widgets::StackWidget;
4381        use std::cell::RefCell;
4382        use std::rc::Rc;
4383
4384        let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4385        let outer_log = order.clone();
4386        let inner_log = order.clone();
4387
4388        let mut tree = WidgetTree::new();
4389        let leaf = tree.add(FillWidget::new().focusable());
4390        let inner = tree.add(
4391            StackWidget::new()
4392                .child(leaf)
4393                .on_key_preview(move |event, _c| {
4394                    if matches!(event, WidgetEvent::KeyDown { .. }) {
4395                        inner_log.borrow_mut().push("inner");
4396                    }
4397                    EventResponse::Ignored
4398                }),
4399        );
4400        let _outer = tree.add(
4401            StackWidget::new()
4402                .child(inner)
4403                .on_key_preview(move |event, _c| {
4404                    if matches!(event, WidgetEvent::KeyDown { .. }) {
4405                        outer_log.borrow_mut().push("outer");
4406                    }
4407                    EventResponse::Ignored
4408                }),
4409        );
4410
4411        tree.layout(SizeProposal::exact(100.0, 50.0));
4412        tree.focus(leaf);
4413        tree.dispatch_event(WidgetEvent::KeyDown {
4414            key: Key::Enter,
4415            modifiers: Modifiers::NONE,
4416            text: None,
4417        });
4418
4419        assert_eq!(
4420            *order.borrow(),
4421            vec!["outer", "inner"],
4422            "preview must walk root → parent-of-target"
4423        );
4424    }
4425
4426    #[test]
4427    fn access_action_routes_to_cursored_target_not_focus() {
4428        // VoiceOver's VO+Space targets the node under the AT cursor (`b`),
4429        // even when keyboard focus is on a different control (`a`). The action
4430        // must fire on `b`, never get redirected to the focused `a`.
4431        use crate::signal::Signal;
4432        let a_fired = Signal::new(false);
4433        let b_fired = Signal::new(false);
4434        let a_cb = a_fired.clone();
4435        let b_cb = b_fired.clone();
4436
4437        let mut tree = WidgetTree::new();
4438        let a = tree.add(
4439            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4440        );
4441        let b = tree.add(
4442            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4443        );
4444        tree.layout(SizeProposal::exact(200.0, 100.0));
4445
4446        tree.focus(a);
4447        tree.dispatch_event(WidgetEvent::AccessAction {
4448            action: accesskit::Action::Click,
4449            target: Some(b),
4450            target_node: crate::accessibility::widget_id_to_node_id(b),
4451            data: None,
4452        });
4453
4454        assert!(b_fired.get(), "the cursored target must receive the action");
4455        assert!(
4456            !a_fired.get(),
4457            "the keyboard-focused widget must NOT receive an action targeting another node"
4458        );
4459    }
4460
4461    #[test]
4462    fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4463        // An action with no (or an inactive) target must be dropped — never
4464        // silently re-routed to whatever holds keyboard focus.
4465        use crate::signal::Signal;
4466        let fired = Signal::new(false);
4467        let cb = fired.clone();
4468
4469        let mut tree = WidgetTree::new();
4470        let widget = tree.add(
4471            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4472        );
4473        tree.layout(SizeProposal::exact(200.0, 100.0));
4474
4475        tree.focus(widget);
4476        tree.dispatch_event(WidgetEvent::AccessAction {
4477            action: accesskit::Action::Click,
4478            target: None,
4479            target_node: crate::accessibility::root_node_id(),
4480            data: None,
4481        });
4482
4483        assert!(
4484            !fired.get(),
4485            "a target-less action must not be redirected to the focused widget"
4486        );
4487    }
4488
4489    // NOTE: legacy `scoped_shortcut_fires_when_focused_in_subtree` test
4490    // removed along with the ShortcutMap dispatch path. Scope-aware
4491    // dispatch is handled by the new ShortcutRegistry.
4492
4493    // --- Intent / Action dispatch ------------------------------
4494
4495    #[test]
4496    fn shortcut_fires_matching_action_on_source_widget() {
4497        use crate::action::Action;
4498        use crate::shortcut::{KeyStroke, Shortcut};
4499        use std::cell::Cell;
4500        use std::rc::Rc;
4501
4502        let fired = Rc::new(Cell::new(false));
4503        let fired_flag = fired.clone();
4504
4505        let mut tree = WidgetTree::new();
4506        let widget = tree.add(FillWidget::new().focusable());
4507        tree.push_action(
4508            widget,
4509            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4510                fired_flag.set(true);
4511            }),
4512        );
4513        tree.shortcut_registry_mut().register(
4514            Shortcut::new("app.save")
4515                .primary(KeyStroke::command(Key::S))
4516                .build(),
4517        );
4518
4519        tree.layout(SizeProposal::exact(100.0, 50.0));
4520        tree.focus(widget);
4521
4522        tree.press_key(Key::S, Modifiers::COMMAND);
4523        assert!(fired.get(), "matching action must fire on KeyDown");
4524    }
4525
4526    #[test]
4527    fn global_shortcut_fires_without_focused_widget() {
4528        use crate::action::Action;
4529        use crate::shortcut::{KeyStroke, Shortcut};
4530        use std::cell::Cell;
4531        use std::rc::Rc;
4532
4533        // Regression: a global shortcut must fire even when no widget
4534        // is focused. A root-registered action should still receive
4535        // the intent (anchored at the root as a fallback).
4536        let fired = Rc::new(Cell::new(false));
4537        let fired_flag = fired.clone();
4538
4539        let mut tree = WidgetTree::new();
4540        let root = tree.add(FillWidget::new());
4541        tree.push_action(
4542            root,
4543            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4544                fired_flag.set(true);
4545            }),
4546        );
4547        tree.shortcut_registry_mut().register(
4548            Shortcut::new("app.save")
4549                .primary(KeyStroke::command(Key::S))
4550                .build(),
4551        );
4552
4553        tree.layout(SizeProposal::exact(100.0, 50.0));
4554        // Deliberately no focus() call.
4555
4556        tree.press_key(Key::S, Modifiers::COMMAND);
4557        assert!(
4558            fired.get(),
4559            "global shortcut must fire without a focused widget"
4560        );
4561    }
4562
4563    #[test]
4564    fn global_shortcut_fires_after_focused_widget_destroyed() {
4565        use crate::action::Action;
4566        use crate::shortcut::{KeyStroke, Shortcut};
4567        use std::cell::Cell;
4568        use std::rc::Rc;
4569
4570        // Regression: if the focused widget is destroyed (e.g. during a
4571        // rebuild after a settings-panel rebind), focus must be cleared
4572        // so the next global shortcut falls through to the root-anchor
4573        // path instead of dispatching from a stale, destroyed id.
4574        let fired = Rc::new(Cell::new(false));
4575        let fired_flag = fired.clone();
4576
4577        let mut tree = WidgetTree::new();
4578        let root = tree.add(FillWidget::new());
4579        let focusable = tree.add_child(root, FillWidget::new().focusable());
4580        tree.push_action(
4581            root,
4582            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4583                fired_flag.set(true);
4584            }),
4585        );
4586        tree.shortcut_registry_mut().register(
4587            Shortcut::new("app.save")
4588                .primary(KeyStroke::command(Key::S))
4589                .build(),
4590        );
4591
4592        tree.layout(SizeProposal::exact(100.0, 50.0));
4593        tree.focus(focusable);
4594        assert_eq!(tree.focused(), Some(focusable));
4595
4596        // Destroy the focused subtree (simulates a rebuild that drops
4597        // the currently-focused Rebind button).
4598        tree.destroy_subtree(focusable);
4599        assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4600
4601        tree.press_key(Key::S, Modifiers::COMMAND);
4602        assert!(
4603            fired.get(),
4604            "global shortcut must still fire after the focused widget is destroyed"
4605        );
4606    }
4607
4608    #[test]
4609    fn scoped_shortcut_matches_only_when_focus_in_scope() {
4610        use crate::action::Action;
4611        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4612        use std::cell::Cell;
4613        use std::rc::Rc;
4614
4615        let fired = Rc::new(Cell::new(0));
4616        let fired_flag = fired.clone();
4617
4618        let mut tree = WidgetTree::new();
4619        let scope_root = tree.add(FillWidget::new().focusable());
4620        let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4621        let outside = tree.add(FillWidget::new().focusable());
4622
4623        tree.push_action(
4624            scope_root,
4625            Action::new("editor.find").on_invoke(move |_i, _c| {
4626                fired_flag.set(fired_flag.get() + 1);
4627            }),
4628        );
4629        tree.shortcut_registry_mut().register(
4630            Shortcut::new("editor.find")
4631                .primary(KeyStroke::command(Key::F))
4632                .scope(ShortcutScope::Scoped(scope_root))
4633                .build(),
4634        );
4635
4636        tree.layout(SizeProposal::exact(200.0, 100.0));
4637
4638        // Focus outside the scope: the shortcut does NOT activate.
4639        tree.focus(outside);
4640        tree.press_key(Key::F, Modifiers::COMMAND);
4641        assert_eq!(
4642            fired.get(),
4643            0,
4644            "scoped shortcut must not fire outside scope"
4645        );
4646
4647        // Focus inside the scope: it fires.
4648        tree.focus(inside);
4649        tree.press_key(Key::F, Modifiers::COMMAND);
4650        assert_eq!(
4651            fired.get(),
4652            1,
4653            "scoped shortcut must fire when focus in scope"
4654        );
4655    }
4656
4657    #[test]
4658    fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4659        // Defect 1: a Scoped binding that sorts first by id must NOT
4660        // shadow the slot when focus is outside its subtree — the
4661        // applicable Global binding fires instead. (`editor.saveBlock`
4662        // < `zzz.global.save`, so the scoped one wins the id-order race
4663        // that `find_by_keystroke` used to settle on.)
4664        use crate::action::Action;
4665        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4666        use std::cell::Cell;
4667        use std::rc::Rc;
4668
4669        let scoped_fired = Rc::new(Cell::new(0));
4670        let global_fired = Rc::new(Cell::new(0));
4671        let sf = scoped_fired.clone();
4672        let gf = global_fired.clone();
4673
4674        let mut tree = WidgetTree::new();
4675        let root = tree.add(FillWidget::new());
4676        let editor = tree.add_child(root, FillWidget::new().focusable());
4677        let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4678        let sidebar = tree.add_child(root, FillWidget::new().focusable());
4679
4680        tree.push_action(
4681            editor,
4682            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4683        );
4684        tree.push_action(
4685            root,
4686            Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4687        );
4688        tree.shortcut_registry_mut().register(
4689            Shortcut::new("editor.saveBlock")
4690                .primary(KeyStroke::command(Key::S))
4691                .scope(ShortcutScope::Scoped(editor))
4692                .build(),
4693        );
4694        tree.shortcut_registry_mut().register(
4695            Shortcut::new("zzz.global.save")
4696                .primary(KeyStroke::command(Key::S))
4697                .build(),
4698        );
4699
4700        tree.layout(SizeProposal::exact(200.0, 100.0));
4701        tree.focus(sidebar);
4702        tree.press_key(Key::S, Modifiers::COMMAND);
4703
4704        assert_eq!(global_fired.get(), 1, "applicable global must fire");
4705        assert_eq!(
4706            scoped_fired.get(),
4707            0,
4708            "inapplicable scoped binding must not eat the chord"
4709        );
4710    }
4711
4712    #[test]
4713    fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4714        // Defect 2: a Global binding that sorts first by id must yield to
4715        // an in-focus Scoped binding (most-specific-scope wins), then
4716        // reclaim the chord once focus leaves the scope. (`app.save` <
4717        // `editor.saveBlock`, so the global one wins id order.)
4718        use crate::action::Action;
4719        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4720        use std::cell::Cell;
4721        use std::rc::Rc;
4722
4723        let scoped_fired = Rc::new(Cell::new(0));
4724        let global_fired = Rc::new(Cell::new(0));
4725        let sf = scoped_fired.clone();
4726        let gf = global_fired.clone();
4727
4728        let mut tree = WidgetTree::new();
4729        let root = tree.add(FillWidget::new());
4730        let editor = tree.add_child(root, FillWidget::new().focusable());
4731        let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4732        let sidebar = tree.add_child(root, FillWidget::new().focusable());
4733
4734        tree.push_action(
4735            editor,
4736            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4737        );
4738        tree.push_action(
4739            root,
4740            Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4741        );
4742        tree.shortcut_registry_mut().register(
4743            Shortcut::new("app.save")
4744                .primary(KeyStroke::command(Key::S))
4745                .build(),
4746        );
4747        tree.shortcut_registry_mut().register(
4748            Shortcut::new("editor.saveBlock")
4749                .primary(KeyStroke::command(Key::S))
4750                .scope(ShortcutScope::Scoped(editor))
4751                .build(),
4752        );
4753
4754        tree.layout(SizeProposal::exact(200.0, 100.0));
4755
4756        // Focus inside the editor: the scoped binding wins over global.
4757        tree.focus(editor_inner);
4758        tree.press_key(Key::S, Modifiers::COMMAND);
4759        assert_eq!(
4760            scoped_fired.get(),
4761            1,
4762            "in-focus scoped must win over global"
4763        );
4764        assert_eq!(
4765            global_fired.get(),
4766            0,
4767            "global must yield to the scoped binding"
4768        );
4769
4770        // Focus outside the editor: global reclaims the chord.
4771        tree.focus(sidebar);
4772        tree.press_key(Key::S, Modifiers::COMMAND);
4773        assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4774        assert_eq!(
4775            global_fired.get(),
4776            1,
4777            "global fires when focus leaves the scope"
4778        );
4779    }
4780
4781    #[test]
4782    fn propagated_action_lets_ancestor_handle() {
4783        use crate::action::Action;
4784        use crate::intent::IntentResponse;
4785        use crate::shortcut::{KeyStroke, Shortcut};
4786        use std::cell::Cell;
4787        use std::rc::Rc;
4788
4789        let inner_seen = Rc::new(Cell::new(false));
4790        let outer_seen = Rc::new(Cell::new(false));
4791        let inner_flag = inner_seen.clone();
4792        let outer_flag = outer_seen.clone();
4793
4794        let mut tree = WidgetTree::new();
4795        let outer = tree.add(FillWidget::new().focusable());
4796        let inner = tree.add_child(outer, FillWidget::new().focusable());
4797
4798        // Inner observes then propagates; outer consumes.
4799        tree.push_action(
4800            inner,
4801            Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4802                inner_flag.set(true);
4803                IntentResponse::Propagated
4804            }),
4805        );
4806        tree.push_action(
4807            outer,
4808            Action::new("app.save").on_invoke(move |_i, _c| {
4809                outer_flag.set(true);
4810            }),
4811        );
4812        tree.shortcut_registry_mut().register(
4813            Shortcut::new("app.save")
4814                .primary(KeyStroke::command(Key::S))
4815                .build(),
4816        );
4817
4818        tree.layout(SizeProposal::exact(100.0, 50.0));
4819        tree.focus(inner);
4820
4821        tree.press_key(Key::S, Modifiers::COMMAND);
4822        assert!(inner_seen.get(), "inner action observed the intent");
4823        assert!(outer_seen.get(), "outer action reached after Propagated");
4824    }
4825
4826    #[test]
4827    fn handled_action_stops_propagation() {
4828        use crate::action::Action;
4829        use crate::shortcut::{KeyStroke, Shortcut};
4830        use std::cell::Cell;
4831        use std::rc::Rc;
4832
4833        let inner_seen = Rc::new(Cell::new(false));
4834        let outer_seen = Rc::new(Cell::new(false));
4835        let inner_flag = inner_seen.clone();
4836        let outer_flag = outer_seen.clone();
4837
4838        let mut tree = WidgetTree::new();
4839        let outer = tree.add(FillWidget::new().focusable());
4840        let inner = tree.add_child(outer, FillWidget::new().focusable());
4841
4842        tree.push_action(
4843            inner,
4844            Action::new("app.save").on_invoke(move |_i, _c| {
4845                inner_flag.set(true);
4846            }),
4847        );
4848        tree.push_action(
4849            outer,
4850            Action::new("app.save").on_invoke(move |_i, _c| {
4851                outer_flag.set(true);
4852            }),
4853        );
4854        tree.shortcut_registry_mut().register(
4855            Shortcut::new("app.save")
4856                .primary(KeyStroke::command(Key::S))
4857                .build(),
4858        );
4859
4860        tree.layout(SizeProposal::exact(100.0, 50.0));
4861        tree.focus(inner);
4862
4863        tree.press_key(Key::S, Modifiers::COMMAND);
4864        assert!(inner_seen.get());
4865        assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4866    }
4867
4868    #[test]
4869    fn disabled_action_propagates_by_default() {
4870        use crate::action::Action;
4871        use crate::shortcut::{KeyStroke, Shortcut};
4872        use crate::signal::Signal;
4873        use std::cell::Cell;
4874        use std::rc::Rc;
4875
4876        let inner_seen = Rc::new(Cell::new(false));
4877        let outer_seen = Rc::new(Cell::new(false));
4878        let inner_flag = inner_seen.clone();
4879        let outer_flag = outer_seen.clone();
4880
4881        let mut tree = WidgetTree::new();
4882        let outer = tree.add(FillWidget::new().focusable());
4883        let inner = tree.add_child(outer, FillWidget::new().focusable());
4884
4885        let enabled = Signal::new(false);
4886        tree.push_action(
4887            inner,
4888            Action::new("app.save")
4889                .enabled_when(enabled.clone())
4890                .on_invoke(move |_i, _c| {
4891                    inner_flag.set(true);
4892                }),
4893        );
4894        tree.push_action(
4895            outer,
4896            Action::new("app.save").on_invoke(move |_i, _c| {
4897                outer_flag.set(true);
4898            }),
4899        );
4900        tree.shortcut_registry_mut().register(
4901            Shortcut::new("app.save")
4902                .primary(KeyStroke::command(Key::S))
4903                .build(),
4904        );
4905
4906        tree.layout(SizeProposal::exact(100.0, 50.0));
4907        tree.focus(inner);
4908
4909        tree.press_key(Key::S, Modifiers::COMMAND);
4910        assert!(!inner_seen.get(), "disabled inner must not run");
4911        assert!(
4912            outer_seen.get(),
4913            "intent must propagate past disabled inner"
4914        );
4915    }
4916
4917    #[test]
4918    fn disabled_action_with_non_propagating_shortcut_consumes() {
4919        use crate::action::Action;
4920        use crate::shortcut::{KeyStroke, Shortcut};
4921        use crate::signal::Signal;
4922        use std::cell::Cell;
4923        use std::rc::Rc;
4924
4925        let inner_seen = Rc::new(Cell::new(false));
4926        let outer_seen = Rc::new(Cell::new(false));
4927        let inner_flag = inner_seen.clone();
4928        let outer_flag = outer_seen.clone();
4929
4930        let mut tree = WidgetTree::new();
4931        let outer = tree.add(FillWidget::new().focusable());
4932        let inner = tree.add_child(outer, FillWidget::new().focusable());
4933
4934        let enabled = Signal::new(false);
4935        tree.push_action(
4936            inner,
4937            Action::new("app.save")
4938                .enabled_when(enabled.clone())
4939                .on_invoke(move |_i, _c| {
4940                    inner_flag.set(true);
4941                }),
4942        );
4943        tree.push_action(
4944            outer,
4945            Action::new("app.save").on_invoke(move |_i, _c| {
4946                outer_flag.set(true);
4947            }),
4948        );
4949        tree.shortcut_registry_mut().register(
4950            Shortcut::new("app.save")
4951                .primary(KeyStroke::command(Key::S))
4952                .propagate_when_disabled(false)
4953                .build(),
4954        );
4955
4956        tree.layout(SizeProposal::exact(100.0, 50.0));
4957        tree.focus(inner);
4958
4959        tree.press_key(Key::S, Modifiers::COMMAND);
4960        assert!(!inner_seen.get(), "disabled inner still does not run");
4961        assert!(
4962            !outer_seen.get(),
4963            "intent must NOT propagate when shortcut disallows it"
4964        );
4965    }
4966
4967    #[test]
4968    fn send_intent_from_handler_reaches_ancestor_action() {
4969        use crate::action::Action;
4970        use crate::intent::Intent;
4971        use std::cell::Cell;
4972        use std::rc::Rc;
4973
4974        let save_seen = Rc::new(Cell::new(false));
4975        let save_flag = save_seen.clone();
4976
4977        let mut tree = WidgetTree::new();
4978        let root = tree.add(FillWidget::new());
4979        let button = tree.add_child(
4980            root,
4981            FillWidget::new().on_tap(|_pos, ctx| {
4982                ctx.send_intent(Intent::new("app.save"));
4983            }),
4984        );
4985        tree.push_action(
4986            root,
4987            Action::new("app.save").on_invoke(move |_i, _c| {
4988                save_flag.set(true);
4989            }),
4990        );
4991
4992        tree.layout(SizeProposal::exact(100.0, 50.0));
4993        tree.click(button);
4994        assert!(
4995            save_seen.get(),
4996            "ctx.send_intent must reach ancestor action"
4997        );
4998    }
4999
5000    #[test]
5001    fn widget_type_histogram_counts_distinct_types() {
5002        // The histogram surfaces concrete widget types
5003        // by std::any::type_name_of_val. Widgets become active
5004        // after the first layout pass, so we run that before
5005        // checking the histogram.
5006        let mut tree = WidgetTree::new();
5007        let _ = tree.add(FillWidget::new());
5008        let _ = tree.add(FillWidget::new());
5009        let _ = tree.add(FillWidget::new());
5010        tree.layout(SizeProposal::exact(100.0, 100.0));
5011        let histogram = tree.widget_type_histogram();
5012        let total: u32 = histogram.values().sum();
5013        assert!(
5014            total >= 3,
5015            "expected at least 3 active widgets, got {total}: {histogram:?}"
5016        );
5017        let fillwidget_entries: u32 = histogram
5018            .iter()
5019            .filter(|(k, _)| k.contains("FillWidget"))
5020            .map(|(_, v)| *v)
5021            .sum();
5022        assert!(
5023            fillwidget_entries >= 3,
5024            "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5025        );
5026        assert_eq!(tree.active_widget_count() as u32, total);
5027    }
5028
5029    #[test]
5030    fn intent_source_tagged_handler_for_tap_activation() {
5031        // A tap-driven `ctx.send_intent` must surface as
5032        // `IntentSource::Handler` to ancestor actions, not the
5033        // `Programmatic` default of `Intent::new`.
5034        use crate::action::Action;
5035        use crate::intent::Intent;
5036        use crate::telemetry::IntentSource;
5037        use std::cell::Cell;
5038        use std::rc::Rc;
5039        let captured = Rc::new(Cell::new(IntentSource::Unknown));
5040        let captured_for_action = captured.clone();
5041
5042        let mut tree = WidgetTree::new();
5043        let root = tree.add(FillWidget::new());
5044        let button = tree.add_child(
5045            root,
5046            FillWidget::new().on_tap(|_pos, ctx| {
5047                ctx.send_intent(Intent::new("app.save"));
5048            }),
5049        );
5050        tree.push_action(
5051            root,
5052            Action::new("app.save").on_invoke(move |intent, _c| {
5053                captured_for_action.set(intent.source);
5054            }),
5055        );
5056
5057        tree.layout(SizeProposal::exact(100.0, 50.0));
5058        tree.click(button);
5059        assert_eq!(
5060            captured.get(),
5061            IntentSource::Handler,
5062            "tap-driven intent must tag IntentSource::Handler"
5063        );
5064    }
5065
5066    #[test]
5067    fn intent_source_programmatic_when_no_handler_active() {
5068        use crate::intent::Intent;
5069        use crate::telemetry::IntentSource;
5070        let intent = Intent::new("app.demo");
5071        assert_eq!(intent.source, IntentSource::Programmatic);
5072
5073        // ctx.send_intent without a handler scope keeps it Programmatic.
5074        let mut ctx = EventContext::new();
5075        ctx.send_intent(Intent::new("app.demo"));
5076        let queued = ctx.pending_intents.first().expect("intent queued");
5077        assert_eq!(queued.source, IntentSource::Programmatic);
5078    }
5079
5080    #[test]
5081    fn with_intent_source_overrides_for_managed_widgets() {
5082        use crate::intent::Intent;
5083        use crate::telemetry::IntentSource;
5084        let mut ctx = EventContext::new();
5085        ctx.with_intent_source(IntentSource::Menu, |ctx| {
5086            ctx.send_intent(Intent::new("app.demo"));
5087        });
5088        let queued = ctx.pending_intents.first().expect("intent queued");
5089        assert_eq!(
5090            queued.source,
5091            IntentSource::Menu,
5092            "with_intent_source(Menu) must tag the dispatched intent"
5093        );
5094
5095        // After the closure returns, current_source is restored —
5096        // a follow-up send_intent without a wrapping closure goes
5097        // back to the default (no override).
5098        ctx.send_intent(Intent::new("app.next"));
5099        let next = ctx.pending_intents.last().expect("second intent");
5100        assert_eq!(next.source, IntentSource::Programmatic);
5101    }
5102
5103    #[test]
5104    fn disabled_shortcut_falls_through_to_focused_widget() {
5105        use crate::action::Action;
5106        use crate::shortcut::{KeyStroke, Shortcut};
5107        use crate::signal::Signal;
5108        use std::cell::Cell;
5109        use std::rc::Rc;
5110
5111        let action_fired = Rc::new(Cell::new(false));
5112        let on_key_fired = Rc::new(Cell::new(false));
5113        let af = action_fired.clone();
5114        let kf = on_key_fired.clone();
5115
5116        let enabled = Signal::new(false);
5117
5118        let mut tree = WidgetTree::new();
5119        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5120            if matches!(
5121                event,
5122                WidgetEvent::KeyDown {
5123                    key: Key::S,
5124                    modifiers,
5125                    ..
5126                } if modifiers.command()
5127            ) {
5128                kf.set(true);
5129                return EventResponse::Handled;
5130            }
5131            EventResponse::Ignored
5132        }));
5133        tree.push_action(
5134            widget,
5135            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5136        );
5137        tree.shortcut_registry_mut().register(
5138            Shortcut::new("app.save")
5139                .primary(KeyStroke::command(Key::S))
5140                .enabled_when(enabled.clone())
5141                .build(),
5142        );
5143
5144        tree.layout(SizeProposal::exact(100.0, 50.0));
5145        tree.focus(widget);
5146
5147        // Disabled: keystroke falls through to on_key.
5148        tree.press_key(Key::S, Modifiers::COMMAND);
5149        assert!(
5150            !action_fired.get(),
5151            "disabled shortcut must not invoke its action"
5152        );
5153        assert!(
5154            on_key_fired.get(),
5155            "disabled shortcut must let KeyDown reach the focused widget"
5156        );
5157
5158        // Re-enable → action fires, on_key does not.
5159        on_key_fired.set(false);
5160        enabled.set(true);
5161        tree.press_key(Key::S, Modifiers::COMMAND);
5162        assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5163        assert!(
5164            !on_key_fired.get(),
5165            "enabled shortcut must consume the KeyDown"
5166        );
5167    }
5168
5169    #[test]
5170    fn keyboard_capture_bypasses_shortcut() {
5171        use crate::action::Action;
5172        use crate::shortcut::{KeyStroke, Shortcut};
5173        use std::cell::Cell;
5174        use std::rc::Rc;
5175
5176        // A focused keyboard-capture surface (e.g. a terminal) must receive
5177        // the accelerator chord itself (⌘S on macOS, Ctrl+S elsewhere), even
5178        // though an ENABLED global shortcut binds it — the whole point of
5179        // GAP 1. A non-capturing widget must yield to the shortcut (the
5180        // control case).
5181        fn run(capture: bool) -> (bool, bool) {
5182            let action_fired = Rc::new(Cell::new(false));
5183            let on_key_fired = Rc::new(Cell::new(false));
5184            let af = action_fired.clone();
5185            let kf = on_key_fired.clone();
5186
5187            let mut tree = WidgetTree::new();
5188            let widget = tree.add(
5189                FillWidget::new()
5190                    .focusable()
5191                    .keyboard_capture(capture)
5192                    .on_key(move |event, _ctx| {
5193                        if matches!(
5194                            event,
5195                            WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5196                        ) {
5197                            kf.set(true);
5198                            return EventResponse::Handled;
5199                        }
5200                        EventResponse::Ignored
5201                    }),
5202            );
5203            tree.push_action(
5204                widget,
5205                Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5206            );
5207            tree.shortcut_registry_mut().register(
5208                Shortcut::new("app.save")
5209                    .primary(KeyStroke::command(Key::S))
5210                    .build(),
5211            );
5212
5213            tree.layout(SizeProposal::exact(100.0, 50.0));
5214            tree.focus(widget);
5215            tree.press_key(Key::S, Modifiers::COMMAND);
5216            (action_fired.get(), on_key_fired.get())
5217        }
5218
5219        // Capture on: the shortcut is bypassed, the widget sees the key.
5220        let (action, on_key) = run(true);
5221        assert!(
5222            !action,
5223            "keyboard_capture must suppress the shortcut action"
5224        );
5225        assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5226
5227        // Capture off (control): the shortcut consumes the key.
5228        let (action, on_key) = run(false);
5229        assert!(action, "without capture the shortcut must fire");
5230        assert!(!on_key, "without capture the widget must not see the key");
5231    }
5232
5233    /// A focused widget that records every `KeyDown` it is handed.
5234    fn key_recorder(
5235        tree: &mut WidgetTree,
5236    ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5237        let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5238        let log = seen.clone();
5239        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5240            if let WidgetEvent::KeyDown { key, .. } = event {
5241                log.borrow_mut().push(*key);
5242            }
5243            EventResponse::Ignored
5244        }));
5245        tree.layout(SizeProposal::exact(100.0, 50.0));
5246        tree.focus(widget);
5247        (widget, seen)
5248    }
5249
5250    #[test]
5251    fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5252        use crate::action::Action;
5253        use crate::intent::IntentResponse;
5254        use crate::shortcut::{KeyStroke, Shortcut};
5255
5256        let mut tree = WidgetTree::new();
5257        let (widget, seen) = key_recorder(&mut tree);
5258        tree.shortcut_registry_mut().register(
5259            Shortcut::new("app.save")
5260                .primary(KeyStroke::command(Key::S))
5261                .build(),
5262        );
5263
5264        // Nothing handles `app.save`.
5265        tree.press_key(Key::S, Modifiers::COMMAND);
5266        assert_eq!(
5267            *seen.borrow(),
5268            vec![Key::S],
5269            "an unhandled intent must not eat the key"
5270        );
5271
5272        // An action that runs and passes the intent on has not consumed it.
5273        tree.push_action(
5274            widget,
5275            Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5276        );
5277        tree.press_key(Key::S, Modifiers::COMMAND);
5278        assert_eq!(
5279            seen.borrow().len(),
5280            2,
5281            "a propagated intent must leave the key too"
5282        );
5283
5284        // Deleting the fall-through makes both of the above go red; this is
5285        // the control: a consuming action still takes the key.
5286        let mut tree = WidgetTree::new();
5287        let (widget, seen) = key_recorder(&mut tree);
5288        tree.shortcut_registry_mut().register(
5289            Shortcut::new("app.save")
5290                .primary(KeyStroke::command(Key::S))
5291                .build(),
5292        );
5293        tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5294        tree.press_key(Key::S, Modifiers::COMMAND);
5295        assert!(
5296            seen.borrow().is_empty(),
5297            "a handled shortcut consumes its key"
5298        );
5299    }
5300
5301    #[test]
5302    fn standard_edit_chords_follow_a_rebind() {
5303        use crate::action::Action;
5304        use crate::shortcut::{KeyStroke, StandardEdit};
5305        use std::cell::Cell;
5306        use std::rc::Rc;
5307
5308        let mut tree = WidgetTree::new();
5309        let (widget, seen) = key_recorder(&mut tree);
5310        let copies = Rc::new(Cell::new(0));
5311        let count = copies.clone();
5312        tree.push_action(
5313            widget,
5314            Action::new(StandardEdit::Copy.id())
5315                .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5316        );
5317
5318        // Registered by `WidgetTree::new`, with no help from the app.
5319        tree.press_key(Key::C, Modifiers::COMMAND);
5320        assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5321        assert!(seen.borrow().is_empty());
5322
5323        // Move Copy elsewhere: the old chord no longer copies (it reaches the
5324        // widget as a plain key), the new one does.
5325        let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5326        tree.shortcut_registry_mut()
5327            .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5328        tree.press_key(Key::C, Modifiers::COMMAND);
5329        assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5330        assert_eq!(*seen.borrow(), vec![Key::C]);
5331        tree.press_key(Key::Character('à'), Modifiers::CTRL);
5332        assert_eq!(copies.get(), 2, "the new chord copies");
5333    }
5334
5335    #[test]
5336    fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5337        use std::cell::Cell;
5338        use std::rc::Rc;
5339
5340        // WCAG 2.1.2. A capture surface answers `Handled` to every key —
5341        // that is what it is for — so the "cycle focus only when the focused
5342        // widget did not handle Tab" rule can never get focus out of one.
5343        // Ctrl+Tab / Ctrl+Shift+Tab are therefore reserved by the dispatcher
5344        // and never reach the widget at all.
5345        let saw_key = Rc::new(Cell::new(false));
5346        let sk = saw_key.clone();
5347
5348        let mut tree = WidgetTree::new();
5349        let capture = tree.add(
5350            FillWidget::new()
5351                .focusable()
5352                .keyboard_capture(true)
5353                // The greediest possible handler: everything is consumed.
5354                .on_key(move |_event, _ctx| {
5355                    sk.set(true);
5356                    EventResponse::Handled
5357                }),
5358        );
5359        let neighbour = tree.add(FillWidget::new().focusable());
5360        tree.layout(SizeProposal::exact(100.0, 50.0));
5361
5362        // Plain Tab stays inside: the widget consumed it (a terminal writes
5363        // it to the child as `\t`).
5364        tree.focus(capture);
5365        tree.press_key(Key::Tab, Modifiers::NONE);
5366        assert!(saw_key.get(), "plain Tab must reach the capture surface");
5367        assert_eq!(
5368            tree.focused(),
5369            Some(capture),
5370            "plain Tab must not move focus off a capture surface"
5371        );
5372
5373        // Ctrl+Tab escapes forward, without the widget ever seeing it.
5374        saw_key.set(false);
5375        tree.press_key(Key::Tab, Modifiers::CTRL);
5376        assert!(
5377            !saw_key.get(),
5378            "Ctrl+Tab is reserved and must not reach the capture surface"
5379        );
5380        assert_eq!(
5381            tree.focused(),
5382            Some(neighbour),
5383            "Ctrl+Tab must move focus out of a capture surface"
5384        );
5385
5386        // And backwards.
5387        tree.focus(capture);
5388        tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5389        assert_eq!(
5390            tree.focused(),
5391            Some(neighbour),
5392            "Ctrl+Shift+Tab must move focus out of a capture surface"
5393        );
5394    }
5395
5396    #[test]
5397    fn scope_mismatch_does_not_invoke_on_activate() {
5398        use crate::intent::Intent;
5399        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5400        use std::cell::Cell;
5401        use std::rc::Rc;
5402
5403        // Regression: before the find/invoke split, `on_activate` ran
5404        // even when the focused widget was outside the shortcut's
5405        // scope, and any side effects on its ctx were silently
5406        // dropped. The closure must now only run when the scope
5407        // check has already passed.
5408        let activated = Rc::new(Cell::new(false));
5409        let activated_flag = activated.clone();
5410
5411        let mut tree = WidgetTree::new();
5412        let scope_root = tree.add(FillWidget::new().focusable());
5413        let outside = tree.add(FillWidget::new().focusable());
5414
5415        tree.shortcut_registry_mut().register(
5416            Shortcut::new("editor.find")
5417                .primary(KeyStroke::command(Key::F))
5418                .scope(ShortcutScope::Scoped(scope_root))
5419                .on_activate(move |_ks, _ctx| {
5420                    activated_flag.set(true);
5421                    Intent::new("editor.find")
5422                })
5423                .build(),
5424        );
5425
5426        tree.layout(SizeProposal::exact(200.0, 100.0));
5427        tree.focus(outside);
5428
5429        tree.press_key(Key::F, Modifiers::COMMAND);
5430        assert!(
5431            !activated.get(),
5432            "on_activate must not run when focus is outside the shortcut's scope"
5433        );
5434    }
5435
5436    #[test]
5437    fn key_capture_runs_callback_and_bypasses_registry() {
5438        use crate::action::Action;
5439        use crate::shortcut::{KeyStroke, Shortcut};
5440        use std::cell::Cell;
5441        use std::rc::Rc;
5442
5443        let action_fired = Rc::new(Cell::new(false));
5444        let af = action_fired.clone();
5445        let captured = Rc::new(Cell::new(None));
5446        let cf = captured.clone();
5447
5448        let mut tree = WidgetTree::new();
5449        let widget = tree.add(FillWidget::new().focusable());
5450        tree.push_action(
5451            widget,
5452            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5453        );
5454        tree.shortcut_registry_mut().register(
5455            Shortcut::new("app.save")
5456                .primary(KeyStroke::command(Key::S))
5457                .build(),
5458        );
5459
5460        tree.layout(SizeProposal::exact(100.0, 50.0));
5461        tree.focus(widget);
5462
5463        let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5464        assert!(tree.is_capturing_keys());
5465
5466        tree.press_key(Key::S, Modifiers::COMMAND);
5467        assert_eq!(
5468            captured.get(),
5469            Some(KeyStroke::command(Key::S)),
5470            "capture callback must receive the chord"
5471        );
5472        assert!(
5473            !action_fired.get(),
5474            "shortcut action must not fire while capture is armed"
5475        );
5476        assert!(
5477            !tree.is_capturing_keys(),
5478            "capture is one-shot; next KeyDown flows normally"
5479        );
5480        drop(handle);
5481    }
5482
5483    #[test]
5484    fn key_capture_can_rebind_through_registry() {
5485        use crate::shortcut::{KeyStroke, Shortcut};
5486
5487        let mut tree = WidgetTree::new();
5488        let widget = tree.add(FillWidget::new().focusable());
5489        tree.shortcut_registry_mut().register(
5490            Shortcut::new("app.save")
5491                .primary(KeyStroke::command(Key::S))
5492                .build(),
5493        );
5494
5495        tree.layout(SizeProposal::exact(100.0, 50.0));
5496        tree.focus(widget);
5497
5498        // Arm capture: whatever chord comes next, rebind app.save to it.
5499        let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5500            reg.rebind_primary("app.save", Some(ks));
5501        });
5502
5503        tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5504        assert_eq!(
5505            tree.shortcut_registry()
5506                .effective("app.save")
5507                .unwrap()
5508                .primary,
5509            Some(KeyStroke::command_shift(Key::B))
5510        );
5511    }
5512
5513    #[test]
5514    fn dropping_capture_handle_cancels_capture() {
5515        use crate::shortcut::{KeyStroke, Shortcut};
5516        use std::cell::Cell;
5517        use std::rc::Rc;
5518
5519        let action_fired = Rc::new(Cell::new(false));
5520        let af = action_fired.clone();
5521        let capture_fired = Rc::new(Cell::new(false));
5522        let cf = capture_fired.clone();
5523
5524        let mut tree = WidgetTree::new();
5525        let widget = tree.add(FillWidget::new().focusable());
5526        tree.push_action(
5527            widget,
5528            crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5529        );
5530        tree.shortcut_registry_mut().register(
5531            Shortcut::new("app.save")
5532                .primary(KeyStroke::command(Key::S))
5533                .build(),
5534        );
5535        tree.layout(SizeProposal::exact(100.0, 50.0));
5536        tree.focus(widget);
5537
5538        // Arm capture in a scope, then drop the handle before any key
5539        // is pressed. The next KeyDown must fall through to the normal
5540        // shortcut path, firing the action — not the cancelled capture.
5541        {
5542            let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5543            assert!(tree.is_capturing_keys());
5544            // `_h` drops here → cancel.
5545        }
5546        assert!(
5547            !tree.is_capturing_keys(),
5548            "dropping the handle must cancel the capture"
5549        );
5550
5551        tree.press_key(Key::S, Modifiers::COMMAND);
5552        assert!(!capture_fired.get(), "cancelled capture must not fire");
5553        assert!(
5554            action_fired.get(),
5555            "shortcut action runs after capture was cancelled"
5556        );
5557    }
5558
5559    #[test]
5560    fn second_begin_key_capture_does_not_racecancel_first() {
5561        use std::cell::Cell;
5562        use std::rc::Rc;
5563
5564        let first = Rc::new(Cell::new(false));
5565        let second = Rc::new(Cell::new(false));
5566        let f = first.clone();
5567        let s = second.clone();
5568
5569        let mut tree = WidgetTree::new();
5570        let widget = tree.add(FillWidget::new().focusable());
5571        tree.layout(SizeProposal::exact(100.0, 50.0));
5572        tree.focus(widget);
5573
5574        // Arm #1 then replace with #2. #1's handle is later dropped,
5575        // which would have cancelled the active capture under the old
5576        // `Option<Box<FnOnce>>` design — CaptureHandle now ties each
5577        // session to its own slot, so the drop only clears #1's
5578        // (orphaned) slot, not #2.
5579        let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5580        let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5581        drop(h1);
5582
5583        assert!(
5584            tree.is_capturing_keys(),
5585            "dropping the older handle must not cancel the active capture"
5586        );
5587        tree.press_key(Key::K, Modifiers::COMMAND);
5588        assert!(!first.get());
5589        assert!(second.get(), "newest capture wins");
5590    }
5591
5592    #[test]
5593    fn capture_callback_can_send_intent() {
5594        use crate::action::Action;
5595        use crate::intent::Intent;
5596
5597        use std::cell::Cell;
5598        use std::rc::Rc;
5599
5600        let ran = Rc::new(Cell::new(false));
5601        let flag = ran.clone();
5602
5603        let mut tree = WidgetTree::new();
5604        let widget = tree.add(FillWidget::new().focusable());
5605        tree.push_action(
5606            widget,
5607            Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5608        );
5609        tree.layout(SizeProposal::exact(100.0, 50.0));
5610        tree.focus(widget);
5611
5612        let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5613            ctx.send_intent(Intent::new("app.save"));
5614        });
5615        tree.press_key(Key::X, Modifiers::COMMAND);
5616        assert!(
5617            ran.get(),
5618            "intent queued from capture callback must dispatch"
5619        );
5620    }
5621
5622    #[test]
5623    fn binding_registry_does_not_accumulate_across_rebuilds() {
5624        use crate::binding::BindingLevel;
5625        use crate::signal::Signal;
5626
5627        #[derive(Debug)]
5628        struct BoundLeaf {
5629            tick: Signal<u64>,
5630        }
5631        impl crate::widget::Widget for BoundLeaf {
5632            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5633                self.tick.bind_to(
5634                    ctx.self_id(),
5635                    ctx.binding_registry(),
5636                    BindingLevel::Relayout,
5637                );
5638                Vec::new()
5639            }
5640            fn layout_response(
5641                &self,
5642                proposal: SizeProposal,
5643                _ctx: &crate::widget::LayoutContext,
5644            ) -> crate::widget::LayoutResponse {
5645                proposal.resolve(10.0, 10.0).into()
5646            }
5647        }
5648
5649        let mut tree = WidgetTree::new();
5650        let tick = Signal::new(0_u64);
5651        let widget = tree.add(BoundLeaf { tick: tick.clone() });
5652        tree.layout(SizeProposal::exact(200.0, 200.0));
5653        let after_first_build = tree.binding_registry().len();
5654        assert!(after_first_build >= 1);
5655
5656        // Force rebuild a handful of times and verify the binding
5657        // count does not keep growing. Pre-fix: each rebuild pushed
5658        // a new entry for the same (widget, signal) pair.
5659        for _ in 0..5 {
5660            tree.arena.mark_needs_rebuild(widget);
5661            tree.layout(SizeProposal::exact(200.0, 200.0));
5662        }
5663        assert_eq!(
5664            tree.binding_registry().len(),
5665            after_first_build,
5666            "bindings must be cleared on rebuild"
5667        );
5668
5669        tree.destroy_subtree(widget);
5670        assert_eq!(
5671            tree.binding_registry().len(),
5672            0,
5673            "bindings must be cleared on destroy"
5674        );
5675        // Silence unused-variable warning for the signal.
5676        let _ = tick;
5677    }
5678
5679    #[test]
5680    fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5681        // Regression: `EventContext::destroy` queues
5682        // `TreeMutation::Destroy`, which used to be applied with the
5683        // bare `arena.destroy` — unlinking the node but leaking the
5684        // animation-scheduler entry (it holds a strong `Signal<f32>`
5685        // clone, so the widget kept animating after destruction) and
5686        // the widget's bindings. It must route through
5687        // `destroy_subtree` like every other destroy path does.
5688        use crate::binding::BindingLevel;
5689        use crate::signal::Signal;
5690        use std::time::{Duration, Instant};
5691        use teksilo_tokens::Easing;
5692
5693        #[derive(Debug)]
5694        struct BoundLeaf {
5695            tick: Signal<u64>,
5696        }
5697        impl crate::widget::Widget for BoundLeaf {
5698            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5699                self.tick.bind_to(
5700                    ctx.self_id(),
5701                    ctx.binding_registry(),
5702                    BindingLevel::Relayout,
5703                );
5704                Vec::new()
5705            }
5706            fn layout_response(
5707                &self,
5708                proposal: SizeProposal,
5709                _ctx: &crate::widget::LayoutContext,
5710            ) -> crate::widget::LayoutResponse {
5711                proposal.resolve(10.0, 10.0).into()
5712            }
5713        }
5714
5715        let mut tree = WidgetTree::new();
5716        let widget = tree.add(BoundLeaf {
5717            tick: Signal::new(0_u64),
5718        });
5719        tree.layout(SizeProposal::exact(200.0, 200.0));
5720        assert!(tree.binding_registry().len() >= 1);
5721
5722        // Seed an animation owned by the widget — exactly the strong
5723        // `Signal<f32>` clone the scheduler outlives the widget with.
5724        let anim = Signal::<f32>::new_animated(0.0);
5725        tree.animation_scheduler.animate(
5726            &anim,
5727            widget,
5728            1.0,
5729            Duration::from_secs(10),
5730            Easing::Linear,
5731            Instant::now(),
5732        );
5733        assert_eq!(tree.animation_scheduler.active_count(), 1);
5734
5735        // Destroy via the deferred handler-time path.
5736        let mut noop = crate::window::NoopWindowOps;
5737        tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5738
5739        assert_eq!(
5740            tree.animation_scheduler.active_count(),
5741            0,
5742            "ctx.destroy must cancel animations owned by the destroyed widget"
5743        );
5744        assert_eq!(
5745            tree.binding_registry().len(),
5746            0,
5747            "ctx.destroy must unregister the destroyed widget's bindings"
5748        );
5749        assert!(
5750            tree.arena.get(widget).is_none(),
5751            "node must be removed from the arena"
5752        );
5753    }
5754
5755    #[test]
5756    fn clear_shortcut_override_via_event_context_restores_default() {
5757        use crate::shortcut::{KeyStroke, Shortcut};
5758
5759        let mut tree = WidgetTree::new();
5760        tree.shortcut_registry_mut().register(
5761            Shortcut::new("app.save")
5762                .primary(KeyStroke::command(Key::S))
5763                .build(),
5764        );
5765        tree.shortcut_registry_mut()
5766            .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5767
5768        let source = tree.add(FillWidget::new());
5769        let mut ctx = EventContext::new();
5770        ctx.clear_shortcut_override("app.save");
5771        tree.collect_from_ctx(ctx, source);
5772
5773        assert_eq!(
5774            tree.shortcut_registry()
5775                .effective("app.save")
5776                .unwrap()
5777                .primary,
5778            Some(KeyStroke::command(Key::S))
5779        );
5780    }
5781
5782    #[test]
5783    fn rebind_shortcut_primary_via_event_context() {
5784        use crate::shortcut::{KeyStroke, Shortcut};
5785
5786        let mut tree = WidgetTree::new();
5787        tree.shortcut_registry_mut().register(
5788            Shortcut::new("app.save")
5789                .primary(KeyStroke::command(Key::S))
5790                .build(),
5791        );
5792        let source = tree.add(FillWidget::new());
5793
5794        let mut ctx = EventContext::new();
5795        ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5796        tree.collect_from_ctx(ctx, source);
5797
5798        assert_eq!(
5799            tree.shortcut_registry()
5800                .effective("app.save")
5801                .unwrap()
5802                .primary,
5803            Some(KeyStroke::alt(Key::S))
5804        );
5805    }
5806
5807    #[test]
5808    fn unregister_all_for_owner_called_on_destroy() {
5809        use crate::shortcut::{KeyStroke, Shortcut};
5810
5811        let mut tree = WidgetTree::new();
5812        let widget = tree.add(FillWidget::new());
5813        let widget_owner = widget;
5814        tree.shortcut_registry_mut().register_owned(
5815            Shortcut::new("scoped.thing")
5816                .primary(KeyStroke::command(Key::K))
5817                .build(),
5818            widget_owner,
5819        );
5820        assert!(
5821            tree.shortcut_registry()
5822                .get_default("scoped.thing")
5823                .is_some()
5824        );
5825
5826        tree.destroy_subtree(widget);
5827        assert!(
5828            tree.shortcut_registry()
5829                .get_default("scoped.thing")
5830                .is_none(),
5831            "destroying the owner must unregister its shortcut"
5832        );
5833    }
5834
5835    /// A global action fires for an intent dispatched from a widget in a
5836    /// completely unrelated subtree — proving it is a position-independent
5837    /// fallback (the menu-bar-vs-content case).
5838    #[test]
5839    fn global_action_reached_from_unrelated_source() {
5840        use crate::action::Action;
5841        use crate::intent::Intent;
5842        use std::cell::Cell;
5843        use std::rc::Rc;
5844
5845        #[derive(Debug)]
5846        struct Registrar(Rc<Cell<bool>>);
5847        impl crate::widget::Widget for Registrar {
5848            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5849                let flag = self.0.clone();
5850                ctx.register_action_global(
5851                    Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5852                );
5853                vec![]
5854            }
5855            fn layout_response(
5856                &self,
5857                _p: teksilo_canvas::SizeProposal,
5858                _c: &crate::widget::LayoutContext,
5859            ) -> crate::widget::LayoutResponse {
5860                teksilo_canvas::Size::new(0.0, 0.0).into()
5861            }
5862        }
5863
5864        let mut tree = WidgetTree::new();
5865        let fired = Rc::new(Cell::new(false));
5866        let registrar = tree.add(Registrar(fired.clone()));
5867        let source = tree.add(FillWidget::new()); // unrelated sibling root
5868        let mut ops = crate::window::NoopWindowOps;
5869
5870        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5871        assert!(
5872            fired.get(),
5873            "global action must fire from an unrelated source"
5874        );
5875
5876        // And it is torn down with its owner.
5877        fired.set(false);
5878        tree.destroy_subtree(registrar);
5879        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5880        assert!(
5881            !fired.get(),
5882            "destroying the owner must remove its global action"
5883        );
5884    }
5885
5886    // --- Transform-aware hit-testing -------------------------------------
5887    //
5888    // `set_transform` scopes are paint-only: the renderer pushes the
5889    // transform around the subtree, so the visually-displayed area is
5890    // shifted relative to `arena.bounds(id)`. Hit-testing must inverse-
5891    // transform the screen-space input point as it descends through each
5892    // transform scope so that a click on the visually-rendered area lands
5893    // on the correct widget. Pre-fix, screen-space `bounds.contains(point)`
5894    // returned the *pre-transform* widget for in-bounds-pre-transform
5895    // points and missed the visually-shifted hit area entirely.
5896
5897    #[test]
5898    fn hit_test_through_translate_scope() {
5899        use crate::test_widgets::StackWidget;
5900        let mut tree = WidgetTree::new();
5901        let child = tree.add(FillWidget::new());
5902        let parent = tree.add(StackWidget::new().child(child));
5903        // Visually shift the entire subtree right by 100px.
5904        tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5905        tree.layout(SizeProposal::exact(100.0, 50.0));
5906
5907        // (50, 25) is inside the *pre-transform* bounds but the widget is
5908        // visually painted at x=100..200; a click at (50, 25) lands on
5909        // empty space.
5910        assert_eq!(
5911            tree.hit_test(Point::new(50.0, 25.0)),
5912            None,
5913            "pre-transform area is not visually populated and must not hit"
5914        );
5915        // (150, 25) is inside the visually-rendered area (post-translate).
5916        assert_eq!(
5917            tree.hit_test(Point::new(150.0, 25.0)),
5918            Some(child),
5919            "visually-rendered area must hit the child"
5920        );
5921        // Off everything.
5922        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5923    }
5924
5925    #[test]
5926    fn hit_test_through_scale_scope() {
5927        use crate::test_widgets::StackWidget;
5928        let mut tree = WidgetTree::new();
5929        let child = tree.add(FillWidget::new());
5930        let parent = tree.add(StackWidget::new().child(child));
5931        // Halve the visual size: pre-transform bounds (0,0,100,50) →
5932        // visually (0,0,50,25).
5933        tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5934        tree.layout(SizeProposal::exact(100.0, 50.0));
5935
5936        // Inside the visual area.
5937        assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5938        // Outside the visual area but inside the pre-transform bounds.
5939        // Without the fix this would (incorrectly) hit the child.
5940        assert_eq!(
5941            tree.hit_test(Point::new(75.0, 25.0)),
5942            None,
5943            "scaled-out region must not hit"
5944        );
5945    }
5946
5947    #[test]
5948    fn hit_test_through_nested_transforms_compose() {
5949        use crate::test_widgets::StackWidget;
5950        let mut tree = WidgetTree::new();
5951        let leaf = tree.add(FillWidget::new());
5952        let inner = tree.add(StackWidget::new().child(leaf));
5953        let outer = tree.add(StackWidget::new().child(inner));
5954        // Outer translates by (100, 0); inner additionally scales by 2.
5955        // Effective at leaf = scale(2,2).then(translate(100,0)) — the
5956        // renderer composes deepest-first (see `effective_transform`).
5957        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5958        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5959        tree.layout(SizeProposal::exact(50.0, 25.0));
5960
5961        // Leaf-local (0, 0) → scale → (0, 0) → translate → (100, 0).
5962        // Leaf-local (50, 25) → scale → (100, 50) → translate → (200, 50).
5963        // So the visual hit area is x in [100, 200], y in [0, 50].
5964        assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5965        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5966        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5967    }
5968
5969    #[test]
5970    fn hit_test_identity_transform_unchanged() {
5971        // Sanity: an identity transform must not perturb the existing
5972        // hit-test behavior. Guards against accidental over-application
5973        // of inversion on the hot path.
5974        let mut tree = WidgetTree::new();
5975        let widget = tree.add(FillWidget::new());
5976        tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5977        tree.layout(SizeProposal::exact(100.0, 50.0));
5978        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5979    }
5980
5981    #[test]
5982    fn arena_effective_transform_composes_ancestors() {
5983        // `arena.effective_transform(id)` must equal the renderer's
5984        // transform-stack top by the time it begins painting `id` —
5985        // i.e. mapping `id`'s pre-transform local point to screen space.
5986        // The renderer's `PushTransform` handler composes as
5987        // `device_t.then(prev_top)` (see `teksilo-render/src/renderer.rs`),
5988        // so the *innermost* transform applies first to a local point.
5989        // For ancestors [outer, inner] both with transforms, this means
5990        // effective = inner.then(outer), NOT outer.then(inner).
5991        // teksilo-scene relies on this to project scene-coord bounds to
5992        // screen space when emitting AT nodes for view-transformed items.
5993        use crate::test_widgets::StackWidget;
5994        let mut tree = WidgetTree::new();
5995        let leaf = tree.add(FillWidget::new());
5996        let inner = tree.add(StackWidget::new().child(leaf));
5997        let outer = tree.add(StackWidget::new().child(inner));
5998        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5999        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6000        tree.layout(SizeProposal::exact(50.0, 25.0));
6001
6002        let eff = tree.arena.effective_transform(leaf);
6003        let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6004            .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6005        for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6006            assert!(
6007                (a - b).abs() < 1e-5,
6008                "effective_transform mismatch: got {:?}, want {:?}",
6009                eff.m,
6010                expected.m
6011            );
6012        }
6013
6014        // Concrete-point check that pins the composition order without
6015        // relying on matrix equality alone: a leaf-local point at the
6016        // bounds origin (0, 0) should land at screen (100, 0) — scale
6017        // first (still (0,0)), then translate by 100 in x. With the
6018        // wrong composition order it would land at (200, 0).
6019        let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6020        assert!((screen_origin.x - 100.0).abs() < 1e-5);
6021        assert!((screen_origin.y - 0.0).abs() < 1e-5);
6022        // Far corner: leaf-local (50, 25) → scale → (100, 50) → translate
6023        // by 100 in x → (200, 50).
6024        let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6025        assert!((screen_corner.x - 200.0).abs() < 1e-5);
6026        assert!((screen_corner.y - 50.0).abs() < 1e-5);
6027    }
6028
6029    // ─── Context-menu factory: position, ctx, None fall-through ─────────
6030
6031    /// A throwaway content widget the factory mounts. We never paint
6032    /// it — the test only checks that it lands in the overlay manager.
6033    #[derive(Debug)]
6034    struct StubMenu;
6035    impl crate::widget::Widget for StubMenu {
6036        fn layout_response(
6037            &self,
6038            _proposal: SizeProposal,
6039            _ctx: &crate::widget::LayoutContext,
6040        ) -> crate::widget::LayoutResponse {
6041            teksilo_canvas::Size::new(100.0, 40.0).into()
6042        }
6043    }
6044
6045    // The keyboard route to a context menu.
6046    //
6047    // Until this existed there was none at all: no `Key::ContextMenu`, no
6048    // Shift+F10, and `Action::ShowContextMenu` appears in zero of the three
6049    // AccessKit adapters, so the assistive-technology route is dead on every
6050    // platform too. A menu reachable only by right-click is a menu a keyboard
6051    // user does not have.
6052
6053    /// A widget that hands the keyboard a different target than itself, the way
6054    /// every data view does: the container has focus, the row is what the menu
6055    /// is about.
6056    #[derive(Debug)]
6057    struct NominatingWidget {
6058        row: std::cell::Cell<Option<WidgetId>>,
6059    }
6060
6061    impl crate::widget::Widget for NominatingWidget {
6062        fn layout_response(
6063            &self,
6064            proposal: SizeProposal,
6065            _ctx: &LayoutContext,
6066        ) -> crate::widget::LayoutResponse {
6067            proposal.resolve(50.0, 20.0).into()
6068        }
6069
6070        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6071            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6072            Vec::new()
6073        }
6074
6075        fn context_menu_key_target(&self) -> Option<WidgetId> {
6076            self.row.get()
6077        }
6078    }
6079
6080    fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6081        tree.dispatch_event(WidgetEvent::KeyDown {
6082            key,
6083            modifiers,
6084            text: None,
6085        });
6086    }
6087
6088    #[test]
6089    fn the_context_menu_key_opens_the_focused_widget_menu() {
6090        use std::cell::Cell;
6091        use std::rc::Rc;
6092
6093        let opened = Rc::new(Cell::new(false));
6094        let flag = opened.clone();
6095        let mut tree = WidgetTree::new();
6096        let widget = tree.add(
6097            FillWidget::new()
6098                .focusable()
6099                .context_menu(move |_pos, _ctx| {
6100                    flag.set(true);
6101                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6102                }),
6103        );
6104        tree.layout(SizeProposal::exact(200.0, 100.0));
6105        tree.focus(widget);
6106
6107        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6108        assert!(opened.get(), "the dedicated Menu key must open the menu");
6109    }
6110
6111    /// The chord every Windows and Linux keyboard can reach, including the many
6112    /// that have no dedicated Menu key at all.
6113    #[test]
6114    fn shift_f10_opens_the_focused_widget_menu() {
6115        use std::cell::Cell;
6116        use std::rc::Rc;
6117
6118        let opened = Rc::new(Cell::new(false));
6119        let flag = opened.clone();
6120        let mut tree = WidgetTree::new();
6121        let widget = tree.add(
6122            FillWidget::new()
6123                .focusable()
6124                .context_menu(move |_pos, _ctx| {
6125                    flag.set(true);
6126                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6127                }),
6128        );
6129        tree.layout(SizeProposal::exact(200.0, 100.0));
6130        tree.focus(widget);
6131
6132        press(&mut tree, Key::F10, Modifiers::SHIFT);
6133        assert!(opened.get(), "Shift+F10 must open the menu");
6134    }
6135
6136    /// Modifiers are matched exactly. Ctrl+Shift+F10 is a different gesture and
6137    /// belongs to the application.
6138    #[test]
6139    fn a_near_miss_chord_is_not_a_context_menu_request() {
6140        use std::cell::Cell;
6141        use std::rc::Rc;
6142
6143        let opened = Rc::new(Cell::new(false));
6144        let flag = opened.clone();
6145        let mut tree = WidgetTree::new();
6146        let widget = tree.add(
6147            FillWidget::new()
6148                .focusable()
6149                .context_menu(move |_pos, _ctx| {
6150                    flag.set(true);
6151                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6152                }),
6153        );
6154        tree.layout(SizeProposal::exact(200.0, 100.0));
6155        tree.focus(widget);
6156
6157        press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6158        press(&mut tree, Key::F10, Modifiers::NONE);
6159        assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6160    }
6161
6162    /// The correction the design needed. A data view is focusable and its rows
6163    /// are not, so "the focused widget" is the list, and the menu a user asked
6164    /// for on row 4 would have been the list's own.
6165    #[test]
6166    fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6167        use std::cell::Cell;
6168        use std::rc::Rc;
6169
6170        let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6171
6172        let row_flag = menu_owner.clone();
6173        let container_flag = menu_owner.clone();
6174
6175        let mut tree = WidgetTree::new();
6176        let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6177            row_flag.set(Some("row"));
6178            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6179        }));
6180        let container = tree.add(
6181            crate::test_widgets::StackWidget::new()
6182                .child(row)
6183                .context_menu(move |_pos, _ctx| {
6184                    container_flag.set(Some("container"));
6185                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6186                }),
6187        );
6188        tree.layout(SizeProposal::exact(200.0, 100.0));
6189
6190        // The container is focused, and nominates the row.
6191        let nominator = tree.add(NominatingWidget {
6192            row: std::cell::Cell::new(Some(row)),
6193        });
6194        tree.layout(SizeProposal::exact(200.0, 100.0));
6195        tree.focus(nominator);
6196        let _ = container;
6197
6198        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6199        assert_eq!(
6200            menu_owner.get(),
6201            Some("row"),
6202            "the nominated row's factory must be the one that runs"
6203        );
6204    }
6205
6206    /// Nothing on the chain owns a factory, so the framework must not swallow
6207    /// the key: a widget that wants to handle Shift+F10 itself still can.
6208    #[test]
6209    fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6210        use std::cell::Cell;
6211        use std::rc::Rc;
6212
6213        let saw_key = Rc::new(Cell::new(false));
6214        let flag = saw_key.clone();
6215        let mut tree = WidgetTree::new();
6216        let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6217            if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6218                flag.set(true);
6219            }
6220            crate::event::EventResponse::Ignored
6221        }));
6222        tree.layout(SizeProposal::exact(200.0, 100.0));
6223        tree.focus(widget);
6224
6225        press(&mut tree, Key::F10, Modifiers::SHIFT);
6226        assert!(
6227            saw_key.get(),
6228            "with no factory anywhere, the key must reach the widget"
6229        );
6230    }
6231
6232    /// Every route to a menu tells the factory what asked for it, because only
6233    /// a pointer's point is a place the user chose. A text field's factory
6234    /// moves the caret to its point for a right-click; told nothing, it did the
6235    /// same for Shift+F10, and the menu's Paste wrote into the middle of the
6236    /// field.
6237    #[test]
6238    fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6239        use crate::widget_builder::ContextMenuTrigger;
6240        use std::cell::RefCell;
6241        use std::rc::Rc;
6242
6243        let seen = Rc::new(RefCell::new(Vec::new()));
6244        let log = seen.clone();
6245        let mut tree = WidgetTree::new();
6246        let widget = tree.add(
6247            FillWidget::new()
6248                .focusable()
6249                .context_menu(move |_pos, ctx| {
6250                    log.borrow_mut().push(ctx.context_menu_trigger());
6251                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6252                }),
6253        );
6254        tree.layout(SizeProposal::exact(200.0, 100.0));
6255        tree.focus(widget);
6256        let inside = tree.bounds(widget).center();
6257
6258        press(&mut tree, Key::F10, Modifiers::SHIFT);
6259        press(&mut tree, Key::Escape, Modifiers::NONE);
6260        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6261        press(&mut tree, Key::Escape, Modifiers::NONE);
6262        tree.dispatch_event(WidgetEvent::pointer_down(
6263            inside,
6264            crate::event::PointerButton::Secondary,
6265            Modifiers::NONE,
6266        ));
6267        tree.dispatch_event(WidgetEvent::pointer_up(
6268            inside,
6269            crate::event::PointerButton::Secondary,
6270            Modifiers::NONE,
6271        ));
6272        press(&mut tree, Key::Escape, Modifiers::NONE);
6273        tree.dispatch_event(WidgetEvent::AccessAction {
6274            action: accesskit::Action::ShowContextMenu,
6275            target: Some(widget),
6276            target_node: crate::accessibility::widget_id_to_node_id(widget),
6277            data: None,
6278        });
6279        press(&mut tree, Key::Escape, Modifiers::NONE);
6280        tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6281
6282        assert_eq!(
6283            *seen.borrow(),
6284            vec![
6285                Some(ContextMenuTrigger::Keyboard),
6286                Some(ContextMenuTrigger::Keyboard),
6287                Some(ContextMenuTrigger::Pointer),
6288                Some(ContextMenuTrigger::Accessibility),
6289                Some(ContextMenuTrigger::Pointer),
6290            ],
6291            "Shift+F10, the Menu key, a right-click, the assistive action and a \
6292             hold, in that order"
6293        );
6294    }
6295
6296    #[test]
6297    fn context_menu_factory_receives_click_position() {
6298        use crate::event::{Modifiers, PointerButton};
6299        use std::cell::Cell;
6300        use std::rc::Rc;
6301
6302        let captured_position = Rc::new(Cell::new(None::<Point>));
6303        let cap = captured_position.clone();
6304        let mut tree = WidgetTree::new();
6305        let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6306            cap.set(Some(pos));
6307            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6308        }));
6309        tree.layout(SizeProposal::exact(200.0, 100.0));
6310
6311        let click = Point::new(73.0, 42.0);
6312        tree.dispatch_event(WidgetEvent::pointer_down(
6313            click,
6314            PointerButton::Secondary,
6315            Modifiers::NONE,
6316        ));
6317
6318        let got = captured_position.get();
6319        assert_eq!(
6320            got,
6321            Some(click),
6322            "factory must receive the click position; got {:?}",
6323            got
6324        );
6325        let _ = widget;
6326    }
6327
6328    #[test]
6329    fn context_menu_factory_returning_none_falls_through_to_parent() {
6330        use crate::event::{Modifiers, PointerButton};
6331        use crate::test_widgets::StackWidget;
6332        use std::cell::Cell;
6333        use std::rc::Rc;
6334
6335        // Outer factory always returns Some(StubMenu); inner factory
6336        // returns None. Right-click should walk past the inner and
6337        // mount the outer's menu.
6338        let outer_called = Rc::new(Cell::new(0_u32));
6339        let outer_flag = outer_called.clone();
6340        let mut tree = WidgetTree::new();
6341        let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6342        let _outer = tree.add(
6343            StackWidget::new()
6344                .child(inner)
6345                .context_menu(move |_pos, _ctx| {
6346                    outer_flag.set(outer_flag.get() + 1);
6347                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6348                }),
6349        );
6350        tree.layout(SizeProposal::exact(200.0, 100.0));
6351
6352        tree.dispatch_event(WidgetEvent::pointer_down(
6353            Point::new(50.0, 25.0),
6354            PointerButton::Secondary,
6355            Modifiers::NONE,
6356        ));
6357
6358        assert_eq!(
6359            outer_called.get(),
6360            1,
6361            "inner returning None must fall through to the outer factory"
6362        );
6363    }
6364
6365    #[test]
6366    fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6367        use crate::event::{Modifiers, PointerButton};
6368
6369        // Single factory returning None → no overlay shown, no panic.
6370        let mut tree = WidgetTree::new();
6371        tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6372        tree.layout(SizeProposal::exact(200.0, 100.0));
6373
6374        let overlay_count_before = tree.overlay_manager.len();
6375        tree.dispatch_event(WidgetEvent::pointer_down(
6376            Point::new(50.0, 25.0),
6377            PointerButton::Secondary,
6378            Modifiers::NONE,
6379        ));
6380        let overlay_count_after = tree.overlay_manager.len();
6381        assert_eq!(
6382            overlay_count_before, overlay_count_after,
6383            "a factory returning None must not mount any overlay"
6384        );
6385    }
6386
6387    // ---- Reconcile-on-rebuild (`preserves_children_on_rebuild`) ----------
6388    //
6389    // These pin the contract that the preserve path RECONCILES: it keeps the
6390    // children a rebuild re-attaches (and any subtree re-parented into the new
6391    // tree) while reaping the ones it drops — so memoizing widgets are both
6392    // stateful and leak-free. Regression guard for the orphan-leak the old
6393    // "preserve = destroy nothing" behaviour caused.
6394
6395    /// `build()` mints a fresh child every time and returns only it, abandoning
6396    /// the previous one. Used to prove dropped children are reaped, not leaked.
6397    #[derive(Debug)]
6398    struct FreshChildHost {
6399        preserve: bool,
6400    }
6401    impl Widget for FreshChildHost {
6402        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6403            vec![ctx.add(FillWidget::new())]
6404        }
6405        fn layout_response(
6406            &self,
6407            p: SizeProposal,
6408            _c: &LayoutContext,
6409        ) -> crate::widget::LayoutResponse {
6410            p.resolve(10.0, 10.0).into()
6411        }
6412        fn preserves_children_on_rebuild(&self) -> bool {
6413            self.preserve
6414        }
6415    }
6416
6417    #[test]
6418    fn reconcile_reaps_dropped_children_no_leak() {
6419        // preserve=false (destroy-all) and preserve=true (reconcile) must BOTH
6420        // keep the arena bounded when a rebuild drops its old child. Before the
6421        // reconcile fix, preserve=true grew the arena (and the active set) by
6422        // one stranded orphan per rebuild.
6423        for preserve in [false, true] {
6424            let mut tree = WidgetTree::new();
6425            let host = tree.add(FreshChildHost { preserve });
6426            tree.layout(SizeProposal::exact(100.0, 100.0));
6427            let total0 = tree.arena.len();
6428            let active0 = tree.active_widget_count();
6429            for _ in 0..5 {
6430                tree.arena_mark_needs_rebuild_for_testing(host);
6431                tree.layout(SizeProposal::exact(100.0, 100.0));
6432            }
6433            assert_eq!(
6434                tree.arena.len(),
6435                total0,
6436                "preserve={preserve}: dropped children must be reaped, not leaked"
6437            );
6438            assert_eq!(
6439                tree.active_widget_count(),
6440                active0,
6441                "preserve={preserve}: no stranded still-active orphans"
6442            );
6443        }
6444    }
6445
6446    /// `build()` mints one **detached** node every time — the shape of every
6447    /// pre-built popup in the widget crate (a dropdown, a calendar, a
6448    /// tooltip's cascade children): parked dormant, shown later through an
6449    /// overlay, and deliberately not a child, since activation and paint both
6450    /// descend through `children`.
6451    #[derive(Debug)]
6452    struct DetachedContentHost {
6453        preserve: bool,
6454    }
6455    impl Widget for DetachedContentHost {
6456        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6457            let popup = ctx.add_detached(FillWidget::new());
6458            ctx.set_dormant(popup);
6459            vec![ctx.add(FillWidget::new())]
6460        }
6461        fn layout_response(
6462            &self,
6463            p: SizeProposal,
6464            _c: &LayoutContext,
6465        ) -> crate::widget::LayoutResponse {
6466            p.resolve(10.0, 10.0).into()
6467        }
6468        fn preserves_children_on_rebuild(&self) -> bool {
6469            self.preserve
6470        }
6471    }
6472
6473    #[test]
6474    fn rebuilding_reaps_detached_content_no_leak() {
6475        // A parentless node is reachable from no walk at all — not the child
6476        // teardown, not the accessibility tree, not `active_widget_count`. Held
6477        // by a bare `ctx.add` it simply accumulated: one stranded popup per
6478        // rebuild, for the lifetime of the process. `add_detached` records the
6479        // ownership edge that makes it reapable.
6480        for preserve in [false, true] {
6481            let mut tree = WidgetTree::new();
6482            let host = tree.add(DetachedContentHost { preserve });
6483            tree.layout(SizeProposal::exact(100.0, 100.0));
6484            let total0 = tree.arena.len();
6485            for _ in 0..5 {
6486                tree.arena_mark_needs_rebuild_for_testing(host);
6487                tree.layout(SizeProposal::exact(100.0, 100.0));
6488            }
6489            assert_eq!(
6490                tree.arena.len(),
6491                total0,
6492                "preserve={preserve}: the previous build's detached content must be reaped"
6493            );
6494        }
6495    }
6496
6497    #[test]
6498    fn destroying_a_host_reaps_its_detached_content() {
6499        let mut tree = WidgetTree::new();
6500        let outer = tree.add(FillWidget::new());
6501        tree.layout(SizeProposal::exact(100.0, 100.0));
6502        let empty = tree.arena.len();
6503
6504        let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6505        tree.layout(SizeProposal::exact(100.0, 100.0));
6506        assert!(tree.arena.len() > empty);
6507
6508        tree.destroy_subtree(host);
6509        assert_eq!(
6510            tree.arena.len(),
6511            empty,
6512            "the popup must die with the widget that built it"
6513        );
6514    }
6515
6516    /// Memoizes one child and re-attaches the same id every build.
6517    #[derive(Debug)]
6518    struct StableChildHost {
6519        child: Option<WidgetId>,
6520        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6521    }
6522    impl Widget for StableChildHost {
6523        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6524            let id = match self.child {
6525                Some(id) => id,
6526                None => {
6527                    let id = ctx.add(FillWidget::new());
6528                    self.child = Some(id);
6529                    self.probe.set(Some(id));
6530                    id
6531                }
6532            };
6533            vec![id]
6534        }
6535        fn layout_response(
6536            &self,
6537            p: SizeProposal,
6538            _c: &LayoutContext,
6539        ) -> crate::widget::LayoutResponse {
6540            p.resolve(10.0, 10.0).into()
6541        }
6542        fn preserves_children_on_rebuild(&self) -> bool {
6543            true
6544        }
6545    }
6546
6547    #[test]
6548    fn reconcile_preserves_reattached_child() {
6549        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6550        let mut tree = WidgetTree::new();
6551        let host = tree.add(StableChildHost {
6552            child: None,
6553            probe: probe.clone(),
6554        });
6555        tree.layout(SizeProposal::exact(100.0, 100.0));
6556        let child = probe.get().expect("child mounted");
6557        let total0 = tree.arena.len();
6558        for _ in 0..5 {
6559            tree.arena_mark_needs_rebuild_for_testing(host);
6560            tree.layout(SizeProposal::exact(100.0, 100.0));
6561        }
6562        assert!(
6563            tree.arena.is_active(child),
6564            "the re-attached child must survive every rebuild"
6565        );
6566        assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6567    }
6568
6569    /// Re-homes a node returned from its `build()` under itself.
6570    #[derive(Debug)]
6571    struct Wrapper {
6572        child: WidgetId,
6573    }
6574    impl Widget for Wrapper {
6575        fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6576            vec![self.child]
6577        }
6578        fn layout_response(
6579            &self,
6580            p: SizeProposal,
6581            _c: &LayoutContext,
6582        ) -> crate::widget::LayoutResponse {
6583            p.resolve(10.0, 10.0).into()
6584        }
6585    }
6586
6587    /// Memoizes a body, then wraps it in a FRESH `Wrapper` each build —
6588    /// re-parenting the body out of the previous (now dropped) wrapper. This is
6589    /// the TabWidget / CompositeTooltip pattern in miniature.
6590    #[derive(Debug)]
6591    struct ReparentHost {
6592        body: Option<WidgetId>,
6593        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6594    }
6595    impl Widget for ReparentHost {
6596        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6597            let body = match self.body {
6598                Some(id) => id,
6599                None => {
6600                    let id = ctx.add(FillWidget::new());
6601                    self.body = Some(id);
6602                    self.probe.set(Some(id));
6603                    id
6604                }
6605            };
6606            vec![ctx.add(Wrapper { child: body })]
6607        }
6608        fn layout_response(
6609            &self,
6610            p: SizeProposal,
6611            _c: &LayoutContext,
6612        ) -> crate::widget::LayoutResponse {
6613            p.resolve(10.0, 10.0).into()
6614        }
6615        fn preserves_children_on_rebuild(&self) -> bool {
6616            true
6617        }
6618    }
6619
6620    #[test]
6621    fn reconcile_spares_reparented_survivor() {
6622        // The memoized body is re-parented into a fresh wrapper each rebuild;
6623        // the old wrapper is dropped. The body must survive (it is re-homed),
6624        // and the old wrappers must be reaped (no leak). This is the exact
6625        // failure that destroyed TabWidget's static panel before the fix: the
6626        // parent-authoritative recursion + single-node arena removal spare the
6627        // re-homed body while still reaping the dropped wrapper subtree.
6628        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6629        let mut tree = WidgetTree::new();
6630        let host = tree.add(ReparentHost {
6631            body: None,
6632            probe: probe.clone(),
6633        });
6634        tree.layout(SizeProposal::exact(100.0, 100.0));
6635        let body = probe.get().expect("body mounted");
6636        let total0 = tree.arena.len();
6637        for _ in 0..5 {
6638            tree.arena_mark_needs_rebuild_for_testing(host);
6639            tree.layout(SizeProposal::exact(100.0, 100.0));
6640        }
6641        assert!(
6642            tree.arena.is_active(body),
6643            "the re-parented body must survive — it was moved into the new tree, \
6644             not swept with the dropped wrapper"
6645        );
6646        assert_eq!(
6647            tree.arena.len(),
6648            total0,
6649            "dropped wrappers reaped — no per-rebuild leak"
6650        );
6651    }
6652
6653    // -----------------------------------------------------------------
6654    // EventContext::ensure_visible / ensure_widget_visible — the
6655    // rect/id-based outer-scroll chase drained in `collect_from_ctx`.
6656    // -----------------------------------------------------------------
6657
6658    /// A `clips_children` container that places its single child at a fixed
6659    /// vertical offset — used to give a child arena bounds *outside* the
6660    /// container's viewport so the id-based `ensure_widget_visible` walk has a
6661    /// reason to dispatch `ScrollIntoView`.
6662    #[derive(Debug)]
6663    struct BelowContainer {
6664        child: Option<WidgetId>,
6665        offset: f32,
6666    }
6667
6668    impl crate::widget::Widget for BelowContainer {
6669        fn layout_response(
6670            &self,
6671            proposal: SizeProposal,
6672            _ctx: &crate::widget::LayoutContext,
6673        ) -> crate::widget::LayoutResponse {
6674            proposal.resolve(0.0, 0.0).into()
6675        }
6676        fn place_children(
6677            &self,
6678            bounds: Rect,
6679            _proposal: SizeProposal,
6680            children: &mut [crate::widget::WidgetPlacement],
6681            _ctx: &crate::widget::LayoutContext,
6682        ) {
6683            for c in children.iter_mut() {
6684                c.origin = Point::new(bounds.x, bounds.y + self.offset);
6685                c.size = bounds.size();
6686            }
6687        }
6688        fn children(&self) -> Vec<WidgetId> {
6689            self.child.into_iter().collect()
6690        }
6691    }
6692
6693    /// A `clips_children` container that records the `ScrollIntoView` it
6694    /// receives, so a test can assert what the framework dispatched to it.
6695    fn recording_scroll_container(
6696        tree: &mut WidgetTree,
6697        child: WidgetId,
6698        recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6699    ) -> WidgetId {
6700        use crate::test_widgets::StackWidget;
6701        tree.add(
6702            StackWidget::new()
6703                .child(child)
6704                .on_scroll(move |ev, _ctx| match ev {
6705                    WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6706                        recorded.set(Some(*target_bounds));
6707                        EventResponse::Handled
6708                    }
6709                    _ => EventResponse::Ignored,
6710                })
6711                .clips_children(true),
6712        )
6713    }
6714
6715    #[test]
6716    fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6717        use std::cell::Cell;
6718        use std::rc::Rc;
6719        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6720        let mut tree = WidgetTree::new();
6721        let actor = tree.add(FillWidget::new());
6722        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6723        tree.layout(SizeProposal::exact(100.0, 100.0));
6724
6725        // A rect well below the 100px viewport — the container must be asked to
6726        // reveal it.
6727        let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6728        let mut ctx = EventContext::new();
6729        ctx.ensure_visible(target);
6730        tree.collect_from_ctx(ctx, actor);
6731
6732        assert_eq!(
6733            recorded.get(),
6734            Some(target),
6735            "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6736             to the clips_children ancestor"
6737        );
6738    }
6739
6740    #[test]
6741    fn ensure_visible_is_noop_when_rect_already_visible() {
6742        use std::cell::Cell;
6743        use std::rc::Rc;
6744        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6745        let mut tree = WidgetTree::new();
6746        let actor = tree.add(FillWidget::new());
6747        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6748        tree.layout(SizeProposal::exact(100.0, 100.0));
6749
6750        // Fully inside the viewport → the ancestor already shows it, so no
6751        // ScrollIntoView is dispatched.
6752        let mut ctx = EventContext::new();
6753        ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6754        tree.collect_from_ctx(ctx, actor);
6755
6756        assert_eq!(
6757            recorded.get(),
6758            None,
6759            "a rect already inside the viewport must not trigger a scroll"
6760        );
6761    }
6762
6763    #[test]
6764    fn ensure_visible_margin_forces_scroll_near_edge() {
6765        use std::cell::Cell;
6766        use std::rc::Rc;
6767        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6768        let mut tree = WidgetTree::new();
6769        let actor = tree.add(FillWidget::new());
6770        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6771        tree.layout(SizeProposal::exact(100.0, 100.0));
6772
6773        // Rect at y=95..99 is visible at margin 0, but with a 10px margin its
6774        // padded bottom (109) spills past the 100px viewport → scroll.
6775        let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6776        let mut ctx = EventContext::new();
6777        ctx.ensure_visible_with_margin(rect, 10.0);
6778        tree.collect_from_ctx(ctx, actor);
6779
6780        assert_eq!(
6781            recorded.get(),
6782            Some(rect),
6783            "the margin must widen the visibility test so a near-edge rect scrolls"
6784        );
6785    }
6786
6787    /// A `clips_children` container that records the alignment and motion of the
6788    /// `ScrollIntoView` it receives.
6789    fn recording_align_container(
6790        tree: &mut WidgetTree,
6791        child: WidgetId,
6792        recorded: std::rc::Rc<
6793            std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6794        >,
6795    ) -> WidgetId {
6796        use crate::test_widgets::StackWidget;
6797        tree.add(
6798            StackWidget::new()
6799                .child(child)
6800                .on_scroll(move |ev, _ctx| match ev {
6801                    WidgetEvent::ScrollIntoView { align, motion, .. } => {
6802                        recorded.set(Some((*align, *motion)));
6803                        EventResponse::Handled
6804                    }
6805                    _ => EventResponse::Ignored,
6806                })
6807                .clips_children(true),
6808        )
6809    }
6810
6811    #[test]
6812    fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6813        use std::cell::Cell;
6814        use std::rc::Rc;
6815        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6816        let mut tree = WidgetTree::new();
6817        let actor = tree.add(FillWidget::new());
6818        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6819        tree.layout(SizeProposal::exact(100.0, 100.0));
6820
6821        // Comfortably inside the viewport — a *minimal* reveal would decline
6822        // (see `ensure_visible_is_noop_when_rect_already_visible`). A pin must
6823        // still fire: re-asserting unconditionally is the whole difference
6824        // between "keep it on screen" and "hold it at this height".
6825        let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6826        let mut ctx = EventContext::new();
6827        ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6828        tree.collect_from_ctx(ctx, actor);
6829
6830        assert_eq!(
6831            recorded.get(),
6832            Some(target),
6833            "an aligned reveal must dispatch even when the rect is already visible"
6834        );
6835    }
6836
6837    #[test]
6838    fn ensure_visible_aligned_forwards_fraction_and_motion() {
6839        use std::cell::Cell;
6840        use std::rc::Rc;
6841        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6842            Rc::new(Cell::new(None));
6843        let mut tree = WidgetTree::new();
6844        let actor = tree.add(FillWidget::new());
6845        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6846        tree.layout(SizeProposal::exact(100.0, 100.0));
6847
6848        let mut ctx = EventContext::new();
6849        ctx.ensure_visible_aligned(
6850            Rect::new(10.0, 10.0, 20.0, 15.0),
6851            0.25,
6852            crate::event::ScrollMotion::Smooth,
6853        );
6854        tree.collect_from_ctx(ctx, actor);
6855
6856        assert_eq!(
6857            recorded.get(),
6858            Some((
6859                crate::event::ScrollAlign::Fraction(0.25),
6860                crate::event::ScrollMotion::Smooth
6861            )),
6862            "the container must receive the requested fraction and motion verbatim"
6863        );
6864    }
6865
6866    #[test]
6867    fn ensure_visible_aligned_clamps_the_fraction() {
6868        use std::cell::Cell;
6869        use std::rc::Rc;
6870        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6871            Rc::new(Cell::new(None));
6872        let mut tree = WidgetTree::new();
6873        let actor = tree.add(FillWidget::new());
6874        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6875        tree.layout(SizeProposal::exact(100.0, 100.0));
6876
6877        let mut ctx = EventContext::new();
6878        ctx.ensure_visible_aligned(
6879            Rect::new(10.0, 10.0, 20.0, 15.0),
6880            4.2,
6881            crate::event::ScrollMotion::Instant,
6882        );
6883        tree.collect_from_ctx(ctx, actor);
6884
6885        assert_eq!(
6886            recorded.get().map(|(a, _)| a),
6887            Some(crate::event::ScrollAlign::Fraction(1.0)),
6888            "an out-of-range fraction must clamp rather than aim the pin off-screen"
6889        );
6890    }
6891
6892    #[test]
6893    fn plain_ensure_visible_requests_minimal_alignment() {
6894        use std::cell::Cell;
6895        use std::rc::Rc;
6896        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6897            Rc::new(Cell::new(None));
6898        let mut tree = WidgetTree::new();
6899        let actor = tree.add(FillWidget::new());
6900        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6901        tree.layout(SizeProposal::exact(100.0, 100.0));
6902
6903        let mut ctx = EventContext::new();
6904        ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6905        tree.collect_from_ctx(ctx, actor);
6906
6907        assert_eq!(
6908            recorded.get(),
6909            Some((
6910                crate::event::ScrollAlign::Minimal,
6911                crate::event::ScrollMotion::Instant
6912            )),
6913            "the pre-existing reveal API must keep its exact semantics"
6914        );
6915    }
6916
6917    #[test]
6918    fn only_the_innermost_container_aligns() {
6919        use std::cell::Cell;
6920        use std::rc::Rc;
6921        let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6922            Rc::new(Cell::new(None));
6923        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6924            Rc::new(Cell::new(None));
6925
6926        let mut tree = WidgetTree::new();
6927        let actor = tree.add(FillWidget::new());
6928        let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6929        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6930        tree.layout(SizeProposal::exact(100.0, 100.0));
6931
6932        // Off-screen, so the outer container is asked too (a `Minimal` request
6933        // is gated on visibility).
6934        let mut ctx = EventContext::new();
6935        ctx.ensure_visible_aligned(
6936            Rect::new(10.0, 500.0, 20.0, 15.0),
6937            0.5,
6938            crate::event::ScrollMotion::Instant,
6939        );
6940        tree.collect_from_ctx(ctx, actor);
6941
6942        assert_eq!(
6943            inner_rec.get().map(|(a, _)| a),
6944            Some(crate::event::ScrollAlign::Fraction(0.5)),
6945            "the innermost clipping ancestor owns the pin"
6946        );
6947        assert_eq!(
6948            outer_rec.get().map(|(a, _)| a),
6949            Some(crate::event::ScrollAlign::Minimal),
6950            "an outer container must only bring the inner viewport into view — a \
6951             fraction names a height in one viewport, not in every ancestor's"
6952        );
6953    }
6954
6955    #[test]
6956    fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6957        // The shape of every capped text column: the editor inside a `MaxSize`
6958        // (which clips once a cap is set, and has no `on_scroll`) inside the
6959        // page's scroll area. The wrapper cannot act on a `ScrollIntoView`, so
6960        // it must not be the one the pin is spent on. Otherwise the scroll
6961        // area is handed a plain reveal and typewriter scrolling silently
6962        // becomes ordinary caret-following.
6963        use crate::test_widgets::StackWidget;
6964        use std::cell::Cell;
6965        use std::rc::Rc;
6966        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6967            Rc::new(Cell::new(None));
6968
6969        let mut tree = WidgetTree::new();
6970        let actor = tree.add(FillWidget::new());
6971        let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6972        let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6973        tree.layout(SizeProposal::exact(100.0, 100.0));
6974
6975        // Inside the viewport, so only a pin (never a minimal reveal) reaches
6976        // the scroller at all.
6977        let mut ctx = EventContext::new();
6978        ctx.ensure_visible_aligned(
6979            Rect::new(10.0, 10.0, 20.0, 15.0),
6980            0.5,
6981            crate::event::ScrollMotion::Instant,
6982        );
6983        tree.collect_from_ctx(ctx, actor);
6984
6985        assert_eq!(
6986            outer_rec.get().map(|(a, _)| a),
6987            Some(crate::event::ScrollAlign::Fraction(0.5)),
6988            "a clipping wrapper with no scroll handler must pass the pin through \
6989             to the scroll container above it"
6990        );
6991    }
6992
6993    #[test]
6994    fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6995        // What the scroller does with the pin is its own business: a `SceneView`
6996        // already holding the target where asked answers `Ignored`, since it
6997        // moved nothing. That is not an invitation for the next container out
6998        // to align a rectangle inside a viewport it does not own.
6999        use crate::test_widgets::StackWidget;
7000        use std::cell::Cell;
7001        use std::rc::Rc;
7002        let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7003        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7004            Rc::new(Cell::new(None));
7005
7006        let mut tree = WidgetTree::new();
7007        let actor = tree.add(FillWidget::new());
7008        let rec = inner_rec.clone();
7009        let inner = tree.add(
7010            StackWidget::new()
7011                .child(actor)
7012                .on_scroll(move |ev, _ctx| match ev {
7013                    WidgetEvent::ScrollIntoView { align, .. } => {
7014                        rec.set(Some(*align));
7015                        EventResponse::Ignored
7016                    }
7017                    _ => EventResponse::Ignored,
7018                })
7019                .clips_children(true),
7020        );
7021        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7022        tree.layout(SizeProposal::exact(100.0, 100.0));
7023
7024        // Off-screen, so the outer container is asked too.
7025        let mut ctx = EventContext::new();
7026        ctx.ensure_visible_aligned(
7027            Rect::new(10.0, 500.0, 20.0, 15.0),
7028            0.5,
7029            crate::event::ScrollMotion::Instant,
7030        );
7031        tree.collect_from_ctx(ctx, actor);
7032
7033        assert_eq!(
7034            inner_rec.get(),
7035            Some(crate::event::ScrollAlign::Fraction(0.5)),
7036            "the innermost scroller is offered the pin"
7037        );
7038        assert_eq!(
7039            outer_rec.get().map(|(a, _)| a),
7040            Some(crate::event::ScrollAlign::Minimal),
7041            "declining to move must not pass the pin outward"
7042        );
7043    }
7044
7045    #[test]
7046    fn ensure_widget_visible_uses_target_arena_bounds() {
7047        use std::cell::Cell;
7048        use std::rc::Rc;
7049        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7050        let mut tree = WidgetTree::new();
7051        // Target lives 500px below the container's top — off the viewport.
7052        let target = tree.add(FillWidget::new());
7053        let rec = recorded.clone();
7054        let container = tree.add(
7055            BelowContainer {
7056                child: Some(target),
7057                offset: 500.0,
7058            }
7059            .on_scroll(move |ev, _ctx| match ev {
7060                WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7061                    rec.set(Some(*target_bounds));
7062                    EventResponse::Handled
7063                }
7064                _ => EventResponse::Ignored,
7065            })
7066            .clips_children(true),
7067        );
7068        tree.layout(SizeProposal::exact(100.0, 100.0));
7069
7070        let expected = tree.bounds(target);
7071        assert!(
7072            expected.y > 100.0,
7073            "fixture sanity: the target must sit below the viewport (y={})",
7074            expected.y
7075        );
7076
7077        // The source widget is irrelevant for the id-based walk — it starts
7078        // from the *target's* parent — so pass the container itself.
7079        let mut ctx = EventContext::new();
7080        ctx.ensure_widget_visible(target);
7081        tree.collect_from_ctx(ctx, container);
7082
7083        assert_eq!(
7084            recorded.get(),
7085            Some(expected),
7086            "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7087             target's current arena bounds"
7088        );
7089    }
7090
7091    #[test]
7092    fn ensure_widget_visible_ignores_missing_widget() {
7093        // A never-mounted id must neither panic nor dispatch a spurious scroll.
7094        use std::cell::Cell;
7095        use std::rc::Rc;
7096        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7097        let mut tree = WidgetTree::new();
7098        let actor = tree.add(FillWidget::new());
7099        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7100        tree.layout(SizeProposal::exact(100.0, 100.0));
7101
7102        let mut ctx = EventContext::new();
7103        ctx.ensure_widget_visible(WidgetId::default());
7104        tree.collect_from_ctx(ctx, actor); // must not panic
7105
7106        assert_eq!(
7107            recorded.get(),
7108            None,
7109            "an unmounted id must not trigger a scroll"
7110        );
7111    }
7112
7113    #[test]
7114    fn context_menu_inside_a_modal_keeps_the_modal() {
7115        // Regression: right-clicking a widget that lives inside an open modal must
7116        // open its context menu WITHOUT tearing down the modal. `show_context_menu_for`
7117        // used to `dismiss_all()`, which closed the very overlay hosting the editor.
7118        use crate::event::{Modifiers, PointerButton, WidgetEvent};
7119        use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7120        use crate::test_widgets::{FillWidget, StackWidget};
7121
7122        let mut tree = WidgetTree::new();
7123        // A container standing in for the modal's content subtree, with the editor
7124        // (a right-clickable widget) inside it.
7125        let modal_content = tree.add(StackWidget::new());
7126        let _editor = tree.add_child(
7127            modal_content,
7128            FillWidget::new()
7129                .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7130        );
7131        tree.layout(SizeProposal::exact(200.0, 100.0));
7132
7133        let modal = tree.overlay_manager.show(OverlayRequest::new(
7134            modal_content,
7135            modal_content,
7136            OverlayPlacement::Centered,
7137            DismissBehavior::EscapeKey,
7138        ));
7139        // Give the overlay real bounds so the right-click hit-tests inside it.
7140        tree.overlay_manager
7141            .stack
7142            .iter_mut()
7143            .find(|o| o.id == modal)
7144            .unwrap()
7145            .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7146        assert_eq!(tree.overlay_manager.len(), 1);
7147
7148        // Right-click the editor inside the modal.
7149        tree.dispatch_event(WidgetEvent::pointer_down(
7150            Point::new(50.0, 25.0),
7151            PointerButton::Secondary,
7152            Modifiers::NONE,
7153        ));
7154
7155        assert!(
7156            tree.overlay_manager.active_ids().contains(&modal),
7157            "the modal must survive opening a context menu inside it"
7158        );
7159        assert_eq!(
7160            tree.overlay_manager.len(),
7161            2,
7162            "the context menu should now be open on top of the surviving modal"
7163        );
7164    }
7165}
7166
7167/// The stage-1 input ingress: the two sample doors, the scroll routing rule,
7168/// and the guarantee that a mouse still behaves exactly as it did.
7169#[cfg(test)]
7170mod input_ingress_tests {
7171    use super::*;
7172    use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7173    use crate::pointer::{
7174        EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7175        ScrollSource,
7176    };
7177    use crate::test_widgets::FillWidget;
7178    use crate::widget::{LayoutContext, WidgetPlacement};
7179    use crate::widget_builder::WidgetBuilder;
7180    use std::cell::RefCell;
7181    use std::rc::Rc;
7182
7183    /// A container that lays children out side by side across its bounds, so a
7184    /// hit test at a given x picks a specific child.
7185    ///
7186    /// Local rather than shared: the common `StackWidget` deliberately stacks
7187    /// its children at one origin, which is the opposite of what a routing test
7188    /// needs.
7189    #[derive(Debug)]
7190    struct RowWidget {
7191        children: Vec<WidgetId>,
7192    }
7193
7194    impl crate::widget::Widget for RowWidget {
7195        fn layout_response(
7196            &self,
7197            proposal: SizeProposal,
7198            _ctx: &LayoutContext,
7199        ) -> crate::widget::LayoutResponse {
7200            proposal.resolve(0.0, 0.0).into()
7201        }
7202
7203        fn place_children(
7204            &self,
7205            bounds: Rect,
7206            _proposal: SizeProposal,
7207            children: &mut [WidgetPlacement],
7208            _ctx: &LayoutContext,
7209        ) {
7210            let n = children.len().max(1) as f32;
7211            let w = bounds.width / n;
7212            for (i, child) in children.iter_mut().enumerate() {
7213                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7214                child.size = teksilo_canvas::Size::new(w, bounds.height);
7215            }
7216        }
7217
7218        fn children(&self) -> Vec<WidgetId> {
7219            self.children.clone()
7220        }
7221    }
7222
7223    /// Everything a widget observes of a pointer interaction, as text, so two
7224    /// runs can be compared for exact equality rather than field by field.
7225    fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7226        let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7227
7228        let mut tree = WidgetTree::new();
7229        let pointer_log = log.clone();
7230        let hover_log = log.clone();
7231        tree.add(
7232            FillWidget::new()
7233                .on_pointer_event(move |event, _ctx| {
7234                    pointer_log.borrow_mut().push(format!("{event:?}"));
7235                    EventResponse::Ignored
7236                })
7237                .on_hover(move |entered, _ctx| {
7238                    hover_log.borrow_mut().push(format!("hover({entered})"));
7239                }),
7240        );
7241        tree.layout(SizeProposal::exact(100.0, 100.0));
7242
7243        drive(&mut tree);
7244        log.borrow().clone()
7245    }
7246
7247    /// The load-bearing compatibility claim of this package: lowering a mouse
7248    /// `PointerSample` produces the *same* `WidgetEvent` stream, in the same
7249    /// order, as writing the events out by hand. If this ever diverges, the
7250    /// door has started meaning something different from the events it lowers
7251    /// to.
7252    #[test]
7253    fn a_mouse_sample_reproduces_todays_event_stream() {
7254        let inside = Point::new(40.0, 40.0);
7255        let moved = Point::new(60.0, 55.0);
7256        let outside = Point::new(400.0, 400.0);
7257
7258        let legacy = record(|tree| {
7259            tree.dispatch_event(WidgetEvent::pointer_move(inside));
7260            tree.dispatch_event(WidgetEvent::pointer_down(
7261                inside,
7262                PointerButton::Primary,
7263                Modifiers::NONE,
7264            ));
7265            tree.dispatch_event(WidgetEvent::pointer_move(moved));
7266            tree.dispatch_event(WidgetEvent::pointer_up(
7267                moved,
7268                PointerButton::Primary,
7269                Modifiers::NONE,
7270            ));
7271            tree.dispatch_event(WidgetEvent::pointer_move(outside));
7272        });
7273
7274        let sampled = record(|tree| {
7275            let t = EventTime::ZERO;
7276            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7277            tree.dispatch_pointer(
7278                PointerSample::mouse(PointerPhase::Down, inside, t)
7279                    .with_button(PointerButton::Primary),
7280            );
7281            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7282            tree.dispatch_pointer(
7283                PointerSample::mouse(PointerPhase::Up, moved, t)
7284                    .with_button(PointerButton::Primary),
7285            );
7286            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7287        });
7288
7289        assert_eq!(legacy, sampled);
7290        assert!(
7291            legacy.contains(&"hover(true)".to_string())
7292                && legacy.contains(&"hover(false)".to_string()),
7293            "the fixture must actually exercise enter and leave: {legacy:?}"
7294        );
7295    }
7296
7297    /// A press with no button — what a bare direct-pointer contact reports —
7298    /// still reads as the primary press, because that is what a tap has always
7299    /// been.
7300    #[test]
7301    fn a_buttonless_press_lowers_to_primary() {
7302        let events = record(|tree| {
7303            tree.dispatch_pointer(PointerSample::mouse(
7304                PointerPhase::Down,
7305                Point::new(20.0, 20.0),
7306                EventTime::ZERO,
7307            ));
7308        });
7309        assert!(
7310            events.iter().any(|e| e.contains("button: Primary")),
7311            "{events:?}"
7312        );
7313    }
7314
7315    /// A mouse press and release delivered as samples, as a test writes them
7316    /// by hand with timestamps of its own, form a tap: the sample door reaches
7317    /// the tap recognizer, not only raw `on_pointer_event` handlers. A port
7318    /// review reported the opposite for 0.16.0 and it did not reproduce; this
7319    /// pins it, with a stamp unrelated to the tree's clock on purpose.
7320    #[test]
7321    fn a_sampled_press_and_release_form_a_tap() {
7322        let taps = Rc::new(std::cell::Cell::new(0));
7323        let seen = taps.clone();
7324        let mut tree = WidgetTree::new();
7325        tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7326        tree.layout(SizeProposal::exact(100.0, 100.0));
7327
7328        let at = Point::new(50.0, 50.0);
7329        tree.dispatch_pointer(PointerSample::mouse(
7330            PointerPhase::Down,
7331            at,
7332            EventTime::from_millis(0),
7333        ));
7334        tree.dispatch_pointer(PointerSample::mouse(
7335            PointerPhase::Up,
7336            at,
7337            EventTime::from_millis(5),
7338        ));
7339
7340        assert_eq!(taps.get(), 1);
7341    }
7342
7343    // --- scroll routing --------------------------------------------------
7344
7345    /// Two leaves side by side across a 200-wide tree: `top` owns x < 100,
7346    /// `bottom` owns x >= 100.
7347    fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7348        let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7349        let mut tree = WidgetTree::new();
7350
7351        let leading_hits = hits.clone();
7352        let trailing_hits = hits.clone();
7353        let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7354            leading_hits.borrow_mut().push("leading");
7355            EventResponse::Handled
7356        }));
7357        let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7358            trailing_hits.borrow_mut().push("trailing");
7359            EventResponse::Handled
7360        }));
7361        tree.add(RowWidget {
7362            children: vec![leading, trailing],
7363        });
7364        tree.layout(SizeProposal::exact(200.0, 100.0));
7365        (tree, hits)
7366    }
7367
7368    fn notch() -> ScrollDelta {
7369        ScrollDelta::Lines { x: 0.0, y: -1.0 }
7370    }
7371
7372    /// The mouse path: a wheel notch carries no position, so it routes by
7373    /// hover exactly as it always has. This is the no-op claim.
7374    #[test]
7375    fn a_positionless_scroll_routes_by_hover() {
7376        let (mut tree, hits) = scroll_fixture();
7377
7378        tree.pointer_move(Point::new(150.0, 50.0)); // hover the trailing leaf
7379        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7380        assert_eq!(*hits.borrow(), vec!["trailing"]);
7381
7382        tree.pointer_move(Point::new(50.0, 50.0)); // hover the leading leaf
7383        tree.dispatch_scroll(ScrollSample::wheel(
7384            notch(),
7385            Modifiers::NONE,
7386            EventTime::ZERO,
7387        ));
7388        assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7389    }
7390
7391    /// A positioned scroll routes by hit test, *against* the hover. This is
7392    /// the only thing that makes a synthesised touch pan routable at all: a
7393    /// contact never writes hover, so hover would send the pan to whatever the
7394    /// mouse last touched — or nowhere.
7395    #[test]
7396    fn a_positioned_scroll_routes_by_hit_test() {
7397        let (mut tree, hits) = scroll_fixture();
7398        tree.pointer_move(Point::new(150.0, 50.0)); // hover the TRAILING leaf
7399
7400        tree.dispatch_event(WidgetEvent::scroll_at(
7401            notch(),
7402            Modifiers::NONE,
7403            Point::new(50.0, 50.0), // …but scroll over the LEADING one
7404        ));
7405        assert_eq!(*hits.borrow(), vec!["leading"]);
7406
7407        tree.dispatch_scroll(
7408            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7409                .at(Point::new(150.0, 50.0)),
7410        );
7411        assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7412    }
7413
7414    /// With nothing hovered and nothing focused a positionless scroll goes
7415    /// nowhere — the pre-existing behaviour, pinned rather than left
7416    /// incidental.
7417    #[test]
7418    fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7419        let (mut tree, hits) = scroll_fixture();
7420        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7421        assert!(hits.borrow().is_empty());
7422    }
7423
7424    // --- the per-dispatch snapshot ---------------------------------------
7425
7426    /// A handler can ask which pointer it is serving, and what phase and
7427    /// source a scroll had.
7428    #[test]
7429    fn a_handler_sees_the_sample_it_is_serving() {
7430        let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7431            Rc::new(RefCell::new(Vec::new()));
7432        let sink = seen.clone();
7433
7434        let mut tree = WidgetTree::new();
7435        // A `ScrollSource::TouchPan` sample is delivered along the pan
7436        // claimants and nowhere else (see `widget_tree::pan_arbiter`), so the
7437        // fixture has to be a pan surface for the sample below to reach it at
7438        // all. Nothing about what this test *asserts* changes — only that the
7439        // widget it asserts against is now the kind of widget a touch pan is
7440        // addressed to.
7441        tree.add(
7442            FillWidget::new()
7443                .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7444                .on_scroll(move |_event, ctx| {
7445                    sink.borrow_mut().push((
7446                        ctx.scroll_phase(),
7447                        ctx.scroll_source(),
7448                        ctx.pointer_position(),
7449                    ));
7450                    EventResponse::Handled
7451                }),
7452        );
7453        tree.layout(SizeProposal::exact(100.0, 100.0));
7454
7455        tree.dispatch_scroll(ScrollSample {
7456            delta: notch(),
7457            position: Some(Point::new(50.0, 50.0)),
7458            phase: ScrollPhase::Momentum,
7459            source: ScrollSource::TouchPan,
7460            pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7461            modifiers: Modifiers::NONE,
7462        });
7463
7464        assert_eq!(
7465            *seen.borrow(),
7466            vec![(
7467                ScrollPhase::Momentum,
7468                ScrollSource::TouchPan,
7469                Some(Point::new(50.0, 50.0))
7470            )]
7471        );
7472    }
7473
7474    /// Outside a pointer dispatch a handler sees the default mouse — the same
7475    /// answer every such handler got before pointers were distinguishable.
7476    #[test]
7477    fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7478        let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7479        let sink = seen.clone();
7480
7481        let mut tree = WidgetTree::new();
7482        tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7483            *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7484            EventResponse::Ignored
7485        }));
7486        tree.layout(SizeProposal::exact(100.0, 100.0));
7487        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7488
7489        assert_eq!(
7490            *seen.borrow(),
7491            Some((PointerId::MOUSE, ScrollPhase::Discrete))
7492        );
7493    }
7494
7495    /// The snapshot is saved and restored around a dispatch, so a nested one
7496    /// (a synthetic click, a scroll-into-view walk) does not strand the outer
7497    /// sample's view of the world.
7498    #[test]
7499    fn the_snapshot_is_restored_after_a_dispatch() {
7500        let mut tree = WidgetTree::new();
7501        tree.add(FillWidget::new());
7502        tree.layout(SizeProposal::exact(100.0, 100.0));
7503
7504        let before = tree.current_input.clone();
7505        tree.dispatch_scroll(
7506            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7507                .at(Point::new(10.0, 10.0)),
7508        );
7509        assert_eq!(tree.current_input, before);
7510    }
7511}
7512
7513#[cfg(test)]
7514mod press_and_focus_tests {
7515    //! The framework press, focus-on-release for direct pointers, and the one
7516    //! `focus_visible` signal — driven through the real ingress doors against
7517    //! real trees.
7518
7519    use std::cell::{Cell, RefCell};
7520    use std::rc::Rc;
7521    use teksilo_canvas::{Point, SizeProposal};
7522
7523    use crate::WidgetId;
7524    use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7525    use crate::focus::FocusOrigin;
7526    use crate::pointer::clock::ManualClock;
7527    use crate::pointer::touch_action::PanClaim;
7528    use crate::pointer::{
7529        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7530        PointerSample,
7531    };
7532    use crate::test_widgets::{FillWidget, StackWidget};
7533    use crate::widget_builder::WidgetBuilder;
7534    use crate::widget_tree::WidgetTree;
7535
7536    // -----------------------------------------------------------------
7537    // Fixtures
7538    // -----------------------------------------------------------------
7539
7540    /// A fresh contact identity, minted through the real allocator.
7541    fn contact_id() -> PointerId {
7542        use std::sync::atomic::{AtomicU64, Ordering};
7543        static NEXT: AtomicU64 = AtomicU64::new(1);
7544        PointerIdAllocator::global().begin(
7545            BackendDeviceKey::new(0x0B11),
7546            NEXT.fetch_add(1, Ordering::Relaxed),
7547        )
7548    }
7549
7550    fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7551        PointerSample {
7552            pointer: PointerInfo::touch(id, t),
7553            phase,
7554            position: at,
7555            button: None,
7556            modifiers: Modifiers::NONE,
7557            coalesced: Vec::new(),
7558        }
7559    }
7560
7561    /// A tree on a clock the test drives, so every deadline in these tests is
7562    /// virtual.
7563    fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7564        let mut tree = WidgetTree::new();
7565        let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7566        tree.set_input_clock(clock.clone());
7567        (tree, clock)
7568    }
7569
7570    /// A tappable leaf with a framework press signal installed on it, plus the
7571    /// signal itself.
7572    ///
7573    /// `on_tap` is what gives the node a gesture arena, which is what makes it
7574    /// the press owner — the same thing a `Button` gets from its own tap
7575    /// handler.
7576    fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7577        let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7578        let signal = tree.pressed_signal(id);
7579        (id, signal)
7580    }
7581
7582    // -----------------------------------------------------------------
7583    // The enum
7584    // -----------------------------------------------------------------
7585
7586    /// `FocusOrigin` now names the device behind a pointer focus, and the two
7587    /// accessors that replaced `== FocusOrigin::Pointer` answer for every arm.
7588    #[test]
7589    fn a_pointer_origin_names_its_device() {
7590        use teksilo_tokens::{PenKind, PointerKind};
7591
7592        assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7593        assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7594        assert!(FocusOrigin::POINTER.is_pointer());
7595        assert!(!FocusOrigin::Keyboard.is_pointer());
7596        assert!(!FocusOrigin::Programmatic.is_pointer());
7597        assert!(!FocusOrigin::Accessibility.is_pointer());
7598
7599        assert_eq!(
7600            FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7601            Some(PointerKind::Touch),
7602        );
7603        assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7604        assert_eq!(
7605            FocusOrigin::POINTER.pointer_kind(),
7606            Some(PointerKind::Unknown),
7607            "a widget deriving its own origin says so rather than naming a device it never saw",
7608        );
7609    }
7610
7611    /// The `:focus-visible` verdict, per origin. `Programmatic` abstains — a
7612    /// scripted focus declares no modality, so the ring stays where the user's
7613    /// last real interaction left it.
7614    #[test]
7615    fn only_a_real_navigation_declares_a_modality() {
7616        use teksilo_tokens::PointerKind;
7617
7618        assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7619        assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7620        assert_eq!(
7621            FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7622            Some(false),
7623        );
7624        assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7625    }
7626
7627    /// The three Tier-3 configs still carry `Signal<Option<FocusOrigin>>`, and
7628    /// a style reading one still gets what it was written against.
7629    #[test]
7630    fn the_tier_three_configs_are_unchanged() {
7631        let origin: crate::signal::Signal<Option<FocusOrigin>> =
7632            crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7633        let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7634        let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7635        let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7636        for field in [slider, splitter, segmented] {
7637            assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7638        }
7639        origin.set(Some(FocusOrigin::POINTER));
7640        assert_ne!(
7641            origin.get(),
7642            Some(FocusOrigin::Keyboard),
7643            "the `== Some(Keyboard)` test every consumer makes still discriminates",
7644        );
7645    }
7646
7647    // -----------------------------------------------------------------
7648    // focus-visible per kind
7649    // -----------------------------------------------------------------
7650
7651    /// Keyboard focus, then a touch tap, leaves no ring.
7652    ///
7653    /// The behaviour predates this package; what is new is that the focus the
7654    /// tap installs lands on the **release**, so this pins that moving the
7655    /// assignment did not leave a keyboard ring standing over a control the
7656    /// finger just took.
7657    #[test]
7658    fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7659        let (mut tree, _clock) = tree_on_a_clock();
7660        let a = tree.add(FillWidget::new().focusable());
7661        let b = tree.add(FillWidget::new().focusable());
7662        let root = tree.add(SideBySide {
7663            children: vec![a, b],
7664        });
7665        tree.layout(SizeProposal::exact(200.0, 100.0));
7666        let _ = root;
7667
7668        let visible = tree.focus_visible_signal();
7669        tree.press_key(Key::Tab, Modifiers::NONE);
7670        assert_eq!(tree.focused(), Some(a));
7671        assert!(visible.get(), "keyboard navigation reveals the ring");
7672
7673        let id = contact_id();
7674        let at = tree.bounds(b).center();
7675        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7676        tree.dispatch_pointer(contact(
7677            id,
7678            PointerPhase::Up,
7679            at,
7680            EventTime::from_millis(30),
7681        ));
7682
7683        assert_eq!(tree.focused(), Some(b), "the release moved focus");
7684        assert!(!visible.get(), "and the ring did not come with it");
7685        assert_eq!(
7686            tree.focus_origin(),
7687            Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7688        );
7689    }
7690
7691    /// An assistive `Action::Focus` reveals the ring. The user is navigating —
7692    /// they are simply not doing it with a key — and this used to route through
7693    /// `Programmatic`, which declares nothing and left a screen-reader user
7694    /// with an invisible focus after any click.
7695    #[test]
7696    fn an_assistive_focus_reveals_the_ring() {
7697        let mut tree = WidgetTree::new();
7698        let a = tree.add(FillWidget::new().focusable());
7699        let b = tree.add(FillWidget::new().focusable());
7700        let root = tree.add(SideBySide {
7701            children: vec![a, b],
7702        });
7703        tree.layout(SizeProposal::exact(200.0, 50.0));
7704        let _ = root;
7705        let visible = tree.focus_visible_signal();
7706
7707        tree.click(a);
7708        assert_eq!(tree.focused(), Some(a));
7709        assert!(!visible.get(), "the click hid it");
7710
7711        tree.dispatch_event(WidgetEvent::AccessAction {
7712            target: Some(b),
7713            action: accesskit::Action::Focus,
7714            target_node: crate::accessibility::widget_id_to_node_id(b),
7715            data: None,
7716        });
7717        assert_eq!(tree.focused(), Some(b));
7718        assert!(visible.get(), "an assistive move is a navigation");
7719        assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7720    }
7721
7722    /// A programmatic focus abstains: it declares no modality, so the ring
7723    /// stays exactly where the last real interaction left it. This is what
7724    /// `Button` / `Checkbox`'s pre-existing focus-ring tests pin, and what
7725    /// browsers do for `element.focus()`.
7726    #[test]
7727    fn a_programmatic_focus_leaves_the_modality_alone() {
7728        let mut tree = WidgetTree::new();
7729        let a = tree.add(FillWidget::new().focusable());
7730        let b = tree.add(FillWidget::new().focusable());
7731        let root = tree.add(SideBySide {
7732            children: vec![a, b],
7733        });
7734        tree.layout(SizeProposal::exact(200.0, 100.0));
7735        let _ = root;
7736        let visible = tree.focus_visible_signal();
7737
7738        tree.press_key(Key::Tab, Modifiers::NONE);
7739        assert_eq!(tree.focused(), Some(a));
7740        assert!(visible.get());
7741        tree.focus(b);
7742        assert!(
7743            visible.get(),
7744            "a scripted focus does not hide a keyboard ring"
7745        );
7746
7747        tree.click(a);
7748        assert!(!visible.get());
7749        tree.focus(b);
7750        assert!(
7751            !visible.get(),
7752            "and does not reveal one after a click either",
7753        );
7754    }
7755
7756    // -----------------------------------------------------------------
7757    // Activation on release
7758    // -----------------------------------------------------------------
7759
7760    /// A mouse focuses on press, exactly as it always has.
7761    #[test]
7762    fn a_mouse_still_focuses_on_press() {
7763        let mut tree = WidgetTree::new();
7764        let w = tree.add(FillWidget::new().focusable());
7765        tree.layout(SizeProposal::exact(100.0, 50.0));
7766        let center = tree.bounds(w).center();
7767
7768        tree.dispatch_event(WidgetEvent::pointer_down(
7769            center,
7770            PointerButton::Primary,
7771            Modifiers::NONE,
7772        ));
7773        assert_eq!(
7774            tree.focused(),
7775            Some(w),
7776            "focus lands on the press for an indirect pointer",
7777        );
7778    }
7779
7780    /// A finger's focus waits for the release.
7781    #[test]
7782    fn a_finger_focuses_on_release() {
7783        let (mut tree, _clock) = tree_on_a_clock();
7784        let w = tree.add(FillWidget::new().focusable());
7785        tree.layout(SizeProposal::exact(100.0, 50.0));
7786        let center = tree.bounds(w).center();
7787
7788        let id = contact_id();
7789        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7790        assert_eq!(
7791            tree.focused(),
7792            None,
7793            "a finger that has only landed has chosen nothing yet",
7794        );
7795
7796        tree.dispatch_pointer(contact(
7797            id,
7798            PointerPhase::Up,
7799            center,
7800            EventTime::from_millis(40),
7801        ));
7802        assert_eq!(tree.focused(), Some(w));
7803        assert_eq!(
7804            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7805            Some(teksilo_tokens::PointerKind::Touch),
7806            "the origin names the device that delivered it",
7807        );
7808    }
7809
7810    /// …and only when the release lands back on the same focusable. A finger
7811    /// that presses one control, slides onto its neighbour and lifts has
7812    /// activated nothing and must move focus nowhere.
7813    #[test]
7814    fn a_release_on_a_different_focusable_moves_no_focus() {
7815        let (mut tree, _clock) = tree_on_a_clock();
7816        let a = tree.add(FillWidget::new().focusable());
7817        let b = tree.add(FillWidget::new().focusable());
7818        let root = tree.add(SideBySide {
7819            children: vec![a, b],
7820        });
7821        tree.layout(SizeProposal::exact(200.0, 50.0));
7822        let _ = root;
7823
7824        let on_a = tree.bounds(a).center();
7825        let on_b = tree.bounds(b).center();
7826        let id = contact_id();
7827        tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7828        tree.dispatch_pointer(contact(
7829            id,
7830            PointerPhase::Move,
7831            on_b,
7832            EventTime::from_millis(20),
7833        ));
7834        tree.dispatch_pointer(contact(
7835            id,
7836            PointerPhase::Up,
7837            on_b,
7838            EventTime::from_millis(40),
7839        ));
7840
7841        assert_eq!(
7842            tree.focused(),
7843            None,
7844            "the guard is `the release landed on the same focusable as the press`",
7845        );
7846    }
7847
7848    // -----------------------------------------------------------------
7849    // The press visual
7850    // -----------------------------------------------------------------
7851
7852    /// A mouse press lights the visual at once and the release clears it —
7853    /// the pre-touch rule, and no press-feedback delay anywhere near it.
7854    #[test]
7855    fn a_mouse_press_visual_is_unchanged() {
7856        let mut tree = WidgetTree::new();
7857        let (w, pressed) = tappable(&mut tree);
7858        tree.layout(SizeProposal::exact(100.0, 50.0));
7859        let center = tree.bounds(w).center();
7860
7861        assert!(!pressed.get(), "not pressed at rest");
7862        tree.dispatch_event(WidgetEvent::pointer_down(
7863            center,
7864            PointerButton::Primary,
7865            Modifiers::NONE,
7866        ));
7867        assert!(pressed.get(), "an indirect pointer lights up on the press");
7868        assert!(
7869            !tree.press_pending(w),
7870            "and never waits: a mouse opens no pan session, so there is no \
7871             ambiguity to wait out",
7872        );
7873        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7874
7875        tree.dispatch_event(WidgetEvent::pointer_up(
7876            center,
7877            PointerButton::Primary,
7878            Modifiers::NONE,
7879        ));
7880        assert!(!pressed.get(), "the release clears it");
7881        assert_eq!(tree.pressed_by(w), None);
7882    }
7883
7884    /// A slide off the target clears the visual, and sliding back on restores
7885    /// it. WCAG 2.2 SC 2.5.2's abort gesture, and reversible right up to the
7886    /// release.
7887    #[test]
7888    fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7889        let (mut tree, _clock) = tree_on_a_clock();
7890        let (w, pressed) = tappable(&mut tree);
7891        tree.layout(SizeProposal::exact(100.0, 200.0));
7892        let inside = tree.bounds(w).center();
7893        let outside = Point::new(inside.x, inside.y + 400.0);
7894
7895        let id = contact_id();
7896        tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7897        assert!(
7898            pressed.get(),
7899            "nothing here claims a pan, so no delay applies"
7900        );
7901
7902        tree.dispatch_pointer(contact(
7903            id,
7904            PointerPhase::Move,
7905            outside,
7906            EventTime::from_millis(20),
7907        ));
7908        assert!(!pressed.get(), "the press has left its target");
7909        assert!(!tree.press_is_inside(w));
7910        assert_eq!(
7911            tree.pressed_by(w),
7912            Some(id),
7913            "the contact still holds the press — it is the *visual* that is off",
7914        );
7915
7916        tree.dispatch_pointer(contact(
7917            id,
7918            PointerPhase::Move,
7919            inside,
7920            EventTime::from_millis(40),
7921        ));
7922        assert!(pressed.get(), "sliding back on restores it");
7923
7924        tree.dispatch_pointer(contact(
7925            id,
7926            PointerPhase::Up,
7927            inside,
7928            EventTime::from_millis(60),
7929        ));
7930        assert!(!pressed.get());
7931    }
7932
7933    /// A second contact cannot clear the first's visual. Its own release
7934    /// removes only the press it owns.
7935    #[test]
7936    fn a_second_contact_cannot_clear_the_first_visual() {
7937        let (mut tree, _clock) = tree_on_a_clock();
7938        let (w, pressed) = tappable(&mut tree);
7939        tree.layout(SizeProposal::exact(100.0, 50.0));
7940        let bounds = tree.bounds(w);
7941        let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7942        let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7943
7944        let first = contact_id();
7945        let second = contact_id();
7946        tree.dispatch_pointer(contact(
7947            first,
7948            PointerPhase::Down,
7949            first_at,
7950            EventTime::ZERO,
7951        ));
7952        assert!(pressed.get());
7953        assert_eq!(tree.pressed_by(w), Some(first));
7954
7955        tree.dispatch_pointer(contact(
7956            second,
7957            PointerPhase::Down,
7958            second_at,
7959            EventTime::from_millis(10),
7960        ));
7961        assert_eq!(
7962            tree.pressed_by(w),
7963            Some(first),
7964            "under `MultiContact::First` the second contact is terminated before \
7965             it reaches the arena at all",
7966        );
7967
7968        tree.dispatch_pointer(contact(
7969            second,
7970            PointerPhase::Up,
7971            second_at,
7972            EventTime::from_millis(20),
7973        ));
7974        assert!(
7975            pressed.get(),
7976            "so the second contact's release cannot clear a visual it never owned",
7977        );
7978        assert_eq!(tree.pressed_by(w), Some(first));
7979
7980        tree.dispatch_pointer(contact(
7981            first,
7982            PointerPhase::Up,
7983            first_at,
7984            EventTime::from_millis(30),
7985        ));
7986        assert!(!pressed.get(), "the owner's release does clear it");
7987    }
7988
7989    /// A cancel clears the visual. The node is never sent an `Up` to clear it
7990    /// from, so nothing else could.
7991    #[test]
7992    fn a_cancel_clears_the_visual() {
7993        let (mut tree, _clock) = tree_on_a_clock();
7994        let (w, pressed) = tappable(&mut tree);
7995        tree.layout(SizeProposal::exact(100.0, 50.0));
7996        let center = tree.bounds(w).center();
7997
7998        let id = contact_id();
7999        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
8000        assert!(pressed.get());
8001
8002        tree.dispatch_pointer(contact(
8003            id,
8004            PointerPhase::Cancel,
8005            center,
8006            EventTime::from_millis(20),
8007        ));
8008        assert!(!pressed.get(), "a press that was taken away is not painted");
8009        assert_eq!(tree.pressed_by(w), None);
8010    }
8011
8012    /// A peer winning the arbitration clears the visual. The pressed control is
8013    /// never told; only the router knows it has lost the press.
8014    #[test]
8015    fn a_pan_claim_clears_the_visual() {
8016        let (mut tree, clock) = tree_on_a_clock();
8017        let (row, pressed) = tappable(&mut tree);
8018        let list = tree.add(
8019            StackWidget::new()
8020                .child(row)
8021                .pan_claim(PanClaim::vertical())
8022                .on_scroll(|_e, _c| EventResponse::Handled),
8023        );
8024        tree.layout(SizeProposal::exact(200.0, 400.0));
8025        let _ = list;
8026        let start = Point::new(100.0, 200.0);
8027
8028        let id = contact_id();
8029        tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8030        // Inside a claimant, so the visual waits.
8031        assert!(tree.press_pending(row));
8032        clock.set(EventTime::from_millis(100));
8033        tree.tick_gestures(std::time::Instant::now());
8034        assert!(pressed.get(), "…and appears once the delay has elapsed");
8035
8036        // Past the touch profile's 36 dp pan slop: the list claims.
8037        tree.dispatch_pointer(contact(
8038            id,
8039            PointerPhase::Move,
8040            Point::new(100.0, 260.0),
8041            EventTime::from_millis(120),
8042        ));
8043        assert!(
8044            !pressed.get(),
8045            "the row lost the press to the list and must stop advertising it",
8046        );
8047        assert_eq!(tree.pressed_by(row), None);
8048    }
8049
8050    // -----------------------------------------------------------------
8051    // Which button may raise the visual
8052    // -----------------------------------------------------------------
8053
8054    /// The visual answers to the same buttons the activation does. A node
8055    /// carrying a plain `on_tap` accepts `PRIMARY` and nothing else, so a
8056    /// middle, back or forward press must light nothing up — the press it
8057    /// would be advertising can never complete.
8058    #[test]
8059    fn a_button_the_control_cannot_act_on_raises_no_visual() {
8060        let taps = Rc::new(Cell::new(0u32));
8061        let count = taps.clone();
8062        let mut tree = WidgetTree::new();
8063        let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8064            count.set(count.get() + 1);
8065        }));
8066        let pressed = tree.pressed_signal(w);
8067        tree.layout(SizeProposal::exact(100.0, 50.0));
8068        let center = tree.bounds(w).center();
8069
8070        for button in [
8071            PointerButton::Middle,
8072            PointerButton::Back,
8073            PointerButton::Forward,
8074        ] {
8075            tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8076            assert!(
8077                !pressed.get(),
8078                "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8079            );
8080            assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8081            tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8082            assert_eq!(
8083                taps.get(),
8084                0,
8085                "the tap recognizer refuses {button:?} too — that is the point",
8086            );
8087        }
8088
8089        // …and the button it does act on is untouched.
8090        tree.dispatch_event(WidgetEvent::pointer_down(
8091            center,
8092            PointerButton::Primary,
8093            Modifiers::NONE,
8094        ));
8095        assert!(pressed.get(), "a primary press lights up as it always has");
8096        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8097        tree.dispatch_event(WidgetEvent::pointer_up(
8098            center,
8099            PointerButton::Primary,
8100            Modifiers::NONE,
8101        ));
8102        assert!(!pressed.get());
8103        assert_eq!(taps.get(), 1);
8104    }
8105
8106    /// The case a real user hits: a right-click on a control with no context
8107    /// menu. The secondary arm finds nothing to open and falls through to the
8108    /// ordinary press path, which must still raise nothing.
8109    #[test]
8110    fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8111        let mut tree = WidgetTree::new();
8112        let (w, pressed) = tappable(&mut tree);
8113        tree.layout(SizeProposal::exact(100.0, 50.0));
8114        let center = tree.bounds(w).center();
8115
8116        tree.dispatch_event(WidgetEvent::pointer_down(
8117            center,
8118            PointerButton::Secondary,
8119            Modifiers::NONE,
8120        ));
8121        assert!(
8122            !pressed.get(),
8123            "nothing opened, and nothing may look pressed either",
8124        );
8125        assert_eq!(tree.pressed_by(w), None);
8126        assert!(!tree.press_is_inside(w));
8127    }
8128
8129    /// A widget that widened its own mask keeps the visual on the buttons it
8130    /// widened to: the gate reads the node's declared acceptance, it does not
8131    /// hardcode `PRIMARY`.
8132    #[test]
8133    fn a_widened_mask_widens_the_visual_with_it() {
8134        use crate::event::ButtonMask;
8135
8136        let mut tree = WidgetTree::new();
8137        let w = tree.add(
8138            FillWidget::new()
8139                .on_tap(|_e, _c| {})
8140                .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8141        );
8142        let pressed = tree.pressed_signal(w);
8143        tree.layout(SizeProposal::exact(100.0, 50.0));
8144        let center = tree.bounds(w).center();
8145
8146        tree.dispatch_event(WidgetEvent::pointer_down(
8147            center,
8148            PointerButton::Middle,
8149            Modifiers::NONE,
8150        ));
8151        assert!(pressed.get(), "this node really does act on a middle-click");
8152
8153        tree.dispatch_event(WidgetEvent::pointer_up(
8154            center,
8155            PointerButton::Middle,
8156            Modifiers::NONE,
8157        ));
8158        assert!(!pressed.get());
8159    }
8160
8161    /// A press that raises no visual still records the focus a direct pointer
8162    /// defers to its release. A stylus barrel button reports `Secondary`, and
8163    /// a pen that presses a control and lifts on it has chosen that control
8164    /// whether or not the button lit it up — the record exists for the focus,
8165    /// not only for the visual.
8166    #[test]
8167    fn a_press_that_raises_no_visual_still_defers_its_focus() {
8168        use teksilo_tokens::{PenKind, PointerKind};
8169
8170        let (mut tree, _clock) = tree_on_a_clock();
8171        let (w, pressed) = tappable(&mut tree);
8172        tree.layout(SizeProposal::exact(100.0, 50.0));
8173        let center = tree.bounds(w).center();
8174
8175        let id = contact_id();
8176        let barrel = |phase, t| PointerSample {
8177            pointer: PointerInfo {
8178                kind: PointerKind::Pen(PenKind::Pen),
8179                ..PointerInfo::touch(id, t)
8180            },
8181            phase,
8182            position: center,
8183            button: Some(PointerButton::Secondary),
8184            modifiers: Modifiers::NONE,
8185            coalesced: Vec::new(),
8186        };
8187
8188        tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8189        assert!(!pressed.get(), "the barrel button activates nothing here");
8190        assert_eq!(tree.pressed_by(w), None);
8191        assert_eq!(
8192            tree.focused(),
8193            None,
8194            "a direct pointer has chosen nothing until it lifts",
8195        );
8196
8197        tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8198        assert_eq!(
8199            tree.focused(),
8200            Some(w),
8201            "the deferral is not button-gated: the release landed where the press did",
8202        );
8203        assert_eq!(
8204            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8205            Some(PointerKind::Pen(PenKind::Pen)),
8206        );
8207    }
8208
8209    // -----------------------------------------------------------------
8210    // The press-feedback delay
8211    // -----------------------------------------------------------------
8212
8213    /// The delay applies only inside a pan claimant — a control nothing can
8214    /// scroll out from under has no ambiguity to wait out.
8215    #[test]
8216    fn the_feedback_delay_applies_only_inside_a_claimant() {
8217        // Outside a claimant: immediate.
8218        let (mut tree, _clock) = tree_on_a_clock();
8219        let (w, pressed) = tappable(&mut tree);
8220        tree.layout(SizeProposal::exact(200.0, 400.0));
8221        let id = contact_id();
8222        let at = tree.bounds(w).center();
8223        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8224        assert!(
8225            !tree.press_pending(w),
8226            "no claimant above it, so nothing to rule out",
8227        );
8228        assert!(pressed.get());
8229
8230        // Inside one: withheld, then released by the delay.
8231        let (mut tree, clock) = tree_on_a_clock();
8232        let (row, pressed) = tappable(&mut tree);
8233        let list = tree.add(
8234            StackWidget::new()
8235                .child(row)
8236                .pan_claim(PanClaim::vertical())
8237                .on_scroll(|_e, _c| EventResponse::Handled),
8238        );
8239        tree.layout(SizeProposal::exact(200.0, 400.0));
8240        let _ = list;
8241        let id = contact_id();
8242        let at = Point::new(100.0, 200.0);
8243        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8244        assert!(tree.press_pending(row), "a finger might be about to scroll");
8245        assert!(!pressed.get(), "so the row does not flash");
8246        assert!(
8247            tree.press_is_inside(row),
8248            "the press is real; only its visual is being withheld",
8249        );
8250
8251        clock.set(EventTime::from_millis(99));
8252        tree.tick_gestures(std::time::Instant::now());
8253        assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8254
8255        clock.set(EventTime::from_millis(100));
8256        tree.tick_gestures(std::time::Instant::now());
8257        assert!(pressed.get(), "and 100 ms is it");
8258        assert!(!tree.press_pending(row));
8259    }
8260
8261    /// A mouse pressing inside the very same claimant waits for nothing: an
8262    /// indirect pointer opens no pan session, so the delay never reaches it.
8263    #[test]
8264    fn a_mouse_inside_a_claimant_never_waits() {
8265        let mut tree = WidgetTree::new();
8266        let (row, pressed) = tappable(&mut tree);
8267        let list = tree.add(
8268            StackWidget::new()
8269                .child(row)
8270                .pan_claim(PanClaim::vertical())
8271                .on_scroll(|_e, _c| EventResponse::Handled),
8272        );
8273        tree.layout(SizeProposal::exact(200.0, 400.0));
8274        let _ = list;
8275
8276        tree.dispatch_event(WidgetEvent::pointer_down(
8277            Point::new(100.0, 200.0),
8278            PointerButton::Primary,
8279            Modifiers::NONE,
8280        ));
8281        assert!(!tree.press_pending(row));
8282        assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8283    }
8284
8285    // -----------------------------------------------------------------
8286    // The EventContext queries
8287    // -----------------------------------------------------------------
8288
8289    /// The three queries `teksilo-widgets`' `common/interaction.rs` consumes,
8290    /// read from inside a real handler.
8291    #[test]
8292    fn a_handler_can_read_the_press_it_is_inside() {
8293        let (mut tree, _clock) = tree_on_a_clock();
8294        let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8295        let log = seen.clone();
8296        let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8297            move |event, ctx| {
8298                if matches!(event, WidgetEvent::PointerUp { .. }) {
8299                    log.borrow_mut().push((
8300                        ctx.is_pressed(),
8301                        ctx.press_is_inside(),
8302                        ctx.press_pending(),
8303                    ));
8304                }
8305                EventResponse::Ignored
8306            },
8307        ));
8308        let list = tree.add(
8309            StackWidget::new()
8310                .child(row)
8311                .pan_claim(PanClaim::vertical())
8312                .on_scroll(|_e, _c| EventResponse::Handled),
8313        );
8314        tree.layout(SizeProposal::exact(200.0, 400.0));
8315        let _ = list;
8316
8317        let id = contact_id();
8318        let at = Point::new(100.0, 200.0);
8319        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8320        tree.dispatch_pointer(contact(
8321            id,
8322            PointerPhase::Up,
8323            at,
8324            EventTime::from_millis(30),
8325        ));
8326
8327        assert_eq!(
8328            seen.borrow().as_slice(),
8329            &[(false, true, true)],
8330            "released inside the claimant before the delay elapsed: inside, \
8331             pending, and therefore not yet showing",
8332        );
8333    }
8334
8335    /// A handler outside any press reads three falses rather than a panic or a
8336    /// stale answer.
8337    #[test]
8338    fn a_handler_outside_a_press_reads_nothing() {
8339        let mut tree = WidgetTree::new();
8340        let asked = Rc::new(Cell::new(false));
8341        let flag = asked.clone();
8342        let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8343            flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8344            EventResponse::Ignored
8345        }));
8346        tree.layout(SizeProposal::exact(100.0, 50.0));
8347        tree.focus(w);
8348        tree.press_key(Key::ArrowDown, Modifiers::NONE);
8349        assert!(!asked.get());
8350    }
8351
8352    /// A completed interaction leaves no press behind, and the leak detector
8353    /// says so — it now reads the press table, which it could not before this
8354    /// package landed one.
8355    #[test]
8356    fn a_completed_press_leaves_the_detector_clean() {
8357        let (mut tree, _clock) = tree_on_a_clock();
8358        let (w, pressed) = tappable(&mut tree);
8359        tree.layout(SizeProposal::exact(100.0, 50.0));
8360        let at = tree.bounds(w).center();
8361
8362        let id = contact_id();
8363        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8364        assert!(pressed.get());
8365        tree.dispatch_pointer(contact(
8366            id,
8367            PointerPhase::Up,
8368            at,
8369            EventTime::from_millis(30),
8370        ));
8371        tree.assert_no_leaked_pointer_state();
8372    }
8373
8374    /// …and it would have caught the opposite. Driven by holding the press open
8375    /// rather than by faking state, so the assertion is about the real exit
8376    /// path.
8377    #[test]
8378    fn the_detector_reports_a_press_still_held() {
8379        let (mut tree, _clock) = tree_on_a_clock();
8380        let (w, _pressed) = tappable(&mut tree);
8381        tree.layout(SizeProposal::exact(100.0, 50.0));
8382        let at = tree.bounds(w).center();
8383
8384        let id = contact_id();
8385        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8386        let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8387            tree.assert_no_leaked_pointer_state()
8388        }));
8389        let message = *held
8390            .expect_err("a live press is leaked state")
8391            .downcast::<String>()
8392            .expect("the detector panics with a message");
8393        assert!(
8394            message.contains("is still pressed by"),
8395            "the press is named in the report, got: {message}",
8396        );
8397    }
8398
8399    /// A container that lays its children out side by side, so a hit test at a
8400    /// given x picks a specific child. `StackWidget` deliberately stacks at one
8401    /// origin, which is the opposite of what a release-elsewhere test needs.
8402    #[derive(Debug)]
8403    struct SideBySide {
8404        children: Vec<WidgetId>,
8405    }
8406
8407    impl crate::widget::Widget for SideBySide {
8408        fn layout_response(
8409            &self,
8410            proposal: SizeProposal,
8411            _ctx: &crate::widget::LayoutContext,
8412        ) -> crate::widget::LayoutResponse {
8413            proposal.resolve(0.0, 0.0).into()
8414        }
8415
8416        fn place_children(
8417            &self,
8418            bounds: teksilo_canvas::Rect,
8419            _proposal: SizeProposal,
8420            children: &mut [crate::widget::WidgetPlacement],
8421            _ctx: &crate::widget::LayoutContext,
8422        ) {
8423            let n = children.len().max(1) as f32;
8424            let w = bounds.width / n;
8425            for (i, child) in children.iter_mut().enumerate() {
8426                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8427                child.size = teksilo_canvas::Size::new(w, bounds.height);
8428            }
8429        }
8430
8431        fn children(&self) -> Vec<WidgetId> {
8432            self.children.clone()
8433        }
8434    }
8435}
8436
8437#[cfg(test)]
8438mod overlay_release_dismissal_tests {
8439    //! Outside-press overlay dismissal, driven through the real ingress door.
8440    //!
8441    //! The defect: `handle_click_outside` ran on the `PointerDown` and then
8442    //! *fell through*, so one press both closed a menu and actuated whatever
8443    //! the menu was covering. With a cursor that is defensible — the user aimed
8444    //! at a pixel they could see the whole time. With a finger it is not: the
8445    //! menu is the only thing they were looking at, and the control underneath
8446    //! is one they never saw.
8447
8448    use std::cell::Cell;
8449    use std::rc::Rc;
8450
8451    use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8452
8453    use crate::WidgetId;
8454    use crate::event::{Modifiers, PointerButton, WidgetEvent};
8455    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8456    use crate::pointer::{
8457        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8458        PointerSample,
8459    };
8460    use crate::test_widgets::FillWidget;
8461    use crate::widget::{LayoutContext, LayoutResponse, Widget};
8462    use crate::widget_builder::WidgetBuilder;
8463    use crate::widget_tree::WidgetTree;
8464
8465    // -----------------------------------------------------------------
8466    // Fixtures
8467    // -----------------------------------------------------------------
8468
8469    /// A leaf with an intrinsic size, so an overlay hung off it gets real
8470    /// bounds out of `position_overlays` instead of a zero rect.
8471    #[derive(Debug)]
8472    struct Panel(Size);
8473
8474    impl Widget for Panel {
8475        fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8476            self.0.into()
8477        }
8478    }
8479
8480    /// A root that puts its first child over the whole window and its second in
8481    /// a 10 dp corner.
8482    ///
8483    /// Two bare roots would both be laid out at the window's full size — the
8484    /// trigger would then contain every press point, and the pre-existing
8485    /// "a press on a click-opened overlay's own anchor is consumed" rule would
8486    /// swallow the very presses these tests are about.
8487    #[derive(Debug)]
8488    struct PageAndTrigger {
8489        children: Vec<WidgetId>,
8490    }
8491
8492    impl Widget for PageAndTrigger {
8493        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8494            proposal.resolve(0.0, 0.0).into()
8495        }
8496
8497        fn place_children(
8498            &self,
8499            bounds: Rect,
8500            _proposal: SizeProposal,
8501            children: &mut [crate::widget::WidgetPlacement],
8502            _ctx: &LayoutContext,
8503        ) {
8504            if let Some(page) = children.get_mut(0) {
8505                page.origin = bounds.origin();
8506                page.size = bounds.size();
8507            }
8508            if let Some(trigger) = children.get_mut(1) {
8509                trigger.origin = bounds.origin();
8510                trigger.size = Size::new(10.0, 10.0);
8511            }
8512        }
8513
8514        fn children(&self) -> Vec<WidgetId> {
8515            self.children.clone()
8516        }
8517    }
8518
8519    fn contact_id(n: u64) -> PointerId {
8520        PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8521    }
8522
8523    fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8524        PointerSample {
8525            pointer: PointerInfo::touch(id, EventTime::ZERO),
8526            phase,
8527            position: at,
8528            button: None,
8529            modifiers: Modifiers::NONE,
8530            coalesced: Vec::new(),
8531        }
8532    }
8533
8534    /// The scene every test below shares: a tappable page filling the window,
8535    /// and a menu overlay floating over part of it at (100, 100, 200, 200).
8536    ///
8537    /// The menu's content is a separate root, so a press outside it lands on
8538    /// the page and a press inside it lands on the menu — which is exactly the
8539    /// arrangement the dismissal rule is about.
8540    fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8541        let mut tree = WidgetTree::new();
8542        let taps = Rc::new(Cell::new(0u32));
8543        let counter = taps.clone();
8544        let page = tree.add(
8545            FillWidget::new()
8546                .focusable()
8547                .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8548        );
8549        // A small trigger in the corner, well clear of both press points — see
8550        // `PageAndTrigger` for why it cannot simply be a second root.
8551        let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8552        let _root = tree.add(PageAndTrigger {
8553            children: vec![page, trigger],
8554        });
8555        let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8556        tree.show_overlay(OverlayRequest::new(
8557            menu,
8558            trigger,
8559            OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8560            DismissBehavior::ClickOutside,
8561        ));
8562        tree.layout(SizeProposal::exact(800.0, 600.0));
8563        assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8564        (tree, page, taps)
8565    }
8566
8567    fn outside() -> Point {
8568        Point::new(600.0, 500.0)
8569    }
8570
8571    fn inside_menu() -> Point {
8572        Point::new(150.0, 150.0)
8573    }
8574
8575    // -----------------------------------------------------------------
8576    // The direct-pointer contract
8577    // -----------------------------------------------------------------
8578
8579    /// The arming press reaches nothing: no press record, no capture, and the
8580    /// menu is still up because the decision belongs to the release.
8581    #[test]
8582    fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8583        let (mut tree, page, taps) = page_with_a_menu();
8584        let finger = contact_id(1);
8585        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8586
8587        assert_eq!(
8588            tree.active_overlays().len(),
8589            1,
8590            "the press does not close the menu; the release does"
8591        );
8592        assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8593        let entry = tree.pointers.get(finger).expect("the contact is live");
8594        assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8595        assert!(entry.sequence.is_none(), "no arbitration was opened");
8596        assert_eq!(taps.get(), 0);
8597    }
8598
8599    /// …and the release closes the menu without actuating what it covered.
8600    #[test]
8601    fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8602        let (mut tree, page, taps) = page_with_a_menu();
8603        let finger = contact_id(2);
8604        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8605        tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8606
8607        assert!(tree.active_overlays().is_empty(), "the menu closed");
8608        assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8609        assert_eq!(tree.pressed_by(page), None);
8610        tree.assert_no_leaked_pointer_state();
8611    }
8612
8613    /// A press that never completes delivers nothing at all — neither the
8614    /// dismissal it armed nor the press it withheld.
8615    #[test]
8616    fn a_cancelled_press_aborts_the_arm() {
8617        let (mut tree, page, taps) = page_with_a_menu();
8618        let finger = contact_id(3);
8619        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8620        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8621
8622        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8623        assert_eq!(taps.get(), 0);
8624        assert_eq!(tree.pressed_by(page), None);
8625        tree.assert_no_leaked_pointer_state();
8626    }
8627
8628    /// Land beside the menu, drag onto it, lift there. The finger changed its
8629    /// mind: the menu stays, and the page under the arming press was never
8630    /// touched either.
8631    #[test]
8632    fn a_press_slid_onto_the_menu_dismisses_nothing() {
8633        let (mut tree, _page, taps) = page_with_a_menu();
8634        let finger = contact_id(4);
8635        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8636        tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8637        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8638
8639        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8640        assert_eq!(taps.get(), 0);
8641        tree.assert_no_leaked_pointer_state();
8642    }
8643
8644    /// A finger working *inside* the menu is an interaction, and a second one
8645    /// landing on the page is not a reason to take it away mid-flight.
8646    #[test]
8647    fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8648        let (mut tree, _page, _taps) = page_with_a_menu();
8649        let first = contact_id(5);
8650        let second = contact_id(6);
8651
8652        tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8653        assert!(
8654            tree.pointers
8655                .get(first)
8656                .is_some_and(|e| e.sequence.is_some()),
8657            "the first contact holds a live press inside the menu"
8658        );
8659
8660        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8661        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8662        assert_eq!(
8663            tree.active_overlays().len(),
8664            1,
8665            "the menu the first finger is holding must not close under it"
8666        );
8667
8668        // Once the first contact's press is over there is nothing left to
8669        // revoke, so it protects nothing and the same tap closes the menu.
8670        tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8671        let third = contact_id(7);
8672        tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8673        tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8674        assert!(tree.active_overlays().is_empty());
8675        tree.assert_no_leaked_pointer_state();
8676    }
8677
8678    /// A contact that is merely *holding an arm* has no sequence and no
8679    /// capture, so `press_is_revocable` says it has no press — and it must not
8680    /// block a second contact's dismissal the way a real press does.
8681    #[test]
8682    fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8683        let (mut tree, _page, _taps) = page_with_a_menu();
8684        let first = contact_id(8);
8685        let second = contact_id(9);
8686
8687        tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8688        assert!(
8689            tree.overlay_manager().has_armed_dismiss(first),
8690            "the first contact armed"
8691        );
8692        // The first contact is live but holds nothing revocable.
8693        assert!(tree.busy_press_points(second).is_empty());
8694
8695        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8696        assert!(tree.overlay_manager().has_armed_dismiss(second));
8697        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8698        assert!(tree.active_overlays().is_empty());
8699
8700        // The first contact's arm now names an overlay that is gone; its own
8701        // release must be a quiet no-op rather than a panic.
8702        tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8703        tree.assert_no_leaked_pointer_state();
8704    }
8705
8706    /// A tap *inside* the menu is not an outside press, so nothing is armed and
8707    /// the menu's own content handles the press exactly as before.
8708    #[test]
8709    fn a_touch_inside_the_menu_arms_nothing() {
8710        let (mut tree, _page, _taps) = page_with_a_menu();
8711        let finger = contact_id(10);
8712        tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8713        assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8714        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8715        assert_eq!(tree.active_overlays().len(), 1);
8716        tree.assert_no_leaked_pointer_state();
8717    }
8718
8719    /// A contact whose press was never opened — it hit nothing, or its `Down`
8720    /// was suppressed by an arm — still ceases to exist when the platform
8721    /// revokes it.
8722    ///
8723    /// The cancel funnel returns early for a pointer with nothing revocable and
8724    /// so never reaches the step that drops the table entry. Before the release
8725    /// dismissal that shape was rare (a press on bare background); the arm makes
8726    /// it the ordinary case, so the ingress door applies the same
8727    /// contact-ceases-to-exist rule it applies to an `Up`.
8728    #[test]
8729    fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8730        let mut tree = WidgetTree::new();
8731        tree.layout(SizeProposal::exact(800.0, 600.0));
8732        let finger = contact_id(99);
8733        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8734        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8735        tree.assert_no_leaked_pointer_state();
8736    }
8737
8738    // -----------------------------------------------------------------
8739    // The mouse, unchanged
8740    // -----------------------------------------------------------------
8741
8742    /// The press dismisses and falls through, exactly as before: one click both
8743    /// closes the menu and actuates the control beneath.
8744    #[test]
8745    fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8746        let (mut tree, _page, taps) = page_with_a_menu();
8747        tree.dispatch_event(WidgetEvent::pointer_down(
8748            outside(),
8749            PointerButton::Primary,
8750            Modifiers::NONE,
8751        ));
8752        assert!(
8753            tree.active_overlays().is_empty(),
8754            "the mouse still dismisses on the press"
8755        );
8756        assert!(
8757            tree.pointers
8758                .get(PointerId::MOUSE)
8759                .is_some_and(|e| e.sequence.is_some()),
8760            "and the press still reaches the page beneath"
8761        );
8762        tree.dispatch_event(WidgetEvent::pointer_up(
8763            outside(),
8764            PointerButton::Primary,
8765            Modifiers::NONE,
8766        ));
8767        assert_eq!(taps.get(), 1, "the control beneath activated");
8768        assert!(
8769            !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8770            "a mouse never arms"
8771        );
8772        tree.assert_no_leaked_pointer_state();
8773    }
8774
8775    // -----------------------------------------------------------------
8776    // Contact avoidance
8777    // -----------------------------------------------------------------
8778
8779    /// A context menu raised by a finger keeps clear of the contact patch; the
8780    /// same menu raised by a mouse lands on the pixel, as it always has.
8781    #[test]
8782    fn a_context_menu_avoids_the_contact_that_opened_it() {
8783        fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8784            let mut tree = WidgetTree::new();
8785            let page = tree.add(
8786                FillWidget::new()
8787                    .focusable()
8788                    .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8789            );
8790            let _ = page;
8791            tree.layout(SizeProposal::exact(800.0, 600.0));
8792            tree.dispatch_pointer(PointerSample {
8793                pointer,
8794                phase: PointerPhase::Down,
8795                position: at,
8796                button: Some(PointerButton::Secondary),
8797                modifiers: Modifiers::NONE,
8798                coalesced: Vec::new(),
8799            });
8800            tree.layout(SizeProposal::exact(800.0, 600.0));
8801            let id = *tree
8802                .active_overlays()
8803                .first()
8804                .expect("the context menu opened");
8805            tree.overlay_manager().bounds_for(id).expect("bounds")
8806        }
8807
8808        let at = Point::new(400.0, 300.0);
8809        let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8810        let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8811        assert!(
8812            finger.x < contact.x && finger.right() <= contact.x,
8813            "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8814        );
8815
8816        let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8817        assert_eq!(
8818            (mouse.x, mouse.y),
8819            (at.x, at.y),
8820            "a mouse menu still opens with its corner on the pointer"
8821        );
8822    }
8823}
8824
8825/// An overlay is chosen by its **bounds**, and what happens when its content
8826/// then claims nothing at the point.
8827///
8828/// Two answers, and the flag on the content root is what picks between them.
8829/// An ordinary overlay ends the search inside itself — that is what keeps the
8830/// miss-only slop pass confined to the one layer the exact pass entered, so a
8831/// near-miss on a menu row can never be re-attributed to a control on the page
8832/// behind the menu. An overlay whose content root declares `event_pass_through`
8833/// is the exception: the flag already means "what I did not claim belongs to
8834/// whatever is behind me", and an overlay root is not an exception to it.
8835///
8836/// This is not a hypothetical. The touch text affordances were specified as a
8837/// viewport-sized pass-through layer, and under the first answer applied to
8838/// both, mounting one made the editor beneath stop taking presses entirely —
8839/// measured, as `hit_test` returning `None` at a point inside the field.
8840#[cfg(test)]
8841mod pass_through_overlay_tests {
8842    use teksilo_canvas::{Point, Rect, SizeProposal};
8843
8844    use crate::WidgetId;
8845    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8846    use crate::test_widgets::FillWidget;
8847    use crate::widget::{LayoutContext, LayoutResponse, Widget};
8848    use crate::widget_builder::WidgetBuilder;
8849    use crate::widget_tree::WidgetTree;
8850
8851    /// The affordance layer's shape: fills whatever it is given, and puts its
8852    /// one child — a selection handle — on a fixed rectangle inside it.
8853    #[derive(Debug)]
8854    struct HandleLayer {
8855        handle: WidgetId,
8856        at: Rect,
8857    }
8858
8859    impl Widget for HandleLayer {
8860        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8861            proposal.resolve(0.0, 0.0).into()
8862        }
8863
8864        fn place_children(
8865            &self,
8866            _bounds: Rect,
8867            _proposal: SizeProposal,
8868            children: &mut [crate::widget::WidgetPlacement],
8869            _ctx: &LayoutContext,
8870        ) {
8871            if let Some(child) = children.get_mut(0) {
8872                child.origin = self.at.origin();
8873                child.size = self.at.size();
8874            }
8875        }
8876
8877        fn children(&self) -> Vec<WidgetId> {
8878            vec![self.handle]
8879        }
8880    }
8881
8882    const HANDLE: Rect = Rect {
8883        x: 10.0,
8884        y: 10.0,
8885        width: 20.0,
8886        height: 20.0,
8887    };
8888
8889    /// Which flag the overlay's content root carries.
8890    #[derive(Clone, Copy, PartialEq)]
8891    enum RootFlag {
8892        /// The affordance layer's: not a target itself, children are.
8893        PassThrough,
8894        /// Neither the root nor its children are targets. Stronger, and
8895        /// deliberately **not** what the fall-through is gated on.
8896        HitTransparent,
8897    }
8898
8899    /// A field under a viewport-sized overlay carrying one handle.
8900    ///
8901    /// Both flags make the subtree answer `None` for a point no handle covers,
8902    /// which is the state the fall-through decides — so the two arms differ in
8903    /// nothing but the flag the router reads.
8904    fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8905        let mut tree = WidgetTree::new();
8906        let field = tree.add(FillWidget::new());
8907        let handle = tree.add(FillWidget::new());
8908        let layer = HandleLayer { handle, at: HANDLE };
8909        let layer = match flag {
8910            RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8911            RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8912        };
8913        tree.show_overlay(OverlayRequest::new(
8914            layer,
8915            field,
8916            OverlayPlacement::FullViewport,
8917            DismissBehavior::Manual,
8918        ));
8919        tree.layout(SizeProposal::exact(400.0, 200.0));
8920        assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8921        (tree, field, handle)
8922    }
8923
8924    /// The door: the surface under a pass-through layer goes on taking presses.
8925    #[test]
8926    fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8927        let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8928
8929        assert_eq!(
8930            tree.hit_test(Point::new(15.0, 15.0)),
8931            Some(handle),
8932            "the layer must still win the point its own child covers"
8933        );
8934        assert_eq!(
8935            tree.hit_test(Point::new(200.0, 100.0)),
8936            Some(field),
8937            "a press the layer did not claim must reach the surface beneath it"
8938        );
8939    }
8940
8941    /// `event_pass_through` removes a node from **hit-testing**, not from the
8942    /// **bubble path** of a descendant that was hit.
8943    ///
8944    /// The distinction is what lets a host mount its affordances at the full
8945    /// viewport and still answer the one press neither the layer nor the surface
8946    /// beneath can: the pass-through root is skipped when a point belongs to
8947    /// nobody in its subtree, and is still told about a press that landed on one
8948    /// of its children. The single-line text stack's `AffordanceHost` is exactly
8949    /// this — a cursor's click on a selection handle, which the handle refuses and
8950    /// the editor never sees.
8951    #[test]
8952    fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8953        use std::cell::Cell;
8954        use std::rc::Rc;
8955
8956        let seen = Rc::new(Cell::new(0u32));
8957        let counter = seen.clone();
8958        let mut tree = WidgetTree::new();
8959        let field = tree.add(FillWidget::new());
8960        let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8961        let layer = tree.add(
8962            HandleLayer { handle, at: HANDLE }
8963                .event_pass_through(true)
8964                .on_pointer_event(move |_event, _ctx| {
8965                    counter.set(counter.get() + 1);
8966                    crate::event::EventResponse::Ignored
8967                }),
8968        );
8969        tree.show_overlay(OverlayRequest::new(
8970            layer,
8971            field,
8972            OverlayPlacement::FullViewport,
8973            DismissBehavior::Manual,
8974        ));
8975        tree.layout(SizeProposal::exact(400.0, 200.0));
8976
8977        tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8978        assert!(
8979            seen.get() > 0,
8980            "a pass-through root heard nothing about a press on its own child"
8981        );
8982
8983        let after_child = seen.get();
8984        tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8985        // …and a press it did not contain a target for never arrives, because it
8986        // was never the hit.
8987        tree.pointer_down_button(
8988            Point::new(200.0, 100.0),
8989            crate::event::PointerButton::Primary,
8990        );
8991        assert_eq!(
8992            seen.get(),
8993            after_child + 1,
8994            "the press-and-lift on the child accounts for the count, and the \
8995             press on the surface beneath added nothing"
8996        );
8997    }
8998
8999    /// …and the widening stops at that one flag.
9000    ///
9001    /// `hit_transparent` is the discriminating case, and the only one available:
9002    /// it is the other way for an overlay's content to claim nothing at a point,
9003    /// so it is the fixture in which the gate — rather than the subtree's own
9004    /// answer — is what decides. A gate that read "the subtree claimed nothing"
9005    /// alone would fall through here too, and with it past every overlay whose
9006    /// content happens to miss, which is what confines the miss-only slop pass to
9007    /// the layer the exact pass entered.
9008    ///
9009    /// The exclusion is deliberate rather than an oversight: the one overlay that
9010    /// must be seen past regardless is the drag preview, and the hit-test already
9011    /// takes an explicit `exclude_overlay` for it.
9012    #[test]
9013    fn a_hit_transparent_root_does_not_get_the_fall_through() {
9014        let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
9015
9016        // Nothing in the overlay is a target, not even the handle.
9017        assert_eq!(
9018            tree.hit_test(Point::new(15.0, 15.0)),
9019            None,
9020            "hit_transparent must exclude the subtree, or this fixture is not \
9021             testing the gate"
9022        );
9023        assert_eq!(
9024            tree.hit_test(Point::new(200.0, 100.0)),
9025            None,
9026            "the fall-through reached past an overlay that did not ask for it"
9027        );
9028        let _ = field;
9029    }
9030}