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