Skip to main content

teksilo_core/widget_tree/
pointer_router.rs

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