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 { .. } => {
2447                let mut fired = false;
2448                if let Some(h) = node.external_handlers.on_focus.as_mut() {
2449                    h(true, ctx);
2450                    fired = true;
2451                }
2452                if let Some(h) = node.handlers.on_focus.as_mut() {
2453                    h(true, ctx);
2454                    fired = true;
2455                }
2456                fired.then_some(EventResponse::Handled)
2457            }
2458            WidgetEvent::FocusLost => {
2459                let mut fired = false;
2460                if let Some(h) = node.external_handlers.on_focus.as_mut() {
2461                    h(false, ctx);
2462                    fired = true;
2463                }
2464                if let Some(h) = node.handlers.on_focus.as_mut() {
2465                    h(false, ctx);
2466                    fired = true;
2467                }
2468                fired.then_some(EventResponse::Handled)
2469            }
2470            WidgetEvent::KeyDown { .. }
2471            | WidgetEvent::KeyUp { .. }
2472            | WidgetEvent::ImeComposition { .. }
2473            | WidgetEvent::ImeCommit { .. } => {
2474                if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
2475                    Some(fire_event_handler_both(
2476                        &mut node.external_handlers.on_key,
2477                        &mut node.handlers.on_key,
2478                        event,
2479                        ctx,
2480                    ))
2481                } else {
2482                    None
2483                }
2484            }
2485            WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
2486                if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
2487                    Some(fire_event_handler_both(
2488                        &mut node.external_handlers.on_scroll,
2489                        &mut node.handlers.on_scroll,
2490                        event,
2491                        ctx,
2492                    ))
2493                } else {
2494                    None
2495                }
2496            }
2497            WidgetEvent::AccessAction {
2498                action,
2499                target_node,
2500                data,
2501                ..
2502            } => {
2503                // Every installed slot fires — both payload shapes, and
2504                // within each shape both the external (app-installed
2505                // `.on_access_action*`) and the widget's own. Button (own)
2506                // and Dialog (external) layered together rely on that for a
2507                // single accesskit click.
2508                //
2509                // The two shapes are layered, not alternatives, because they
2510                // have different owners: `on_access_action_request` is what a
2511                // widget reaches for when it needs `target_node` or `data`
2512                // (`Slider`, `SpinBox`, `TextInputField`, `CodeEditor`,
2513                // `TabBar`), while `.on_access_action(..)` is the app's
2514                // builder-level hook. Preferring the payload shape when it was
2515                // set therefore did not choose between two handlers for the
2516                // same job — it silently disabled the app's handler on exactly
2517                // the widgets that had migrated, with nothing at the call site
2518                // to say so.
2519                //
2520                // Assistive-tech action paths run under the `Accessibility`
2521                // source label. Restored after the block.
2522                let saved_a11y_source = ctx
2523                    .current_source
2524                    .replace(crate::telemetry::IntentSource::Accessibility);
2525                let mut any_slot = false;
2526                let mut any_handled = false;
2527                if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
2528                    any_slot = true;
2529                    any_handled |=
2530                        h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
2531                }
2532                if let Some(h) = node.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.external_handlers.on_access_action.as_mut() {
2538                    any_slot = true;
2539                    any_handled |= h(*action, ctx) == EventResponse::Handled;
2540                }
2541                if let Some(h) = node.handlers.on_access_action.as_mut() {
2542                    any_slot = true;
2543                    any_handled |= h(*action, ctx) == EventResponse::Handled;
2544                }
2545                let user_handled = any_slot.then_some(if any_handled {
2546                    EventResponse::Handled
2547                } else {
2548                    EventResponse::Ignored
2549                });
2550
2551                // Builder-level access_action / access_custom_action
2552                // callbacks. These layer on top of any user-installed
2553                // on_access_action / on_access_action_request — both
2554                // fire for the same dispatched event. Drives the
2555                // SwiftUI `.accessibilityAction(...)` parity.
2556                let mut override_handled = false;
2557                if let Some(ov) = node.access_overrides.as_deref_mut() {
2558                    if matches!(action, accesskit::Action::CustomAction) {
2559                        if let Some(accesskit::ActionData::CustomAction(idx)) = data
2560                            && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
2561                        {
2562                            cb(ctx);
2563                            override_handled = true;
2564                        }
2565                    } else {
2566                        for (a, cb) in ov.actions.iter_mut() {
2567                            if *a == *action {
2568                                cb(ctx);
2569                                override_handled = true;
2570                            }
2571                        }
2572                    }
2573                }
2574
2575                ctx.current_source = saved_a11y_source;
2576                match (user_handled, override_handled) {
2577                    (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
2578                    (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
2579                    (None, false) => None,
2580                }
2581            }
2582            WidgetEvent::Gesture { gesture } => {
2583                // Pre-recognized gestures from the platform (OS trackpad
2584                // pinch/rotation, double-tap, …) bypass the gesture arena
2585                // and go straight to the matching handler. See §10.
2586                let matched = matches!(
2587                    gesture,
2588                    GestureEvent::PinchStarted { .. }
2589                        | GestureEvent::PinchChanged { .. }
2590                        | GestureEvent::PinchEnded
2591                        | GestureEvent::Swipe { .. }
2592                        | GestureEvent::DoubleTap { .. }
2593                        | GestureEvent::TripleTap { .. }
2594                ) && {
2595                    let has_handler = match gesture {
2596                        GestureEvent::PinchStarted { .. }
2597                        | GestureEvent::PinchChanged { .. }
2598                        | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
2599                        GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
2600                        GestureEvent::DoubleTap { .. } => {
2601                            node.any_handler(|h| h.on_double_tap.is_some())
2602                        }
2603                        GestureEvent::TripleTap { .. } => {
2604                            node.any_handler(|h| h.on_triple_tap.is_some())
2605                        }
2606                        _ => false,
2607                    };
2608                    if has_handler {
2609                        Self::dispatch_recognized_gesture(node, *gesture, ctx);
2610                    }
2611                    has_handler
2612                };
2613                if matched {
2614                    Some(EventResponse::Handled)
2615                } else {
2616                    None
2617                }
2618            }
2619            WidgetEvent::PointerDown {
2620                position,
2621                button,
2622                modifiers,
2623                ..
2624            } => {
2625                // Raw pointer handler runs first so widgets can intercept
2626                // events that the gesture recognizers won't catch (e.g.
2627                // right-click → context menu). If it returns Handled the
2628                // gesture arena is skipped; otherwise we fall through.
2629                // Only fire for the target — ancestors already fired
2630                // on_pointer_event during the preview pass.
2631                if fire_on_pointer_event {
2632                    let r = fire_event_handler_both(
2633                        &mut node.external_handlers.on_pointer_event,
2634                        &mut node.handlers.on_pointer_event,
2635                        event,
2636                        ctx,
2637                    );
2638                    if r == EventResponse::Handled {
2639                        return Some(EventResponse::Handled);
2640                    }
2641                }
2642                if arena_blocked {
2643                    // This node lost the arbitration for the press: its
2644                    // recognizers stay out of it, and the event goes on
2645                    // bubbling as if the node carried none.
2646                    return None;
2647                }
2648                Self::ensure_gesture_arena(node, node_id, gesture_owners);
2649                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2650                    let cx = gesture_cx;
2651                    // Implicit capture for the Down..Up sequence so that
2652                    // moves leaving the widget bounds still reach the
2653                    // arena. Without this, a drag that starts inside the
2654                    // widget but crosses its edge before the recognizer
2655                    // latches would be hit-tested to another widget and
2656                    // the press-origin arena would never see a `Move`.
2657                    // Released unconditionally by the `PointerUp` branch
2658                    // in `dispatch_event`.
2659                    //
2660                    // **Implicit**: this is plumbing, not a claim. Routing it
2661                    // through the public `capture_pointer` would enrol every
2662                    // arena-bearing node as a `RawDrag` competitor and decide
2663                    // every mouse sequence at press. See
2664                    // `EventContext::capture_pointer_implicit`.
2665                    ctx.capture_pointer_implicit();
2666                    let result = arena.process(
2667                        &RawPointerEvent::Down {
2668                            position: *position,
2669                            button: *button,
2670                            modifiers: *modifiers,
2671                            pointer: cx.pointer,
2672                            time: cx.now,
2673                        },
2674                        &cx,
2675                    );
2676                    if let Some(gesture) = result {
2677                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2678                    }
2679                    return Some(EventResponse::Handled);
2680                }
2681                None
2682            }
2683            WidgetEvent::PointerUp {
2684                position,
2685                button,
2686                modifiers,
2687                ..
2688            } => {
2689                if fire_on_pointer_event {
2690                    let r = fire_event_handler_both(
2691                        &mut node.external_handlers.on_pointer_event,
2692                        &mut node.handlers.on_pointer_event,
2693                        event,
2694                        ctx,
2695                    );
2696                    if r == EventResponse::Handled {
2697                        return Some(EventResponse::Handled);
2698                    }
2699                }
2700                if arena_blocked {
2701                    return None;
2702                }
2703                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2704                    let cx = gesture_cx;
2705                    let result = arena.process(
2706                        &RawPointerEvent::Up {
2707                            position: *position,
2708                            button: *button,
2709                            modifiers: *modifiers,
2710                            pointer: cx.pointer,
2711                            time: cx.now,
2712                        },
2713                        &cx,
2714                    );
2715                    if let Some(gesture) = result {
2716                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2717                    }
2718                    return Some(EventResponse::Handled);
2719                }
2720                None
2721            }
2722            WidgetEvent::PointerMove { position, .. } => {
2723                if fire_on_pointer_event {
2724                    let r = fire_event_handler_both(
2725                        &mut node.external_handlers.on_pointer_event,
2726                        &mut node.handlers.on_pointer_event,
2727                        event,
2728                        ctx,
2729                    );
2730                    if r == EventResponse::Handled {
2731                        return Some(EventResponse::Handled);
2732                    }
2733                }
2734                if arena_blocked {
2735                    return None;
2736                }
2737                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
2738                    let cx = gesture_cx;
2739                    let result = arena.process(
2740                        &RawPointerEvent::Move {
2741                            position: *position,
2742                            pointer: cx.pointer,
2743                            time: cx.now,
2744                        },
2745                        &cx,
2746                    );
2747                    if let Some(gesture) = result {
2748                        Self::dispatch_recognized_gesture(node, gesture, ctx);
2749                        // A recognized gesture (DragStarted / DragMoved / …)
2750                        // almost always changes visible state — return
2751                        // `Handled` so the bubble loop marks this widget
2752                        // `needs_paint`, which in turn makes
2753                        // `WidgetTree::needs_redraw()` return true and
2754                        // triggers a `request_redraw` for the next frame.
2755                        // Without this, state updates via bound signals are
2756                        // only observed on the *next* layout/render pass,
2757                        // which in turn is never scheduled because
2758                        // `teksilo-app::update_control_flow` only wakes up when
2759                        // `needs_redraw()` is true.
2760                        return Some(EventResponse::Handled);
2761                    }
2762                    return Some(EventResponse::Ignored);
2763                }
2764                None
2765            }
2766            WidgetEvent::PointerCancel {
2767                reason, pointer, ..
2768            } => {
2769                // Two hooks, and the dedicated one always runs. `on_pointer_cancel`
2770                // is a notification, not a route: a widget releasing what its
2771                // press latched has nothing to consume, and letting it report
2772                // `Handled` would make releasing state look like claiming the
2773                // event. The raw `on_pointer_event` hook keeps its ordinary
2774                // consuming semantics for widgets that drive the whole pointer
2775                // stream themselves.
2776                for slot in [
2777                    &mut node.external_handlers.on_pointer_cancel,
2778                    &mut node.handlers.on_pointer_cancel,
2779                ] {
2780                    if let Some(handler) = slot.as_mut() {
2781                        handler(pointer, *reason, ctx);
2782                    }
2783                }
2784                if fire_on_pointer_event {
2785                    let r = fire_event_handler_both(
2786                        &mut node.external_handlers.on_pointer_event,
2787                        &mut node.handlers.on_pointer_event,
2788                        event,
2789                        ctx,
2790                    );
2791                    if r == EventResponse::Handled {
2792                        return Some(EventResponse::Handled);
2793                    }
2794                }
2795                None
2796            }
2797        }
2798    }
2799
2800    pub(super) fn collect_from_ctx<'ops>(
2801        &mut self,
2802        mut ctx: EventContext<'ops>,
2803        source_widget: WidgetId,
2804    ) {
2805        // Take the ops handle out of ctx up front so we can freely
2806        // reborrow it inside the method without fighting the 'ops
2807        // lifetime propagation when other fields of `ctx` are moved.
2808        // When no ops is set (standalone trees / tests), fall back to
2809        // a stack NoopWindowOps.
2810        let local_ops = ctx.window_ops.take();
2811        let mut noop = crate::window::NoopWindowOps;
2812        let ops: &mut dyn crate::window::WindowOps = match local_ops {
2813            Some(o) => o,
2814            None => &mut noop,
2815        };
2816        if ctx.frame_requested {
2817            self.request_frame();
2818        }
2819        // Declared first, handler second — the order the two used to occur in
2820        // when both wrote the same slot, so a handler that speaks during an
2821        // enter still outranks the node it entered.
2822        if let Some(declared) = ctx.declared_cursor_request {
2823            self.node_declared_cursor = declared;
2824            self.current_cursor = declared;
2825        }
2826        match ctx.cursor_request {
2827            Some(crate::widget::CursorRequest::Set(cursor)) => {
2828                self.current_cursor = cursor;
2829            }
2830            // A withdrawal restores the node-declared cursor rather than
2831            // resetting to `Default`: the handler is stepping back, not
2832            // claiming the cursor is nothing.
2833            Some(crate::widget::CursorRequest::Release) => {
2834                self.current_cursor = self.node_declared_cursor;
2835            }
2836            None => {}
2837        }
2838        // Intents queued through `ctx.send_intent` are anchored at
2839        // the originating widget. Programmatic sends default to
2840        // `propagate_when_disabled = true` — there is no shortcut to
2841        // consult, and propagation is the safe, least-surprising
2842        // default.
2843        for intent in ctx.pending_intents {
2844            self.enqueue_intent(source_widget, intent, true);
2845        }
2846        // Key capture: process cancel before arm, matching the
2847        // handler's call order (the handler sets `cancel_key_capture`
2848        // when it calls `ctx.cancel_key_capture()`, and separately
2849        // stores `pending_key_capture` when it calls
2850        // `ctx.begin_key_capture(...)`). If the handler did both,
2851        // arm wins (whichever was called last on the ctx has
2852        // already overwritten the other field's effect via the
2853        // setter logic).
2854        if ctx.cancel_key_capture {
2855            self.cancel_key_capture();
2856        }
2857        if let Some(slot) = ctx.pending_key_capture {
2858            self.key_capture = Some(slot);
2859        }
2860        // Registry mutations queued by settings-UI buttons.
2861        for mutation in ctx.pending_shortcut_mutations {
2862            match mutation {
2863                crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
2864                    self.shortcut_registry.rebind_primary(id, keystroke);
2865                }
2866                crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
2867                    self.shortcut_registry.rebind_secondary(id, keystroke);
2868                }
2869                crate::widget::ShortcutMutation::ClearOverride { id } => {
2870                    self.shortcut_registry.clear_override(&id);
2871                }
2872            }
2873        }
2874        if ctx.close_window_requested {
2875            self.close_window_requested = true;
2876        }
2877        if ctx.force_close_requested {
2878            self.force_close_requested = true;
2879        }
2880        self.pending_modal_requests
2881            .extend(ctx.modal_requests.into_iter().map(|request| {
2882                crate::modal::QueuedModalRequest {
2883                    source_widget,
2884                    request,
2885                }
2886            }));
2887        if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
2888            self.pending_modal_dismissal = true;
2889        }
2890        for callback in ctx.idle_callbacks {
2891            self.idle_queue.push_boxed(callback);
2892        }
2893        match ctx.dismiss_scope {
2894            Some(crate::widget::DismissScope::All) => {
2895                let dismissed = self.overlay_manager.dismiss_all();
2896                self.dormant_dismissed_content(&dismissed, &mut *ops);
2897            }
2898            Some(crate::widget::DismissScope::AllExceptHosts) => {
2899                self.dismiss_all_overlays_except_hosts(&mut *ops);
2900            }
2901            Some(crate::widget::DismissScope::SelfChain) => {
2902                self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
2903            }
2904            Some(crate::widget::DismissScope::Top) => {
2905                if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
2906                {
2907                    self.dormant_dismissed_content(&content_ids, &mut *ops);
2908                    if let Some(restore_id) = focus_restore
2909                        && self.arena.is_active(restore_id)
2910                    {
2911                        self.focus_ops(restore_id, &mut *ops);
2912                    }
2913                }
2914            }
2915            None => {
2916                for id in ctx.overlay_dismissals {
2917                    let dismissed = self.overlay_manager.dismiss(id);
2918                    self.dormant_dismissed_content(&dismissed, &mut *ops);
2919                }
2920            }
2921        }
2922        // Content-keyed dismissals (`dismiss_overlay_by_content`). Drained
2923        // unconditionally — independent of `dismiss_scope` and of the
2924        // pending delayed-overlay list — so a handler can retract a shown
2925        // reusable overlay it identifies only by content. Resolving the
2926        // id here (not at call time) is what lets the caller skip
2927        // tracking the `OverlayId`.
2928        for content_id in ctx.overlay_content_dismissals {
2929            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
2930                let dismissed = self.overlay_manager.dismiss(overlay_id);
2931                self.dormant_dismissed_content(&dismissed, &mut *ops);
2932            }
2933        }
2934        // Apply pause/resume queue (ToastHost hover-pause). Drained
2935        // here so the handler-side `ctx.pause_overlay_auto_dismiss(id)`
2936        // is order-independent with `dismiss_overlay(id)` and the
2937        // scope-based dismissals: pause/resume on an overlay that
2938        // was concurrently dismissed is silently dropped (the find
2939        // inside the OverlayManager methods misses on the gone id).
2940        for (id, pause) in ctx.overlay_pause_requests {
2941            if pause {
2942                self.overlay_manager.pause_auto_dismiss(id);
2943            } else {
2944                self.overlay_manager.resume_auto_dismiss(id);
2945            }
2946        }
2947        for preserve_content in ctx.dismiss_descendant_overlays {
2948            self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
2949        }
2950        let deferred_row_activations =
2951            self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
2952        if ctx.request_a11y_update {
2953            self.a11y_dirty = true;
2954        }
2955        if let Some(visible) = ctx.soft_keyboard_request.take() {
2956            self.request_soft_keyboard(visible);
2957        }
2958        // Handed to the tree's own live regions, which schedule the two
2959        // accessibility syncs each message needs. See `crate::announcer`.
2960        for (message, politeness) in std::mem::take(&mut ctx.announcements) {
2961            self.announce_with(message, politeness);
2962        }
2963        for mut req in ctx.overlay_requests {
2964            if req.parent_overlay.is_none() {
2965                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2966            }
2967            if self
2968                .overlay_manager
2969                .find_by_content(req.content_id)
2970                .is_some()
2971            {
2972                continue;
2973            }
2974            let current_focus = self.focused;
2975            self.overlay_manager.show(req);
2976            // Overlay show changes the AT tree shape — mirror the
2977            // `WidgetTree::show_overlay` path. The dismissal sibling
2978            // (`dismiss_overlay_with_ops`) already flips this.
2979            self.a11y_dirty = true;
2980            if let Some(focus_id) = current_focus {
2981                self.overlay_manager.set_top_focus_restore(focus_id);
2982            }
2983        }
2984        for (mut req, band) in ctx.overlay_band_requests {
2985            if req.parent_overlay.is_none() {
2986                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
2987            }
2988            if self
2989                .overlay_manager
2990                .find_by_content(req.content_id)
2991                .is_some()
2992            {
2993                continue;
2994            }
2995            let content_id = req.content_id;
2996            self.overlay_manager.show_in_band(req, band);
2997            self.arena.activate(content_id);
2998            self.a11y_dirty = true;
2999            // Deliberately no `set_top_focus_restore`: the text-affordance band
3000            // never takes focus from the anchor, so there is nothing to give
3001            // back when it goes.
3002        }
3003        // After the shows, so a handler may raise an overlay and place it in
3004        // the same dispatch.
3005        for (content_id, placement) in ctx.overlay_placement_updates {
3006            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3007                self.overlay_manager.update_placement(overlay_id, placement);
3008            }
3009        }
3010        for (content_id, source) in ctx.overlay_placement_sources {
3011            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3012                self.overlay_manager
3013                    .set_placement_source(overlay_id, source);
3014            }
3015        }
3016        for (mut req, duration) in ctx.timed_overlay_requests {
3017            if req.parent_overlay.is_none() {
3018                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3019            }
3020            if self
3021                .overlay_manager
3022                .find_by_content(req.content_id)
3023                .is_some()
3024            {
3025                continue;
3026            }
3027            let current_focus = self.focused;
3028            let overlay_id = self.overlay_manager.show_for(req, duration);
3029            self.overlay_manager
3030                .set_shown_at_sim(overlay_id, self.sim_clock);
3031            self.a11y_dirty = true;
3032            if let Some(focus_id) = current_focus {
3033                self.overlay_manager.set_top_focus_restore(focus_id);
3034            }
3035        }
3036        for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3037            if req.parent_overlay.is_none() {
3038                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3039            }
3040            if self
3041                .overlay_manager
3042                .find_by_content(req.content_id)
3043                .is_some()
3044            {
3045                continue;
3046            }
3047            let content_id = req.content_id;
3048            let current_focus = self.focused;
3049            let overlay_id = self.overlay_manager.show(req);
3050            self.overlay_manager
3051                .set_shown_at_sim(overlay_id, self.sim_clock);
3052            self.a11y_dirty = true;
3053            if let Some(focus_id) = current_focus {
3054                self.overlay_manager.set_top_focus_restore(focus_id);
3055            }
3056            // Drive the caller's progress signal 0 → 1, and register it
3057            // as the overlay's fade-state signal so every dismiss path
3058            // tweens it 1 → 0 and defers removal until it completes — the
3059            // same deferral machinery as `with_fade`, minus `set_opacity`
3060            // (the caller owns how `progress` paints).
3061            self.register_animated_signal(&progress, content_id);
3062            let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3063                target: 1.0,
3064                duration,
3065                easing: teksilo_tokens::Easing::EaseOut,
3066                frame_interval: None,
3067                looping: false,
3068                epsilon: 0.0,
3069                max_duration: None,
3070            });
3071            self.overlay_manager
3072                .attach_fade(overlay_id, progress, duration);
3073        }
3074        if let Some((pointer, capture)) = ctx.pointer_capture {
3075            // Per pointer, and by default the pointer whose sample the handler
3076            // was serving — so a mouse call site means exactly what it meant
3077            // before, and two contacts on two widgets hold two captures.
3078            let named = pointer;
3079            let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3080            self.set_pointer_capture(pointer, capture.then_some(source_widget));
3081            // An explicit `capture_pointer()` from a handler is an arbitration
3082            // act; the arena's and the drag pipeline's own captures are
3083            // plumbing and route through `capture_pointer_implicit`.
3084            if capture && ctx.explicit_capture && named.is_none() {
3085                self.note_explicit_capture(source_widget);
3086            }
3087        }
3088        if let Some(activation) = ctx.drag_activation_override.take() {
3089            // A press handler chose this node's drag activation for this press.
3090            // Onto the sequence, where it dies with the press — see
3091            // `EventContext::set_drag_activation`.
3092            self.note_drag_activation_override(source_widget, activation);
3093        }
3094        if ctx.recognized_owning_gesture {
3095            // A drag or a swipe recognized on this node owns the rest of the
3096            // press, however the recognizer was reached.
3097            self.note_gesture_recognized(source_widget);
3098        }
3099        if !ctx.gesture_acts.is_empty() {
3100            let acts = std::mem::take(&mut ctx.gesture_acts);
3101            self.apply_gesture_acts(&acts, source_widget);
3102        }
3103        if let Some(reason) = ctx.cancel_pointer_request {
3104            let pointer = self.current_pointer_id();
3105            self.cancel_pointer(pointer, reason, &mut *ops);
3106        }
3107        for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3108            if request.parent_overlay.is_none() {
3109                request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3110            }
3111            if self
3112                .overlay_manager
3113                .find_by_content(request.content_id)
3114                .is_some()
3115            {
3116                continue;
3117            }
3118            let content_id = request.content_id;
3119            self.pending_delayed_overlays
3120                .retain(|pending| pending.request.content_id != content_id);
3121            self.pending_delayed_overlays.push(PendingDelayedOverlay {
3122                request,
3123                delay,
3124                focus_target,
3125                replace_siblings,
3126                real_requested_at: std::time::Instant::now(),
3127                sim_requested_at: self.sim_clock,
3128            });
3129            self.arena.mark_needs_paint(source_widget);
3130        }
3131        for content_id in ctx.cancel_delayed_overlays {
3132            self.pending_delayed_overlays
3133                .retain(|pending| pending.request.content_id != content_id);
3134        }
3135        // Apex = the last sample that was still over the anchor, which
3136        // for the intended caller (the anchor's own hover-leave) is the
3137        // point the diagonal starts from. Ops are applied per node as
3138        // its handler returns, so this lands before the next widget's
3139        // hover-enter and before the move's own pointer-leave
3140        // bookkeeping.
3141        for content_id in ctx.safe_region_arm_requests {
3142            if let Some(apex) = self
3143                .previous_pointer_position
3144                .or_else(|| self.hover_owner_position())
3145            {
3146                self.overlay_manager.arm_safe_region(
3147                    content_id,
3148                    apex,
3149                    std::time::Instant::now(),
3150                    self.sim_clock,
3151                );
3152            }
3153        }
3154        for id in ctx.repaint_requests {
3155            self.arena.mark_needs_paint(id);
3156        }
3157        for id in ctx.synthetic_clicks {
3158            // Over the caller's ops, never a standalone dispatch: the
3159            // tapped widget's own handler runs inside this nested
3160            // dispatch, so a standalone one would deny it the
3161            // multi-window API this dispatch already has in hand.
3162            self.synthesise_tap_with_ops(id, &mut *ops);
3163        }
3164        // `clear_focus` first: it emptied the two request lists as it was
3165        // queued, so anything still in them was requested after it.
3166        if ctx.clear_focus_requested {
3167            self.clear_focus_ops(&mut *ops);
3168        }
3169        // Both request kinds go through `request_focus_with_ops`, which holds
3170        // a request for a dormant node (a page this same handler is showing)
3171        // until the next layout pass has woken it, rather than focusing a node
3172        // the pass's revalidation would take focus straight back from.
3173        if let Some(&id) = ctx.focus_requests.last() {
3174            // If the requested widget is itself not focusable (e.g. a
3175            // composite like `TextInput` whose focus-handling lives on
3176            // an inner leaf), walk into the subtree and land on the
3177            // first focusable descendant in document order. This makes
3178            // `ctx.request_focus(some_composite)` Do The Right Thing
3179            // without every caller having to reach into private inner
3180            // ids. `first_focusable_descendant` returns the node itself
3181            // when it's focusable, so the usual leaf-target case is
3182            // still a no-op lookup.
3183            self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
3184        }
3185        if let Some(&id) = ctx.focus_into_requests.last() {
3186            // "Focus into" semantics: land on the first focusable descendant
3187            // and — unlike `focus_requests` above — do NOT fall back to the
3188            // container itself. A region with no focusable content (and not
3189            // focusable in its own right) leaves focus untouched rather than
3190            // trapping it on a non-interactive node. Drives Enter-on-a-tab →
3191            // into the tab panel.
3192            self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
3193        }
3194
3195        // Rect-based "scroll this into view" requests (`ctx.ensure_visible`).
3196        // Walk outward from the widget whose handler queued the request and
3197        // reveal the rect inside every enclosing scroll container. Run after
3198        // focus so that if the same handler also moved focus, both follows
3199        // settle against the same (pre-relayout) bounds; each dispatch is
3200        // gated on the container not already showing the rect, so ordering is
3201        // harmless. The source widget itself is excluded from the walk — it
3202        // owns revealing an interior rect inside its own viewport.
3203        for req in ctx.scroll_into_view_requests {
3204            self.scroll_rect_into_view(
3205                // Whoever the rect belongs to — the source widget unless the caller
3206                // named another. See `EventContext::ensure_visible_from`.
3207                req.from.unwrap_or(source_widget),
3208                req.rect,
3209                req.margin,
3210                req.align,
3211                req.motion,
3212                &mut *ops,
3213            );
3214        }
3215        // Id-based `ctx.ensure_widget_visible`: resolve to the target's current
3216        // absolute bounds and walk *its* ancestors (skip if it was destroyed
3217        // before the drain). Walking from the target — not `source_widget` —
3218        // means the request reveals that widget wherever it sits, even when the
3219        // handler runs on a different node (a group's roving-key handler
3220        // revealing the child tile it just selected).
3221        for (id, margin) in ctx.scroll_widget_into_view_requests {
3222            if self.arena.get(id).is_some() {
3223                let bounds = self.arena.bounds(id);
3224                self.scroll_rect_into_view(
3225                    id,
3226                    bounds,
3227                    margin,
3228                    crate::event::ScrollAlign::Minimal,
3229                    crate::event::ScrollMotion::Instant,
3230                    &mut *ops,
3231                );
3232            }
3233        }
3234
3235        // Keyboard-highlight tooltip: surface the highlighted (menu) item's
3236        // tooltip immediately and dismiss the previously-highlighted one. Keyed
3237        // on the item id, NOT real focus (which stays on the menu panel for key
3238        // handling). Only the last request per handler is honoured.
3239        if let Some(&id) = ctx.highlight_tooltip_requests.last() {
3240            self.show_highlight_tooltip(id, &mut *ops);
3241        }
3242
3243        // --- Drag and drop ---
3244        if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
3245            let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
3246                // `add_boxed` — NOT `arena.insert` — runs the widget's
3247                // `build()` so composite previews (our `DragPreview`
3248                // wrapper in teksilo-widgets, or anything a user supplies)
3249                // actually instantiate their child subtree. Plain
3250                // `arena.insert` stops at the root node, leaves build
3251                // un-fired, and the overlay renders an empty widget.
3252                let content_id = self.add_boxed(preview);
3253                let overlay_id = self
3254                    .overlay_manager
3255                    .show(crate::overlay::OverlayRequest::new(
3256                        content_id,
3257                        source_widget,
3258                        crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
3259                        crate::overlay::DismissBehavior::Manual,
3260                    ));
3261                // Force the next layout pass to run `position_overlays`
3262                // and `set_content_bounds` — otherwise the preview sits
3263                // at its initial (0, 0) placement forever.
3264                self.arena.mark_needs_layout(content_id);
3265                (Some(content_id), Some(overlay_id))
3266            } else {
3267                (None, None)
3268            };
3269            self.active_drag = Some(crate::drag_state::DragSession {
3270                payload,
3271                // The pointer that armed the drag. `start_drag` is always
3272                // reached from a handler serving a real sample, which is the
3273                // only place this is knowable — from here on the drag runs
3274                // through layout ticks and platform threads that have no
3275                // sample of their own. See `DragSession::pointer`.
3276                pointer: self.current_input.pointer,
3277                source_widget: Some(source_widget),
3278                is_external: false,
3279                current_position: teksilo_canvas::Point::ZERO,
3280                current_target: None,
3281                feedback: crate::drag_state::DropFeedback::NoFeedback,
3282                preview_content_id,
3283                preview_overlay_id,
3284            });
3285            self.set_current_pointer_capture(Some(source_widget));
3286            // Grabbing-hand cursor while the drag is in flight. Reset on
3287            // drop / cancel / source-destroyed below.
3288            self.current_cursor = crate::widget::CursorIcon::Grabbing;
3289        }
3290        if ctx.cancel_drag {
3291            self.cancel_active_drag(&mut *ops);
3292        }
3293
3294        // --- Environment changes (architecture §9.5) ---
3295        if let Some(theme) = ctx.theme_request {
3296            // Stored, not applied: the app layer routes this through
3297            // `WindowManager::set_theme` so every window re-themes, matching
3298            // the app-wide `set_locale` path below. Applying
3299            // `WidgetTree::set_theme` inline would re-theme only the
3300            // originating window.
3301            self.pending_theme_request = Some(theme);
3302        }
3303        if ctx.follow_system_request {
3304            // Stored, not applied: the app layer switches to
3305            // `ThemeMode::Native` and recomputes the theme from the current
3306            // OS colours, fanning it to every window.
3307            self.pending_follow_system_request = true;
3308        }
3309        if let Some(locale) = ctx.locale_request {
3310            // Stored, not applied: the app layer must route this through
3311            // `WindowManager::set_locale` so the `I18nManager`'s active
3312            // locale and direction stay in sync. Applying via
3313            // `WidgetTree::set_locale` alone would leave `tr!` bindings
3314            // reading the old translations.
3315            self.pending_locale_request = Some(locale);
3316        }
3317        if let Some(scale) = ctx.text_scale_request {
3318            // Stored, not applied: the app layer routes this through
3319            // `WindowManager::set_text_scale` so every window re-scales its
3320            // text. Applying `WidgetTree::set_user_text_scale` inline would
3321            // grow only the originating window.
3322            self.pending_text_scale_request = Some(scale);
3323        }
3324        // Last, and with a context of their own: `Space` on a data view's
3325        // focused row runs the row's published toggle, and a checkbox's toggle
3326        // fires the app's `on_change`, which may send an intent or open a
3327        // window. Running them here rather than inside the mutation drain is
3328        // what gives them an `EventContext`; the drain resolved which action to
3329        // run against the live tree and handed it back.
3330        if !deferred_row_activations.is_empty() {
3331            self.run_with_event_context(&mut *ops, move |ctx| {
3332                for action in deferred_row_activations {
3333                    action(ctx);
3334                }
3335            });
3336        }
3337    }
3338
3339    /// Returns the row activations it resolved but could not run: they need an
3340    /// [`EventContext`], and this method has no `ops` to build one from. The
3341    /// caller runs them once the drain is finished, the way
3342    /// [`WidgetTree::run_mount_actions`](crate::WidgetTree::run_mount_actions)
3343    /// does.
3344    #[must_use]
3345    fn apply_tree_mutations(
3346        &mut self,
3347        mutations: Vec<crate::widget::TreeMutation>,
3348    ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
3349        let mut deferred_row_activations = Vec::new();
3350        use crate::binding::BindingLevel;
3351        use crate::widget::TreeMutation;
3352
3353        for mutation in mutations {
3354            match mutation {
3355                TreeMutation::SetDormant(id) => {
3356                    self.park_subtree(id);
3357                }
3358                TreeMutation::Activate(id) => self.arena.activate(id),
3359                TreeMutation::Destroy(id) => {
3360                    // Route through `destroy_subtree`, NOT the bare
3361                    // `arena.destroy`: the latter only unlinks nodes from
3362                    // the slotmap and leaks everything the widget owned —
3363                    // animation-scheduler entries (which hold strong
3364                    // `Signal<f32>` clones, so the widget keeps animating
3365                    // after it's gone), animated-quad slots, event-source
3366                    // subscriptions, registered shortcuts, bindings, and
3367                    // gesture ownership — and leaves `focused`/`hovered`
3368                    // dangling at a removed id. This mirrors the build-time
3369                    // `BuildContext::destroy_subtree`, including dismissing
3370                    // any overlay that still references the subtree so the
3371                    // manager doesn't retain a stale content reference.
3372                    if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
3373                        self.dismiss_overlay(overlay_id);
3374                    }
3375                    self.destroy_subtree(id);
3376                }
3377                // Build now, not next frame: the same handler is about to show
3378                // an overlay over this node and move focus into it, and both
3379                // read the subtree. See `EventContext::materialize_now`.
3380                TreeMutation::MaterializeNow(id) => {
3381                    if self.arena.get(id).is_some() {
3382                        self.rebuild_single_widget(id);
3383                    }
3384                }
3385                TreeMutation::RowSpaceActivate { row, fallback } => {
3386                    // Resolve against the *live* tree: a data view rebuilds its
3387                    // rows as they realize, so the row that was focused when
3388                    // the key arrived may have been rebuilt since.
3389                    //
3390                    // Resolve here, run later. The action carries an
3391                    // `EventContext` so a row's checkbox fires its `on_change`
3392                    // on this path exactly as it does under the pointer; this
3393                    // method has no `ops` to build one from, so the caller runs
3394                    // it after the drain.
3395                    deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
3396                }
3397                TreeMutation::WithWidgetMut { id, dirty, apply } => {
3398                    // Run the typed mutation while `&mut arena` is live, then
3399                    // drop the borrow before dirty-marking (the `mark_*` calls
3400                    // re-borrow the arena). Only dirty-mark a live node so we
3401                    // never call `mark_ancestors_need_layout` on a destroyed id.
3402                    let existed = if let Some(any) =
3403                        self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
3404                    {
3405                        apply(any);
3406                        true
3407                    } else {
3408                        false
3409                    };
3410                    if existed {
3411                        match dirty {
3412                            BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
3413                            BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
3414                            BindingLevel::Relayout => {
3415                                self.arena.mark_needs_layout(id);
3416                                self.arena.mark_ancestors_need_layout(id);
3417                            }
3418                            BindingLevel::Rebuild => {
3419                                self.arena.mark_needs_rebuild(id);
3420                                self.arena.mark_ancestors_need_layout(id);
3421                            }
3422                            BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
3423                        }
3424                    }
3425                }
3426            }
3427        }
3428        deferred_row_activations
3429    }
3430
3431    /// Hit-test at a point for the **mouse, exactly** — the meaning this door
3432    /// has always had, and keeps.
3433    ///
3434    /// A mouse cursor's hot-spot is exact, so neither hit-targeting mechanism
3435    /// applies to it: the outset pre-pass sees zero insets and the miss-only
3436    /// slop pass short-circuits on a zero radius. A caller that holds a pointer
3437    /// should use [`hit_test_for`](Self::hit_test_for) instead, which is the
3438    /// same test for a mouse and the widened one for a finger or a stylus.
3439    pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
3440        self.hit_test_excluding_overlay_and_widget(point, None, None)
3441    }
3442
3443    /// Hit-test at a point on behalf of a named pointer.
3444    ///
3445    /// Runs the exact pass with that pointer's `Widget::hit_outset`, then — only
3446    /// if the exact pass found nothing eligible — the miss-only slop pass. For
3447    /// [`PointerKind::Mouse`](teksilo_tokens::PointerKind::Mouse) this is
3448    /// exactly [`hit_test`](Self::hit_test).
3449    ///
3450    /// Candidates are restricted to the **topmost overlay layer the exact pass
3451    /// entered**: a press inside an open menu can be re-attributed to a menu
3452    /// row, never to a control on the page behind it.
3453    pub fn hit_test_for(
3454        &self,
3455        point: Point,
3456        pointer: &crate::pointer::PointerInfo,
3457    ) -> Option<WidgetId> {
3458        self.hit_test_for_excluding(point, pointer, None, None)
3459    }
3460
3461    /// [`hit_test_for`](Self::hit_test_for) with the drag-and-drop exclusions of
3462    /// [`hit_test_excluding_overlay_and_widget`](Self::hit_test_excluding_overlay_and_widget).
3463    pub fn hit_test_for_excluding(
3464        &self,
3465        point: Point,
3466        pointer: &crate::pointer::PointerInfo,
3467        exclude_overlay: Option<crate::overlay::OverlayId>,
3468        exclude_widget: Option<WidgetId>,
3469    ) -> Option<WidgetId> {
3470        let surfaces = self.text_surfaces();
3471        let read_only = |id: WidgetId| surfaces.is_read_only(id);
3472        let hit =
3473            crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
3474                .direction(self.layout_direction)
3475                .read_only_probe(&read_only);
3476        self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
3477    }
3478
3479    /// Hit-test at a point, excluding a specific overlay and widget from consideration.
3480    /// Used during drag-and-drop to exclude the preview overlay and its content widget,
3481    /// so they don't block hit-testing of the actual drop targets underneath.
3482    ///
3483    /// **Mouse, exact** — the pointer-aware twin is
3484    /// [`hit_test_for_excluding`](Self::hit_test_for_excluding).
3485    pub fn hit_test_excluding_overlay_and_widget(
3486        &self,
3487        point: Point,
3488        exclude_overlay: Option<crate::overlay::OverlayId>,
3489        exclude_widget: Option<WidgetId>,
3490    ) -> Option<WidgetId> {
3491        self.hit_test_with(
3492            point,
3493            exclude_overlay,
3494            exclude_widget,
3495            &crate::pointer::hit_slop::HitContext::mouse(),
3496        )
3497    }
3498
3499    /// The one hit-test body: overlay first, then the arena, under whichever
3500    /// [`HitContext`](crate::pointer::hit_slop::HitContext) the caller built.
3501    pub(crate) fn hit_test_with(
3502        &self,
3503        point: Point,
3504        exclude_overlay: Option<crate::overlay::OverlayId>,
3505        exclude_widget: Option<WidgetId>,
3506        hit: &crate::pointer::hit_slop::HitContext<'_>,
3507    ) -> Option<WidgetId> {
3508        if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
3509            if Some(overlay_id) == exclude_overlay {
3510                // Skip this excluded overlay, fall through to widget tree
3511            } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
3512                // Scoped to the overlay's content: this is what restricts the
3513                // slop pass's candidates to the topmost layer the exact pass
3514                // entered.
3515                let content_id = overlay.content_id;
3516                if let Some(found) =
3517                    self.arena
3518                        .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
3519                {
3520                    return Some(found);
3521                }
3522                // The overlay was chosen by its **bounds** and its content
3523                // claimed nothing at this point. For every ordinary overlay
3524                // that is the end of the search — answering `None` is what
3525                // keeps the slop pass inside the one layer the exact pass
3526                // entered, so a near-miss on a menu row cannot be beaten by a
3527                // control behind the menu.
3528                //
3529                // An overlay whose content root declares `event_pass_through`
3530                // is the exception, because that flag already means "what I did
3531                // not claim belongs to whatever is behind me": the tree's
3532                // walker honours it for an ordinary node *after* its children
3533                // miss, and an overlay root is not an exception to it. Without
3534                // this, a viewport-sized pass-through layer — the placement the
3535                // text-affordance band was written for — stops the surface
3536                // under it taking presses at all. The widening is confined to
3537                // that flag: an overlay that does not set it still returns
3538                // here, so no ordinary overlay's candidate set changes.
3539                if !self
3540                    .arena
3541                    .get(content_id)
3542                    .is_some_and(|node| node.event_pass_through)
3543                {
3544                    return None;
3545                }
3546            }
3547        }
3548
3549        if self.overlay_manager.topmost_centered().is_some() {
3550            return None;
3551        }
3552
3553        // The walk of the arena's roots honours event_pass_through and
3554        // clips_children. An overlay's content is one of those roots, and an
3555        // inert one is left out: the stack has stepped over it above, and a
3556        // point over it with nothing beneath it on the stack would otherwise
3557        // land in it here, wherever its root falls in the walk.
3558        let mut roots = self.arena.roots();
3559        let inert: Vec<WidgetId> = self
3560            .overlay_manager
3561            .stack
3562            .iter()
3563            .filter(|overlay| overlay.inert)
3564            .map(|overlay| overlay.content_id)
3565            .collect();
3566        if !inert.is_empty() {
3567            roots.retain(|root| !inert.contains(root));
3568        }
3569        self.arena
3570            .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
3571    }
3572}
3573
3574/// Whether this keystroke is one of the chords that ask for a context menu.
3575///
3576/// Three routes, because no single one exists on every platform:
3577///
3578/// * **The dedicated key.** `VK_APPS` on Windows, `keysyms::Menu` on X11 and
3579///   Wayland. `winit-0.30.13`'s AppKit backend references
3580///   `NamedKey::ContextMenu` zero times, so macOS never produces it.
3581/// * **Shift+F10.** The convention Windows, GTK and Qt all honour, and the one
3582///   thing a Windows or Linux keyboard without a Menu key can still reach.
3583/// * **Ctrl+Shift+M on macOS.** Neither of the above is available there: Mac
3584///   keyboards have no Menu key, and F10 is a media key under the default
3585///   "Use F1, F2 etc. as standard function keys = off" setting, so Shift+F10
3586///   may never arrive as F10 at all. Kept off the other platforms, where
3587///   Ctrl+Shift+M is a plausible application binding.
3588///
3589/// Modifiers are matched exactly. Shift+F10 with Ctrl held is a different
3590/// gesture and must reach the application unchanged.
3591fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
3592    match key {
3593        Key::ContextMenu => modifiers == Modifiers::NONE,
3594        Key::F10 => modifiers == Modifiers::SHIFT,
3595        #[cfg(target_os = "macos")]
3596        Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
3597        _ => false,
3598    }
3599}
3600
3601#[cfg(test)]
3602mod tests {
3603    use super::*;
3604    use crate::test_widgets::FillWidget;
3605    use crate::widget::CursorIcon;
3606    use crate::widget_builder::WidgetBuilder;
3607
3608    #[test]
3609    fn pointer_enter_leave_synthesized() {
3610        let mut tree = WidgetTree::new();
3611        let widget = tree.add(FillWidget::new());
3612        tree.layout(SizeProposal::exact(100.0, 50.0));
3613        tree.pointer_move(Point::new(50.0, 25.0));
3614        assert_eq!(tree.hovered(), Some(widget));
3615        tree.pointer_move(Point::new(200.0, 200.0));
3616        assert_eq!(tree.hovered(), None);
3617    }
3618
3619    #[test]
3620    fn pointer_hover_updates_current_cursor() {
3621        let mut tree = WidgetTree::new();
3622        tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3623        tree.layout(SizeProposal::exact(100.0, 50.0));
3624
3625        tree.pointer_move(Point::new(50.0, 25.0));
3626        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3627
3628        tree.pointer_move(Point::new(200.0, 200.0));
3629        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3630    }
3631
3632    /// A leaf that declares its cursor from `build()` through
3633    /// `apply_self_handlers`, the route a composing widget takes.
3634    #[derive(Debug)]
3635    struct SelfDeclared(CursorIcon);
3636
3637    impl crate::widget::Widget for SelfDeclared {
3638        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
3639            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
3640            Vec::new()
3641        }
3642
3643        fn layout_response(
3644            &self,
3645            proposal: SizeProposal,
3646            _ctx: &crate::widget::LayoutContext,
3647        ) -> crate::widget::LayoutResponse {
3648            proposal.resolve(100.0, 50.0).into()
3649        }
3650    }
3651
3652    /// A test reads the cursor two ways: `current_cursor` is the window's
3653    /// running answer and follows the hover, `declared_cursor` is what a node
3654    /// asks for and does not care where the pointer is. Both declaration
3655    /// routes (`.cursor(..)` and `apply_self_handlers`) land in the latter,
3656    /// and "declares nothing" is `None`, distinct from declaring `Default`.
3657    #[test]
3658    fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
3659        let over = Point::new(50.0, 25.0);
3660        let away = Point::new(200.0, 200.0);
3661
3662        // `.cursor(..)` through the builder.
3663        let mut tree = WidgetTree::new();
3664        let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
3665        tree.layout(SizeProposal::exact(100.0, 50.0));
3666        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3667        assert_eq!(
3668            tree.current_cursor(),
3669            CursorIcon::Default,
3670            "not hovered yet"
3671        );
3672        tree.pointer_move(over);
3673        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3674        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
3675        tree.pointer_move(away);
3676        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3677        assert_eq!(
3678            tree.declared_cursor(built),
3679            Some(CursorIcon::ColResize),
3680            "the declaration does not depend on the pointer",
3681        );
3682
3683        // `apply_self_handlers` in `build()`.
3684        let mut tree = WidgetTree::new();
3685        let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
3686        tree.layout(SizeProposal::exact(100.0, 50.0));
3687        assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
3688        tree.pointer_move(over);
3689        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3690        tree.pointer_move(away);
3691        assert_eq!(tree.current_cursor(), CursorIcon::Default);
3692
3693        // Nothing declared, and an explicit `Default`.
3694        let mut tree = WidgetTree::new();
3695        let plain = tree.add(FillWidget::new());
3696        tree.layout(SizeProposal::exact(100.0, 50.0));
3697        assert_eq!(tree.declared_cursor(plain), None);
3698        let mut tree = WidgetTree::new();
3699        let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
3700        tree.layout(SizeProposal::exact(100.0, 50.0));
3701        assert_eq!(
3702            tree.declared_cursor(explicit),
3703            Some(CursorIcon::Default),
3704            "declaring the default cursor is not declaring nothing",
3705        );
3706    }
3707
3708    /// A handler's `set_cursor` outlives its dispatch, so a handler that
3709    /// re-decides the cursor on every move needs a way to stop deciding.
3710    ///
3711    /// Going quiet does not do it: the cursor moves only when something writes
3712    /// to it, and the node-declared cursor is written on `PointerEnter` /
3713    /// `PointerLeave` alone — so while the pointer stays inside one node there
3714    /// is no second writer, and the handler's last word simply stands. That is
3715    /// what `release_cursor` withdraws, and it withdraws *to the node's own
3716    /// declaration*, not to `Default`.
3717    #[test]
3718    fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
3719        // Overrides itself to `Crosshair` on the left third of the widget and
3720        // withdraws everywhere else — the shape of any handler that arbitrates
3721        // its own affordance against the node it sits on.
3722        let mut tree = WidgetTree::new();
3723        tree.add(
3724            FillWidget::new()
3725                .cursor(CursorIcon::ColResize)
3726                .on_pointer_event(|event, ctx| {
3727                    if let WidgetEvent::PointerMove { position, .. } = event {
3728                        if position.x < 30.0 {
3729                            ctx.set_cursor(CursorIcon::Crosshair);
3730                        } else {
3731                            ctx.release_cursor();
3732                        }
3733                    }
3734                    EventResponse::Ignored
3735                }),
3736        );
3737        tree.layout(SizeProposal::exact(100.0, 50.0));
3738
3739        tree.pointer_move(Point::new(10.0, 25.0));
3740        assert_eq!(
3741            tree.current_cursor(),
3742            CursorIcon::Crosshair,
3743            "the handler outranks the node it is attached to",
3744        );
3745        tree.pointer_move(Point::new(60.0, 25.0));
3746        assert_eq!(
3747            tree.current_cursor(),
3748            CursorIcon::ColResize,
3749            "and hands it back to the node, not to Default — no enter/leave \
3750             fires on a move within one node, so nothing else could",
3751        );
3752        tree.pointer_move(Point::new(10.0, 25.0));
3753        assert_eq!(
3754            tree.current_cursor(),
3755            CursorIcon::Crosshair,
3756            "and can take it again"
3757        );
3758        tree.pointer_move(Point::new(200.0, 200.0));
3759        assert_eq!(
3760            tree.current_cursor(),
3761            CursorIcon::Default,
3762            "leaving the node clears both the override and the declaration",
3763        );
3764    }
3765
3766    /// The withdrawal is a no-op for a handler that never spoke, and resolves
3767    /// to `Default` when the chain declares nothing — so a handler may call it
3768    /// unconditionally without having to remember whether it once set a cursor.
3769    #[test]
3770    fn release_cursor_is_a_no_op_with_nothing_to_undo() {
3771        let mut tree = WidgetTree::new();
3772        tree.add(
3773            FillWidget::new()
3774                .cursor(CursorIcon::ColResize)
3775                .on_pointer_event(|event, ctx| {
3776                    if matches!(event, WidgetEvent::PointerMove { .. }) {
3777                        ctx.release_cursor();
3778                    }
3779                    EventResponse::Ignored
3780                }),
3781        );
3782        tree.layout(SizeProposal::exact(100.0, 50.0));
3783        tree.pointer_move(Point::new(50.0, 25.0));
3784        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
3785
3786        // Same handler over a node that declares nothing.
3787        let mut tree = WidgetTree::new();
3788        tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
3789            if let WidgetEvent::PointerMove { position, .. } = event {
3790                if position.x < 30.0 {
3791                    ctx.set_cursor(CursorIcon::Crosshair);
3792                } else {
3793                    ctx.release_cursor();
3794                }
3795            }
3796            EventResponse::Ignored
3797        }));
3798        tree.layout(SizeProposal::exact(100.0, 50.0));
3799        tree.pointer_move(Point::new(10.0, 25.0));
3800        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
3801        tree.pointer_move(Point::new(60.0, 25.0));
3802        assert_eq!(
3803            tree.current_cursor(),
3804            CursorIcon::Default,
3805            "nothing declared a cursor for this chain, so the hand-back \
3806             resolves to Default",
3807        );
3808    }
3809
3810    /// A handler that speaks during the `PointerEnter` itself still outranks
3811    /// the node it entered — and the node's declaration is remembered anyway,
3812    /// so the hand-back has somewhere to go.
3813    ///
3814    /// The two used to share one slot, where the handler's later write erased
3815    /// the declaration outright; they are separate channels now precisely so
3816    /// this case keeps both.
3817    #[test]
3818    fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
3819        let mut tree = WidgetTree::new();
3820        tree.add(
3821            FillWidget::new()
3822                .cursor(CursorIcon::ColResize)
3823                .on_hover(|entered, ctx| {
3824                    if entered {
3825                        ctx.set_cursor(CursorIcon::Crosshair);
3826                    }
3827                })
3828                .on_pointer_event(|event, ctx| {
3829                    if let WidgetEvent::PointerMove { position, .. } = event
3830                        && position.x >= 30.0
3831                    {
3832                        ctx.release_cursor();
3833                    }
3834                    EventResponse::Ignored
3835                }),
3836        );
3837        tree.layout(SizeProposal::exact(100.0, 50.0));
3838
3839        tree.pointer_move(Point::new(10.0, 25.0));
3840        assert_eq!(
3841            tree.current_cursor(),
3842            CursorIcon::Crosshair,
3843            "on_hover runs after the node cursor is applied, so it wins",
3844        );
3845        tree.pointer_move(Point::new(60.0, 25.0));
3846        assert_eq!(
3847            tree.current_cursor(),
3848            CursorIcon::ColResize,
3849            "and the node's declaration survived being overridden",
3850        );
3851    }
3852
3853    // A leaf that opts into typed introspection, so `with_widget_mut` /
3854    // `widget_as_any(_mut)` can reach it (the default `as_any_mut` is `None`).
3855    #[derive(Debug)]
3856    struct Bumpable {
3857        value: i32,
3858    }
3859
3860    impl crate::widget::Widget for Bumpable {
3861        fn layout_response(
3862            &self,
3863            proposal: SizeProposal,
3864            _ctx: &crate::widget::LayoutContext,
3865        ) -> crate::widget::LayoutResponse {
3866            proposal.resolve(10.0, 10.0).into()
3867        }
3868        fn as_any(&self) -> Option<&dyn std::any::Any> {
3869            Some(self)
3870        }
3871        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3872            Some(self)
3873        }
3874    }
3875
3876    #[test]
3877    fn with_widget_mut_applies_and_dirty_marks() {
3878        let mut tree = WidgetTree::new();
3879        let id = tree.add(Bumpable { value: 0 });
3880        tree.layout(SizeProposal::exact(100.0, 100.0));
3881
3882        let mut ctx = EventContext::new();
3883        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
3884            b.value = 42;
3885        });
3886        tree.collect_from_ctx(ctx, id);
3887
3888        let value = tree
3889            .widget_as_any(id)
3890            .and_then(|a| a.downcast_ref::<Bumpable>())
3891            .map(|b| b.value);
3892        assert_eq!(
3893            value,
3894            Some(42),
3895            "the deferred closure must mutate the live widget"
3896        );
3897        assert!(
3898            tree.needs_layout(),
3899            "Relayout dirty level must mark the tree for relayout"
3900        );
3901    }
3902
3903    #[test]
3904    #[cfg(debug_assertions)]
3905    #[should_panic(expected = "not the requested type")]
3906    fn with_widget_mut_wrong_type_panics_in_debug() {
3907        struct Other;
3908        let mut tree = WidgetTree::new();
3909        let id = tree.add(Bumpable { value: 0 });
3910        let mut ctx = EventContext::new();
3911        ctx.with_widget_mut::<Other>(
3912            id,
3913            crate::binding::BindingLevel::RepaintOnly,
3914            |_o: &mut Other| {},
3915        );
3916        // Bumpable opts into as_any_mut, so the closure runs and the
3917        // wrong-type downcast trips the debug_assert.
3918        tree.collect_from_ctx(ctx, id);
3919    }
3920
3921    #[test]
3922    fn with_widget_mut_closure_may_fire_observed_signals() {
3923        // Reentrancy guard. The closure runs inside `apply_tree_mutations`
3924        // while the target arena node is mutably borrowed. If it fires a
3925        // `Signal` whose observer sets *another* signal — the exact
3926        // `SceneView` shape (`item_change_signal` → bump `reconcile_dirty`) —
3927        // nothing may double-borrow the arena. The arena borrow is scoped to
3928        // the closure call and dropped before dirty-marking; signal/observer
3929        // work touches the binding registry, not the arena.
3930        use crate::signal::Signal;
3931        let mut tree = WidgetTree::new();
3932        let id = tree.add(Bumpable { value: 0 });
3933        tree.layout(SizeProposal::exact(100.0, 100.0));
3934
3935        let trigger = Signal::new(0_u64);
3936        let echo = Signal::new(0_u64);
3937        let echo_for_obs = echo.clone();
3938        let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
3939
3940        let trigger_in = trigger.clone();
3941        let mut ctx = EventContext::new();
3942        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
3943            b.value = 7;
3944            // Fires `_obs` synchronously, mid-deferred-apply.
3945            trigger_in.set(99);
3946        });
3947        tree.collect_from_ctx(ctx, id); // must not panic / double-borrow
3948
3949        assert_eq!(
3950            echo.get(),
3951            99,
3952            "the observer ran during the deferred mutation"
3953        );
3954        let value = tree
3955            .widget_as_any(id)
3956            .and_then(|a| a.downcast_ref::<Bumpable>())
3957            .map(|b| b.value);
3958        assert_eq!(value, Some(7));
3959    }
3960
3961    #[test]
3962    fn request_accessibility_update_forces_rewalk() {
3963        let mut tree = WidgetTree::new();
3964        let id = tree.add(Bumpable { value: 0 });
3965        tree.layout(SizeProposal::exact(100.0, 100.0));
3966        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
3967        assert!(
3968            !tree.a11y_dirty,
3969            "sync_accessibility should clear the dirty flag"
3970        );
3971
3972        let mut ctx = EventContext::new();
3973        ctx.request_accessibility_update();
3974        tree.collect_from_ctx(ctx, id);
3975        assert!(
3976            tree.a11y_dirty,
3977            "request_accessibility_update must force an AT re-walk"
3978        );
3979    }
3980
3981    #[test]
3982    fn rebuild_dirties_accessibility_tree() {
3983        // Regression for audit Blocker G1: every `BindingLevel::Rebuild`
3984        // consumer (ListView / TreeView / TableView / ComboBox / Calendar /
3985        // DockingLayout / ...) tears down and re-creates its subtree on an
3986        // ordinary model change, allocating fresh WidgetIds and changing the
3987        // AccessKit tree shape. That pass must dirty the cached AT snapshot,
3988        // or screen readers keep reading the pre-mutation tree indefinitely.
3989        let mut tree = WidgetTree::new();
3990        let id = tree.add(Bumpable { value: 0 });
3991        tree.layout(SizeProposal::exact(100.0, 100.0));
3992        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
3993        assert!(
3994            !tree.a11y_dirty,
3995            "sync_accessibility should clear the dirty flag"
3996        );
3997
3998        // Marking for rebuild is exactly what a Rebuild-level binding does;
3999        // the following layout pass drains pending rebuilds.
4000        tree.arena_mark_needs_rebuild_for_testing(id);
4001        tree.layout(SizeProposal::exact(100.0, 100.0));
4002        assert!(
4003            tree.a11y_dirty,
4004            "a rebuild must dirty the AT tree so the next sync re-walks"
4005        );
4006    }
4007
4008    #[test]
4009    fn bound_access_label_change_dirties_accessibility_tree() {
4010        use crate::signal::Signal;
4011        use crate::test_widgets::FillWidget;
4012        use crate::widget_builder::WidgetBuilder;
4013
4014        // Regression for audit G15: a reactive `.access_label(signal)` (and
4015        // likewise description / value) must register at AccessibilityOnly so
4016        // changing the signal re-walks the AT tree and re-resolves the
4017        // announced name. Previously only `access_hidden` was registered, so
4018        // label / description / value updates were invisible to screen readers.
4019        let label = Signal::new("first".to_string());
4020        let mut tree = WidgetTree::new();
4021        let _id = tree.add(FillWidget::new().access_label(label.clone()));
4022        tree.layout(SizeProposal::exact(100.0, 100.0));
4023        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4024        assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4025
4026        label.set("second".to_string());
4027        tree.layout(SizeProposal::exact(100.0, 100.0));
4028        assert!(
4029            tree.a11y_dirty,
4030            "changing a bound access_label must dirty the AT tree"
4031        );
4032    }
4033
4034    #[test]
4035    fn disabled_ancestor_blocks_event_to_descendant() {
4036        use crate::signal::Signal;
4037        use crate::test_widgets::StackWidget;
4038        use std::cell::Cell;
4039        use std::rc::Rc;
4040
4041        let tapped = Rc::new(Cell::new(false));
4042        let flag = tapped.clone();
4043        let enabled = Signal::new(true);
4044
4045        let mut tree = WidgetTree::new();
4046        let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4047            flag.set(true);
4048        }));
4049        let parent = tree.add(StackWidget::new().child(child));
4050        tree.enabled_when(parent, enabled.clone());
4051        tree.layout(SizeProposal::exact(100.0, 50.0));
4052
4053        enabled.set(false);
4054        tree.click(child);
4055        assert!(
4056            !tapped.get(),
4057            "disabled ancestor should block descendant tap"
4058        );
4059
4060        enabled.set(true);
4061        tree.click(child);
4062        assert!(tapped.get(), "re-enabling should restore dispatch");
4063    }
4064
4065    #[test]
4066    fn pointer_positions_are_widget_local_at_nonzero_origin() {
4067        use crate::event::{Modifiers, PointerButton};
4068        use crate::test_widgets::InsetWidget;
4069        use std::cell::Cell;
4070        use std::rc::Rc;
4071
4072        // A 20px inset places the child at window origin (20, 20).
4073        let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4074        let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4075        let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4076        let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4077
4078        let mut tree = WidgetTree::new();
4079        let child = tree.add(
4080            FillWidget::new()
4081                .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
4082                .on_pointer_event(move |ev, _ctx| {
4083                    if let WidgetEvent::PointerDown { position, .. } = ev {
4084                        dp.set(Some(*position));
4085                    }
4086                    crate::event::EventResponse::Ignored
4087                })
4088                .on_drag(move |phase, _ctx| {
4089                    use crate::gesture::DragPhase;
4090                    match phase {
4091                        DragPhase::Started { position, .. }
4092                        | DragPhase::Moved { position, .. }
4093                        | DragPhase::Ended { position, .. } => gp.set(Some(position)),
4094                        _ => {}
4095                    }
4096                }),
4097        );
4098        let inset = tree.add(InsetWidget::new(20.0).set_child(child));
4099        let _ = inset;
4100        tree.layout(SizeProposal::exact(200.0, 200.0));
4101        assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
4102
4103        // A tap at window (50, 40) must reach the handler as local (30, 20).
4104        tree.dispatch_event(WidgetEvent::pointer_down(
4105            Point::new(50.0, 40.0),
4106            PointerButton::Primary,
4107            Modifiers::NONE,
4108        ));
4109        assert_eq!(
4110            down_pos.get(),
4111            Some(Point::new(30.0, 20.0)),
4112            "on_pointer_event PointerDown must be widget-local"
4113        );
4114        tree.dispatch_event(WidgetEvent::pointer_up(
4115            Point::new(50.0, 40.0),
4116            PointerButton::Primary,
4117            Modifiers::NONE,
4118        ));
4119        assert_eq!(
4120            tap_pos.get(),
4121            Some(Point::new(30.0, 20.0)),
4122            "on_tap position must be widget-local"
4123        );
4124
4125        // A drag (down then a move past the recognizer threshold) must
4126        // also deliver widget-local coordinates.
4127        tree.dispatch_event(WidgetEvent::pointer_down(
4128            Point::new(50.0, 40.0),
4129            PointerButton::Primary,
4130            Modifiers::NONE,
4131        ));
4132        // First move crosses the recognizer threshold (DragStarted);
4133        // the second reports a known DragMoved position.
4134        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
4135        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
4136        assert_eq!(
4137            drag_pos.get(),
4138            Some(Point::new(70.0, 50.0)),
4139            "on_drag position must be widget-local"
4140        );
4141    }
4142
4143    #[test]
4144    fn dormant_widget_not_hit_tested() {
4145        let mut tree = WidgetTree::new();
4146        let widget = tree.add(FillWidget::new());
4147        tree.layout(SizeProposal::exact(100.0, 50.0));
4148
4149        tree.pointer_move(Point::new(50.0, 25.0));
4150        assert_eq!(tree.hovered(), Some(widget));
4151
4152        tree.set_dormant(widget);
4153        tree.pointer_move(Point::new(200.0, 200.0));
4154        tree.pointer_move(Point::new(50.0, 25.0));
4155        assert_eq!(tree.hovered(), None);
4156    }
4157
4158    #[test]
4159    fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
4160        use crate::event::EventResponse;
4161        use crate::test_widgets::StackWidget;
4162        use std::cell::Cell;
4163        use std::rc::Rc;
4164
4165        // The child reports its own hover transitions via `on_hover`.
4166        let hovered = Rc::new(Cell::new(false));
4167        let h = hovered.clone();
4168
4169        let mut tree = WidgetTree::new();
4170        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
4171        // An ancestor whose `on_pointer_event` greedily claims everything it
4172        // previews — exactly the "drag-detecting ancestor" footgun. Before the
4173        // fix it consumed the descendant's `PointerEnter`/`Leave` in the
4174        // preview pass and the child's hover never fired.
4175        tree.add(
4176            StackWidget::new()
4177                .child(child)
4178                .on_pointer_event(|_event, _ctx| EventResponse::Handled),
4179        );
4180        tree.layout(SizeProposal::exact(100.0, 50.0));
4181
4182        tree.pointer_move(Point::new(50.0, 25.0));
4183        assert!(
4184            hovered.get(),
4185            "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
4186        );
4187
4188        tree.pointer_move(Point::new(500.0, 500.0));
4189        assert!(
4190            !hovered.get(),
4191            "PointerLeave must likewise reach the child despite the ancestor"
4192        );
4193    }
4194
4195    /// **The other direction: a child must not swallow its ancestor's hover.**
4196    ///
4197    /// A row that reveals controls on hover puts interactive children inside
4198    /// itself, and the pointer leaves the row *through* one of them. The child's
4199    /// own `on_hover` used to handle the `PointerLeave` and stop the bubble there,
4200    /// so the row went on believing the pointer was still over it and kept its
4201    /// controls showing after the pointer had gone.
4202    #[test]
4203    fn a_child_hover_handler_does_not_swallow_its_ancestors() {
4204        use crate::test_widgets::StackWidget;
4205        use std::cell::Cell;
4206        use std::rc::Rc;
4207
4208        let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
4209        let (r, b) = (row.clone(), button.clone());
4210
4211        let mut tree = WidgetTree::new();
4212        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
4213        tree.add(
4214            StackWidget::new()
4215                .child(child)
4216                .on_hover(move |entered, _ctx| r.set(entered)),
4217        );
4218        tree.layout(SizeProposal::exact(100.0, 50.0));
4219
4220        tree.pointer_move(Point::new(50.0, 25.0));
4221        assert!(button.get(), "the child is hovered");
4222        assert!(row.get(), "and so is the row it is inside");
4223
4224        tree.pointer_move(Point::new(500.0, 500.0));
4225        assert!(!button.get(), "the child heard the leave");
4226        assert!(
4227            !row.get(),
4228            "and so did the row — a container is not still hovered because the \
4229             pointer left it through a button"
4230        );
4231    }
4232
4233    // NOTE: legacy `shortcut_intercepts_before_widget` test removed with
4234    // the ShortcutMap dispatch path. The new shortcut→intent interception
4235    // is built on top of `ShortcutRegistry` + `Action`.
4236
4237    // ── on_key_preview ──────────────────────────────────────────
4238
4239    #[test]
4240    fn key_preview_consumes_before_focused_on_key() {
4241        // root → mid → leaf (focused). Root consumes Enter via
4242        // on_key_preview; the leaf's on_key must NOT fire.
4243        use crate::event::EventResponse;
4244        use crate::test_widgets::StackWidget;
4245        use std::cell::Cell;
4246        use std::rc::Rc;
4247
4248        let leaf_fired = Rc::new(Cell::new(false));
4249        let leaf_flag = leaf_fired.clone();
4250        let preview_fired = Rc::new(Cell::new(false));
4251        let preview_flag = preview_fired.clone();
4252
4253        let mut tree = WidgetTree::new();
4254        let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
4255            // Only count KeyDown so the trailing KeyUp from
4256            // press_key doesn't trigger us spuriously.
4257            if matches!(event, WidgetEvent::KeyDown { .. }) {
4258                leaf_flag.set(true);
4259            }
4260            EventResponse::Handled
4261        }));
4262        let mid = tree.add(StackWidget::new().child(leaf));
4263        let _root = tree.add(
4264            StackWidget::new()
4265                .child(mid)
4266                .on_key_preview(move |event, _c| match event {
4267                    WidgetEvent::KeyDown {
4268                        key: Key::Enter, ..
4269                    } => {
4270                        preview_flag.set(true);
4271                        EventResponse::Handled
4272                    }
4273                    _ => EventResponse::Ignored,
4274                }),
4275        );
4276
4277        tree.layout(SizeProposal::exact(100.0, 50.0));
4278        tree.focus(leaf);
4279        tree.press_key(Key::Enter, Modifiers::NONE);
4280
4281        assert!(
4282            preview_fired.get(),
4283            "ancestor on_key_preview must fire for KeyDown on a focused descendant"
4284        );
4285        assert!(
4286            !leaf_fired.get(),
4287            "consuming the event in preview must prevent the focused widget's on_key from running"
4288        );
4289    }
4290
4291    #[test]
4292    fn key_preview_falls_through_when_returning_ignored() {
4293        // Same shape; this time the preview returns Ignored, so
4294        // the leaf's on_key must still fire.
4295        use crate::event::EventResponse;
4296        use crate::test_widgets::StackWidget;
4297        use std::cell::Cell;
4298        use std::rc::Rc;
4299
4300        let leaf_fired = Rc::new(Cell::new(false));
4301        let leaf_flag = leaf_fired.clone();
4302        let preview_fired = Rc::new(Cell::new(false));
4303        let preview_flag = preview_fired.clone();
4304
4305        let mut tree = WidgetTree::new();
4306        let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
4307            leaf_flag.set(true);
4308            EventResponse::Handled
4309        }));
4310        let mid = tree.add(StackWidget::new().child(leaf));
4311        let _root = tree.add(
4312            StackWidget::new()
4313                .child(mid)
4314                .on_key_preview(move |_event, _c| {
4315                    preview_flag.set(true);
4316                    EventResponse::Ignored
4317                }),
4318        );
4319
4320        tree.layout(SizeProposal::exact(100.0, 50.0));
4321        tree.focus(leaf);
4322        tree.press_key(Key::Enter, Modifiers::NONE);
4323
4324        assert!(preview_fired.get(), "preview must always be invoked");
4325        assert!(
4326            leaf_fired.get(),
4327            "preview returning Ignored must not block the focused widget's on_key"
4328        );
4329    }
4330
4331    #[test]
4332    fn key_preview_excludes_focused_target_itself() {
4333        // Strict-ancestors-only: the focused widget's own
4334        // on_key_preview must NOT fire — the preview pass walks
4335        // strict ancestors only.
4336        use crate::event::EventResponse;
4337        use std::cell::Cell;
4338        use std::rc::Rc;
4339
4340        let preview_on_target = Rc::new(Cell::new(false));
4341        let pf = preview_on_target.clone();
4342
4343        let mut tree = WidgetTree::new();
4344        let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
4345            pf.set(true);
4346            EventResponse::Handled
4347        }));
4348        tree.layout(SizeProposal::exact(100.0, 50.0));
4349        tree.focus(leaf);
4350        tree.press_key(Key::Enter, Modifiers::NONE);
4351
4352        assert!(
4353            !preview_on_target.get(),
4354            "the focused widget itself must not see its own on_key_preview"
4355        );
4356    }
4357
4358    #[test]
4359    fn key_preview_root_to_target_order() {
4360        // Two ancestors with on_key_preview attached. The outer
4361        // (root-side) one must fire first; the closer one (still
4362        // ancestor of the focused leaf) fires second.
4363        use crate::event::EventResponse;
4364        use crate::test_widgets::StackWidget;
4365        use std::cell::RefCell;
4366        use std::rc::Rc;
4367
4368        let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
4369        let outer_log = order.clone();
4370        let inner_log = order.clone();
4371
4372        let mut tree = WidgetTree::new();
4373        let leaf = tree.add(FillWidget::new().focusable());
4374        let inner = tree.add(
4375            StackWidget::new()
4376                .child(leaf)
4377                .on_key_preview(move |event, _c| {
4378                    if matches!(event, WidgetEvent::KeyDown { .. }) {
4379                        inner_log.borrow_mut().push("inner");
4380                    }
4381                    EventResponse::Ignored
4382                }),
4383        );
4384        let _outer = tree.add(
4385            StackWidget::new()
4386                .child(inner)
4387                .on_key_preview(move |event, _c| {
4388                    if matches!(event, WidgetEvent::KeyDown { .. }) {
4389                        outer_log.borrow_mut().push("outer");
4390                    }
4391                    EventResponse::Ignored
4392                }),
4393        );
4394
4395        tree.layout(SizeProposal::exact(100.0, 50.0));
4396        tree.focus(leaf);
4397        tree.dispatch_event(WidgetEvent::KeyDown {
4398            key: Key::Enter,
4399            modifiers: Modifiers::NONE,
4400            text: None,
4401        });
4402
4403        assert_eq!(
4404            *order.borrow(),
4405            vec!["outer", "inner"],
4406            "preview must walk root → parent-of-target"
4407        );
4408    }
4409
4410    #[test]
4411    fn access_action_routes_to_cursored_target_not_focus() {
4412        // VoiceOver's VO+Space targets the node under the AT cursor (`b`),
4413        // even when keyboard focus is on a different control (`a`). The action
4414        // must fire on `b`, never get redirected to the focused `a`.
4415        use crate::signal::Signal;
4416        let a_fired = Signal::new(false);
4417        let b_fired = Signal::new(false);
4418        let a_cb = a_fired.clone();
4419        let b_cb = b_fired.clone();
4420
4421        let mut tree = WidgetTree::new();
4422        let a = tree.add(
4423            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
4424        );
4425        let b = tree.add(
4426            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
4427        );
4428        tree.layout(SizeProposal::exact(200.0, 100.0));
4429
4430        tree.focus(a);
4431        tree.dispatch_event(WidgetEvent::AccessAction {
4432            action: accesskit::Action::Click,
4433            target: Some(b),
4434            target_node: crate::accessibility::widget_id_to_node_id(b),
4435            data: None,
4436        });
4437
4438        assert!(b_fired.get(), "the cursored target must receive the action");
4439        assert!(
4440            !a_fired.get(),
4441            "the keyboard-focused widget must NOT receive an action targeting another node"
4442        );
4443    }
4444
4445    #[test]
4446    fn access_action_without_target_is_dropped_not_redirected_to_focus() {
4447        // An action with no (or an inactive) target must be dropped — never
4448        // silently re-routed to whatever holds keyboard focus.
4449        use crate::signal::Signal;
4450        let fired = Signal::new(false);
4451        let cb = fired.clone();
4452
4453        let mut tree = WidgetTree::new();
4454        let widget = tree.add(
4455            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
4456        );
4457        tree.layout(SizeProposal::exact(200.0, 100.0));
4458
4459        tree.focus(widget);
4460        tree.dispatch_event(WidgetEvent::AccessAction {
4461            action: accesskit::Action::Click,
4462            target: None,
4463            target_node: crate::accessibility::root_node_id(),
4464            data: None,
4465        });
4466
4467        assert!(
4468            !fired.get(),
4469            "a target-less action must not be redirected to the focused widget"
4470        );
4471    }
4472
4473    // NOTE: legacy `scoped_shortcut_fires_when_focused_in_subtree` test
4474    // removed along with the ShortcutMap dispatch path. Scope-aware
4475    // dispatch is handled by the new ShortcutRegistry.
4476
4477    // --- Intent / Action dispatch ------------------------------
4478
4479    #[test]
4480    fn shortcut_fires_matching_action_on_source_widget() {
4481        use crate::action::Action;
4482        use crate::shortcut::{KeyStroke, Shortcut};
4483        use std::cell::Cell;
4484        use std::rc::Rc;
4485
4486        let fired = Rc::new(Cell::new(false));
4487        let fired_flag = fired.clone();
4488
4489        let mut tree = WidgetTree::new();
4490        let widget = tree.add(FillWidget::new().focusable());
4491        tree.push_action(
4492            widget,
4493            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4494                fired_flag.set(true);
4495            }),
4496        );
4497        tree.shortcut_registry_mut().register(
4498            Shortcut::new("app.save")
4499                .primary(KeyStroke::command(Key::S))
4500                .build(),
4501        );
4502
4503        tree.layout(SizeProposal::exact(100.0, 50.0));
4504        tree.focus(widget);
4505
4506        tree.press_key(Key::S, Modifiers::COMMAND);
4507        assert!(fired.get(), "matching action must fire on KeyDown");
4508    }
4509
4510    #[test]
4511    fn global_shortcut_fires_without_focused_widget() {
4512        use crate::action::Action;
4513        use crate::shortcut::{KeyStroke, Shortcut};
4514        use std::cell::Cell;
4515        use std::rc::Rc;
4516
4517        // Regression: a global shortcut must fire even when no widget
4518        // is focused. A root-registered action should still receive
4519        // the intent (anchored at the root as a fallback).
4520        let fired = Rc::new(Cell::new(false));
4521        let fired_flag = fired.clone();
4522
4523        let mut tree = WidgetTree::new();
4524        let root = tree.add(FillWidget::new());
4525        tree.push_action(
4526            root,
4527            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4528                fired_flag.set(true);
4529            }),
4530        );
4531        tree.shortcut_registry_mut().register(
4532            Shortcut::new("app.save")
4533                .primary(KeyStroke::command(Key::S))
4534                .build(),
4535        );
4536
4537        tree.layout(SizeProposal::exact(100.0, 50.0));
4538        // Deliberately no focus() call.
4539
4540        tree.press_key(Key::S, Modifiers::COMMAND);
4541        assert!(
4542            fired.get(),
4543            "global shortcut must fire without a focused widget"
4544        );
4545    }
4546
4547    #[test]
4548    fn global_shortcut_fires_after_focused_widget_destroyed() {
4549        use crate::action::Action;
4550        use crate::shortcut::{KeyStroke, Shortcut};
4551        use std::cell::Cell;
4552        use std::rc::Rc;
4553
4554        // Regression: if the focused widget is destroyed (e.g. during a
4555        // rebuild after a settings-panel rebind), focus must be cleared
4556        // so the next global shortcut falls through to the root-anchor
4557        // path instead of dispatching from a stale, destroyed id.
4558        let fired = Rc::new(Cell::new(false));
4559        let fired_flag = fired.clone();
4560
4561        let mut tree = WidgetTree::new();
4562        let root = tree.add(FillWidget::new());
4563        let focusable = tree.add_child(root, FillWidget::new().focusable());
4564        tree.push_action(
4565            root,
4566            Action::new("app.save").on_invoke(move |_intent, _ctx| {
4567                fired_flag.set(true);
4568            }),
4569        );
4570        tree.shortcut_registry_mut().register(
4571            Shortcut::new("app.save")
4572                .primary(KeyStroke::command(Key::S))
4573                .build(),
4574        );
4575
4576        tree.layout(SizeProposal::exact(100.0, 50.0));
4577        tree.focus(focusable);
4578        assert_eq!(tree.focused(), Some(focusable));
4579
4580        // Destroy the focused subtree (simulates a rebuild that drops
4581        // the currently-focused Rebind button).
4582        tree.destroy_subtree(focusable);
4583        assert_eq!(tree.focused(), None, "focus must clear when destroyed");
4584
4585        tree.press_key(Key::S, Modifiers::COMMAND);
4586        assert!(
4587            fired.get(),
4588            "global shortcut must still fire after the focused widget is destroyed"
4589        );
4590    }
4591
4592    #[test]
4593    fn scoped_shortcut_matches_only_when_focus_in_scope() {
4594        use crate::action::Action;
4595        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4596        use std::cell::Cell;
4597        use std::rc::Rc;
4598
4599        let fired = Rc::new(Cell::new(0));
4600        let fired_flag = fired.clone();
4601
4602        let mut tree = WidgetTree::new();
4603        let scope_root = tree.add(FillWidget::new().focusable());
4604        let inside = tree.add_child(scope_root, FillWidget::new().focusable());
4605        let outside = tree.add(FillWidget::new().focusable());
4606
4607        tree.push_action(
4608            scope_root,
4609            Action::new("editor.find").on_invoke(move |_i, _c| {
4610                fired_flag.set(fired_flag.get() + 1);
4611            }),
4612        );
4613        tree.shortcut_registry_mut().register(
4614            Shortcut::new("editor.find")
4615                .primary(KeyStroke::command(Key::F))
4616                .scope(ShortcutScope::Scoped(scope_root))
4617                .build(),
4618        );
4619
4620        tree.layout(SizeProposal::exact(200.0, 100.0));
4621
4622        // Focus outside the scope: the shortcut does NOT activate.
4623        tree.focus(outside);
4624        tree.press_key(Key::F, Modifiers::COMMAND);
4625        assert_eq!(
4626            fired.get(),
4627            0,
4628            "scoped shortcut must not fire outside scope"
4629        );
4630
4631        // Focus inside the scope: it fires.
4632        tree.focus(inside);
4633        tree.press_key(Key::F, Modifiers::COMMAND);
4634        assert_eq!(
4635            fired.get(),
4636            1,
4637            "scoped shortcut must fire when focus in scope"
4638        );
4639    }
4640
4641    #[test]
4642    fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
4643        // Defect 1: a Scoped binding that sorts first by id must NOT
4644        // shadow the slot when focus is outside its subtree — the
4645        // applicable Global binding fires instead. (`editor.saveBlock`
4646        // < `zzz.global.save`, so the scoped one wins the id-order race
4647        // that `find_by_keystroke` used to settle on.)
4648        use crate::action::Action;
4649        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4650        use std::cell::Cell;
4651        use std::rc::Rc;
4652
4653        let scoped_fired = Rc::new(Cell::new(0));
4654        let global_fired = Rc::new(Cell::new(0));
4655        let sf = scoped_fired.clone();
4656        let gf = global_fired.clone();
4657
4658        let mut tree = WidgetTree::new();
4659        let root = tree.add(FillWidget::new());
4660        let editor = tree.add_child(root, FillWidget::new().focusable());
4661        let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4662        let sidebar = tree.add_child(root, FillWidget::new().focusable());
4663
4664        tree.push_action(
4665            editor,
4666            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4667        );
4668        tree.push_action(
4669            root,
4670            Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4671        );
4672        tree.shortcut_registry_mut().register(
4673            Shortcut::new("editor.saveBlock")
4674                .primary(KeyStroke::command(Key::S))
4675                .scope(ShortcutScope::Scoped(editor))
4676                .build(),
4677        );
4678        tree.shortcut_registry_mut().register(
4679            Shortcut::new("zzz.global.save")
4680                .primary(KeyStroke::command(Key::S))
4681                .build(),
4682        );
4683
4684        tree.layout(SizeProposal::exact(200.0, 100.0));
4685        tree.focus(sidebar);
4686        tree.press_key(Key::S, Modifiers::COMMAND);
4687
4688        assert_eq!(global_fired.get(), 1, "applicable global must fire");
4689        assert_eq!(
4690            scoped_fired.get(),
4691            0,
4692            "inapplicable scoped binding must not eat the chord"
4693        );
4694    }
4695
4696    #[test]
4697    fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
4698        // Defect 2: a Global binding that sorts first by id must yield to
4699        // an in-focus Scoped binding (most-specific-scope wins), then
4700        // reclaim the chord once focus leaves the scope. (`app.save` <
4701        // `editor.saveBlock`, so the global one wins id order.)
4702        use crate::action::Action;
4703        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
4704        use std::cell::Cell;
4705        use std::rc::Rc;
4706
4707        let scoped_fired = Rc::new(Cell::new(0));
4708        let global_fired = Rc::new(Cell::new(0));
4709        let sf = scoped_fired.clone();
4710        let gf = global_fired.clone();
4711
4712        let mut tree = WidgetTree::new();
4713        let root = tree.add(FillWidget::new());
4714        let editor = tree.add_child(root, FillWidget::new().focusable());
4715        let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
4716        let sidebar = tree.add_child(root, FillWidget::new().focusable());
4717
4718        tree.push_action(
4719            editor,
4720            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
4721        );
4722        tree.push_action(
4723            root,
4724            Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
4725        );
4726        tree.shortcut_registry_mut().register(
4727            Shortcut::new("app.save")
4728                .primary(KeyStroke::command(Key::S))
4729                .build(),
4730        );
4731        tree.shortcut_registry_mut().register(
4732            Shortcut::new("editor.saveBlock")
4733                .primary(KeyStroke::command(Key::S))
4734                .scope(ShortcutScope::Scoped(editor))
4735                .build(),
4736        );
4737
4738        tree.layout(SizeProposal::exact(200.0, 100.0));
4739
4740        // Focus inside the editor: the scoped binding wins over global.
4741        tree.focus(editor_inner);
4742        tree.press_key(Key::S, Modifiers::COMMAND);
4743        assert_eq!(
4744            scoped_fired.get(),
4745            1,
4746            "in-focus scoped must win over global"
4747        );
4748        assert_eq!(
4749            global_fired.get(),
4750            0,
4751            "global must yield to the scoped binding"
4752        );
4753
4754        // Focus outside the editor: global reclaims the chord.
4755        tree.focus(sidebar);
4756        tree.press_key(Key::S, Modifiers::COMMAND);
4757        assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
4758        assert_eq!(
4759            global_fired.get(),
4760            1,
4761            "global fires when focus leaves the scope"
4762        );
4763    }
4764
4765    #[test]
4766    fn propagated_action_lets_ancestor_handle() {
4767        use crate::action::Action;
4768        use crate::intent::IntentResponse;
4769        use crate::shortcut::{KeyStroke, Shortcut};
4770        use std::cell::Cell;
4771        use std::rc::Rc;
4772
4773        let inner_seen = Rc::new(Cell::new(false));
4774        let outer_seen = Rc::new(Cell::new(false));
4775        let inner_flag = inner_seen.clone();
4776        let outer_flag = outer_seen.clone();
4777
4778        let mut tree = WidgetTree::new();
4779        let outer = tree.add(FillWidget::new().focusable());
4780        let inner = tree.add_child(outer, FillWidget::new().focusable());
4781
4782        // Inner observes then propagates; outer consumes.
4783        tree.push_action(
4784            inner,
4785            Action::new("app.save").on_invoke_with_response(move |_i, _c| {
4786                inner_flag.set(true);
4787                IntentResponse::Propagated
4788            }),
4789        );
4790        tree.push_action(
4791            outer,
4792            Action::new("app.save").on_invoke(move |_i, _c| {
4793                outer_flag.set(true);
4794            }),
4795        );
4796        tree.shortcut_registry_mut().register(
4797            Shortcut::new("app.save")
4798                .primary(KeyStroke::command(Key::S))
4799                .build(),
4800        );
4801
4802        tree.layout(SizeProposal::exact(100.0, 50.0));
4803        tree.focus(inner);
4804
4805        tree.press_key(Key::S, Modifiers::COMMAND);
4806        assert!(inner_seen.get(), "inner action observed the intent");
4807        assert!(outer_seen.get(), "outer action reached after Propagated");
4808    }
4809
4810    #[test]
4811    fn handled_action_stops_propagation() {
4812        use crate::action::Action;
4813        use crate::shortcut::{KeyStroke, Shortcut};
4814        use std::cell::Cell;
4815        use std::rc::Rc;
4816
4817        let inner_seen = Rc::new(Cell::new(false));
4818        let outer_seen = Rc::new(Cell::new(false));
4819        let inner_flag = inner_seen.clone();
4820        let outer_flag = outer_seen.clone();
4821
4822        let mut tree = WidgetTree::new();
4823        let outer = tree.add(FillWidget::new().focusable());
4824        let inner = tree.add_child(outer, FillWidget::new().focusable());
4825
4826        tree.push_action(
4827            inner,
4828            Action::new("app.save").on_invoke(move |_i, _c| {
4829                inner_flag.set(true);
4830            }),
4831        );
4832        tree.push_action(
4833            outer,
4834            Action::new("app.save").on_invoke(move |_i, _c| {
4835                outer_flag.set(true);
4836            }),
4837        );
4838        tree.shortcut_registry_mut().register(
4839            Shortcut::new("app.save")
4840                .primary(KeyStroke::command(Key::S))
4841                .build(),
4842        );
4843
4844        tree.layout(SizeProposal::exact(100.0, 50.0));
4845        tree.focus(inner);
4846
4847        tree.press_key(Key::S, Modifiers::COMMAND);
4848        assert!(inner_seen.get());
4849        assert!(!outer_seen.get(), "Handled at inner must stop propagation");
4850    }
4851
4852    #[test]
4853    fn disabled_action_propagates_by_default() {
4854        use crate::action::Action;
4855        use crate::shortcut::{KeyStroke, Shortcut};
4856        use crate::signal::Signal;
4857        use std::cell::Cell;
4858        use std::rc::Rc;
4859
4860        let inner_seen = Rc::new(Cell::new(false));
4861        let outer_seen = Rc::new(Cell::new(false));
4862        let inner_flag = inner_seen.clone();
4863        let outer_flag = outer_seen.clone();
4864
4865        let mut tree = WidgetTree::new();
4866        let outer = tree.add(FillWidget::new().focusable());
4867        let inner = tree.add_child(outer, FillWidget::new().focusable());
4868
4869        let enabled = Signal::new(false);
4870        tree.push_action(
4871            inner,
4872            Action::new("app.save")
4873                .enabled_when(enabled.clone())
4874                .on_invoke(move |_i, _c| {
4875                    inner_flag.set(true);
4876                }),
4877        );
4878        tree.push_action(
4879            outer,
4880            Action::new("app.save").on_invoke(move |_i, _c| {
4881                outer_flag.set(true);
4882            }),
4883        );
4884        tree.shortcut_registry_mut().register(
4885            Shortcut::new("app.save")
4886                .primary(KeyStroke::command(Key::S))
4887                .build(),
4888        );
4889
4890        tree.layout(SizeProposal::exact(100.0, 50.0));
4891        tree.focus(inner);
4892
4893        tree.press_key(Key::S, Modifiers::COMMAND);
4894        assert!(!inner_seen.get(), "disabled inner must not run");
4895        assert!(
4896            outer_seen.get(),
4897            "intent must propagate past disabled inner"
4898        );
4899    }
4900
4901    #[test]
4902    fn disabled_action_with_non_propagating_shortcut_consumes() {
4903        use crate::action::Action;
4904        use crate::shortcut::{KeyStroke, Shortcut};
4905        use crate::signal::Signal;
4906        use std::cell::Cell;
4907        use std::rc::Rc;
4908
4909        let inner_seen = Rc::new(Cell::new(false));
4910        let outer_seen = Rc::new(Cell::new(false));
4911        let inner_flag = inner_seen.clone();
4912        let outer_flag = outer_seen.clone();
4913
4914        let mut tree = WidgetTree::new();
4915        let outer = tree.add(FillWidget::new().focusable());
4916        let inner = tree.add_child(outer, FillWidget::new().focusable());
4917
4918        let enabled = Signal::new(false);
4919        tree.push_action(
4920            inner,
4921            Action::new("app.save")
4922                .enabled_when(enabled.clone())
4923                .on_invoke(move |_i, _c| {
4924                    inner_flag.set(true);
4925                }),
4926        );
4927        tree.push_action(
4928            outer,
4929            Action::new("app.save").on_invoke(move |_i, _c| {
4930                outer_flag.set(true);
4931            }),
4932        );
4933        tree.shortcut_registry_mut().register(
4934            Shortcut::new("app.save")
4935                .primary(KeyStroke::command(Key::S))
4936                .propagate_when_disabled(false)
4937                .build(),
4938        );
4939
4940        tree.layout(SizeProposal::exact(100.0, 50.0));
4941        tree.focus(inner);
4942
4943        tree.press_key(Key::S, Modifiers::COMMAND);
4944        assert!(!inner_seen.get(), "disabled inner still does not run");
4945        assert!(
4946            !outer_seen.get(),
4947            "intent must NOT propagate when shortcut disallows it"
4948        );
4949    }
4950
4951    #[test]
4952    fn send_intent_from_handler_reaches_ancestor_action() {
4953        use crate::action::Action;
4954        use crate::intent::Intent;
4955        use std::cell::Cell;
4956        use std::rc::Rc;
4957
4958        let save_seen = Rc::new(Cell::new(false));
4959        let save_flag = save_seen.clone();
4960
4961        let mut tree = WidgetTree::new();
4962        let root = tree.add(FillWidget::new());
4963        let button = tree.add_child(
4964            root,
4965            FillWidget::new().on_tap(|_pos, ctx| {
4966                ctx.send_intent(Intent::new("app.save"));
4967            }),
4968        );
4969        tree.push_action(
4970            root,
4971            Action::new("app.save").on_invoke(move |_i, _c| {
4972                save_flag.set(true);
4973            }),
4974        );
4975
4976        tree.layout(SizeProposal::exact(100.0, 50.0));
4977        tree.click(button);
4978        assert!(
4979            save_seen.get(),
4980            "ctx.send_intent must reach ancestor action"
4981        );
4982    }
4983
4984    #[test]
4985    fn widget_type_histogram_counts_distinct_types() {
4986        // The histogram surfaces concrete widget types
4987        // by std::any::type_name_of_val. Widgets become active
4988        // after the first layout pass, so we run that before
4989        // checking the histogram.
4990        let mut tree = WidgetTree::new();
4991        let _ = tree.add(FillWidget::new());
4992        let _ = tree.add(FillWidget::new());
4993        let _ = tree.add(FillWidget::new());
4994        tree.layout(SizeProposal::exact(100.0, 100.0));
4995        let histogram = tree.widget_type_histogram();
4996        let total: u32 = histogram.values().sum();
4997        assert!(
4998            total >= 3,
4999            "expected at least 3 active widgets, got {total}: {histogram:?}"
5000        );
5001        let fillwidget_entries: u32 = histogram
5002            .iter()
5003            .filter(|(k, _)| k.contains("FillWidget"))
5004            .map(|(_, v)| *v)
5005            .sum();
5006        assert!(
5007            fillwidget_entries >= 3,
5008            "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5009        );
5010        assert_eq!(tree.active_widget_count() as u32, total);
5011    }
5012
5013    #[test]
5014    fn intent_source_tagged_handler_for_tap_activation() {
5015        // A tap-driven `ctx.send_intent` must surface as
5016        // `IntentSource::Handler` to ancestor actions, not the
5017        // `Programmatic` default of `Intent::new`.
5018        use crate::action::Action;
5019        use crate::intent::Intent;
5020        use crate::telemetry::IntentSource;
5021        use std::cell::Cell;
5022        use std::rc::Rc;
5023        let captured = Rc::new(Cell::new(IntentSource::Unknown));
5024        let captured_for_action = captured.clone();
5025
5026        let mut tree = WidgetTree::new();
5027        let root = tree.add(FillWidget::new());
5028        let button = tree.add_child(
5029            root,
5030            FillWidget::new().on_tap(|_pos, ctx| {
5031                ctx.send_intent(Intent::new("app.save"));
5032            }),
5033        );
5034        tree.push_action(
5035            root,
5036            Action::new("app.save").on_invoke(move |intent, _c| {
5037                captured_for_action.set(intent.source);
5038            }),
5039        );
5040
5041        tree.layout(SizeProposal::exact(100.0, 50.0));
5042        tree.click(button);
5043        assert_eq!(
5044            captured.get(),
5045            IntentSource::Handler,
5046            "tap-driven intent must tag IntentSource::Handler"
5047        );
5048    }
5049
5050    #[test]
5051    fn intent_source_programmatic_when_no_handler_active() {
5052        use crate::intent::Intent;
5053        use crate::telemetry::IntentSource;
5054        let intent = Intent::new("app.demo");
5055        assert_eq!(intent.source, IntentSource::Programmatic);
5056
5057        // ctx.send_intent without a handler scope keeps it Programmatic.
5058        let mut ctx = EventContext::new();
5059        ctx.send_intent(Intent::new("app.demo"));
5060        let queued = ctx.pending_intents.first().expect("intent queued");
5061        assert_eq!(queued.source, IntentSource::Programmatic);
5062    }
5063
5064    #[test]
5065    fn with_intent_source_overrides_for_managed_widgets() {
5066        use crate::intent::Intent;
5067        use crate::telemetry::IntentSource;
5068        let mut ctx = EventContext::new();
5069        ctx.with_intent_source(IntentSource::Menu, |ctx| {
5070            ctx.send_intent(Intent::new("app.demo"));
5071        });
5072        let queued = ctx.pending_intents.first().expect("intent queued");
5073        assert_eq!(
5074            queued.source,
5075            IntentSource::Menu,
5076            "with_intent_source(Menu) must tag the dispatched intent"
5077        );
5078
5079        // After the closure returns, current_source is restored —
5080        // a follow-up send_intent without a wrapping closure goes
5081        // back to the default (no override).
5082        ctx.send_intent(Intent::new("app.next"));
5083        let next = ctx.pending_intents.last().expect("second intent");
5084        assert_eq!(next.source, IntentSource::Programmatic);
5085    }
5086
5087    #[test]
5088    fn disabled_shortcut_falls_through_to_focused_widget() {
5089        use crate::action::Action;
5090        use crate::shortcut::{KeyStroke, Shortcut};
5091        use crate::signal::Signal;
5092        use std::cell::Cell;
5093        use std::rc::Rc;
5094
5095        let action_fired = Rc::new(Cell::new(false));
5096        let on_key_fired = Rc::new(Cell::new(false));
5097        let af = action_fired.clone();
5098        let kf = on_key_fired.clone();
5099
5100        let enabled = Signal::new(false);
5101
5102        let mut tree = WidgetTree::new();
5103        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5104            if matches!(
5105                event,
5106                WidgetEvent::KeyDown {
5107                    key: Key::S,
5108                    modifiers,
5109                    ..
5110                } if modifiers.command()
5111            ) {
5112                kf.set(true);
5113                return EventResponse::Handled;
5114            }
5115            EventResponse::Ignored
5116        }));
5117        tree.push_action(
5118            widget,
5119            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5120        );
5121        tree.shortcut_registry_mut().register(
5122            Shortcut::new("app.save")
5123                .primary(KeyStroke::command(Key::S))
5124                .enabled_when(enabled.clone())
5125                .build(),
5126        );
5127
5128        tree.layout(SizeProposal::exact(100.0, 50.0));
5129        tree.focus(widget);
5130
5131        // Disabled: keystroke falls through to on_key.
5132        tree.press_key(Key::S, Modifiers::COMMAND);
5133        assert!(
5134            !action_fired.get(),
5135            "disabled shortcut must not invoke its action"
5136        );
5137        assert!(
5138            on_key_fired.get(),
5139            "disabled shortcut must let KeyDown reach the focused widget"
5140        );
5141
5142        // Re-enable → action fires, on_key does not.
5143        on_key_fired.set(false);
5144        enabled.set(true);
5145        tree.press_key(Key::S, Modifiers::COMMAND);
5146        assert!(action_fired.get(), "re-enabled shortcut must dispatch");
5147        assert!(
5148            !on_key_fired.get(),
5149            "enabled shortcut must consume the KeyDown"
5150        );
5151    }
5152
5153    #[test]
5154    fn keyboard_capture_bypasses_shortcut() {
5155        use crate::action::Action;
5156        use crate::shortcut::{KeyStroke, Shortcut};
5157        use std::cell::Cell;
5158        use std::rc::Rc;
5159
5160        // A focused keyboard-capture surface (e.g. a terminal) must receive
5161        // the accelerator chord itself (⌘S on macOS, Ctrl+S elsewhere), even
5162        // though an ENABLED global shortcut binds it — the whole point of
5163        // GAP 1. A non-capturing widget must yield to the shortcut (the
5164        // control case).
5165        fn run(capture: bool) -> (bool, bool) {
5166            let action_fired = Rc::new(Cell::new(false));
5167            let on_key_fired = Rc::new(Cell::new(false));
5168            let af = action_fired.clone();
5169            let kf = on_key_fired.clone();
5170
5171            let mut tree = WidgetTree::new();
5172            let widget = tree.add(
5173                FillWidget::new()
5174                    .focusable()
5175                    .keyboard_capture(capture)
5176                    .on_key(move |event, _ctx| {
5177                        if matches!(
5178                            event,
5179                            WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
5180                        ) {
5181                            kf.set(true);
5182                            return EventResponse::Handled;
5183                        }
5184                        EventResponse::Ignored
5185                    }),
5186            );
5187            tree.push_action(
5188                widget,
5189                Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5190            );
5191            tree.shortcut_registry_mut().register(
5192                Shortcut::new("app.save")
5193                    .primary(KeyStroke::command(Key::S))
5194                    .build(),
5195            );
5196
5197            tree.layout(SizeProposal::exact(100.0, 50.0));
5198            tree.focus(widget);
5199            tree.press_key(Key::S, Modifiers::COMMAND);
5200            (action_fired.get(), on_key_fired.get())
5201        }
5202
5203        // Capture on: the shortcut is bypassed, the widget sees the key.
5204        let (action, on_key) = run(true);
5205        assert!(
5206            !action,
5207            "keyboard_capture must suppress the shortcut action"
5208        );
5209        assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
5210
5211        // Capture off (control): the shortcut consumes the key.
5212        let (action, on_key) = run(false);
5213        assert!(action, "without capture the shortcut must fire");
5214        assert!(!on_key, "without capture the widget must not see the key");
5215    }
5216
5217    /// A focused widget that records every `KeyDown` it is handed.
5218    fn key_recorder(
5219        tree: &mut WidgetTree,
5220    ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
5221        let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
5222        let log = seen.clone();
5223        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
5224            if let WidgetEvent::KeyDown { key, .. } = event {
5225                log.borrow_mut().push(*key);
5226            }
5227            EventResponse::Ignored
5228        }));
5229        tree.layout(SizeProposal::exact(100.0, 50.0));
5230        tree.focus(widget);
5231        (widget, seen)
5232    }
5233
5234    #[test]
5235    fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
5236        use crate::action::Action;
5237        use crate::intent::IntentResponse;
5238        use crate::shortcut::{KeyStroke, Shortcut};
5239
5240        let mut tree = WidgetTree::new();
5241        let (widget, seen) = key_recorder(&mut tree);
5242        tree.shortcut_registry_mut().register(
5243            Shortcut::new("app.save")
5244                .primary(KeyStroke::command(Key::S))
5245                .build(),
5246        );
5247
5248        // Nothing handles `app.save`.
5249        tree.press_key(Key::S, Modifiers::COMMAND);
5250        assert_eq!(
5251            *seen.borrow(),
5252            vec![Key::S],
5253            "an unhandled intent must not eat the key"
5254        );
5255
5256        // An action that runs and passes the intent on has not consumed it.
5257        tree.push_action(
5258            widget,
5259            Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
5260        );
5261        tree.press_key(Key::S, Modifiers::COMMAND);
5262        assert_eq!(
5263            seen.borrow().len(),
5264            2,
5265            "a propagated intent must leave the key too"
5266        );
5267
5268        // Deleting the fall-through makes both of the above go red; this is
5269        // the control: a consuming action still takes the key.
5270        let mut tree = WidgetTree::new();
5271        let (widget, seen) = key_recorder(&mut tree);
5272        tree.shortcut_registry_mut().register(
5273            Shortcut::new("app.save")
5274                .primary(KeyStroke::command(Key::S))
5275                .build(),
5276        );
5277        tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
5278        tree.press_key(Key::S, Modifiers::COMMAND);
5279        assert!(
5280            seen.borrow().is_empty(),
5281            "a handled shortcut consumes its key"
5282        );
5283    }
5284
5285    #[test]
5286    fn standard_edit_chords_follow_a_rebind() {
5287        use crate::action::Action;
5288        use crate::shortcut::{KeyStroke, StandardEdit};
5289        use std::cell::Cell;
5290        use std::rc::Rc;
5291
5292        let mut tree = WidgetTree::new();
5293        let (widget, seen) = key_recorder(&mut tree);
5294        let copies = Rc::new(Cell::new(0));
5295        let count = copies.clone();
5296        tree.push_action(
5297            widget,
5298            Action::new(StandardEdit::Copy.id())
5299                .on_invoke(move |_i, _c| count.set(count.get() + 1)),
5300        );
5301
5302        // Registered by `WidgetTree::new`, with no help from the app.
5303        tree.press_key(Key::C, Modifiers::COMMAND);
5304        assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
5305        assert!(seen.borrow().is_empty());
5306
5307        // Move Copy elsewhere: the old chord no longer copies (it reaches the
5308        // widget as a plain key), the new one does.
5309        let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
5310        tree.shortcut_registry_mut()
5311            .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
5312        tree.press_key(Key::C, Modifiers::COMMAND);
5313        assert_eq!(copies.get(), 1, "the replaced chord must not copy");
5314        assert_eq!(*seen.borrow(), vec![Key::C]);
5315        tree.press_key(Key::Character('à'), Modifiers::CTRL);
5316        assert_eq!(copies.get(), 2, "the new chord copies");
5317    }
5318
5319    #[test]
5320    fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
5321        use std::cell::Cell;
5322        use std::rc::Rc;
5323
5324        // WCAG 2.1.2. A capture surface answers `Handled` to every key —
5325        // that is what it is for — so the "cycle focus only when the focused
5326        // widget did not handle Tab" rule can never get focus out of one.
5327        // Ctrl+Tab / Ctrl+Shift+Tab are therefore reserved by the dispatcher
5328        // and never reach the widget at all.
5329        let saw_key = Rc::new(Cell::new(false));
5330        let sk = saw_key.clone();
5331
5332        let mut tree = WidgetTree::new();
5333        let capture = tree.add(
5334            FillWidget::new()
5335                .focusable()
5336                .keyboard_capture(true)
5337                // The greediest possible handler: everything is consumed.
5338                .on_key(move |_event, _ctx| {
5339                    sk.set(true);
5340                    EventResponse::Handled
5341                }),
5342        );
5343        let neighbour = tree.add(FillWidget::new().focusable());
5344        tree.layout(SizeProposal::exact(100.0, 50.0));
5345
5346        // Plain Tab stays inside: the widget consumed it (a terminal writes
5347        // it to the child as `\t`).
5348        tree.focus(capture);
5349        tree.press_key(Key::Tab, Modifiers::NONE);
5350        assert!(saw_key.get(), "plain Tab must reach the capture surface");
5351        assert_eq!(
5352            tree.focused(),
5353            Some(capture),
5354            "plain Tab must not move focus off a capture surface"
5355        );
5356
5357        // Ctrl+Tab escapes forward, without the widget ever seeing it.
5358        saw_key.set(false);
5359        tree.press_key(Key::Tab, Modifiers::CTRL);
5360        assert!(
5361            !saw_key.get(),
5362            "Ctrl+Tab is reserved and must not reach the capture surface"
5363        );
5364        assert_eq!(
5365            tree.focused(),
5366            Some(neighbour),
5367            "Ctrl+Tab must move focus out of a capture surface"
5368        );
5369
5370        // And backwards.
5371        tree.focus(capture);
5372        tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
5373        assert_eq!(
5374            tree.focused(),
5375            Some(neighbour),
5376            "Ctrl+Shift+Tab must move focus out of a capture surface"
5377        );
5378    }
5379
5380    #[test]
5381    fn scope_mismatch_does_not_invoke_on_activate() {
5382        use crate::intent::Intent;
5383        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5384        use std::cell::Cell;
5385        use std::rc::Rc;
5386
5387        // Regression: before the find/invoke split, `on_activate` ran
5388        // even when the focused widget was outside the shortcut's
5389        // scope, and any side effects on its ctx were silently
5390        // dropped. The closure must now only run when the scope
5391        // check has already passed.
5392        let activated = Rc::new(Cell::new(false));
5393        let activated_flag = activated.clone();
5394
5395        let mut tree = WidgetTree::new();
5396        let scope_root = tree.add(FillWidget::new().focusable());
5397        let outside = tree.add(FillWidget::new().focusable());
5398
5399        tree.shortcut_registry_mut().register(
5400            Shortcut::new("editor.find")
5401                .primary(KeyStroke::command(Key::F))
5402                .scope(ShortcutScope::Scoped(scope_root))
5403                .on_activate(move |_ks, _ctx| {
5404                    activated_flag.set(true);
5405                    Intent::new("editor.find")
5406                })
5407                .build(),
5408        );
5409
5410        tree.layout(SizeProposal::exact(200.0, 100.0));
5411        tree.focus(outside);
5412
5413        tree.press_key(Key::F, Modifiers::COMMAND);
5414        assert!(
5415            !activated.get(),
5416            "on_activate must not run when focus is outside the shortcut's scope"
5417        );
5418    }
5419
5420    #[test]
5421    fn key_capture_runs_callback_and_bypasses_registry() {
5422        use crate::action::Action;
5423        use crate::shortcut::{KeyStroke, Shortcut};
5424        use std::cell::Cell;
5425        use std::rc::Rc;
5426
5427        let action_fired = Rc::new(Cell::new(false));
5428        let af = action_fired.clone();
5429        let captured = Rc::new(Cell::new(None));
5430        let cf = captured.clone();
5431
5432        let mut tree = WidgetTree::new();
5433        let widget = tree.add(FillWidget::new().focusable());
5434        tree.push_action(
5435            widget,
5436            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5437        );
5438        tree.shortcut_registry_mut().register(
5439            Shortcut::new("app.save")
5440                .primary(KeyStroke::command(Key::S))
5441                .build(),
5442        );
5443
5444        tree.layout(SizeProposal::exact(100.0, 50.0));
5445        tree.focus(widget);
5446
5447        let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
5448        assert!(tree.is_capturing_keys());
5449
5450        tree.press_key(Key::S, Modifiers::COMMAND);
5451        assert_eq!(
5452            captured.get(),
5453            Some(KeyStroke::command(Key::S)),
5454            "capture callback must receive the chord"
5455        );
5456        assert!(
5457            !action_fired.get(),
5458            "shortcut action must not fire while capture is armed"
5459        );
5460        assert!(
5461            !tree.is_capturing_keys(),
5462            "capture is one-shot; next KeyDown flows normally"
5463        );
5464        drop(handle);
5465    }
5466
5467    #[test]
5468    fn key_capture_can_rebind_through_registry() {
5469        use crate::shortcut::{KeyStroke, Shortcut};
5470
5471        let mut tree = WidgetTree::new();
5472        let widget = tree.add(FillWidget::new().focusable());
5473        tree.shortcut_registry_mut().register(
5474            Shortcut::new("app.save")
5475                .primary(KeyStroke::command(Key::S))
5476                .build(),
5477        );
5478
5479        tree.layout(SizeProposal::exact(100.0, 50.0));
5480        tree.focus(widget);
5481
5482        // Arm capture: whatever chord comes next, rebind app.save to it.
5483        let _h = tree.begin_key_capture(|ks, reg, _ctx| {
5484            reg.rebind_primary("app.save", Some(ks));
5485        });
5486
5487        tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
5488        assert_eq!(
5489            tree.shortcut_registry()
5490                .effective("app.save")
5491                .unwrap()
5492                .primary,
5493            Some(KeyStroke::command_shift(Key::B))
5494        );
5495    }
5496
5497    #[test]
5498    fn dropping_capture_handle_cancels_capture() {
5499        use crate::shortcut::{KeyStroke, Shortcut};
5500        use std::cell::Cell;
5501        use std::rc::Rc;
5502
5503        let action_fired = Rc::new(Cell::new(false));
5504        let af = action_fired.clone();
5505        let capture_fired = Rc::new(Cell::new(false));
5506        let cf = capture_fired.clone();
5507
5508        let mut tree = WidgetTree::new();
5509        let widget = tree.add(FillWidget::new().focusable());
5510        tree.push_action(
5511            widget,
5512            crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
5513        );
5514        tree.shortcut_registry_mut().register(
5515            Shortcut::new("app.save")
5516                .primary(KeyStroke::command(Key::S))
5517                .build(),
5518        );
5519        tree.layout(SizeProposal::exact(100.0, 50.0));
5520        tree.focus(widget);
5521
5522        // Arm capture in a scope, then drop the handle before any key
5523        // is pressed. The next KeyDown must fall through to the normal
5524        // shortcut path, firing the action — not the cancelled capture.
5525        {
5526            let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
5527            assert!(tree.is_capturing_keys());
5528            // `_h` drops here → cancel.
5529        }
5530        assert!(
5531            !tree.is_capturing_keys(),
5532            "dropping the handle must cancel the capture"
5533        );
5534
5535        tree.press_key(Key::S, Modifiers::COMMAND);
5536        assert!(!capture_fired.get(), "cancelled capture must not fire");
5537        assert!(
5538            action_fired.get(),
5539            "shortcut action runs after capture was cancelled"
5540        );
5541    }
5542
5543    #[test]
5544    fn second_begin_key_capture_does_not_racecancel_first() {
5545        use std::cell::Cell;
5546        use std::rc::Rc;
5547
5548        let first = Rc::new(Cell::new(false));
5549        let second = Rc::new(Cell::new(false));
5550        let f = first.clone();
5551        let s = second.clone();
5552
5553        let mut tree = WidgetTree::new();
5554        let widget = tree.add(FillWidget::new().focusable());
5555        tree.layout(SizeProposal::exact(100.0, 50.0));
5556        tree.focus(widget);
5557
5558        // Arm #1 then replace with #2. #1's handle is later dropped,
5559        // which would have cancelled the active capture under the old
5560        // `Option<Box<FnOnce>>` design — CaptureHandle now ties each
5561        // session to its own slot, so the drop only clears #1's
5562        // (orphaned) slot, not #2.
5563        let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
5564        let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
5565        drop(h1);
5566
5567        assert!(
5568            tree.is_capturing_keys(),
5569            "dropping the older handle must not cancel the active capture"
5570        );
5571        tree.press_key(Key::K, Modifiers::COMMAND);
5572        assert!(!first.get());
5573        assert!(second.get(), "newest capture wins");
5574    }
5575
5576    #[test]
5577    fn capture_callback_can_send_intent() {
5578        use crate::action::Action;
5579        use crate::intent::Intent;
5580
5581        use std::cell::Cell;
5582        use std::rc::Rc;
5583
5584        let ran = Rc::new(Cell::new(false));
5585        let flag = ran.clone();
5586
5587        let mut tree = WidgetTree::new();
5588        let widget = tree.add(FillWidget::new().focusable());
5589        tree.push_action(
5590            widget,
5591            Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
5592        );
5593        tree.layout(SizeProposal::exact(100.0, 50.0));
5594        tree.focus(widget);
5595
5596        let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
5597            ctx.send_intent(Intent::new("app.save"));
5598        });
5599        tree.press_key(Key::X, Modifiers::COMMAND);
5600        assert!(
5601            ran.get(),
5602            "intent queued from capture callback must dispatch"
5603        );
5604    }
5605
5606    #[test]
5607    fn binding_registry_does_not_accumulate_across_rebuilds() {
5608        use crate::binding::BindingLevel;
5609        use crate::signal::Signal;
5610
5611        #[derive(Debug)]
5612        struct BoundLeaf {
5613            tick: Signal<u64>,
5614        }
5615        impl crate::widget::Widget for BoundLeaf {
5616            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5617                self.tick.bind_to(
5618                    ctx.self_id(),
5619                    ctx.binding_registry(),
5620                    BindingLevel::Relayout,
5621                );
5622                Vec::new()
5623            }
5624            fn layout_response(
5625                &self,
5626                proposal: SizeProposal,
5627                _ctx: &crate::widget::LayoutContext,
5628            ) -> crate::widget::LayoutResponse {
5629                proposal.resolve(10.0, 10.0).into()
5630            }
5631        }
5632
5633        let mut tree = WidgetTree::new();
5634        let tick = Signal::new(0_u64);
5635        let widget = tree.add(BoundLeaf { tick: tick.clone() });
5636        tree.layout(SizeProposal::exact(200.0, 200.0));
5637        let after_first_build = tree.binding_registry().len();
5638        assert!(after_first_build >= 1);
5639
5640        // Force rebuild a handful of times and verify the binding
5641        // count does not keep growing. Pre-fix: each rebuild pushed
5642        // a new entry for the same (widget, signal) pair.
5643        for _ in 0..5 {
5644            tree.arena.mark_needs_rebuild(widget);
5645            tree.layout(SizeProposal::exact(200.0, 200.0));
5646        }
5647        assert_eq!(
5648            tree.binding_registry().len(),
5649            after_first_build,
5650            "bindings must be cleared on rebuild"
5651        );
5652
5653        tree.destroy_subtree(widget);
5654        assert_eq!(
5655            tree.binding_registry().len(),
5656            0,
5657            "bindings must be cleared on destroy"
5658        );
5659        // Silence unused-variable warning for the signal.
5660        let _ = tick;
5661    }
5662
5663    #[test]
5664    fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
5665        // Regression: `EventContext::destroy` queues
5666        // `TreeMutation::Destroy`, which used to be applied with the
5667        // bare `arena.destroy` — unlinking the node but leaking the
5668        // animation-scheduler entry (it holds a strong `Signal<f32>`
5669        // clone, so the widget kept animating after destruction) and
5670        // the widget's bindings. It must route through
5671        // `destroy_subtree` like every other destroy path does.
5672        use crate::binding::BindingLevel;
5673        use crate::signal::Signal;
5674        use std::time::{Duration, Instant};
5675        use teksilo_tokens::Easing;
5676
5677        #[derive(Debug)]
5678        struct BoundLeaf {
5679            tick: Signal<u64>,
5680        }
5681        impl crate::widget::Widget for BoundLeaf {
5682            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5683                self.tick.bind_to(
5684                    ctx.self_id(),
5685                    ctx.binding_registry(),
5686                    BindingLevel::Relayout,
5687                );
5688                Vec::new()
5689            }
5690            fn layout_response(
5691                &self,
5692                proposal: SizeProposal,
5693                _ctx: &crate::widget::LayoutContext,
5694            ) -> crate::widget::LayoutResponse {
5695                proposal.resolve(10.0, 10.0).into()
5696            }
5697        }
5698
5699        let mut tree = WidgetTree::new();
5700        let widget = tree.add(BoundLeaf {
5701            tick: Signal::new(0_u64),
5702        });
5703        tree.layout(SizeProposal::exact(200.0, 200.0));
5704        assert!(tree.binding_registry().len() >= 1);
5705
5706        // Seed an animation owned by the widget — exactly the strong
5707        // `Signal<f32>` clone the scheduler outlives the widget with.
5708        let anim = Signal::<f32>::new_animated(0.0);
5709        tree.animation_scheduler.animate(
5710            &anim,
5711            widget,
5712            1.0,
5713            Duration::from_secs(10),
5714            Easing::Linear,
5715            Instant::now(),
5716        );
5717        assert_eq!(tree.animation_scheduler.active_count(), 1);
5718
5719        // Destroy via the deferred handler-time path.
5720        let mut noop = crate::window::NoopWindowOps;
5721        tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
5722
5723        assert_eq!(
5724            tree.animation_scheduler.active_count(),
5725            0,
5726            "ctx.destroy must cancel animations owned by the destroyed widget"
5727        );
5728        assert_eq!(
5729            tree.binding_registry().len(),
5730            0,
5731            "ctx.destroy must unregister the destroyed widget's bindings"
5732        );
5733        assert!(
5734            tree.arena.get(widget).is_none(),
5735            "node must be removed from the arena"
5736        );
5737    }
5738
5739    #[test]
5740    fn clear_shortcut_override_via_event_context_restores_default() {
5741        use crate::shortcut::{KeyStroke, Shortcut};
5742
5743        let mut tree = WidgetTree::new();
5744        tree.shortcut_registry_mut().register(
5745            Shortcut::new("app.save")
5746                .primary(KeyStroke::command(Key::S))
5747                .build(),
5748        );
5749        tree.shortcut_registry_mut()
5750            .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
5751
5752        let source = tree.add(FillWidget::new());
5753        let mut ctx = EventContext::new();
5754        ctx.clear_shortcut_override("app.save");
5755        tree.collect_from_ctx(ctx, source);
5756
5757        assert_eq!(
5758            tree.shortcut_registry()
5759                .effective("app.save")
5760                .unwrap()
5761                .primary,
5762            Some(KeyStroke::command(Key::S))
5763        );
5764    }
5765
5766    #[test]
5767    fn rebind_shortcut_primary_via_event_context() {
5768        use crate::shortcut::{KeyStroke, Shortcut};
5769
5770        let mut tree = WidgetTree::new();
5771        tree.shortcut_registry_mut().register(
5772            Shortcut::new("app.save")
5773                .primary(KeyStroke::command(Key::S))
5774                .build(),
5775        );
5776        let source = tree.add(FillWidget::new());
5777
5778        let mut ctx = EventContext::new();
5779        ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
5780        tree.collect_from_ctx(ctx, source);
5781
5782        assert_eq!(
5783            tree.shortcut_registry()
5784                .effective("app.save")
5785                .unwrap()
5786                .primary,
5787            Some(KeyStroke::alt(Key::S))
5788        );
5789    }
5790
5791    #[test]
5792    fn unregister_all_for_owner_called_on_destroy() {
5793        use crate::shortcut::{KeyStroke, Shortcut};
5794
5795        let mut tree = WidgetTree::new();
5796        let widget = tree.add(FillWidget::new());
5797        let widget_owner = widget;
5798        tree.shortcut_registry_mut().register_owned(
5799            Shortcut::new("scoped.thing")
5800                .primary(KeyStroke::command(Key::K))
5801                .build(),
5802            widget_owner,
5803        );
5804        assert!(
5805            tree.shortcut_registry()
5806                .get_default("scoped.thing")
5807                .is_some()
5808        );
5809
5810        tree.destroy_subtree(widget);
5811        assert!(
5812            tree.shortcut_registry()
5813                .get_default("scoped.thing")
5814                .is_none(),
5815            "destroying the owner must unregister its shortcut"
5816        );
5817    }
5818
5819    /// A global action fires for an intent dispatched from a widget in a
5820    /// completely unrelated subtree — proving it is a position-independent
5821    /// fallback (the menu-bar-vs-content case).
5822    #[test]
5823    fn global_action_reached_from_unrelated_source() {
5824        use crate::action::Action;
5825        use crate::intent::Intent;
5826        use std::cell::Cell;
5827        use std::rc::Rc;
5828
5829        #[derive(Debug)]
5830        struct Registrar(Rc<Cell<bool>>);
5831        impl crate::widget::Widget for Registrar {
5832            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
5833                let flag = self.0.clone();
5834                ctx.register_action_global(
5835                    Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
5836                );
5837                vec![]
5838            }
5839            fn layout_response(
5840                &self,
5841                _p: teksilo_canvas::SizeProposal,
5842                _c: &crate::widget::LayoutContext,
5843            ) -> crate::widget::LayoutResponse {
5844                teksilo_canvas::Size::new(0.0, 0.0).into()
5845            }
5846        }
5847
5848        let mut tree = WidgetTree::new();
5849        let fired = Rc::new(Cell::new(false));
5850        let registrar = tree.add(Registrar(fired.clone()));
5851        let source = tree.add(FillWidget::new()); // unrelated sibling root
5852        let mut ops = crate::window::NoopWindowOps;
5853
5854        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5855        assert!(
5856            fired.get(),
5857            "global action must fire from an unrelated source"
5858        );
5859
5860        // And it is torn down with its owner.
5861        fired.set(false);
5862        tree.destroy_subtree(registrar);
5863        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
5864        assert!(
5865            !fired.get(),
5866            "destroying the owner must remove its global action"
5867        );
5868    }
5869
5870    // --- Transform-aware hit-testing -------------------------------------
5871    //
5872    // `set_transform` scopes are paint-only: the renderer pushes the
5873    // transform around the subtree, so the visually-displayed area is
5874    // shifted relative to `arena.bounds(id)`. Hit-testing must inverse-
5875    // transform the screen-space input point as it descends through each
5876    // transform scope so that a click on the visually-rendered area lands
5877    // on the correct widget. Pre-fix, screen-space `bounds.contains(point)`
5878    // returned the *pre-transform* widget for in-bounds-pre-transform
5879    // points and missed the visually-shifted hit area entirely.
5880
5881    #[test]
5882    fn hit_test_through_translate_scope() {
5883        use crate::test_widgets::StackWidget;
5884        let mut tree = WidgetTree::new();
5885        let child = tree.add(FillWidget::new());
5886        let parent = tree.add(StackWidget::new().child(child));
5887        // Visually shift the entire subtree right by 100px.
5888        tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5889        tree.layout(SizeProposal::exact(100.0, 50.0));
5890
5891        // (50, 25) is inside the *pre-transform* bounds but the widget is
5892        // visually painted at x=100..200; a click at (50, 25) lands on
5893        // empty space.
5894        assert_eq!(
5895            tree.hit_test(Point::new(50.0, 25.0)),
5896            None,
5897            "pre-transform area is not visually populated and must not hit"
5898        );
5899        // (150, 25) is inside the visually-rendered area (post-translate).
5900        assert_eq!(
5901            tree.hit_test(Point::new(150.0, 25.0)),
5902            Some(child),
5903            "visually-rendered area must hit the child"
5904        );
5905        // Off everything.
5906        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5907    }
5908
5909    #[test]
5910    fn hit_test_through_scale_scope() {
5911        use crate::test_widgets::StackWidget;
5912        let mut tree = WidgetTree::new();
5913        let child = tree.add(FillWidget::new());
5914        let parent = tree.add(StackWidget::new().child(child));
5915        // Halve the visual size: pre-transform bounds (0,0,100,50) →
5916        // visually (0,0,50,25).
5917        tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
5918        tree.layout(SizeProposal::exact(100.0, 50.0));
5919
5920        // Inside the visual area.
5921        assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
5922        // Outside the visual area but inside the pre-transform bounds.
5923        // Without the fix this would (incorrectly) hit the child.
5924        assert_eq!(
5925            tree.hit_test(Point::new(75.0, 25.0)),
5926            None,
5927            "scaled-out region must not hit"
5928        );
5929    }
5930
5931    #[test]
5932    fn hit_test_through_nested_transforms_compose() {
5933        use crate::test_widgets::StackWidget;
5934        let mut tree = WidgetTree::new();
5935        let leaf = tree.add(FillWidget::new());
5936        let inner = tree.add(StackWidget::new().child(leaf));
5937        let outer = tree.add(StackWidget::new().child(inner));
5938        // Outer translates by (100, 0); inner additionally scales by 2.
5939        // Effective at leaf = scale(2,2).then(translate(100,0)) — the
5940        // renderer composes deepest-first (see `effective_transform`).
5941        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5942        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5943        tree.layout(SizeProposal::exact(50.0, 25.0));
5944
5945        // Leaf-local (0, 0) → scale → (0, 0) → translate → (100, 0).
5946        // Leaf-local (50, 25) → scale → (100, 50) → translate → (200, 50).
5947        // So the visual hit area is x in [100, 200], y in [0, 50].
5948        assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
5949        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
5950        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
5951    }
5952
5953    #[test]
5954    fn hit_test_identity_transform_unchanged() {
5955        // Sanity: an identity transform must not perturb the existing
5956        // hit-test behavior. Guards against accidental over-application
5957        // of inversion on the hot path.
5958        let mut tree = WidgetTree::new();
5959        let widget = tree.add(FillWidget::new());
5960        tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
5961        tree.layout(SizeProposal::exact(100.0, 50.0));
5962        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
5963    }
5964
5965    #[test]
5966    fn arena_effective_transform_composes_ancestors() {
5967        // `arena.effective_transform(id)` must equal the renderer's
5968        // transform-stack top by the time it begins painting `id` —
5969        // i.e. mapping `id`'s pre-transform local point to screen space.
5970        // The renderer's `PushTransform` handler composes as
5971        // `device_t.then(prev_top)` (see `teksilo-render/src/renderer.rs`),
5972        // so the *innermost* transform applies first to a local point.
5973        // For ancestors [outer, inner] both with transforms, this means
5974        // effective = inner.then(outer), NOT outer.then(inner).
5975        // teksilo-scene relies on this to project scene-coord bounds to
5976        // screen space when emitting AT nodes for view-transformed items.
5977        use crate::test_widgets::StackWidget;
5978        let mut tree = WidgetTree::new();
5979        let leaf = tree.add(FillWidget::new());
5980        let inner = tree.add(StackWidget::new().child(leaf));
5981        let outer = tree.add(StackWidget::new().child(inner));
5982        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
5983        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
5984        tree.layout(SizeProposal::exact(50.0, 25.0));
5985
5986        let eff = tree.arena.effective_transform(leaf);
5987        let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
5988            .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
5989        for (a, b) in eff.m.iter().zip(expected.m.iter()) {
5990            assert!(
5991                (a - b).abs() < 1e-5,
5992                "effective_transform mismatch: got {:?}, want {:?}",
5993                eff.m,
5994                expected.m
5995            );
5996        }
5997
5998        // Concrete-point check that pins the composition order without
5999        // relying on matrix equality alone: a leaf-local point at the
6000        // bounds origin (0, 0) should land at screen (100, 0) — scale
6001        // first (still (0,0)), then translate by 100 in x. With the
6002        // wrong composition order it would land at (200, 0).
6003        let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6004        assert!((screen_origin.x - 100.0).abs() < 1e-5);
6005        assert!((screen_origin.y - 0.0).abs() < 1e-5);
6006        // Far corner: leaf-local (50, 25) → scale → (100, 50) → translate
6007        // by 100 in x → (200, 50).
6008        let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6009        assert!((screen_corner.x - 200.0).abs() < 1e-5);
6010        assert!((screen_corner.y - 50.0).abs() < 1e-5);
6011    }
6012
6013    // ─── Context-menu factory: position, ctx, None fall-through ─────────
6014
6015    /// A throwaway content widget the factory mounts. We never paint
6016    /// it — the test only checks that it lands in the overlay manager.
6017    #[derive(Debug)]
6018    struct StubMenu;
6019    impl crate::widget::Widget for StubMenu {
6020        fn layout_response(
6021            &self,
6022            _proposal: SizeProposal,
6023            _ctx: &crate::widget::LayoutContext,
6024        ) -> crate::widget::LayoutResponse {
6025            teksilo_canvas::Size::new(100.0, 40.0).into()
6026        }
6027    }
6028
6029    // The keyboard route to a context menu.
6030    //
6031    // Until this existed there was none at all: no `Key::ContextMenu`, no
6032    // Shift+F10, and `Action::ShowContextMenu` appears in zero of the three
6033    // AccessKit adapters, so the assistive-technology route is dead on every
6034    // platform too. A menu reachable only by right-click is a menu a keyboard
6035    // user does not have.
6036
6037    /// A widget that hands the keyboard a different target than itself, the way
6038    /// every data view does: the container has focus, the row is what the menu
6039    /// is about.
6040    #[derive(Debug)]
6041    struct NominatingWidget {
6042        row: std::cell::Cell<Option<WidgetId>>,
6043    }
6044
6045    impl crate::widget::Widget for NominatingWidget {
6046        fn layout_response(
6047            &self,
6048            proposal: SizeProposal,
6049            _ctx: &LayoutContext,
6050        ) -> crate::widget::LayoutResponse {
6051            proposal.resolve(50.0, 20.0).into()
6052        }
6053
6054        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6055            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6056            Vec::new()
6057        }
6058
6059        fn context_menu_key_target(&self) -> Option<WidgetId> {
6060            self.row.get()
6061        }
6062    }
6063
6064    fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6065        tree.dispatch_event(WidgetEvent::KeyDown {
6066            key,
6067            modifiers,
6068            text: None,
6069        });
6070    }
6071
6072    #[test]
6073    fn the_context_menu_key_opens_the_focused_widget_menu() {
6074        use std::cell::Cell;
6075        use std::rc::Rc;
6076
6077        let opened = Rc::new(Cell::new(false));
6078        let flag = opened.clone();
6079        let mut tree = WidgetTree::new();
6080        let widget = tree.add(
6081            FillWidget::new()
6082                .focusable()
6083                .context_menu(move |_pos, _ctx| {
6084                    flag.set(true);
6085                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6086                }),
6087        );
6088        tree.layout(SizeProposal::exact(200.0, 100.0));
6089        tree.focus(widget);
6090
6091        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6092        assert!(opened.get(), "the dedicated Menu key must open the menu");
6093    }
6094
6095    /// The chord every Windows and Linux keyboard can reach, including the many
6096    /// that have no dedicated Menu key at all.
6097    #[test]
6098    fn shift_f10_opens_the_focused_widget_menu() {
6099        use std::cell::Cell;
6100        use std::rc::Rc;
6101
6102        let opened = Rc::new(Cell::new(false));
6103        let flag = opened.clone();
6104        let mut tree = WidgetTree::new();
6105        let widget = tree.add(
6106            FillWidget::new()
6107                .focusable()
6108                .context_menu(move |_pos, _ctx| {
6109                    flag.set(true);
6110                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6111                }),
6112        );
6113        tree.layout(SizeProposal::exact(200.0, 100.0));
6114        tree.focus(widget);
6115
6116        press(&mut tree, Key::F10, Modifiers::SHIFT);
6117        assert!(opened.get(), "Shift+F10 must open the menu");
6118    }
6119
6120    /// Modifiers are matched exactly. Ctrl+Shift+F10 is a different gesture and
6121    /// belongs to the application.
6122    #[test]
6123    fn a_near_miss_chord_is_not_a_context_menu_request() {
6124        use std::cell::Cell;
6125        use std::rc::Rc;
6126
6127        let opened = Rc::new(Cell::new(false));
6128        let flag = opened.clone();
6129        let mut tree = WidgetTree::new();
6130        let widget = tree.add(
6131            FillWidget::new()
6132                .focusable()
6133                .context_menu(move |_pos, _ctx| {
6134                    flag.set(true);
6135                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6136                }),
6137        );
6138        tree.layout(SizeProposal::exact(200.0, 100.0));
6139        tree.focus(widget);
6140
6141        press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
6142        press(&mut tree, Key::F10, Modifiers::NONE);
6143        assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
6144    }
6145
6146    /// The correction the design needed. A data view is focusable and its rows
6147    /// are not, so "the focused widget" is the list, and the menu a user asked
6148    /// for on row 4 would have been the list's own.
6149    #[test]
6150    fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
6151        use std::cell::Cell;
6152        use std::rc::Rc;
6153
6154        let menu_owner = Rc::new(Cell::new(None::<&'static str>));
6155
6156        let row_flag = menu_owner.clone();
6157        let container_flag = menu_owner.clone();
6158
6159        let mut tree = WidgetTree::new();
6160        let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
6161            row_flag.set(Some("row"));
6162            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6163        }));
6164        let container = tree.add(
6165            crate::test_widgets::StackWidget::new()
6166                .child(row)
6167                .context_menu(move |_pos, _ctx| {
6168                    container_flag.set(Some("container"));
6169                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6170                }),
6171        );
6172        tree.layout(SizeProposal::exact(200.0, 100.0));
6173
6174        // The container is focused, and nominates the row.
6175        let nominator = tree.add(NominatingWidget {
6176            row: std::cell::Cell::new(Some(row)),
6177        });
6178        tree.layout(SizeProposal::exact(200.0, 100.0));
6179        tree.focus(nominator);
6180        let _ = container;
6181
6182        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6183        assert_eq!(
6184            menu_owner.get(),
6185            Some("row"),
6186            "the nominated row's factory must be the one that runs"
6187        );
6188    }
6189
6190    /// Nothing on the chain owns a factory, so the framework must not swallow
6191    /// the key: a widget that wants to handle Shift+F10 itself still can.
6192    #[test]
6193    fn the_chord_falls_through_when_there_is_no_menu_to_show() {
6194        use std::cell::Cell;
6195        use std::rc::Rc;
6196
6197        let saw_key = Rc::new(Cell::new(false));
6198        let flag = saw_key.clone();
6199        let mut tree = WidgetTree::new();
6200        let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
6201            if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
6202                flag.set(true);
6203            }
6204            crate::event::EventResponse::Ignored
6205        }));
6206        tree.layout(SizeProposal::exact(200.0, 100.0));
6207        tree.focus(widget);
6208
6209        press(&mut tree, Key::F10, Modifiers::SHIFT);
6210        assert!(
6211            saw_key.get(),
6212            "with no factory anywhere, the key must reach the widget"
6213        );
6214    }
6215
6216    /// Every route to a menu tells the factory what asked for it, because only
6217    /// a pointer's point is a place the user chose. A text field's factory
6218    /// moves the caret to its point for a right-click; told nothing, it did the
6219    /// same for Shift+F10, and the menu's Paste wrote into the middle of the
6220    /// field.
6221    #[test]
6222    fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
6223        use crate::widget_builder::ContextMenuTrigger;
6224        use std::cell::RefCell;
6225        use std::rc::Rc;
6226
6227        let seen = Rc::new(RefCell::new(Vec::new()));
6228        let log = seen.clone();
6229        let mut tree = WidgetTree::new();
6230        let widget = tree.add(
6231            FillWidget::new()
6232                .focusable()
6233                .context_menu(move |_pos, ctx| {
6234                    log.borrow_mut().push(ctx.context_menu_trigger());
6235                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6236                }),
6237        );
6238        tree.layout(SizeProposal::exact(200.0, 100.0));
6239        tree.focus(widget);
6240        let inside = tree.bounds(widget).center();
6241
6242        press(&mut tree, Key::F10, Modifiers::SHIFT);
6243        press(&mut tree, Key::Escape, Modifiers::NONE);
6244        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
6245        press(&mut tree, Key::Escape, Modifiers::NONE);
6246        tree.dispatch_event(WidgetEvent::pointer_down(
6247            inside,
6248            crate::event::PointerButton::Secondary,
6249            Modifiers::NONE,
6250        ));
6251        tree.dispatch_event(WidgetEvent::pointer_up(
6252            inside,
6253            crate::event::PointerButton::Secondary,
6254            Modifiers::NONE,
6255        ));
6256        press(&mut tree, Key::Escape, Modifiers::NONE);
6257        tree.dispatch_event(WidgetEvent::AccessAction {
6258            action: accesskit::Action::ShowContextMenu,
6259            target: Some(widget),
6260            target_node: crate::accessibility::widget_id_to_node_id(widget),
6261            data: None,
6262        });
6263        press(&mut tree, Key::Escape, Modifiers::NONE);
6264        tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
6265
6266        assert_eq!(
6267            *seen.borrow(),
6268            vec![
6269                Some(ContextMenuTrigger::Keyboard),
6270                Some(ContextMenuTrigger::Keyboard),
6271                Some(ContextMenuTrigger::Pointer),
6272                Some(ContextMenuTrigger::Accessibility),
6273                Some(ContextMenuTrigger::Pointer),
6274            ],
6275            "Shift+F10, the Menu key, a right-click, the assistive action and a \
6276             hold, in that order"
6277        );
6278    }
6279
6280    #[test]
6281    fn context_menu_factory_receives_click_position() {
6282        use crate::event::{Modifiers, PointerButton};
6283        use std::cell::Cell;
6284        use std::rc::Rc;
6285
6286        let captured_position = Rc::new(Cell::new(None::<Point>));
6287        let cap = captured_position.clone();
6288        let mut tree = WidgetTree::new();
6289        let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
6290            cap.set(Some(pos));
6291            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6292        }));
6293        tree.layout(SizeProposal::exact(200.0, 100.0));
6294
6295        let click = Point::new(73.0, 42.0);
6296        tree.dispatch_event(WidgetEvent::pointer_down(
6297            click,
6298            PointerButton::Secondary,
6299            Modifiers::NONE,
6300        ));
6301
6302        let got = captured_position.get();
6303        assert_eq!(
6304            got,
6305            Some(click),
6306            "factory must receive the click position; got {:?}",
6307            got
6308        );
6309        let _ = widget;
6310    }
6311
6312    #[test]
6313    fn context_menu_factory_returning_none_falls_through_to_parent() {
6314        use crate::event::{Modifiers, PointerButton};
6315        use crate::test_widgets::StackWidget;
6316        use std::cell::Cell;
6317        use std::rc::Rc;
6318
6319        // Outer factory always returns Some(StubMenu); inner factory
6320        // returns None. Right-click should walk past the inner and
6321        // mount the outer's menu.
6322        let outer_called = Rc::new(Cell::new(0_u32));
6323        let outer_flag = outer_called.clone();
6324        let mut tree = WidgetTree::new();
6325        let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6326        let _outer = tree.add(
6327            StackWidget::new()
6328                .child(inner)
6329                .context_menu(move |_pos, _ctx| {
6330                    outer_flag.set(outer_flag.get() + 1);
6331                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
6332                }),
6333        );
6334        tree.layout(SizeProposal::exact(200.0, 100.0));
6335
6336        tree.dispatch_event(WidgetEvent::pointer_down(
6337            Point::new(50.0, 25.0),
6338            PointerButton::Secondary,
6339            Modifiers::NONE,
6340        ));
6341
6342        assert_eq!(
6343            outer_called.get(),
6344            1,
6345            "inner returning None must fall through to the outer factory"
6346        );
6347    }
6348
6349    #[test]
6350    fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
6351        use crate::event::{Modifiers, PointerButton};
6352
6353        // Single factory returning None → no overlay shown, no panic.
6354        let mut tree = WidgetTree::new();
6355        tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
6356        tree.layout(SizeProposal::exact(200.0, 100.0));
6357
6358        let overlay_count_before = tree.overlay_manager.len();
6359        tree.dispatch_event(WidgetEvent::pointer_down(
6360            Point::new(50.0, 25.0),
6361            PointerButton::Secondary,
6362            Modifiers::NONE,
6363        ));
6364        let overlay_count_after = tree.overlay_manager.len();
6365        assert_eq!(
6366            overlay_count_before, overlay_count_after,
6367            "a factory returning None must not mount any overlay"
6368        );
6369    }
6370
6371    // ---- Reconcile-on-rebuild (`preserves_children_on_rebuild`) ----------
6372    //
6373    // These pin the contract that the preserve path RECONCILES: it keeps the
6374    // children a rebuild re-attaches (and any subtree re-parented into the new
6375    // tree) while reaping the ones it drops — so memoizing widgets are both
6376    // stateful and leak-free. Regression guard for the orphan-leak the old
6377    // "preserve = destroy nothing" behaviour caused.
6378
6379    /// `build()` mints a fresh child every time and returns only it, abandoning
6380    /// the previous one. Used to prove dropped children are reaped, not leaked.
6381    #[derive(Debug)]
6382    struct FreshChildHost {
6383        preserve: bool,
6384    }
6385    impl Widget for FreshChildHost {
6386        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6387            vec![ctx.add(FillWidget::new())]
6388        }
6389        fn layout_response(
6390            &self,
6391            p: SizeProposal,
6392            _c: &LayoutContext,
6393        ) -> crate::widget::LayoutResponse {
6394            p.resolve(10.0, 10.0).into()
6395        }
6396        fn preserves_children_on_rebuild(&self) -> bool {
6397            self.preserve
6398        }
6399    }
6400
6401    #[test]
6402    fn reconcile_reaps_dropped_children_no_leak() {
6403        // preserve=false (destroy-all) and preserve=true (reconcile) must BOTH
6404        // keep the arena bounded when a rebuild drops its old child. Before the
6405        // reconcile fix, preserve=true grew the arena (and the active set) by
6406        // one stranded orphan per rebuild.
6407        for preserve in [false, true] {
6408            let mut tree = WidgetTree::new();
6409            let host = tree.add(FreshChildHost { preserve });
6410            tree.layout(SizeProposal::exact(100.0, 100.0));
6411            let total0 = tree.arena.len();
6412            let active0 = tree.active_widget_count();
6413            for _ in 0..5 {
6414                tree.arena_mark_needs_rebuild_for_testing(host);
6415                tree.layout(SizeProposal::exact(100.0, 100.0));
6416            }
6417            assert_eq!(
6418                tree.arena.len(),
6419                total0,
6420                "preserve={preserve}: dropped children must be reaped, not leaked"
6421            );
6422            assert_eq!(
6423                tree.active_widget_count(),
6424                active0,
6425                "preserve={preserve}: no stranded still-active orphans"
6426            );
6427        }
6428    }
6429
6430    /// `build()` mints one **detached** node every time — the shape of every
6431    /// pre-built popup in the widget crate (a dropdown, a calendar, a
6432    /// tooltip's cascade children): parked dormant, shown later through an
6433    /// overlay, and deliberately not a child, since activation and paint both
6434    /// descend through `children`.
6435    #[derive(Debug)]
6436    struct DetachedContentHost {
6437        preserve: bool,
6438    }
6439    impl Widget for DetachedContentHost {
6440        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6441            let popup = ctx.add_detached(FillWidget::new());
6442            ctx.set_dormant(popup);
6443            vec![ctx.add(FillWidget::new())]
6444        }
6445        fn layout_response(
6446            &self,
6447            p: SizeProposal,
6448            _c: &LayoutContext,
6449        ) -> crate::widget::LayoutResponse {
6450            p.resolve(10.0, 10.0).into()
6451        }
6452        fn preserves_children_on_rebuild(&self) -> bool {
6453            self.preserve
6454        }
6455    }
6456
6457    #[test]
6458    fn rebuilding_reaps_detached_content_no_leak() {
6459        // A parentless node is reachable from no walk at all — not the child
6460        // teardown, not the accessibility tree, not `active_widget_count`. Held
6461        // by a bare `ctx.add` it simply accumulated: one stranded popup per
6462        // rebuild, for the lifetime of the process. `add_detached` records the
6463        // ownership edge that makes it reapable.
6464        for preserve in [false, true] {
6465            let mut tree = WidgetTree::new();
6466            let host = tree.add(DetachedContentHost { preserve });
6467            tree.layout(SizeProposal::exact(100.0, 100.0));
6468            let total0 = tree.arena.len();
6469            for _ in 0..5 {
6470                tree.arena_mark_needs_rebuild_for_testing(host);
6471                tree.layout(SizeProposal::exact(100.0, 100.0));
6472            }
6473            assert_eq!(
6474                tree.arena.len(),
6475                total0,
6476                "preserve={preserve}: the previous build's detached content must be reaped"
6477            );
6478        }
6479    }
6480
6481    #[test]
6482    fn destroying_a_host_reaps_its_detached_content() {
6483        let mut tree = WidgetTree::new();
6484        let outer = tree.add(FillWidget::new());
6485        tree.layout(SizeProposal::exact(100.0, 100.0));
6486        let empty = tree.arena.len();
6487
6488        let host = tree.add_child(outer, DetachedContentHost { preserve: false });
6489        tree.layout(SizeProposal::exact(100.0, 100.0));
6490        assert!(tree.arena.len() > empty);
6491
6492        tree.destroy_subtree(host);
6493        assert_eq!(
6494            tree.arena.len(),
6495            empty,
6496            "the popup must die with the widget that built it"
6497        );
6498    }
6499
6500    /// Memoizes one child and re-attaches the same id every build.
6501    #[derive(Debug)]
6502    struct StableChildHost {
6503        child: Option<WidgetId>,
6504        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6505    }
6506    impl Widget for StableChildHost {
6507        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6508            let id = match self.child {
6509                Some(id) => id,
6510                None => {
6511                    let id = ctx.add(FillWidget::new());
6512                    self.child = Some(id);
6513                    self.probe.set(Some(id));
6514                    id
6515                }
6516            };
6517            vec![id]
6518        }
6519        fn layout_response(
6520            &self,
6521            p: SizeProposal,
6522            _c: &LayoutContext,
6523        ) -> crate::widget::LayoutResponse {
6524            p.resolve(10.0, 10.0).into()
6525        }
6526        fn preserves_children_on_rebuild(&self) -> bool {
6527            true
6528        }
6529    }
6530
6531    #[test]
6532    fn reconcile_preserves_reattached_child() {
6533        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6534        let mut tree = WidgetTree::new();
6535        let host = tree.add(StableChildHost {
6536            child: None,
6537            probe: probe.clone(),
6538        });
6539        tree.layout(SizeProposal::exact(100.0, 100.0));
6540        let child = probe.get().expect("child mounted");
6541        let total0 = tree.arena.len();
6542        for _ in 0..5 {
6543            tree.arena_mark_needs_rebuild_for_testing(host);
6544            tree.layout(SizeProposal::exact(100.0, 100.0));
6545        }
6546        assert!(
6547            tree.arena.is_active(child),
6548            "the re-attached child must survive every rebuild"
6549        );
6550        assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
6551    }
6552
6553    /// Re-homes a node returned from its `build()` under itself.
6554    #[derive(Debug)]
6555    struct Wrapper {
6556        child: WidgetId,
6557    }
6558    impl Widget for Wrapper {
6559        fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6560            vec![self.child]
6561        }
6562        fn layout_response(
6563            &self,
6564            p: SizeProposal,
6565            _c: &LayoutContext,
6566        ) -> crate::widget::LayoutResponse {
6567            p.resolve(10.0, 10.0).into()
6568        }
6569    }
6570
6571    /// Memoizes a body, then wraps it in a FRESH `Wrapper` each build —
6572    /// re-parenting the body out of the previous (now dropped) wrapper. This is
6573    /// the TabWidget / CompositeTooltip pattern in miniature.
6574    #[derive(Debug)]
6575    struct ReparentHost {
6576        body: Option<WidgetId>,
6577        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
6578    }
6579    impl Widget for ReparentHost {
6580        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6581            let body = match self.body {
6582                Some(id) => id,
6583                None => {
6584                    let id = ctx.add(FillWidget::new());
6585                    self.body = Some(id);
6586                    self.probe.set(Some(id));
6587                    id
6588                }
6589            };
6590            vec![ctx.add(Wrapper { child: body })]
6591        }
6592        fn layout_response(
6593            &self,
6594            p: SizeProposal,
6595            _c: &LayoutContext,
6596        ) -> crate::widget::LayoutResponse {
6597            p.resolve(10.0, 10.0).into()
6598        }
6599        fn preserves_children_on_rebuild(&self) -> bool {
6600            true
6601        }
6602    }
6603
6604    #[test]
6605    fn reconcile_spares_reparented_survivor() {
6606        // The memoized body is re-parented into a fresh wrapper each rebuild;
6607        // the old wrapper is dropped. The body must survive (it is re-homed),
6608        // and the old wrappers must be reaped (no leak). This is the exact
6609        // failure that destroyed TabWidget's static panel before the fix: the
6610        // parent-authoritative recursion + single-node arena removal spare the
6611        // re-homed body while still reaping the dropped wrapper subtree.
6612        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
6613        let mut tree = WidgetTree::new();
6614        let host = tree.add(ReparentHost {
6615            body: None,
6616            probe: probe.clone(),
6617        });
6618        tree.layout(SizeProposal::exact(100.0, 100.0));
6619        let body = probe.get().expect("body mounted");
6620        let total0 = tree.arena.len();
6621        for _ in 0..5 {
6622            tree.arena_mark_needs_rebuild_for_testing(host);
6623            tree.layout(SizeProposal::exact(100.0, 100.0));
6624        }
6625        assert!(
6626            tree.arena.is_active(body),
6627            "the re-parented body must survive — it was moved into the new tree, \
6628             not swept with the dropped wrapper"
6629        );
6630        assert_eq!(
6631            tree.arena.len(),
6632            total0,
6633            "dropped wrappers reaped — no per-rebuild leak"
6634        );
6635    }
6636
6637    // -----------------------------------------------------------------
6638    // EventContext::ensure_visible / ensure_widget_visible — the
6639    // rect/id-based outer-scroll chase drained in `collect_from_ctx`.
6640    // -----------------------------------------------------------------
6641
6642    /// A `clips_children` container that places its single child at a fixed
6643    /// vertical offset — used to give a child arena bounds *outside* the
6644    /// container's viewport so the id-based `ensure_widget_visible` walk has a
6645    /// reason to dispatch `ScrollIntoView`.
6646    #[derive(Debug)]
6647    struct BelowContainer {
6648        child: Option<WidgetId>,
6649        offset: f32,
6650    }
6651
6652    impl crate::widget::Widget for BelowContainer {
6653        fn layout_response(
6654            &self,
6655            proposal: SizeProposal,
6656            _ctx: &crate::widget::LayoutContext,
6657        ) -> crate::widget::LayoutResponse {
6658            proposal.resolve(0.0, 0.0).into()
6659        }
6660        fn place_children(
6661            &self,
6662            bounds: Rect,
6663            _proposal: SizeProposal,
6664            children: &mut [crate::widget::WidgetPlacement],
6665            _ctx: &crate::widget::LayoutContext,
6666        ) {
6667            for c in children.iter_mut() {
6668                c.origin = Point::new(bounds.x, bounds.y + self.offset);
6669                c.size = bounds.size();
6670            }
6671        }
6672        fn children(&self) -> Vec<WidgetId> {
6673            self.child.into_iter().collect()
6674        }
6675    }
6676
6677    /// A `clips_children` container that records the `ScrollIntoView` it
6678    /// receives, so a test can assert what the framework dispatched to it.
6679    fn recording_scroll_container(
6680        tree: &mut WidgetTree,
6681        child: WidgetId,
6682        recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
6683    ) -> WidgetId {
6684        use crate::test_widgets::StackWidget;
6685        tree.add(
6686            StackWidget::new()
6687                .child(child)
6688                .on_scroll(move |ev, _ctx| match ev {
6689                    WidgetEvent::ScrollIntoView { target_bounds, .. } => {
6690                        recorded.set(Some(*target_bounds));
6691                        EventResponse::Handled
6692                    }
6693                    _ => EventResponse::Ignored,
6694                })
6695                .clips_children(true),
6696        )
6697    }
6698
6699    #[test]
6700    fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
6701        use std::cell::Cell;
6702        use std::rc::Rc;
6703        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6704        let mut tree = WidgetTree::new();
6705        let actor = tree.add(FillWidget::new());
6706        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6707        tree.layout(SizeProposal::exact(100.0, 100.0));
6708
6709        // A rect well below the 100px viewport — the container must be asked to
6710        // reveal it.
6711        let target = Rect::new(10.0, 500.0, 20.0, 15.0);
6712        let mut ctx = EventContext::new();
6713        ctx.ensure_visible(target);
6714        tree.collect_from_ctx(ctx, actor);
6715
6716        assert_eq!(
6717            recorded.get(),
6718            Some(target),
6719            "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
6720             to the clips_children ancestor"
6721        );
6722    }
6723
6724    #[test]
6725    fn ensure_visible_is_noop_when_rect_already_visible() {
6726        use std::cell::Cell;
6727        use std::rc::Rc;
6728        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6729        let mut tree = WidgetTree::new();
6730        let actor = tree.add(FillWidget::new());
6731        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6732        tree.layout(SizeProposal::exact(100.0, 100.0));
6733
6734        // Fully inside the viewport → the ancestor already shows it, so no
6735        // ScrollIntoView is dispatched.
6736        let mut ctx = EventContext::new();
6737        ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
6738        tree.collect_from_ctx(ctx, actor);
6739
6740        assert_eq!(
6741            recorded.get(),
6742            None,
6743            "a rect already inside the viewport must not trigger a scroll"
6744        );
6745    }
6746
6747    #[test]
6748    fn ensure_visible_margin_forces_scroll_near_edge() {
6749        use std::cell::Cell;
6750        use std::rc::Rc;
6751        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6752        let mut tree = WidgetTree::new();
6753        let actor = tree.add(FillWidget::new());
6754        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6755        tree.layout(SizeProposal::exact(100.0, 100.0));
6756
6757        // Rect at y=95..99 is visible at margin 0, but with a 10px margin its
6758        // padded bottom (109) spills past the 100px viewport → scroll.
6759        let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
6760        let mut ctx = EventContext::new();
6761        ctx.ensure_visible_with_margin(rect, 10.0);
6762        tree.collect_from_ctx(ctx, actor);
6763
6764        assert_eq!(
6765            recorded.get(),
6766            Some(rect),
6767            "the margin must widen the visibility test so a near-edge rect scrolls"
6768        );
6769    }
6770
6771    /// A `clips_children` container that records the alignment and motion of the
6772    /// `ScrollIntoView` it receives.
6773    fn recording_align_container(
6774        tree: &mut WidgetTree,
6775        child: WidgetId,
6776        recorded: std::rc::Rc<
6777            std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
6778        >,
6779    ) -> WidgetId {
6780        use crate::test_widgets::StackWidget;
6781        tree.add(
6782            StackWidget::new()
6783                .child(child)
6784                .on_scroll(move |ev, _ctx| match ev {
6785                    WidgetEvent::ScrollIntoView { align, motion, .. } => {
6786                        recorded.set(Some((*align, *motion)));
6787                        EventResponse::Handled
6788                    }
6789                    _ => EventResponse::Ignored,
6790                })
6791                .clips_children(true),
6792        )
6793    }
6794
6795    #[test]
6796    fn ensure_visible_aligned_scrolls_even_when_already_visible() {
6797        use std::cell::Cell;
6798        use std::rc::Rc;
6799        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
6800        let mut tree = WidgetTree::new();
6801        let actor = tree.add(FillWidget::new());
6802        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
6803        tree.layout(SizeProposal::exact(100.0, 100.0));
6804
6805        // Comfortably inside the viewport — a *minimal* reveal would decline
6806        // (see `ensure_visible_is_noop_when_rect_already_visible`). A pin must
6807        // still fire: re-asserting unconditionally is the whole difference
6808        // between "keep it on screen" and "hold it at this height".
6809        let target = Rect::new(10.0, 10.0, 20.0, 15.0);
6810        let mut ctx = EventContext::new();
6811        ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
6812        tree.collect_from_ctx(ctx, actor);
6813
6814        assert_eq!(
6815            recorded.get(),
6816            Some(target),
6817            "an aligned reveal must dispatch even when the rect is already visible"
6818        );
6819    }
6820
6821    #[test]
6822    fn ensure_visible_aligned_forwards_fraction_and_motion() {
6823        use std::cell::Cell;
6824        use std::rc::Rc;
6825        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6826            Rc::new(Cell::new(None));
6827        let mut tree = WidgetTree::new();
6828        let actor = tree.add(FillWidget::new());
6829        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6830        tree.layout(SizeProposal::exact(100.0, 100.0));
6831
6832        let mut ctx = EventContext::new();
6833        ctx.ensure_visible_aligned(
6834            Rect::new(10.0, 10.0, 20.0, 15.0),
6835            0.25,
6836            crate::event::ScrollMotion::Smooth,
6837        );
6838        tree.collect_from_ctx(ctx, actor);
6839
6840        assert_eq!(
6841            recorded.get(),
6842            Some((
6843                crate::event::ScrollAlign::Fraction(0.25),
6844                crate::event::ScrollMotion::Smooth
6845            )),
6846            "the container must receive the requested fraction and motion verbatim"
6847        );
6848    }
6849
6850    #[test]
6851    fn ensure_visible_aligned_clamps_the_fraction() {
6852        use std::cell::Cell;
6853        use std::rc::Rc;
6854        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6855            Rc::new(Cell::new(None));
6856        let mut tree = WidgetTree::new();
6857        let actor = tree.add(FillWidget::new());
6858        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6859        tree.layout(SizeProposal::exact(100.0, 100.0));
6860
6861        let mut ctx = EventContext::new();
6862        ctx.ensure_visible_aligned(
6863            Rect::new(10.0, 10.0, 20.0, 15.0),
6864            4.2,
6865            crate::event::ScrollMotion::Instant,
6866        );
6867        tree.collect_from_ctx(ctx, actor);
6868
6869        assert_eq!(
6870            recorded.get().map(|(a, _)| a),
6871            Some(crate::event::ScrollAlign::Fraction(1.0)),
6872            "an out-of-range fraction must clamp rather than aim the pin off-screen"
6873        );
6874    }
6875
6876    #[test]
6877    fn plain_ensure_visible_requests_minimal_alignment() {
6878        use std::cell::Cell;
6879        use std::rc::Rc;
6880        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6881            Rc::new(Cell::new(None));
6882        let mut tree = WidgetTree::new();
6883        let actor = tree.add(FillWidget::new());
6884        let _container = recording_align_container(&mut tree, actor, recorded.clone());
6885        tree.layout(SizeProposal::exact(100.0, 100.0));
6886
6887        let mut ctx = EventContext::new();
6888        ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
6889        tree.collect_from_ctx(ctx, actor);
6890
6891        assert_eq!(
6892            recorded.get(),
6893            Some((
6894                crate::event::ScrollAlign::Minimal,
6895                crate::event::ScrollMotion::Instant
6896            )),
6897            "the pre-existing reveal API must keep its exact semantics"
6898        );
6899    }
6900
6901    #[test]
6902    fn only_the_innermost_container_aligns() {
6903        use std::cell::Cell;
6904        use std::rc::Rc;
6905        let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6906            Rc::new(Cell::new(None));
6907        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6908            Rc::new(Cell::new(None));
6909
6910        let mut tree = WidgetTree::new();
6911        let actor = tree.add(FillWidget::new());
6912        let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
6913        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
6914        tree.layout(SizeProposal::exact(100.0, 100.0));
6915
6916        // Off-screen, so the outer container is asked too (a `Minimal` request
6917        // is gated on visibility).
6918        let mut ctx = EventContext::new();
6919        ctx.ensure_visible_aligned(
6920            Rect::new(10.0, 500.0, 20.0, 15.0),
6921            0.5,
6922            crate::event::ScrollMotion::Instant,
6923        );
6924        tree.collect_from_ctx(ctx, actor);
6925
6926        assert_eq!(
6927            inner_rec.get().map(|(a, _)| a),
6928            Some(crate::event::ScrollAlign::Fraction(0.5)),
6929            "the innermost clipping ancestor owns the pin"
6930        );
6931        assert_eq!(
6932            outer_rec.get().map(|(a, _)| a),
6933            Some(crate::event::ScrollAlign::Minimal),
6934            "an outer container must only bring the inner viewport into view — a \
6935             fraction names a height in one viewport, not in every ancestor's"
6936        );
6937    }
6938
6939    #[test]
6940    fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
6941        // The shape of every capped text column: the editor inside a `MaxSize`
6942        // (which clips once a cap is set, and has no `on_scroll`) inside the
6943        // page's scroll area. The wrapper cannot act on a `ScrollIntoView`, so
6944        // it must not be the one the pin is spent on. Otherwise the scroll
6945        // area is handed a plain reveal and typewriter scrolling silently
6946        // becomes ordinary caret-following.
6947        use crate::test_widgets::StackWidget;
6948        use std::cell::Cell;
6949        use std::rc::Rc;
6950        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6951            Rc::new(Cell::new(None));
6952
6953        let mut tree = WidgetTree::new();
6954        let actor = tree.add(FillWidget::new());
6955        let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
6956        let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
6957        tree.layout(SizeProposal::exact(100.0, 100.0));
6958
6959        // Inside the viewport, so only a pin (never a minimal reveal) reaches
6960        // the scroller at all.
6961        let mut ctx = EventContext::new();
6962        ctx.ensure_visible_aligned(
6963            Rect::new(10.0, 10.0, 20.0, 15.0),
6964            0.5,
6965            crate::event::ScrollMotion::Instant,
6966        );
6967        tree.collect_from_ctx(ctx, actor);
6968
6969        assert_eq!(
6970            outer_rec.get().map(|(a, _)| a),
6971            Some(crate::event::ScrollAlign::Fraction(0.5)),
6972            "a clipping wrapper with no scroll handler must pass the pin through \
6973             to the scroll container above it"
6974        );
6975    }
6976
6977    #[test]
6978    fn a_scroller_that_declines_to_move_still_holds_the_pin() {
6979        // What the scroller does with the pin is its own business: a `SceneView`
6980        // already holding the target where asked answers `Ignored`, since it
6981        // moved nothing. That is not an invitation for the next container out
6982        // to align a rectangle inside a viewport it does not own.
6983        use crate::test_widgets::StackWidget;
6984        use std::cell::Cell;
6985        use std::rc::Rc;
6986        let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
6987        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
6988            Rc::new(Cell::new(None));
6989
6990        let mut tree = WidgetTree::new();
6991        let actor = tree.add(FillWidget::new());
6992        let rec = inner_rec.clone();
6993        let inner = tree.add(
6994            StackWidget::new()
6995                .child(actor)
6996                .on_scroll(move |ev, _ctx| match ev {
6997                    WidgetEvent::ScrollIntoView { align, .. } => {
6998                        rec.set(Some(*align));
6999                        EventResponse::Ignored
7000                    }
7001                    _ => EventResponse::Ignored,
7002                })
7003                .clips_children(true),
7004        );
7005        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7006        tree.layout(SizeProposal::exact(100.0, 100.0));
7007
7008        // Off-screen, so the outer container is asked too.
7009        let mut ctx = EventContext::new();
7010        ctx.ensure_visible_aligned(
7011            Rect::new(10.0, 500.0, 20.0, 15.0),
7012            0.5,
7013            crate::event::ScrollMotion::Instant,
7014        );
7015        tree.collect_from_ctx(ctx, actor);
7016
7017        assert_eq!(
7018            inner_rec.get(),
7019            Some(crate::event::ScrollAlign::Fraction(0.5)),
7020            "the innermost scroller is offered the pin"
7021        );
7022        assert_eq!(
7023            outer_rec.get().map(|(a, _)| a),
7024            Some(crate::event::ScrollAlign::Minimal),
7025            "declining to move must not pass the pin outward"
7026        );
7027    }
7028
7029    #[test]
7030    fn ensure_widget_visible_uses_target_arena_bounds() {
7031        use std::cell::Cell;
7032        use std::rc::Rc;
7033        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7034        let mut tree = WidgetTree::new();
7035        // Target lives 500px below the container's top — off the viewport.
7036        let target = tree.add(FillWidget::new());
7037        let rec = recorded.clone();
7038        let container = tree.add(
7039            BelowContainer {
7040                child: Some(target),
7041                offset: 500.0,
7042            }
7043            .on_scroll(move |ev, _ctx| match ev {
7044                WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7045                    rec.set(Some(*target_bounds));
7046                    EventResponse::Handled
7047                }
7048                _ => EventResponse::Ignored,
7049            })
7050            .clips_children(true),
7051        );
7052        tree.layout(SizeProposal::exact(100.0, 100.0));
7053
7054        let expected = tree.bounds(target);
7055        assert!(
7056            expected.y > 100.0,
7057            "fixture sanity: the target must sit below the viewport (y={})",
7058            expected.y
7059        );
7060
7061        // The source widget is irrelevant for the id-based walk — it starts
7062        // from the *target's* parent — so pass the container itself.
7063        let mut ctx = EventContext::new();
7064        ctx.ensure_widget_visible(target);
7065        tree.collect_from_ctx(ctx, container);
7066
7067        assert_eq!(
7068            recorded.get(),
7069            Some(expected),
7070            "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7071             target's current arena bounds"
7072        );
7073    }
7074
7075    #[test]
7076    fn ensure_widget_visible_ignores_missing_widget() {
7077        // A never-mounted id must neither panic nor dispatch a spurious scroll.
7078        use std::cell::Cell;
7079        use std::rc::Rc;
7080        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7081        let mut tree = WidgetTree::new();
7082        let actor = tree.add(FillWidget::new());
7083        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7084        tree.layout(SizeProposal::exact(100.0, 100.0));
7085
7086        let mut ctx = EventContext::new();
7087        ctx.ensure_widget_visible(WidgetId::default());
7088        tree.collect_from_ctx(ctx, actor); // must not panic
7089
7090        assert_eq!(
7091            recorded.get(),
7092            None,
7093            "an unmounted id must not trigger a scroll"
7094        );
7095    }
7096
7097    #[test]
7098    fn context_menu_inside_a_modal_keeps_the_modal() {
7099        // Regression: right-clicking a widget that lives inside an open modal must
7100        // open its context menu WITHOUT tearing down the modal. `show_context_menu_for`
7101        // used to `dismiss_all()`, which closed the very overlay hosting the editor.
7102        use crate::event::{Modifiers, PointerButton, WidgetEvent};
7103        use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
7104        use crate::test_widgets::{FillWidget, StackWidget};
7105
7106        let mut tree = WidgetTree::new();
7107        // A container standing in for the modal's content subtree, with the editor
7108        // (a right-clickable widget) inside it.
7109        let modal_content = tree.add(StackWidget::new());
7110        let _editor = tree.add_child(
7111            modal_content,
7112            FillWidget::new()
7113                .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
7114        );
7115        tree.layout(SizeProposal::exact(200.0, 100.0));
7116
7117        let modal = tree.overlay_manager.show(OverlayRequest::new(
7118            modal_content,
7119            modal_content,
7120            OverlayPlacement::Centered,
7121            DismissBehavior::EscapeKey,
7122        ));
7123        // Give the overlay real bounds so the right-click hit-tests inside it.
7124        tree.overlay_manager
7125            .stack
7126            .iter_mut()
7127            .find(|o| o.id == modal)
7128            .unwrap()
7129            .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
7130        assert_eq!(tree.overlay_manager.len(), 1);
7131
7132        // Right-click the editor inside the modal.
7133        tree.dispatch_event(WidgetEvent::pointer_down(
7134            Point::new(50.0, 25.0),
7135            PointerButton::Secondary,
7136            Modifiers::NONE,
7137        ));
7138
7139        assert!(
7140            tree.overlay_manager.active_ids().contains(&modal),
7141            "the modal must survive opening a context menu inside it"
7142        );
7143        assert_eq!(
7144            tree.overlay_manager.len(),
7145            2,
7146            "the context menu should now be open on top of the surviving modal"
7147        );
7148    }
7149}
7150
7151/// The stage-1 input ingress: the two sample doors, the scroll routing rule,
7152/// and the guarantee that a mouse still behaves exactly as it did.
7153#[cfg(test)]
7154mod input_ingress_tests {
7155    use super::*;
7156    use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
7157    use crate::pointer::{
7158        EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
7159        ScrollSource,
7160    };
7161    use crate::test_widgets::FillWidget;
7162    use crate::widget::{LayoutContext, WidgetPlacement};
7163    use crate::widget_builder::WidgetBuilder;
7164    use std::cell::RefCell;
7165    use std::rc::Rc;
7166
7167    /// A container that lays children out side by side across its bounds, so a
7168    /// hit test at a given x picks a specific child.
7169    ///
7170    /// Local rather than shared: the common `StackWidget` deliberately stacks
7171    /// its children at one origin, which is the opposite of what a routing test
7172    /// needs.
7173    #[derive(Debug)]
7174    struct RowWidget {
7175        children: Vec<WidgetId>,
7176    }
7177
7178    impl crate::widget::Widget for RowWidget {
7179        fn layout_response(
7180            &self,
7181            proposal: SizeProposal,
7182            _ctx: &LayoutContext,
7183        ) -> crate::widget::LayoutResponse {
7184            proposal.resolve(0.0, 0.0).into()
7185        }
7186
7187        fn place_children(
7188            &self,
7189            bounds: Rect,
7190            _proposal: SizeProposal,
7191            children: &mut [WidgetPlacement],
7192            _ctx: &LayoutContext,
7193        ) {
7194            let n = children.len().max(1) as f32;
7195            let w = bounds.width / n;
7196            for (i, child) in children.iter_mut().enumerate() {
7197                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
7198                child.size = teksilo_canvas::Size::new(w, bounds.height);
7199            }
7200        }
7201
7202        fn children(&self) -> Vec<WidgetId> {
7203            self.children.clone()
7204        }
7205    }
7206
7207    /// Everything a widget observes of a pointer interaction, as text, so two
7208    /// runs can be compared for exact equality rather than field by field.
7209    fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
7210        let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
7211
7212        let mut tree = WidgetTree::new();
7213        let pointer_log = log.clone();
7214        let hover_log = log.clone();
7215        tree.add(
7216            FillWidget::new()
7217                .on_pointer_event(move |event, _ctx| {
7218                    pointer_log.borrow_mut().push(format!("{event:?}"));
7219                    EventResponse::Ignored
7220                })
7221                .on_hover(move |entered, _ctx| {
7222                    hover_log.borrow_mut().push(format!("hover({entered})"));
7223                }),
7224        );
7225        tree.layout(SizeProposal::exact(100.0, 100.0));
7226
7227        drive(&mut tree);
7228        log.borrow().clone()
7229    }
7230
7231    /// The load-bearing compatibility claim of this package: lowering a mouse
7232    /// `PointerSample` produces the *same* `WidgetEvent` stream, in the same
7233    /// order, as writing the events out by hand. If this ever diverges, the
7234    /// door has started meaning something different from the events it lowers
7235    /// to.
7236    #[test]
7237    fn a_mouse_sample_reproduces_todays_event_stream() {
7238        let inside = Point::new(40.0, 40.0);
7239        let moved = Point::new(60.0, 55.0);
7240        let outside = Point::new(400.0, 400.0);
7241
7242        let legacy = record(|tree| {
7243            tree.dispatch_event(WidgetEvent::pointer_move(inside));
7244            tree.dispatch_event(WidgetEvent::pointer_down(
7245                inside,
7246                PointerButton::Primary,
7247                Modifiers::NONE,
7248            ));
7249            tree.dispatch_event(WidgetEvent::pointer_move(moved));
7250            tree.dispatch_event(WidgetEvent::pointer_up(
7251                moved,
7252                PointerButton::Primary,
7253                Modifiers::NONE,
7254            ));
7255            tree.dispatch_event(WidgetEvent::pointer_move(outside));
7256        });
7257
7258        let sampled = record(|tree| {
7259            let t = EventTime::ZERO;
7260            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
7261            tree.dispatch_pointer(
7262                PointerSample::mouse(PointerPhase::Down, inside, t)
7263                    .with_button(PointerButton::Primary),
7264            );
7265            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
7266            tree.dispatch_pointer(
7267                PointerSample::mouse(PointerPhase::Up, moved, t)
7268                    .with_button(PointerButton::Primary),
7269            );
7270            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
7271        });
7272
7273        assert_eq!(legacy, sampled);
7274        assert!(
7275            legacy.contains(&"hover(true)".to_string())
7276                && legacy.contains(&"hover(false)".to_string()),
7277            "the fixture must actually exercise enter and leave: {legacy:?}"
7278        );
7279    }
7280
7281    /// A press with no button — what a bare direct-pointer contact reports —
7282    /// still reads as the primary press, because that is what a tap has always
7283    /// been.
7284    #[test]
7285    fn a_buttonless_press_lowers_to_primary() {
7286        let events = record(|tree| {
7287            tree.dispatch_pointer(PointerSample::mouse(
7288                PointerPhase::Down,
7289                Point::new(20.0, 20.0),
7290                EventTime::ZERO,
7291            ));
7292        });
7293        assert!(
7294            events.iter().any(|e| e.contains("button: Primary")),
7295            "{events:?}"
7296        );
7297    }
7298
7299    /// A mouse press and release delivered as samples, as a test writes them
7300    /// by hand with timestamps of its own, form a tap: the sample door reaches
7301    /// the tap recognizer, not only raw `on_pointer_event` handlers. A port
7302    /// review reported the opposite for 0.16.0 and it did not reproduce; this
7303    /// pins it, with a stamp unrelated to the tree's clock on purpose.
7304    #[test]
7305    fn a_sampled_press_and_release_form_a_tap() {
7306        let taps = Rc::new(std::cell::Cell::new(0));
7307        let seen = taps.clone();
7308        let mut tree = WidgetTree::new();
7309        tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
7310        tree.layout(SizeProposal::exact(100.0, 100.0));
7311
7312        let at = Point::new(50.0, 50.0);
7313        tree.dispatch_pointer(PointerSample::mouse(
7314            PointerPhase::Down,
7315            at,
7316            EventTime::from_millis(0),
7317        ));
7318        tree.dispatch_pointer(PointerSample::mouse(
7319            PointerPhase::Up,
7320            at,
7321            EventTime::from_millis(5),
7322        ));
7323
7324        assert_eq!(taps.get(), 1);
7325    }
7326
7327    // --- scroll routing --------------------------------------------------
7328
7329    /// Two leaves side by side across a 200-wide tree: `top` owns x < 100,
7330    /// `bottom` owns x >= 100.
7331    fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
7332        let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
7333        let mut tree = WidgetTree::new();
7334
7335        let leading_hits = hits.clone();
7336        let trailing_hits = hits.clone();
7337        let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7338            leading_hits.borrow_mut().push("leading");
7339            EventResponse::Handled
7340        }));
7341        let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
7342            trailing_hits.borrow_mut().push("trailing");
7343            EventResponse::Handled
7344        }));
7345        tree.add(RowWidget {
7346            children: vec![leading, trailing],
7347        });
7348        tree.layout(SizeProposal::exact(200.0, 100.0));
7349        (tree, hits)
7350    }
7351
7352    fn notch() -> ScrollDelta {
7353        ScrollDelta::Lines { x: 0.0, y: -1.0 }
7354    }
7355
7356    /// The mouse path: a wheel notch carries no position, so it routes by
7357    /// hover exactly as it always has. This is the no-op claim.
7358    #[test]
7359    fn a_positionless_scroll_routes_by_hover() {
7360        let (mut tree, hits) = scroll_fixture();
7361
7362        tree.pointer_move(Point::new(150.0, 50.0)); // hover the trailing leaf
7363        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7364        assert_eq!(*hits.borrow(), vec!["trailing"]);
7365
7366        tree.pointer_move(Point::new(50.0, 50.0)); // hover the leading leaf
7367        tree.dispatch_scroll(ScrollSample::wheel(
7368            notch(),
7369            Modifiers::NONE,
7370            EventTime::ZERO,
7371        ));
7372        assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
7373    }
7374
7375    /// A positioned scroll routes by hit test, *against* the hover. This is
7376    /// the only thing that makes a synthesised touch pan routable at all: a
7377    /// contact never writes hover, so hover would send the pan to whatever the
7378    /// mouse last touched — or nowhere.
7379    #[test]
7380    fn a_positioned_scroll_routes_by_hit_test() {
7381        let (mut tree, hits) = scroll_fixture();
7382        tree.pointer_move(Point::new(150.0, 50.0)); // hover the TRAILING leaf
7383
7384        tree.dispatch_event(WidgetEvent::scroll_at(
7385            notch(),
7386            Modifiers::NONE,
7387            Point::new(50.0, 50.0), // …but scroll over the LEADING one
7388        ));
7389        assert_eq!(*hits.borrow(), vec!["leading"]);
7390
7391        tree.dispatch_scroll(
7392            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7393                .at(Point::new(150.0, 50.0)),
7394        );
7395        assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
7396    }
7397
7398    /// With nothing hovered and nothing focused a positionless scroll goes
7399    /// nowhere — the pre-existing behaviour, pinned rather than left
7400    /// incidental.
7401    #[test]
7402    fn a_positionless_scroll_with_no_hover_goes_nowhere() {
7403        let (mut tree, hits) = scroll_fixture();
7404        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
7405        assert!(hits.borrow().is_empty());
7406    }
7407
7408    // --- the per-dispatch snapshot ---------------------------------------
7409
7410    /// A handler can ask which pointer it is serving, and what phase and
7411    /// source a scroll had.
7412    #[test]
7413    fn a_handler_sees_the_sample_it_is_serving() {
7414        let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
7415            Rc::new(RefCell::new(Vec::new()));
7416        let sink = seen.clone();
7417
7418        let mut tree = WidgetTree::new();
7419        // A `ScrollSource::TouchPan` sample is delivered along the pan
7420        // claimants and nowhere else (see `widget_tree::pan_arbiter`), so the
7421        // fixture has to be a pan surface for the sample below to reach it at
7422        // all. Nothing about what this test *asserts* changes — only that the
7423        // widget it asserts against is now the kind of widget a touch pan is
7424        // addressed to.
7425        tree.add(
7426            FillWidget::new()
7427                .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
7428                .on_scroll(move |_event, ctx| {
7429                    sink.borrow_mut().push((
7430                        ctx.scroll_phase(),
7431                        ctx.scroll_source(),
7432                        ctx.pointer_position(),
7433                    ));
7434                    EventResponse::Handled
7435                }),
7436        );
7437        tree.layout(SizeProposal::exact(100.0, 100.0));
7438
7439        tree.dispatch_scroll(ScrollSample {
7440            delta: notch(),
7441            position: Some(Point::new(50.0, 50.0)),
7442            phase: ScrollPhase::Momentum,
7443            source: ScrollSource::TouchPan,
7444            pointer: PointerInfo::mouse(EventTime::from_millis(12)),
7445            modifiers: Modifiers::NONE,
7446        });
7447
7448        assert_eq!(
7449            *seen.borrow(),
7450            vec![(
7451                ScrollPhase::Momentum,
7452                ScrollSource::TouchPan,
7453                Some(Point::new(50.0, 50.0))
7454            )]
7455        );
7456    }
7457
7458    /// Outside a pointer dispatch a handler sees the default mouse — the same
7459    /// answer every such handler got before pointers were distinguishable.
7460    #[test]
7461    fn a_legacy_event_reports_the_mouse_at_the_epoch() {
7462        let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
7463        let sink = seen.clone();
7464
7465        let mut tree = WidgetTree::new();
7466        tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
7467            *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
7468            EventResponse::Ignored
7469        }));
7470        tree.layout(SizeProposal::exact(100.0, 100.0));
7471        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
7472
7473        assert_eq!(
7474            *seen.borrow(),
7475            Some((PointerId::MOUSE, ScrollPhase::Discrete))
7476        );
7477    }
7478
7479    /// The snapshot is saved and restored around a dispatch, so a nested one
7480    /// (a synthetic click, a scroll-into-view walk) does not strand the outer
7481    /// sample's view of the world.
7482    #[test]
7483    fn the_snapshot_is_restored_after_a_dispatch() {
7484        let mut tree = WidgetTree::new();
7485        tree.add(FillWidget::new());
7486        tree.layout(SizeProposal::exact(100.0, 100.0));
7487
7488        let before = tree.current_input.clone();
7489        tree.dispatch_scroll(
7490            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
7491                .at(Point::new(10.0, 10.0)),
7492        );
7493        assert_eq!(tree.current_input, before);
7494    }
7495}
7496
7497#[cfg(test)]
7498mod press_and_focus_tests {
7499    //! The framework press, focus-on-release for direct pointers, and the one
7500    //! `focus_visible` signal — driven through the real ingress doors against
7501    //! real trees.
7502
7503    use std::cell::{Cell, RefCell};
7504    use std::rc::Rc;
7505    use teksilo_canvas::{Point, SizeProposal};
7506
7507    use crate::WidgetId;
7508    use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
7509    use crate::focus::FocusOrigin;
7510    use crate::pointer::clock::ManualClock;
7511    use crate::pointer::touch_action::PanClaim;
7512    use crate::pointer::{
7513        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
7514        PointerSample,
7515    };
7516    use crate::test_widgets::{FillWidget, StackWidget};
7517    use crate::widget_builder::WidgetBuilder;
7518    use crate::widget_tree::WidgetTree;
7519
7520    // -----------------------------------------------------------------
7521    // Fixtures
7522    // -----------------------------------------------------------------
7523
7524    /// A fresh contact identity, minted through the real allocator.
7525    fn contact_id() -> PointerId {
7526        use std::sync::atomic::{AtomicU64, Ordering};
7527        static NEXT: AtomicU64 = AtomicU64::new(1);
7528        PointerIdAllocator::global().begin(
7529            BackendDeviceKey::new(0x0B11),
7530            NEXT.fetch_add(1, Ordering::Relaxed),
7531        )
7532    }
7533
7534    fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
7535        PointerSample {
7536            pointer: PointerInfo::touch(id, t),
7537            phase,
7538            position: at,
7539            button: None,
7540            modifiers: Modifiers::NONE,
7541            coalesced: Vec::new(),
7542        }
7543    }
7544
7545    /// A tree on a clock the test drives, so every deadline in these tests is
7546    /// virtual.
7547    fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
7548        let mut tree = WidgetTree::new();
7549        let clock = Rc::new(ManualClock::new(EventTime::ZERO));
7550        tree.set_input_clock(clock.clone());
7551        (tree, clock)
7552    }
7553
7554    /// A tappable leaf with a framework press signal installed on it, plus the
7555    /// signal itself.
7556    ///
7557    /// `on_tap` is what gives the node a gesture arena, which is what makes it
7558    /// the press owner — the same thing a `Button` gets from its own tap
7559    /// handler.
7560    fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
7561        let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
7562        let signal = tree.pressed_signal(id);
7563        (id, signal)
7564    }
7565
7566    // -----------------------------------------------------------------
7567    // The enum
7568    // -----------------------------------------------------------------
7569
7570    /// `FocusOrigin` now names the device behind a pointer focus, and the two
7571    /// accessors that replaced `== FocusOrigin::Pointer` answer for every arm.
7572    #[test]
7573    fn a_pointer_origin_names_its_device() {
7574        use teksilo_tokens::{PenKind, PointerKind};
7575
7576        assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
7577        assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
7578        assert!(FocusOrigin::POINTER.is_pointer());
7579        assert!(!FocusOrigin::Keyboard.is_pointer());
7580        assert!(!FocusOrigin::Programmatic.is_pointer());
7581        assert!(!FocusOrigin::Accessibility.is_pointer());
7582
7583        assert_eq!(
7584            FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
7585            Some(PointerKind::Touch),
7586        );
7587        assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
7588        assert_eq!(
7589            FocusOrigin::POINTER.pointer_kind(),
7590            Some(PointerKind::Unknown),
7591            "a widget deriving its own origin says so rather than naming a device it never saw",
7592        );
7593    }
7594
7595    /// The `:focus-visible` verdict, per origin. `Programmatic` abstains — a
7596    /// scripted focus declares no modality, so the ring stays where the user's
7597    /// last real interaction left it.
7598    #[test]
7599    fn only_a_real_navigation_declares_a_modality() {
7600        use teksilo_tokens::PointerKind;
7601
7602        assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
7603        assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
7604        assert_eq!(
7605            FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
7606            Some(false),
7607        );
7608        assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
7609    }
7610
7611    /// The three Tier-3 configs still carry `Signal<Option<FocusOrigin>>`, and
7612    /// a style reading one still gets what it was written against.
7613    #[test]
7614    fn the_tier_three_configs_are_unchanged() {
7615        let origin: crate::signal::Signal<Option<FocusOrigin>> =
7616            crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
7617        let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7618        let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7619        let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
7620        for field in [slider, splitter, segmented] {
7621            assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
7622        }
7623        origin.set(Some(FocusOrigin::POINTER));
7624        assert_ne!(
7625            origin.get(),
7626            Some(FocusOrigin::Keyboard),
7627            "the `== Some(Keyboard)` test every consumer makes still discriminates",
7628        );
7629    }
7630
7631    // -----------------------------------------------------------------
7632    // focus-visible per kind
7633    // -----------------------------------------------------------------
7634
7635    /// Keyboard focus, then a touch tap, leaves no ring.
7636    ///
7637    /// The behaviour predates this package; what is new is that the focus the
7638    /// tap installs lands on the **release**, so this pins that moving the
7639    /// assignment did not leave a keyboard ring standing over a control the
7640    /// finger just took.
7641    #[test]
7642    fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
7643        let (mut tree, _clock) = tree_on_a_clock();
7644        let a = tree.add(FillWidget::new().focusable());
7645        let b = tree.add(FillWidget::new().focusable());
7646        let root = tree.add(SideBySide {
7647            children: vec![a, b],
7648        });
7649        tree.layout(SizeProposal::exact(200.0, 100.0));
7650        let _ = root;
7651
7652        let visible = tree.focus_visible_signal();
7653        tree.press_key(Key::Tab, Modifiers::NONE);
7654        assert_eq!(tree.focused(), Some(a));
7655        assert!(visible.get(), "keyboard navigation reveals the ring");
7656
7657        let id = contact_id();
7658        let at = tree.bounds(b).center();
7659        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
7660        tree.dispatch_pointer(contact(
7661            id,
7662            PointerPhase::Up,
7663            at,
7664            EventTime::from_millis(30),
7665        ));
7666
7667        assert_eq!(tree.focused(), Some(b), "the release moved focus");
7668        assert!(!visible.get(), "and the ring did not come with it");
7669        assert_eq!(
7670            tree.focus_origin(),
7671            Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
7672        );
7673    }
7674
7675    /// An assistive `Action::Focus` reveals the ring. The user is navigating —
7676    /// they are simply not doing it with a key — and this used to route through
7677    /// `Programmatic`, which declares nothing and left a screen-reader user
7678    /// with an invisible focus after any click.
7679    #[test]
7680    fn an_assistive_focus_reveals_the_ring() {
7681        let mut tree = WidgetTree::new();
7682        let a = tree.add(FillWidget::new().focusable());
7683        let b = tree.add(FillWidget::new().focusable());
7684        let root = tree.add(SideBySide {
7685            children: vec![a, b],
7686        });
7687        tree.layout(SizeProposal::exact(200.0, 50.0));
7688        let _ = root;
7689        let visible = tree.focus_visible_signal();
7690
7691        tree.click(a);
7692        assert_eq!(tree.focused(), Some(a));
7693        assert!(!visible.get(), "the click hid it");
7694
7695        tree.dispatch_event(WidgetEvent::AccessAction {
7696            target: Some(b),
7697            action: accesskit::Action::Focus,
7698            target_node: crate::accessibility::widget_id_to_node_id(b),
7699            data: None,
7700        });
7701        assert_eq!(tree.focused(), Some(b));
7702        assert!(visible.get(), "an assistive move is a navigation");
7703        assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
7704    }
7705
7706    /// A programmatic focus abstains: it declares no modality, so the ring
7707    /// stays exactly where the last real interaction left it. This is what
7708    /// `Button` / `Checkbox`'s pre-existing focus-ring tests pin, and what
7709    /// browsers do for `element.focus()`.
7710    #[test]
7711    fn a_programmatic_focus_leaves_the_modality_alone() {
7712        let mut tree = WidgetTree::new();
7713        let a = tree.add(FillWidget::new().focusable());
7714        let b = tree.add(FillWidget::new().focusable());
7715        let root = tree.add(SideBySide {
7716            children: vec![a, b],
7717        });
7718        tree.layout(SizeProposal::exact(200.0, 100.0));
7719        let _ = root;
7720        let visible = tree.focus_visible_signal();
7721
7722        tree.press_key(Key::Tab, Modifiers::NONE);
7723        assert_eq!(tree.focused(), Some(a));
7724        assert!(visible.get());
7725        tree.focus(b);
7726        assert!(
7727            visible.get(),
7728            "a scripted focus does not hide a keyboard ring"
7729        );
7730
7731        tree.click(a);
7732        assert!(!visible.get());
7733        tree.focus(b);
7734        assert!(
7735            !visible.get(),
7736            "and does not reveal one after a click either",
7737        );
7738    }
7739
7740    // -----------------------------------------------------------------
7741    // Activation on release
7742    // -----------------------------------------------------------------
7743
7744    /// A mouse focuses on press, exactly as it always has.
7745    #[test]
7746    fn a_mouse_still_focuses_on_press() {
7747        let mut tree = WidgetTree::new();
7748        let w = tree.add(FillWidget::new().focusable());
7749        tree.layout(SizeProposal::exact(100.0, 50.0));
7750        let center = tree.bounds(w).center();
7751
7752        tree.dispatch_event(WidgetEvent::pointer_down(
7753            center,
7754            PointerButton::Primary,
7755            Modifiers::NONE,
7756        ));
7757        assert_eq!(
7758            tree.focused(),
7759            Some(w),
7760            "focus lands on the press for an indirect pointer",
7761        );
7762    }
7763
7764    /// A finger's focus waits for the release.
7765    #[test]
7766    fn a_finger_focuses_on_release() {
7767        let (mut tree, _clock) = tree_on_a_clock();
7768        let w = tree.add(FillWidget::new().focusable());
7769        tree.layout(SizeProposal::exact(100.0, 50.0));
7770        let center = tree.bounds(w).center();
7771
7772        let id = contact_id();
7773        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7774        assert_eq!(
7775            tree.focused(),
7776            None,
7777            "a finger that has only landed has chosen nothing yet",
7778        );
7779
7780        tree.dispatch_pointer(contact(
7781            id,
7782            PointerPhase::Up,
7783            center,
7784            EventTime::from_millis(40),
7785        ));
7786        assert_eq!(tree.focused(), Some(w));
7787        assert_eq!(
7788            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
7789            Some(teksilo_tokens::PointerKind::Touch),
7790            "the origin names the device that delivered it",
7791        );
7792    }
7793
7794    /// …and only when the release lands back on the same focusable. A finger
7795    /// that presses one control, slides onto its neighbour and lifts has
7796    /// activated nothing and must move focus nowhere.
7797    #[test]
7798    fn a_release_on_a_different_focusable_moves_no_focus() {
7799        let (mut tree, _clock) = tree_on_a_clock();
7800        let a = tree.add(FillWidget::new().focusable());
7801        let b = tree.add(FillWidget::new().focusable());
7802        let root = tree.add(SideBySide {
7803            children: vec![a, b],
7804        });
7805        tree.layout(SizeProposal::exact(200.0, 50.0));
7806        let _ = root;
7807
7808        let on_a = tree.bounds(a).center();
7809        let on_b = tree.bounds(b).center();
7810        let id = contact_id();
7811        tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
7812        tree.dispatch_pointer(contact(
7813            id,
7814            PointerPhase::Move,
7815            on_b,
7816            EventTime::from_millis(20),
7817        ));
7818        tree.dispatch_pointer(contact(
7819            id,
7820            PointerPhase::Up,
7821            on_b,
7822            EventTime::from_millis(40),
7823        ));
7824
7825        assert_eq!(
7826            tree.focused(),
7827            None,
7828            "the guard is `the release landed on the same focusable as the press`",
7829        );
7830    }
7831
7832    // -----------------------------------------------------------------
7833    // The press visual
7834    // -----------------------------------------------------------------
7835
7836    /// A mouse press lights the visual at once and the release clears it —
7837    /// the pre-touch rule, and no press-feedback delay anywhere near it.
7838    #[test]
7839    fn a_mouse_press_visual_is_unchanged() {
7840        let mut tree = WidgetTree::new();
7841        let (w, pressed) = tappable(&mut tree);
7842        tree.layout(SizeProposal::exact(100.0, 50.0));
7843        let center = tree.bounds(w).center();
7844
7845        assert!(!pressed.get(), "not pressed at rest");
7846        tree.dispatch_event(WidgetEvent::pointer_down(
7847            center,
7848            PointerButton::Primary,
7849            Modifiers::NONE,
7850        ));
7851        assert!(pressed.get(), "an indirect pointer lights up on the press");
7852        assert!(
7853            !tree.press_pending(w),
7854            "and never waits: a mouse opens no pan session, so there is no \
7855             ambiguity to wait out",
7856        );
7857        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
7858
7859        tree.dispatch_event(WidgetEvent::pointer_up(
7860            center,
7861            PointerButton::Primary,
7862            Modifiers::NONE,
7863        ));
7864        assert!(!pressed.get(), "the release clears it");
7865        assert_eq!(tree.pressed_by(w), None);
7866    }
7867
7868    /// A slide off the target clears the visual, and sliding back on restores
7869    /// it. WCAG 2.2 SC 2.5.2's abort gesture, and reversible right up to the
7870    /// release.
7871    #[test]
7872    fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
7873        let (mut tree, _clock) = tree_on_a_clock();
7874        let (w, pressed) = tappable(&mut tree);
7875        tree.layout(SizeProposal::exact(100.0, 200.0));
7876        let inside = tree.bounds(w).center();
7877        let outside = Point::new(inside.x, inside.y + 400.0);
7878
7879        let id = contact_id();
7880        tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
7881        assert!(
7882            pressed.get(),
7883            "nothing here claims a pan, so no delay applies"
7884        );
7885
7886        tree.dispatch_pointer(contact(
7887            id,
7888            PointerPhase::Move,
7889            outside,
7890            EventTime::from_millis(20),
7891        ));
7892        assert!(!pressed.get(), "the press has left its target");
7893        assert!(!tree.press_is_inside(w));
7894        assert_eq!(
7895            tree.pressed_by(w),
7896            Some(id),
7897            "the contact still holds the press — it is the *visual* that is off",
7898        );
7899
7900        tree.dispatch_pointer(contact(
7901            id,
7902            PointerPhase::Move,
7903            inside,
7904            EventTime::from_millis(40),
7905        ));
7906        assert!(pressed.get(), "sliding back on restores it");
7907
7908        tree.dispatch_pointer(contact(
7909            id,
7910            PointerPhase::Up,
7911            inside,
7912            EventTime::from_millis(60),
7913        ));
7914        assert!(!pressed.get());
7915    }
7916
7917    /// A second contact cannot clear the first's visual. Its own release
7918    /// removes only the press it owns.
7919    #[test]
7920    fn a_second_contact_cannot_clear_the_first_visual() {
7921        let (mut tree, _clock) = tree_on_a_clock();
7922        let (w, pressed) = tappable(&mut tree);
7923        tree.layout(SizeProposal::exact(100.0, 50.0));
7924        let bounds = tree.bounds(w);
7925        let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
7926        let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
7927
7928        let first = contact_id();
7929        let second = contact_id();
7930        tree.dispatch_pointer(contact(
7931            first,
7932            PointerPhase::Down,
7933            first_at,
7934            EventTime::ZERO,
7935        ));
7936        assert!(pressed.get());
7937        assert_eq!(tree.pressed_by(w), Some(first));
7938
7939        tree.dispatch_pointer(contact(
7940            second,
7941            PointerPhase::Down,
7942            second_at,
7943            EventTime::from_millis(10),
7944        ));
7945        assert_eq!(
7946            tree.pressed_by(w),
7947            Some(first),
7948            "under `MultiContact::First` the second contact is terminated before \
7949             it reaches the arena at all",
7950        );
7951
7952        tree.dispatch_pointer(contact(
7953            second,
7954            PointerPhase::Up,
7955            second_at,
7956            EventTime::from_millis(20),
7957        ));
7958        assert!(
7959            pressed.get(),
7960            "so the second contact's release cannot clear a visual it never owned",
7961        );
7962        assert_eq!(tree.pressed_by(w), Some(first));
7963
7964        tree.dispatch_pointer(contact(
7965            first,
7966            PointerPhase::Up,
7967            first_at,
7968            EventTime::from_millis(30),
7969        ));
7970        assert!(!pressed.get(), "the owner's release does clear it");
7971    }
7972
7973    /// A cancel clears the visual. The node is never sent an `Up` to clear it
7974    /// from, so nothing else could.
7975    #[test]
7976    fn a_cancel_clears_the_visual() {
7977        let (mut tree, _clock) = tree_on_a_clock();
7978        let (w, pressed) = tappable(&mut tree);
7979        tree.layout(SizeProposal::exact(100.0, 50.0));
7980        let center = tree.bounds(w).center();
7981
7982        let id = contact_id();
7983        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
7984        assert!(pressed.get());
7985
7986        tree.dispatch_pointer(contact(
7987            id,
7988            PointerPhase::Cancel,
7989            center,
7990            EventTime::from_millis(20),
7991        ));
7992        assert!(!pressed.get(), "a press that was taken away is not painted");
7993        assert_eq!(tree.pressed_by(w), None);
7994    }
7995
7996    /// A peer winning the arbitration clears the visual. The pressed control is
7997    /// never told; only the router knows it has lost the press.
7998    #[test]
7999    fn a_pan_claim_clears_the_visual() {
8000        let (mut tree, clock) = tree_on_a_clock();
8001        let (row, pressed) = tappable(&mut tree);
8002        let list = tree.add(
8003            StackWidget::new()
8004                .child(row)
8005                .pan_claim(PanClaim::vertical())
8006                .on_scroll(|_e, _c| EventResponse::Handled),
8007        );
8008        tree.layout(SizeProposal::exact(200.0, 400.0));
8009        let _ = list;
8010        let start = Point::new(100.0, 200.0);
8011
8012        let id = contact_id();
8013        tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
8014        // Inside a claimant, so the visual waits.
8015        assert!(tree.press_pending(row));
8016        clock.set(EventTime::from_millis(100));
8017        tree.tick_gestures(std::time::Instant::now());
8018        assert!(pressed.get(), "…and appears once the delay has elapsed");
8019
8020        // Past the touch profile's 36 dp pan slop: the list claims.
8021        tree.dispatch_pointer(contact(
8022            id,
8023            PointerPhase::Move,
8024            Point::new(100.0, 260.0),
8025            EventTime::from_millis(120),
8026        ));
8027        assert!(
8028            !pressed.get(),
8029            "the row lost the press to the list and must stop advertising it",
8030        );
8031        assert_eq!(tree.pressed_by(row), None);
8032    }
8033
8034    // -----------------------------------------------------------------
8035    // Which button may raise the visual
8036    // -----------------------------------------------------------------
8037
8038    /// The visual answers to the same buttons the activation does. A node
8039    /// carrying a plain `on_tap` accepts `PRIMARY` and nothing else, so a
8040    /// middle, back or forward press must light nothing up — the press it
8041    /// would be advertising can never complete.
8042    #[test]
8043    fn a_button_the_control_cannot_act_on_raises_no_visual() {
8044        let taps = Rc::new(Cell::new(0u32));
8045        let count = taps.clone();
8046        let mut tree = WidgetTree::new();
8047        let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
8048            count.set(count.get() + 1);
8049        }));
8050        let pressed = tree.pressed_signal(w);
8051        tree.layout(SizeProposal::exact(100.0, 50.0));
8052        let center = tree.bounds(w).center();
8053
8054        for button in [
8055            PointerButton::Middle,
8056            PointerButton::Back,
8057            PointerButton::Forward,
8058        ] {
8059            tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
8060            assert!(
8061                !pressed.get(),
8062                "{button:?} cannot activate an `on_tap` node, so it must not light one up",
8063            );
8064            assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
8065            tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
8066            assert_eq!(
8067                taps.get(),
8068                0,
8069                "the tap recognizer refuses {button:?} too — that is the point",
8070            );
8071        }
8072
8073        // …and the button it does act on is untouched.
8074        tree.dispatch_event(WidgetEvent::pointer_down(
8075            center,
8076            PointerButton::Primary,
8077            Modifiers::NONE,
8078        ));
8079        assert!(pressed.get(), "a primary press lights up as it always has");
8080        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
8081        tree.dispatch_event(WidgetEvent::pointer_up(
8082            center,
8083            PointerButton::Primary,
8084            Modifiers::NONE,
8085        ));
8086        assert!(!pressed.get());
8087        assert_eq!(taps.get(), 1);
8088    }
8089
8090    /// The case a real user hits: a right-click on a control with no context
8091    /// menu. The secondary arm finds nothing to open and falls through to the
8092    /// ordinary press path, which must still raise nothing.
8093    #[test]
8094    fn a_secondary_press_with_no_context_menu_raises_no_visual() {
8095        let mut tree = WidgetTree::new();
8096        let (w, pressed) = tappable(&mut tree);
8097        tree.layout(SizeProposal::exact(100.0, 50.0));
8098        let center = tree.bounds(w).center();
8099
8100        tree.dispatch_event(WidgetEvent::pointer_down(
8101            center,
8102            PointerButton::Secondary,
8103            Modifiers::NONE,
8104        ));
8105        assert!(
8106            !pressed.get(),
8107            "nothing opened, and nothing may look pressed either",
8108        );
8109        assert_eq!(tree.pressed_by(w), None);
8110        assert!(!tree.press_is_inside(w));
8111    }
8112
8113    /// A widget that widened its own mask keeps the visual on the buttons it
8114    /// widened to: the gate reads the node's declared acceptance, it does not
8115    /// hardcode `PRIMARY`.
8116    #[test]
8117    fn a_widened_mask_widens_the_visual_with_it() {
8118        use crate::event::ButtonMask;
8119
8120        let mut tree = WidgetTree::new();
8121        let w = tree.add(
8122            FillWidget::new()
8123                .on_tap(|_e, _c| {})
8124                .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
8125        );
8126        let pressed = tree.pressed_signal(w);
8127        tree.layout(SizeProposal::exact(100.0, 50.0));
8128        let center = tree.bounds(w).center();
8129
8130        tree.dispatch_event(WidgetEvent::pointer_down(
8131            center,
8132            PointerButton::Middle,
8133            Modifiers::NONE,
8134        ));
8135        assert!(pressed.get(), "this node really does act on a middle-click");
8136
8137        tree.dispatch_event(WidgetEvent::pointer_up(
8138            center,
8139            PointerButton::Middle,
8140            Modifiers::NONE,
8141        ));
8142        assert!(!pressed.get());
8143    }
8144
8145    /// A press that raises no visual still records the focus a direct pointer
8146    /// defers to its release. A stylus barrel button reports `Secondary`, and
8147    /// a pen that presses a control and lifts on it has chosen that control
8148    /// whether or not the button lit it up — the record exists for the focus,
8149    /// not only for the visual.
8150    #[test]
8151    fn a_press_that_raises_no_visual_still_defers_its_focus() {
8152        use teksilo_tokens::{PenKind, PointerKind};
8153
8154        let (mut tree, _clock) = tree_on_a_clock();
8155        let (w, pressed) = tappable(&mut tree);
8156        tree.layout(SizeProposal::exact(100.0, 50.0));
8157        let center = tree.bounds(w).center();
8158
8159        let id = contact_id();
8160        let barrel = |phase, t| PointerSample {
8161            pointer: PointerInfo {
8162                kind: PointerKind::Pen(PenKind::Pen),
8163                ..PointerInfo::touch(id, t)
8164            },
8165            phase,
8166            position: center,
8167            button: Some(PointerButton::Secondary),
8168            modifiers: Modifiers::NONE,
8169            coalesced: Vec::new(),
8170        };
8171
8172        tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
8173        assert!(!pressed.get(), "the barrel button activates nothing here");
8174        assert_eq!(tree.pressed_by(w), None);
8175        assert_eq!(
8176            tree.focused(),
8177            None,
8178            "a direct pointer has chosen nothing until it lifts",
8179        );
8180
8181        tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
8182        assert_eq!(
8183            tree.focused(),
8184            Some(w),
8185            "the deferral is not button-gated: the release landed where the press did",
8186        );
8187        assert_eq!(
8188            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
8189            Some(PointerKind::Pen(PenKind::Pen)),
8190        );
8191    }
8192
8193    // -----------------------------------------------------------------
8194    // The press-feedback delay
8195    // -----------------------------------------------------------------
8196
8197    /// The delay applies only inside a pan claimant — a control nothing can
8198    /// scroll out from under has no ambiguity to wait out.
8199    #[test]
8200    fn the_feedback_delay_applies_only_inside_a_claimant() {
8201        // Outside a claimant: immediate.
8202        let (mut tree, _clock) = tree_on_a_clock();
8203        let (w, pressed) = tappable(&mut tree);
8204        tree.layout(SizeProposal::exact(200.0, 400.0));
8205        let id = contact_id();
8206        let at = tree.bounds(w).center();
8207        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8208        assert!(
8209            !tree.press_pending(w),
8210            "no claimant above it, so nothing to rule out",
8211        );
8212        assert!(pressed.get());
8213
8214        // Inside one: withheld, then released by the delay.
8215        let (mut tree, clock) = tree_on_a_clock();
8216        let (row, pressed) = tappable(&mut tree);
8217        let list = tree.add(
8218            StackWidget::new()
8219                .child(row)
8220                .pan_claim(PanClaim::vertical())
8221                .on_scroll(|_e, _c| EventResponse::Handled),
8222        );
8223        tree.layout(SizeProposal::exact(200.0, 400.0));
8224        let _ = list;
8225        let id = contact_id();
8226        let at = Point::new(100.0, 200.0);
8227        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8228        assert!(tree.press_pending(row), "a finger might be about to scroll");
8229        assert!(!pressed.get(), "so the row does not flash");
8230        assert!(
8231            tree.press_is_inside(row),
8232            "the press is real; only its visual is being withheld",
8233        );
8234
8235        clock.set(EventTime::from_millis(99));
8236        tree.tick_gestures(std::time::Instant::now());
8237        assert!(!pressed.get(), "99 ms is under the 100 ms delay");
8238
8239        clock.set(EventTime::from_millis(100));
8240        tree.tick_gestures(std::time::Instant::now());
8241        assert!(pressed.get(), "and 100 ms is it");
8242        assert!(!tree.press_pending(row));
8243    }
8244
8245    /// A mouse pressing inside the very same claimant waits for nothing: an
8246    /// indirect pointer opens no pan session, so the delay never reaches it.
8247    #[test]
8248    fn a_mouse_inside_a_claimant_never_waits() {
8249        let mut tree = WidgetTree::new();
8250        let (row, pressed) = tappable(&mut tree);
8251        let list = tree.add(
8252            StackWidget::new()
8253                .child(row)
8254                .pan_claim(PanClaim::vertical())
8255                .on_scroll(|_e, _c| EventResponse::Handled),
8256        );
8257        tree.layout(SizeProposal::exact(200.0, 400.0));
8258        let _ = list;
8259
8260        tree.dispatch_event(WidgetEvent::pointer_down(
8261            Point::new(100.0, 200.0),
8262            PointerButton::Primary,
8263            Modifiers::NONE,
8264        ));
8265        assert!(!tree.press_pending(row));
8266        assert!(pressed.get(), "Compact with a mouse is exactly as it was");
8267    }
8268
8269    // -----------------------------------------------------------------
8270    // The EventContext queries
8271    // -----------------------------------------------------------------
8272
8273    /// The three queries `teksilo-widgets`' `common/interaction.rs` consumes,
8274    /// read from inside a real handler.
8275    #[test]
8276    fn a_handler_can_read_the_press_it_is_inside() {
8277        let (mut tree, _clock) = tree_on_a_clock();
8278        let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
8279        let log = seen.clone();
8280        let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
8281            move |event, ctx| {
8282                if matches!(event, WidgetEvent::PointerUp { .. }) {
8283                    log.borrow_mut().push((
8284                        ctx.is_pressed(),
8285                        ctx.press_is_inside(),
8286                        ctx.press_pending(),
8287                    ));
8288                }
8289                EventResponse::Ignored
8290            },
8291        ));
8292        let list = tree.add(
8293            StackWidget::new()
8294                .child(row)
8295                .pan_claim(PanClaim::vertical())
8296                .on_scroll(|_e, _c| EventResponse::Handled),
8297        );
8298        tree.layout(SizeProposal::exact(200.0, 400.0));
8299        let _ = list;
8300
8301        let id = contact_id();
8302        let at = Point::new(100.0, 200.0);
8303        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8304        tree.dispatch_pointer(contact(
8305            id,
8306            PointerPhase::Up,
8307            at,
8308            EventTime::from_millis(30),
8309        ));
8310
8311        assert_eq!(
8312            seen.borrow().as_slice(),
8313            &[(false, true, true)],
8314            "released inside the claimant before the delay elapsed: inside, \
8315             pending, and therefore not yet showing",
8316        );
8317    }
8318
8319    /// A handler outside any press reads three falses rather than a panic or a
8320    /// stale answer.
8321    #[test]
8322    fn a_handler_outside_a_press_reads_nothing() {
8323        let mut tree = WidgetTree::new();
8324        let asked = Rc::new(Cell::new(false));
8325        let flag = asked.clone();
8326        let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
8327            flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
8328            EventResponse::Ignored
8329        }));
8330        tree.layout(SizeProposal::exact(100.0, 50.0));
8331        tree.focus(w);
8332        tree.press_key(Key::ArrowDown, Modifiers::NONE);
8333        assert!(!asked.get());
8334    }
8335
8336    /// A completed interaction leaves no press behind, and the leak detector
8337    /// says so — it now reads the press table, which it could not before this
8338    /// package landed one.
8339    #[test]
8340    fn a_completed_press_leaves_the_detector_clean() {
8341        let (mut tree, _clock) = tree_on_a_clock();
8342        let (w, pressed) = tappable(&mut tree);
8343        tree.layout(SizeProposal::exact(100.0, 50.0));
8344        let at = tree.bounds(w).center();
8345
8346        let id = contact_id();
8347        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8348        assert!(pressed.get());
8349        tree.dispatch_pointer(contact(
8350            id,
8351            PointerPhase::Up,
8352            at,
8353            EventTime::from_millis(30),
8354        ));
8355        tree.assert_no_leaked_pointer_state();
8356    }
8357
8358    /// …and it would have caught the opposite. Driven by holding the press open
8359    /// rather than by faking state, so the assertion is about the real exit
8360    /// path.
8361    #[test]
8362    fn the_detector_reports_a_press_still_held() {
8363        let (mut tree, _clock) = tree_on_a_clock();
8364        let (w, _pressed) = tappable(&mut tree);
8365        tree.layout(SizeProposal::exact(100.0, 50.0));
8366        let at = tree.bounds(w).center();
8367
8368        let id = contact_id();
8369        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8370        let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
8371            tree.assert_no_leaked_pointer_state()
8372        }));
8373        let message = *held
8374            .expect_err("a live press is leaked state")
8375            .downcast::<String>()
8376            .expect("the detector panics with a message");
8377        assert!(
8378            message.contains("is still pressed by"),
8379            "the press is named in the report, got: {message}",
8380        );
8381    }
8382
8383    /// A container that lays its children out side by side, so a hit test at a
8384    /// given x picks a specific child. `StackWidget` deliberately stacks at one
8385    /// origin, which is the opposite of what a release-elsewhere test needs.
8386    #[derive(Debug)]
8387    struct SideBySide {
8388        children: Vec<WidgetId>,
8389    }
8390
8391    impl crate::widget::Widget for SideBySide {
8392        fn layout_response(
8393            &self,
8394            proposal: SizeProposal,
8395            _ctx: &crate::widget::LayoutContext,
8396        ) -> crate::widget::LayoutResponse {
8397            proposal.resolve(0.0, 0.0).into()
8398        }
8399
8400        fn place_children(
8401            &self,
8402            bounds: teksilo_canvas::Rect,
8403            _proposal: SizeProposal,
8404            children: &mut [crate::widget::WidgetPlacement],
8405            _ctx: &crate::widget::LayoutContext,
8406        ) {
8407            let n = children.len().max(1) as f32;
8408            let w = bounds.width / n;
8409            for (i, child) in children.iter_mut().enumerate() {
8410                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8411                child.size = teksilo_canvas::Size::new(w, bounds.height);
8412            }
8413        }
8414
8415        fn children(&self) -> Vec<WidgetId> {
8416            self.children.clone()
8417        }
8418    }
8419}
8420
8421#[cfg(test)]
8422mod overlay_release_dismissal_tests {
8423    //! Outside-press overlay dismissal, driven through the real ingress door.
8424    //!
8425    //! The defect: `handle_click_outside` ran on the `PointerDown` and then
8426    //! *fell through*, so one press both closed a menu and actuated whatever
8427    //! the menu was covering. With a cursor that is defensible — the user aimed
8428    //! at a pixel they could see the whole time. With a finger it is not: the
8429    //! menu is the only thing they were looking at, and the control underneath
8430    //! is one they never saw.
8431
8432    use std::cell::Cell;
8433    use std::rc::Rc;
8434
8435    use teksilo_canvas::{Point, Rect, Size, SizeProposal};
8436
8437    use crate::WidgetId;
8438    use crate::event::{Modifiers, PointerButton, WidgetEvent};
8439    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8440    use crate::pointer::{
8441        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8442        PointerSample,
8443    };
8444    use crate::test_widgets::FillWidget;
8445    use crate::widget::{LayoutContext, LayoutResponse, Widget};
8446    use crate::widget_builder::WidgetBuilder;
8447    use crate::widget_tree::WidgetTree;
8448
8449    // -----------------------------------------------------------------
8450    // Fixtures
8451    // -----------------------------------------------------------------
8452
8453    /// A leaf with an intrinsic size, so an overlay hung off it gets real
8454    /// bounds out of `position_overlays` instead of a zero rect.
8455    #[derive(Debug)]
8456    struct Panel(Size);
8457
8458    impl Widget for Panel {
8459        fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8460            self.0.into()
8461        }
8462    }
8463
8464    /// A root that puts its first child over the whole window and its second in
8465    /// a 10 dp corner.
8466    ///
8467    /// Two bare roots would both be laid out at the window's full size — the
8468    /// trigger would then contain every press point, and the pre-existing
8469    /// "a press on a click-opened overlay's own anchor is consumed" rule would
8470    /// swallow the very presses these tests are about.
8471    #[derive(Debug)]
8472    struct PageAndTrigger {
8473        children: Vec<WidgetId>,
8474    }
8475
8476    impl Widget for PageAndTrigger {
8477        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8478            proposal.resolve(0.0, 0.0).into()
8479        }
8480
8481        fn place_children(
8482            &self,
8483            bounds: Rect,
8484            _proposal: SizeProposal,
8485            children: &mut [crate::widget::WidgetPlacement],
8486            _ctx: &LayoutContext,
8487        ) {
8488            if let Some(page) = children.get_mut(0) {
8489                page.origin = bounds.origin();
8490                page.size = bounds.size();
8491            }
8492            if let Some(trigger) = children.get_mut(1) {
8493                trigger.origin = bounds.origin();
8494                trigger.size = Size::new(10.0, 10.0);
8495            }
8496        }
8497
8498        fn children(&self) -> Vec<WidgetId> {
8499            self.children.clone()
8500        }
8501    }
8502
8503    fn contact_id(n: u64) -> PointerId {
8504        PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
8505    }
8506
8507    fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
8508        PointerSample {
8509            pointer: PointerInfo::touch(id, EventTime::ZERO),
8510            phase,
8511            position: at,
8512            button: None,
8513            modifiers: Modifiers::NONE,
8514            coalesced: Vec::new(),
8515        }
8516    }
8517
8518    /// The scene every test below shares: a tappable page filling the window,
8519    /// and a menu overlay floating over part of it at (100, 100, 200, 200).
8520    ///
8521    /// The menu's content is a separate root, so a press outside it lands on
8522    /// the page and a press inside it lands on the menu — which is exactly the
8523    /// arrangement the dismissal rule is about.
8524    fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
8525        let mut tree = WidgetTree::new();
8526        let taps = Rc::new(Cell::new(0u32));
8527        let counter = taps.clone();
8528        let page = tree.add(
8529            FillWidget::new()
8530                .focusable()
8531                .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
8532        );
8533        // A small trigger in the corner, well clear of both press points — see
8534        // `PageAndTrigger` for why it cannot simply be a second root.
8535        let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
8536        let _root = tree.add(PageAndTrigger {
8537            children: vec![page, trigger],
8538        });
8539        let menu = tree.add(Panel(Size::new(200.0, 200.0)));
8540        tree.show_overlay(OverlayRequest::new(
8541            menu,
8542            trigger,
8543            OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
8544            DismissBehavior::ClickOutside,
8545        ));
8546        tree.layout(SizeProposal::exact(800.0, 600.0));
8547        assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
8548        (tree, page, taps)
8549    }
8550
8551    fn outside() -> Point {
8552        Point::new(600.0, 500.0)
8553    }
8554
8555    fn inside_menu() -> Point {
8556        Point::new(150.0, 150.0)
8557    }
8558
8559    // -----------------------------------------------------------------
8560    // The direct-pointer contract
8561    // -----------------------------------------------------------------
8562
8563    /// The arming press reaches nothing: no press record, no capture, and the
8564    /// menu is still up because the decision belongs to the release.
8565    #[test]
8566    fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
8567        let (mut tree, page, taps) = page_with_a_menu();
8568        let finger = contact_id(1);
8569        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8570
8571        assert_eq!(
8572            tree.active_overlays().len(),
8573            1,
8574            "the press does not close the menu; the release does"
8575        );
8576        assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
8577        let entry = tree.pointers.get(finger).expect("the contact is live");
8578        assert_eq!(entry.captured_by, None, "nothing beneath captured it");
8579        assert!(entry.sequence.is_none(), "no arbitration was opened");
8580        assert_eq!(taps.get(), 0);
8581    }
8582
8583    /// …and the release closes the menu without actuating what it covered.
8584    #[test]
8585    fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
8586        let (mut tree, page, taps) = page_with_a_menu();
8587        let finger = contact_id(2);
8588        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8589        tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
8590
8591        assert!(tree.active_overlays().is_empty(), "the menu closed");
8592        assert_eq!(taps.get(), 0, "the page beneath was never tapped");
8593        assert_eq!(tree.pressed_by(page), None);
8594        tree.assert_no_leaked_pointer_state();
8595    }
8596
8597    /// A press that never completes delivers nothing at all — neither the
8598    /// dismissal it armed nor the press it withheld.
8599    #[test]
8600    fn a_cancelled_press_aborts_the_arm() {
8601        let (mut tree, page, taps) = page_with_a_menu();
8602        let finger = contact_id(3);
8603        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8604        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8605
8606        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8607        assert_eq!(taps.get(), 0);
8608        assert_eq!(tree.pressed_by(page), None);
8609        tree.assert_no_leaked_pointer_state();
8610    }
8611
8612    /// Land beside the menu, drag onto it, lift there. The finger changed its
8613    /// mind: the menu stays, and the page under the arming press was never
8614    /// touched either.
8615    #[test]
8616    fn a_press_slid_onto_the_menu_dismisses_nothing() {
8617        let (mut tree, _page, taps) = page_with_a_menu();
8618        let finger = contact_id(4);
8619        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8620        tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
8621        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8622
8623        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
8624        assert_eq!(taps.get(), 0);
8625        tree.assert_no_leaked_pointer_state();
8626    }
8627
8628    /// A finger working *inside* the menu is an interaction, and a second one
8629    /// landing on the page is not a reason to take it away mid-flight.
8630    #[test]
8631    fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
8632        let (mut tree, _page, _taps) = page_with_a_menu();
8633        let first = contact_id(5);
8634        let second = contact_id(6);
8635
8636        tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
8637        assert!(
8638            tree.pointers
8639                .get(first)
8640                .is_some_and(|e| e.sequence.is_some()),
8641            "the first contact holds a live press inside the menu"
8642        );
8643
8644        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8645        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8646        assert_eq!(
8647            tree.active_overlays().len(),
8648            1,
8649            "the menu the first finger is holding must not close under it"
8650        );
8651
8652        // Once the first contact's press is over there is nothing left to
8653        // revoke, so it protects nothing and the same tap closes the menu.
8654        tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
8655        let third = contact_id(7);
8656        tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
8657        tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
8658        assert!(tree.active_overlays().is_empty());
8659        tree.assert_no_leaked_pointer_state();
8660    }
8661
8662    /// A contact that is merely *holding an arm* has no sequence and no
8663    /// capture, so `press_is_revocable` says it has no press — and it must not
8664    /// block a second contact's dismissal the way a real press does.
8665    #[test]
8666    fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
8667        let (mut tree, _page, _taps) = page_with_a_menu();
8668        let first = contact_id(8);
8669        let second = contact_id(9);
8670
8671        tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
8672        assert!(
8673            tree.overlay_manager().has_armed_dismiss(first),
8674            "the first contact armed"
8675        );
8676        // The first contact is live but holds nothing revocable.
8677        assert!(tree.busy_press_points(second).is_empty());
8678
8679        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
8680        assert!(tree.overlay_manager().has_armed_dismiss(second));
8681        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
8682        assert!(tree.active_overlays().is_empty());
8683
8684        // The first contact's arm now names an overlay that is gone; its own
8685        // release must be a quiet no-op rather than a panic.
8686        tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
8687        tree.assert_no_leaked_pointer_state();
8688    }
8689
8690    /// A tap *inside* the menu is not an outside press, so nothing is armed and
8691    /// the menu's own content handles the press exactly as before.
8692    #[test]
8693    fn a_touch_inside_the_menu_arms_nothing() {
8694        let (mut tree, _page, _taps) = page_with_a_menu();
8695        let finger = contact_id(10);
8696        tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
8697        assert!(!tree.overlay_manager().has_armed_dismiss(finger));
8698        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
8699        assert_eq!(tree.active_overlays().len(), 1);
8700        tree.assert_no_leaked_pointer_state();
8701    }
8702
8703    /// A contact whose press was never opened — it hit nothing, or its `Down`
8704    /// was suppressed by an arm — still ceases to exist when the platform
8705    /// revokes it.
8706    ///
8707    /// The cancel funnel returns early for a pointer with nothing revocable and
8708    /// so never reaches the step that drops the table entry. Before the release
8709    /// dismissal that shape was rare (a press on bare background); the arm makes
8710    /// it the ordinary case, so the ingress door applies the same
8711    /// contact-ceases-to-exist rule it applies to an `Up`.
8712    #[test]
8713    fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
8714        let mut tree = WidgetTree::new();
8715        tree.layout(SizeProposal::exact(800.0, 600.0));
8716        let finger = contact_id(99);
8717        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
8718        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
8719        tree.assert_no_leaked_pointer_state();
8720    }
8721
8722    // -----------------------------------------------------------------
8723    // The mouse, unchanged
8724    // -----------------------------------------------------------------
8725
8726    /// The press dismisses and falls through, exactly as before: one click both
8727    /// closes the menu and actuates the control beneath.
8728    #[test]
8729    fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
8730        let (mut tree, _page, taps) = page_with_a_menu();
8731        tree.dispatch_event(WidgetEvent::pointer_down(
8732            outside(),
8733            PointerButton::Primary,
8734            Modifiers::NONE,
8735        ));
8736        assert!(
8737            tree.active_overlays().is_empty(),
8738            "the mouse still dismisses on the press"
8739        );
8740        assert!(
8741            tree.pointers
8742                .get(PointerId::MOUSE)
8743                .is_some_and(|e| e.sequence.is_some()),
8744            "and the press still reaches the page beneath"
8745        );
8746        tree.dispatch_event(WidgetEvent::pointer_up(
8747            outside(),
8748            PointerButton::Primary,
8749            Modifiers::NONE,
8750        ));
8751        assert_eq!(taps.get(), 1, "the control beneath activated");
8752        assert!(
8753            !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
8754            "a mouse never arms"
8755        );
8756        tree.assert_no_leaked_pointer_state();
8757    }
8758
8759    // -----------------------------------------------------------------
8760    // Contact avoidance
8761    // -----------------------------------------------------------------
8762
8763    /// A context menu raised by a finger keeps clear of the contact patch; the
8764    /// same menu raised by a mouse lands on the pixel, as it always has.
8765    #[test]
8766    fn a_context_menu_avoids_the_contact_that_opened_it() {
8767        fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
8768            let mut tree = WidgetTree::new();
8769            let page = tree.add(
8770                FillWidget::new()
8771                    .focusable()
8772                    .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
8773            );
8774            let _ = page;
8775            tree.layout(SizeProposal::exact(800.0, 600.0));
8776            tree.dispatch_pointer(PointerSample {
8777                pointer,
8778                phase: PointerPhase::Down,
8779                position: at,
8780                button: Some(PointerButton::Secondary),
8781                modifiers: Modifiers::NONE,
8782                coalesced: Vec::new(),
8783            });
8784            tree.layout(SizeProposal::exact(800.0, 600.0));
8785            let id = *tree
8786                .active_overlays()
8787                .first()
8788                .expect("the context menu opened");
8789            tree.overlay_manager().bounds_for(id).expect("bounds")
8790        }
8791
8792        let at = Point::new(400.0, 300.0);
8793        let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
8794        let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
8795        assert!(
8796            finger.x < contact.x && finger.right() <= contact.x,
8797            "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
8798        );
8799
8800        let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
8801        assert_eq!(
8802            (mouse.x, mouse.y),
8803            (at.x, at.y),
8804            "a mouse menu still opens with its corner on the pointer"
8805        );
8806    }
8807}
8808
8809/// An overlay is chosen by its **bounds**, and what happens when its content
8810/// then claims nothing at the point.
8811///
8812/// Two answers, and the flag on the content root is what picks between them.
8813/// An ordinary overlay ends the search inside itself — that is what keeps the
8814/// miss-only slop pass confined to the one layer the exact pass entered, so a
8815/// near-miss on a menu row can never be re-attributed to a control on the page
8816/// behind the menu. An overlay whose content root declares `event_pass_through`
8817/// is the exception: the flag already means "what I did not claim belongs to
8818/// whatever is behind me", and an overlay root is not an exception to it.
8819///
8820/// This is not a hypothetical. The touch text affordances were specified as a
8821/// viewport-sized pass-through layer, and under the first answer applied to
8822/// both, mounting one made the editor beneath stop taking presses entirely —
8823/// measured, as `hit_test` returning `None` at a point inside the field.
8824#[cfg(test)]
8825mod pass_through_overlay_tests {
8826    use teksilo_canvas::{Point, Rect, SizeProposal};
8827
8828    use crate::WidgetId;
8829    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8830    use crate::test_widgets::FillWidget;
8831    use crate::widget::{LayoutContext, LayoutResponse, Widget};
8832    use crate::widget_builder::WidgetBuilder;
8833    use crate::widget_tree::WidgetTree;
8834
8835    /// The affordance layer's shape: fills whatever it is given, and puts its
8836    /// one child — a selection handle — on a fixed rectangle inside it.
8837    #[derive(Debug)]
8838    struct HandleLayer {
8839        handle: WidgetId,
8840        at: Rect,
8841    }
8842
8843    impl Widget for HandleLayer {
8844        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
8845            proposal.resolve(0.0, 0.0).into()
8846        }
8847
8848        fn place_children(
8849            &self,
8850            _bounds: Rect,
8851            _proposal: SizeProposal,
8852            children: &mut [crate::widget::WidgetPlacement],
8853            _ctx: &LayoutContext,
8854        ) {
8855            if let Some(child) = children.get_mut(0) {
8856                child.origin = self.at.origin();
8857                child.size = self.at.size();
8858            }
8859        }
8860
8861        fn children(&self) -> Vec<WidgetId> {
8862            vec![self.handle]
8863        }
8864    }
8865
8866    const HANDLE: Rect = Rect {
8867        x: 10.0,
8868        y: 10.0,
8869        width: 20.0,
8870        height: 20.0,
8871    };
8872
8873    /// Which flag the overlay's content root carries.
8874    #[derive(Clone, Copy, PartialEq)]
8875    enum RootFlag {
8876        /// The affordance layer's: not a target itself, children are.
8877        PassThrough,
8878        /// Neither the root nor its children are targets. Stronger, and
8879        /// deliberately **not** what the fall-through is gated on.
8880        HitTransparent,
8881    }
8882
8883    /// A field under a viewport-sized overlay carrying one handle.
8884    ///
8885    /// Both flags make the subtree answer `None` for a point no handle covers,
8886    /// which is the state the fall-through decides — so the two arms differ in
8887    /// nothing but the flag the router reads.
8888    fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
8889        let mut tree = WidgetTree::new();
8890        let field = tree.add(FillWidget::new());
8891        let handle = tree.add(FillWidget::new());
8892        let layer = HandleLayer { handle, at: HANDLE };
8893        let layer = match flag {
8894            RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
8895            RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
8896        };
8897        tree.show_overlay(OverlayRequest::new(
8898            layer,
8899            field,
8900            OverlayPlacement::FullViewport,
8901            DismissBehavior::Manual,
8902        ));
8903        tree.layout(SizeProposal::exact(400.0, 200.0));
8904        assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
8905        (tree, field, handle)
8906    }
8907
8908    /// The door: the surface under a pass-through layer goes on taking presses.
8909    #[test]
8910    fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
8911        let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
8912
8913        assert_eq!(
8914            tree.hit_test(Point::new(15.0, 15.0)),
8915            Some(handle),
8916            "the layer must still win the point its own child covers"
8917        );
8918        assert_eq!(
8919            tree.hit_test(Point::new(200.0, 100.0)),
8920            Some(field),
8921            "a press the layer did not claim must reach the surface beneath it"
8922        );
8923    }
8924
8925    /// `event_pass_through` removes a node from **hit-testing**, not from the
8926    /// **bubble path** of a descendant that was hit.
8927    ///
8928    /// The distinction is what lets a host mount its affordances at the full
8929    /// viewport and still answer the one press neither the layer nor the surface
8930    /// beneath can: the pass-through root is skipped when a point belongs to
8931    /// nobody in its subtree, and is still told about a press that landed on one
8932    /// of its children. The single-line text stack's `AffordanceHost` is exactly
8933    /// this — a cursor's click on a selection handle, which the handle refuses and
8934    /// the editor never sees.
8935    #[test]
8936    fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
8937        use std::cell::Cell;
8938        use std::rc::Rc;
8939
8940        let seen = Rc::new(Cell::new(0u32));
8941        let counter = seen.clone();
8942        let mut tree = WidgetTree::new();
8943        let field = tree.add(FillWidget::new());
8944        let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
8945        let layer = tree.add(
8946            HandleLayer { handle, at: HANDLE }
8947                .event_pass_through(true)
8948                .on_pointer_event(move |_event, _ctx| {
8949                    counter.set(counter.get() + 1);
8950                    crate::event::EventResponse::Ignored
8951                }),
8952        );
8953        tree.show_overlay(OverlayRequest::new(
8954            layer,
8955            field,
8956            OverlayPlacement::FullViewport,
8957            DismissBehavior::Manual,
8958        ));
8959        tree.layout(SizeProposal::exact(400.0, 200.0));
8960
8961        tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8962        assert!(
8963            seen.get() > 0,
8964            "a pass-through root heard nothing about a press on its own child"
8965        );
8966
8967        let after_child = seen.get();
8968        tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
8969        // …and a press it did not contain a target for never arrives, because it
8970        // was never the hit.
8971        tree.pointer_down_button(
8972            Point::new(200.0, 100.0),
8973            crate::event::PointerButton::Primary,
8974        );
8975        assert_eq!(
8976            seen.get(),
8977            after_child + 1,
8978            "the press-and-lift on the child accounts for the count, and the \
8979             press on the surface beneath added nothing"
8980        );
8981    }
8982
8983    /// …and the widening stops at that one flag.
8984    ///
8985    /// `hit_transparent` is the discriminating case, and the only one available:
8986    /// it is the other way for an overlay's content to claim nothing at a point,
8987    /// so it is the fixture in which the gate — rather than the subtree's own
8988    /// answer — is what decides. A gate that read "the subtree claimed nothing"
8989    /// alone would fall through here too, and with it past every overlay whose
8990    /// content happens to miss, which is what confines the miss-only slop pass to
8991    /// the layer the exact pass entered.
8992    ///
8993    /// The exclusion is deliberate rather than an oversight: the one overlay that
8994    /// must be seen past regardless is the drag preview, and the hit-test already
8995    /// takes an explicit `exclude_overlay` for it.
8996    #[test]
8997    fn a_hit_transparent_root_does_not_get_the_fall_through() {
8998        let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
8999
9000        // Nothing in the overlay is a target, not even the handle.
9001        assert_eq!(
9002            tree.hit_test(Point::new(15.0, 15.0)),
9003            None,
9004            "hit_transparent must exclude the subtree, or this fixture is not \
9005             testing the gate"
9006        );
9007        assert_eq!(
9008            tree.hit_test(Point::new(200.0, 100.0)),
9009            None,
9010            "the fall-through reached past an overlay that did not ask for it"
9011        );
9012        let _ = field;
9013    }
9014}