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