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, when none did,
174        // or when it is one of the tree's own (the standard edit shortcuts),
175        // whoever re-registered it: a native modal window has its own
176        // widgets' shortcuts and not those of the window it blocks. An app
177        // may re-register a standard edit to rename it, and every text widget
178        // answers those from the focused widget's route, which a modal must
179        // not cut off. With nothing focused, judged from the topmost modal.
180        let focus = self.focus_or_modal();
181        let modal = focus.and_then(|f| self.intent_walk(f, &[]).2);
182        for &(id, scope, propagate) in candidates {
183            // Specificity score, higher = more specific. Global is the
184            // least-specific fallback (0); any applicable scoped binding
185            // outranks it (`usize::MAX - hops`, so fewer hops = deeper
186            // scope = higher score). Tree depth is tiny, so no overflow.
187            let score = match scope {
188                ShortcutScope::Global => (crate::shortcut::is_framework_shortcut_id(id) || {
189                    let owners = self.shortcut_registry.owners(id);
190                    owners.is_empty()
191                        || owners
192                            .iter()
193                            .any(|&owner| self.global_applies(owner, modal))
194                })
195                .then_some(0usize),
196                ShortcutScope::Scoped(scope_id) => focus
197                    .and_then(|f| self.scope_distance_from_focus(f, scope_id))
198                    .map(|hops| usize::MAX - hops),
199            };
200            let Some(score) = score else { continue };
201            // Strictly-greater keeps the first candidate on a tie, so the
202            // existing `(category, id)` precedence holds within a scope.
203            if best.as_ref().is_none_or(|(b, _)| score > *b) {
204                best = Some((score, (id, scope, propagate)));
205            }
206        }
207        best.map(|(_, c)| c)
208    }
209
210    /// Dispatch an event into the widget tree.
211    ///
212    /// Routing rules:
213    /// - Pointer events -> hit testing against layout tree
214    /// - Keyboard/IME events -> focused widget
215    /// - AccessKit actions -> target widget directly
216    /// - Scroll events -> hit testing (scroll target under pointer)
217    ///
218    /// Dispatch an event with the caller-supplied app-level
219    /// [`WindowOps`](crate::window::WindowOps) sink. `teksilo-app` calls
220    /// this variant; handlers can reach the multi-window API
221    /// synchronously (`open_window` creates the winit window inside
222    /// the same call before returning).
223    pub fn dispatch_event_with_ops(
224        &mut self,
225        event: WidgetEvent,
226        ops: &mut dyn crate::window::WindowOps,
227    ) {
228        // A legacy event names no pointer, so it is the mouse at the epoch —
229        // which is exactly what it has always meant.
230        let mut snapshot = crate::pointer::InputSnapshot::from_event(&event);
231        // A legacy `WidgetEvent` carries no timestamp of its own, so stamp it
232        // from the tree clock. Without this the gesture layer would see every
233        // hand-built event at the epoch and no interval — a double tap, a long
234        // press and a swipe would all be undecidable.
235        if snapshot.pointer.time == crate::pointer::EventTime::ZERO {
236            snapshot.pointer.time = self.input_now();
237        }
238        self.dispatch_with_input_snapshot(event, snapshot, ops)
239    }
240
241    /// Dispatch an event on a standalone tree (tests, headless
242    /// scenarios). Handler code that calls `ctx.open_window(...)`
243    /// from within this dispatch will panic — by design. See
244    /// [`dispatch_event_with_ops`](Self::dispatch_event_with_ops)
245    /// for the app-facing variant.
246    pub fn dispatch_event(&mut self, event: WidgetEvent) {
247        let mut noop = crate::window::NoopWindowOps;
248        self.dispatch_event_with_ops(event, &mut noop);
249    }
250
251    // The three ingress doors
252
253    /// Deliver one pointer sample.
254    ///
255    /// This and [`dispatch_scroll`](Self::dispatch_scroll) are the real input
256    /// doors: a backend produces [`PointerSample`](crate::pointer::PointerSample)s
257    /// and [`ScrollSample`](crate::pointer::ScrollSample)s, and everything
258    /// Teksilo knows about *who* is pointing — identity, kind, pressure,
259    /// timestamp, coalesced history — reaches the tree through them.
260    ///
261    /// For now a sample is **lowered** onto the legacy `WidgetEvent` it
262    /// describes and takes the existing route, so a mouse behaves bit for bit
263    /// as it did before the doors existed. What changes here is only that the
264    /// door exists and that the sample's
265    /// [`PointerInfo`](crate::pointer::PointerInfo) is visible to handlers
266    /// through [`EventContext::pointer`](crate::widget::EventContext::pointer).
267    ///
268    /// A `Down` and an `Up` sample at the same point form a tap, whatever
269    /// timestamps a test gives them. For a whole gesture in one call a test
270    /// has [`click`](Self::click) (the centre of a widget) and
271    /// [`tap_with`](Self::tap_with) (a point, for any pointer kind).
272    pub fn dispatch_pointer(&mut self, sample: crate::pointer::PointerSample) {
273        let mut noop = crate::window::NoopWindowOps;
274        self.dispatch_pointer_with_ops(sample, &mut noop);
275    }
276
277    /// [`dispatch_pointer`](Self::dispatch_pointer) with the caller's
278    /// app-level [`WindowOps`](crate::window::WindowOps) sink, so handlers can
279    /// reach the multi-window API synchronously.
280    pub fn dispatch_pointer_with_ops(
281        &mut self,
282        sample: crate::pointer::PointerSample,
283        ops: &mut dyn crate::window::WindowOps,
284    ) {
285        use crate::pointer::PointerPhase;
286
287        crate::trace_input!(
288            Samples,
289            "{:?} {:?} at {:?} buttons={:?} t={:?}",
290            sample.phase,
291            sample.pointer.id,
292            sample.position,
293            sample.pointer.buttons,
294            sample.pointer.time
295        );
296
297        // The button a Down/Up is *about*. A direct-pointer contact reports no
298        // button at all, and the widget layer has always been told
299        // `Primary` for a press — that is what a tap is.
300        let button = sample
301            .button
302            .unwrap_or(crate::event::PointerButton::Primary);
303        let event = match sample.phase {
304            PointerPhase::Down => WidgetEvent::PointerDown {
305                position: sample.position,
306                button,
307                modifiers: sample.modifiers,
308                pointer: sample.pointer,
309            },
310            PointerPhase::Move => WidgetEvent::PointerMove {
311                position: sample.position,
312                modifiers: sample.modifiers,
313                pointer: sample.pointer,
314            },
315            PointerPhase::Up => WidgetEvent::PointerUp {
316                position: sample.position,
317                button,
318                modifiers: sample.modifiers,
319                pointer: sample.pointer,
320            },
321            // The platform revoked the contact (a `wl_touch.cancel`, a
322            // `WM_POINTERCAPTURECHANGED`, a compositor grab). It takes the
323            // cancel funnel directly rather than being lowered onto an event:
324            // the funnel owns the teardown order, and lowering would first
325            // *admit* the pointer the sample is revoking.
326            PointerPhase::Cancel => {
327                // The dismissal this contact had armed goes with it. The funnel
328                // below returns early for a pointer with nothing revocable, and
329                // a suppressed arming `Down` leaves exactly that shape — no
330                // sequence, no capture — so the abort cannot ride on it.
331                self.overlay_manager.abort_dismiss(sample.pointer.id);
332                self.cancel_pointer(
333                    sample.pointer.id,
334                    crate::pointer::CancelReason::Platform,
335                    ops,
336                );
337                // A revoked contact ceases to exist, exactly as a lifted one
338                // does — the `ends_pointer` rule below, which this arm returns
339                // before reaching. The funnel ends the entry itself *when it
340                // runs*; for a pointer that had no press to revoke it returns
341                // first, and the entry would outlive the finger. Idempotent, so
342                // the ordinary path is unaffected.
343                if !sample.pointer.kind.hovers() {
344                    self.pointers.end(sample.pointer.id);
345                }
346                return;
347            }
348        };
349
350        // Admit the pointer before anything is dispatched. A palm, or an
351        // eleventh simultaneous contact, is refused here and produces no event
352        // at all — the alternative (evicting a live contact to make room) turns
353        // a pinch into a fling, and letting a resting palm through turns a hand
354        // on a tablet into a stream of taps.
355        if !self.pointers.would_admit(&sample.pointer) {
356            return;
357        }
358        // A contact ceases to exist when it lifts; a hovering-capable pointer
359        // does not — a mouse that releases a button is still there, still
360        // hovering, and its entry is what every legacy singular accessor reads.
361        let ends_pointer = matches!(sample.phase, PointerPhase::Up | PointerPhase::Cancel)
362            && !sample.pointer.kind.hovers();
363        let pointer_id = sample.pointer.id;
364
365        let snapshot = crate::pointer::InputSnapshot::from_pointer_sample(&sample);
366        self.dispatch_with_input_snapshot(event, snapshot, ops);
367
368        if ends_pointer {
369            self.pointers.end(pointer_id);
370        }
371    }
372
373    /// Deliver one scroll sample.
374    ///
375    /// Routing follows [`ScrollSample::position`](crate::pointer::ScrollSample::position):
376    /// `Some` hit-tests it, `None` falls back to the hovered (else focused)
377    /// widget, which is what every scroll did before. A mouse wheel carries no
378    /// position, so this is a no-op for a mouse; a pan synthesised from a
379    /// direct pointer must carry one, because a contact never writes hover.
380    pub fn dispatch_scroll(&mut self, sample: crate::pointer::ScrollSample) {
381        let mut noop = crate::window::NoopWindowOps;
382        self.dispatch_scroll_with_ops(sample, &mut noop);
383    }
384
385    /// [`dispatch_scroll`](Self::dispatch_scroll) with the caller's app-level
386    /// [`WindowOps`](crate::window::WindowOps) sink.
387    pub fn dispatch_scroll_with_ops(
388        &mut self,
389        sample: crate::pointer::ScrollSample,
390        ops: &mut dyn crate::window::WindowOps,
391    ) {
392        crate::trace_input!(
393            Samples,
394            "scroll {:?} {:?}/{:?} at {:?}",
395            sample.delta,
396            sample.phase,
397            sample.source,
398            sample.position
399        );
400
401        let event = WidgetEvent::Scroll {
402            delta: sample.delta,
403            modifiers: sample.modifiers,
404            window_position: sample.position,
405            phase: sample.phase,
406            pointer: sample.pointer,
407        };
408        let snapshot = crate::pointer::InputSnapshot::from_scroll_sample(&sample);
409        self.dispatch_with_input_snapshot(event, snapshot, ops);
410    }
411
412    /// The pointer left the window.
413    ///
414    /// The third ingress door, and the only one that carries no sample: the OS
415    /// says the cursor crossed the window boundary and nothing else. It exists
416    /// because hover is otherwise cleared *only* by a move that lands
417    /// elsewhere — so a mouse that leaves through an edge would leave the last
418    /// widget hovered for as long as it stays away, with its hover chrome
419    /// painted, its `hover_within` signal true and its tooltip still counting
420    /// down.
421    ///
422    /// Clears hover the way a move to an empty spot does: a `PointerLeave` to
423    /// the hovered widget, the tooltip dwell cancelled, the `hover_within`
424    /// chain updated. It touches nothing else — no pointer is cancelled, no
425    /// capture released, no table entry ended. A mouse that leaves the window
426    /// is still a mouse, and a *captured* pointer is deliberately exempt: a
427    /// drag whose pointer wanders off the window keeps its target, which is
428    /// what makes a drag past the edge (and the OS-drag escalation built on
429    /// it) work at all.
430    ///
431    /// There is no matching `pointer_entered_window`, and that is not an
432    /// omission: the enter carries no position either, and a position only
433    /// ever arrives with a `CursorMoved` — which re-arms hover through the
434    /// ordinary path. A door that could only say "somewhere" would have
435    /// nothing to hit-test.
436    pub fn pointer_left_window(&mut self, ops: &mut dyn crate::window::WindowOps) {
437        // The pointer that owns hover is the one that just left; if it holds a
438        // capture, the interaction it is in the middle of outranks the
439        // boundary crossing.
440        if self
441            .pointers
442            .hover_owner_id()
443            .and_then(|id| self.captured_by(id))
444            .is_some()
445        {
446            return;
447        }
448        self.hover_owner_outside_window = true;
449        self.tooltip_dwell_held = false;
450        let Some(hovered) = self.hovered_id() else {
451            return;
452        };
453        // The hover owner is the pointer that just left — read from the table
454        // because this door carries no sample of its own.
455        let leave = WidgetEvent::PointerLeave {
456            pointer: self.hover_transition_pointer(),
457        };
458        self.dispatch_to_widget(hovered, &leave, &mut *ops);
459        self.tooltip_pointer_leave(hovered, &mut *ops);
460        self.set_hovered(None);
461        self.update_hover_within_signals(Some(hovered), None);
462    }
463
464    /// The pointer a hover transition is credited to.
465    ///
466    /// [`PointerEnter`](WidgetEvent::PointerEnter) /
467    /// [`PointerLeave`](WidgetEvent::PointerLeave) are hover-owner events by
468    /// construction — a contact never writes hover — so the answer is the hover
469    /// owner's own [`PointerInfo`](crate::pointer::PointerInfo), read from the
470    /// table rather than from the in-flight sample: a transition can be raised
471    /// by something that is not a pointer sample at all (a relayout that moves
472    /// a widget out from under the cursor, the window-leave door), and a
473    /// hover-incapable pointer must never be credited with hover.
474    ///
475    /// Falls back to a mouse at the current tree time when the table has no
476    /// hover owner, which is the state a synthesized `dispatch_event` leaves it
477    /// in. Either way this is the tree's own view rather than a producer's — see
478    /// the rule stated on
479    /// [`handle_pointer_move`](Self::handle_pointer_move).
480    pub(super) fn hover_transition_pointer(&self) -> crate::pointer::PointerInfo {
481        self.pointers
482            .hover_owner()
483            .map(|entry| entry.info)
484            .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now()))
485    }
486
487    /// Run one dispatch with `snapshot` installed as the tree's view of the
488    /// in-flight sample, restoring the previous value afterwards.
489    ///
490    /// Save-and-restore rather than reset-to-default so a nested dispatch (a
491    /// synthetic click queued by a handler, a scroll-into-view walk) leaves the
492    /// outer sample's snapshot intact for the rest of the outer dispatch.
493    fn dispatch_with_input_snapshot(
494        &mut self,
495        event: WidgetEvent,
496        snapshot: crate::pointer::InputSnapshot,
497        ops: &mut dyn crate::window::WindowOps,
498    ) {
499        // A dispatch reached from inside a dispatch — a handler's synthetic
500        // click, an assistive-technology action re-entering the door — is
501        // **queued**, not run inline. Running it inline would let it unwind the
502        // pointer state the outer sample is still standing on: the outer
503        // handler would return to a tree whose hover, capture and table entries
504        // had all moved under it. Queued, the outer dispatch finishes on the
505        // state it started with and the nested one replays immediately
506        // afterwards, so from a caller's side nothing changed — the queue is
507        // empty again before the top-level call returns.
508        self.pending_dispatch.push_back(QueuedDispatch::Event {
509            event,
510            snapshot: Box::new(snapshot),
511        });
512        if self.dispatch_depth > 0 {
513            return;
514        }
515        self.drain_pending_dispatch(&mut *ops);
516    }
517
518    /// Run everything the queue holds, in order, each at depth zero.
519    ///
520    /// `pop_front` in a loop rather than `drain`: a replayed dispatch — or a
521    /// cancel's own `PointerCancel` handler — may queue another entry, and
522    /// each must in turn run at depth zero.
523    pub(super) fn drain_pending_dispatch(&mut self, ops: &mut dyn crate::window::WindowOps) {
524        while let Some(queued) = self.pending_dispatch.pop_front() {
525            match queued {
526                QueuedDispatch::Event { event, snapshot } => {
527                    self.run_one_dispatch(event, *snapshot, &mut *ops);
528                }
529                QueuedDispatch::Cancel {
530                    pointer,
531                    reason,
532                    recipient,
533                } => {
534                    self.run_one_cancel(pointer, reason, recipient, &mut *ops);
535                }
536            }
537        }
538    }
539
540    /// One dispatch at depth zero, with `snapshot` installed as the tree's view
541    /// of the in-flight sample and the previous value restored afterwards.
542    ///
543    /// Save-and-restore rather than reset-to-default: the restore is what stops
544    /// one queued dispatch's snapshot standing as the tree's view of the world
545    /// once it has returned, while the drain replays the next entry.
546    fn run_one_dispatch(
547        &mut self,
548        event: WidgetEvent,
549        snapshot: crate::pointer::InputSnapshot,
550        ops: &mut dyn crate::window::WindowOps,
551    ) {
552        let previous = std::mem::replace(&mut self.current_input, snapshot);
553        self.dispatch_depth += 1;
554        self.dispatch_event_impl(event, ops);
555        self.dispatch_depth -= 1;
556        self.current_input = previous;
557    }
558
559    /// Dismiss the top overlay Escape may close, as the Escape key does, and
560    /// give the focus back to where that overlay took it from. `false` when
561    /// nothing was dismissed.
562    fn dismiss_top_on_escape(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
563        let mark = self.dismissal_mark();
564        let Some((_id, content_ids, focus_restore)) =
565            self.overlay_manager.try_dismiss_top_on_escape()
566        else {
567            return false;
568        };
569        self.dormant_dismissed_content(&content_ids, ops);
570        self.return_focus_after_dismissal(focus_restore, mark, ops);
571        true
572    }
573
574    /// Offer Escape to what holds the focus inside the overlay Escape would
575    /// close, before that overlay goes: the `on_key_preview` handlers from the
576    /// overlay's content root down to the focused node's parent, root first,
577    /// as the preview pass runs them. `true` when one took the key.
578    ///
579    /// A widget in a dialog that says "Escape comes back here" (a list row
580    /// being edited, a card being edited, a question waiting for an answer)
581    /// takes the key before the dialog does, as a focused element's `keydown`
582    /// in a browser comes before its `<dialog>`'s cancel. Only the overlay
583    /// Escape would close is asked: a popup over the focused control (a combo
584    /// box's list) is that overlay, the focus is not inside it, and it closes
585    /// first. Nothing fires when no such overlay holds the focus, so a handler
586    /// never sees the key twice: either one takes it here, or the overlay
587    /// closes and the key goes no further.
588    fn escape_taken_inside_top_overlay(
589        &mut self,
590        event: &WidgetEvent,
591        ops: &mut dyn crate::window::WindowOps,
592    ) -> bool {
593        let Some((_, content)) = self.overlay_manager.top_escape_target() else {
594            return false;
595        };
596        let Some(focus) = self.focused else {
597            return false;
598        };
599        let mut path = Vec::new();
600        let mut current = self.arena.parent(focus);
601        let mut inside = false;
602        while let Some(id) = current {
603            path.push(id);
604            if id == content {
605                inside = true;
606                break;
607            }
608            current = self.arena.parent(id);
609        }
610        if !inside || !self.arena.is_enabled(focus) {
611            return false;
612        }
613        for &id in path.iter().rev() {
614            let frame = self.arena.local_frame(id);
615            let mut ctx = self
616                .make_event_context(&mut *ops)
617                .with_dispatch_node(id)
618                .with_dispatch_target(focus)
619                .with_local_frame(frame);
620            let response = if let Some(node) = self.arena.get_mut(id) {
621                Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
622            } else {
623                EventResponse::Ignored
624            };
625            self.collect_from_ctx(ctx, id);
626            if response == EventResponse::Handled {
627                self.arena.mark_needs_paint(id);
628                // Its release goes where the press went, as for any key the
629                // focused widget's dispatch took.
630                self.note_key_press(focus);
631                return true;
632            }
633        }
634        false
635    }
636
637    /// Answer a request to close the window that the user made (Alt+F4, the
638    /// title bar's close button, the taskbar's Close) while a modal overlay is
639    /// open in it. It does what Escape would and nothing more, as Alt+F4 does
640    /// to a dialog window: the top overlay Escape may close goes through its
641    /// own dismissal (a modal's Cancel path), and a modal Escape cannot close
642    /// stays. Returns `true` when a modal was open, so the window's close is
643    /// not asked for: the next request reaches it once no modal is left.
644    /// `false` when none was open, and the window's close goes ahead.
645    ///
646    /// The window manager's seam, called for the user's own close gestures
647    /// only, and the one a title bar's close button reaches through
648    /// `EventContext::give_close_request_to_modal`: `EventContext::close_window`
649    /// and a quit ask the window directly.
650    #[doc(hidden)]
651    pub fn answer_close_request_with_modal(
652        &mut self,
653        ops: &mut dyn crate::window::WindowOps,
654    ) -> bool {
655        if !self.overlay_manager.has_open_modal() {
656            return false;
657        }
658        // As the Escape key does: a tooltip, and any overlay nothing can be
659        // done in, go first without being what the request is spent on.
660        self.tooltip_escape_pressed();
661        self.inert_overlays_escape_pressed(ops);
662        self.dismiss_top_on_escape(ops);
663        true
664    }
665
666    /// Record that the press being dispatched went to `target`. A fresh press
667    /// replaces a record for the same key (its release was lost); an
668    /// auto-repeat keeps the first.
669    fn note_key_press(&mut self, target: WidgetId) {
670        let Some((identity, repeat)) = self.current_key else {
671            return;
672        };
673        match self.key_presses.iter_mut().find(|(id, _)| *id == identity) {
674            Some(_) if repeat => {}
675            Some(record) => record.1 = target,
676            None => self.key_presses.push((identity, target)),
677        }
678    }
679
680    /// Whether `id` asked for every key release it had the press of, even
681    /// behind a modal opened since (a web view, whose engine otherwise keeps
682    /// the key held). See `BuildContext::receive_every_key_release`.
683    fn receives_every_key_release(&self, id: WidgetId) -> bool {
684        self.arena
685            .get(id)
686            .is_some_and(|node| node.receives_every_key_release)
687    }
688
689    /// Deliver a key release to the widget that received its press, not to
690    /// whatever focus moved to while the key was down: the Enter that ran
691    /// "Close tab" must not reach the next tab's button as a release. The
692    /// release bubbles from there (no preview pass), when that widget is
693    /// still active and not behind a modal opened since (a widget that
694    /// captures the keyboard, or one that asked for every release, a web
695    /// view, still gets it); otherwise it is dropped. A release with no press recorded goes to focus as before,
696    /// except Enter and Space, whose release activates a control: a lone one
697    /// (its press closed a dialog in another window, or was taken by a
698    /// shortcut) is dropped.
699    fn dispatch_key_release(
700        &mut self,
701        key: Key,
702        event: &WidgetEvent,
703        ops: &mut dyn crate::window::WindowOps,
704    ) {
705        let identity = self.current_key.map(|(id, _)| id);
706        let pressed = identity.and_then(|identity| {
707            let at = self
708                .key_presses
709                .iter()
710                .position(|(id, _)| *id == identity)?;
711            Some(self.key_presses.remove(at).1)
712        });
713        match pressed {
714            Some(target) if Some(target) == self.focused => {
715                self.dispatch_to_widget(target, event, &mut *ops);
716            }
717            Some(target) => {
718                if self.arena.is_active(target)
719                    && (!self.is_behind_modal(target)
720                        || self.is_keyboard_capture(target)
721                        || self.receives_every_key_release(target))
722                {
723                    // Bubbled from the press target up, as the press was, so
724                    // an ancestor tracking held keys hears it too; no preview
725                    // pass, which belongs to the focused widget's route.
726                    // A target behind a modal (one that asked for every release)
727                    // gets it alone: its ancestors are behind the modal too.
728                    let behind_modal = self.is_behind_modal(target);
729                    let mut current = Some(target);
730                    while let Some(id) = current {
731                        if self.dispatch_to_widget_direct_returning_handled(id, event, &mut *ops)
732                            || behind_modal
733                        {
734                            break;
735                        }
736                        current = self.arena.parent(id);
737                    }
738                }
739            }
740            None if matches!(key, Key::Enter | Key::Space) => {}
741            None => {
742                if let Some(focused) = self.focused {
743                    self.dispatch_to_widget(focused, event, &mut *ops);
744                }
745            }
746        }
747    }
748
749    fn dispatch_event_impl(&mut self, event: WidgetEvent, ops: &mut dyn crate::window::WindowOps) {
750        // Admit the pointer this event belongs to into the table before
751        // anything routes. A legacy `WidgetEvent` names no pointer, so
752        // `current_input` reports the mouse and this creates (or refreshes) the
753        // one mouse entry — which is what every singular accessor then reads,
754        // so a mouse-only tree behaves exactly as it did before the table.
755        if let Some(position) = pointer_event_position(&event) {
756            let is_down = matches!(event, WidgetEvent::PointerDown { .. });
757            let is_move = matches!(event, WidgetEvent::PointerMove { .. });
758            if !self.admit_current_pointer(position, is_down, is_move) {
759                return;
760            }
761            // A hovering-capable pointer takes the hover-owner role by pointing:
762            // the later sample wins, and whoever held it is sent a leave. A
763            // contact is refused the role outright — it has no hover to give.
764            if self.current_input.pointer.kind.hovers() {
765                self.claim_hover_owner_for_current(&mut *ops);
766            }
767        }
768
769        // Which physical key this is, for pairing a release with its press:
770        // what the window manager noted, else the key itself.
771        if let WidgetEvent::KeyDown { key, .. } | WidgetEvent::KeyUp { key, .. } = &event {
772            let noted = self
773                .window_state
774                .as_ref()
775                .and_then(|w| w.take_key_identity());
776            self.current_key = Some(noted.unwrap_or_else(|| (logical_key_identity(*key), false)));
777        } else if self.dispatch_depth <= 1 {
778            // Another event: no key identity is current.
779            self.current_key = None;
780        }
781
782        // Track input modality for `:focus-visible`: keyboard input reveals
783        // focus rings, pointer input hides them. Updated at the dispatch root so
784        // every handler (and the next paint) observes the current modality.
785        match &event {
786            WidgetEvent::KeyDown { .. } if !self.focus_visible.get() => {
787                self.focus_visible.set(true);
788            }
789            WidgetEvent::PointerDown { .. } if self.focus_visible.get() => {
790                self.focus_visible.set(false);
791            }
792            _ => {}
793        }
794
795        // The "back toward the parent overlay" key closes the top nested
796        // overlay (e.g. an open submenu over its parent menu). It is the
797        // inline-start arrow: ArrowLeft under LTR, ArrowRight under RTL.
798        // Without the RTL flip, ArrowLeft would navigate *into* a submenu
799        // in RTL menus yet still dismiss it here.
800        let overlay_back_key = match self.layout_direction {
801            crate::environment::LayoutDirection::RightToLeft => Key::ArrowRight,
802            crate::environment::LayoutDirection::LeftToRight => Key::ArrowLeft,
803        };
804        if let WidgetEvent::KeyDown { key, .. } = &event
805            && *key == overlay_back_key
806        {
807            // Count menu-level (non-host) overlays. A revealed collapsible
808            // `MenuBar` is itself a *host* overlay (Role::MenuBar), so a
809            // single open top-level menu sitting over it must NOT be treated
810            // as a nested submenu — otherwise the back key would close the
811            // menu instead of letting the menubar navigate to the previous
812            // one. Only when ≥2 non-host overlays are stacked (a submenu over
813            // its parent menu) does the back key dismiss the top overlay.
814            //
815            // **Menus only**, which is what the band says. Every mounted text
816            // editor keeps one full-viewport affordance host alive in the
817            // [`TextAffordance`](crate::overlay::OverlayBand::TextAffordance)
818            // band for its selection handles, so counting bands alike made two
819            // editors on one page read as a menu cascade: the back key then
820            // tore down an affordance host and returned, and ArrowLeft stopped
821            // reaching *any* editor in that window for as long as a second one
822            // was mounted. A text affordance is not a cascade level, the same
823            // reason `OverlayBand::dismissed_by_outside_press` already excludes
824            // it from press dismissal.
825            //
826            // An inert overlay (a slider's value readout, say) is no level
827            // either: it takes no input, so it is neither counted nor what the
828            // key closes. Counted, a readout under one open menu made that menu
829            // read as a submenu and closed it; on top of the stack, it was the
830            // overlay the key closed, and the key was spent on it.
831            //
832            // The topmost *non-inert* overlay is the menu this key means: the
833            // stack is band-ordered (`OverlayManager::show_with_auto_dismiss`
834            // inserts, it does not push), so a `Standard` overlay always sits
835            // above every `TextAffordance` one.
836            let nested_menu_overlays = {
837                let ids: Vec<_> = self
838                    .overlay_manager
839                    .stack
840                    .iter()
841                    .filter(|o| o.band == crate::overlay::OverlayBand::Standard && !o.inert)
842                    .map(|o| o.id)
843                    .collect();
844                ids.into_iter()
845                    .filter(|&id| !self.overlay_is_host_surface(id))
846                    .count()
847            };
848            // The back key only navigates *menu* cascades; it must never close a
849            // dialog / alert / modal that happens to sit on top. Each modal is a
850            // scrim+panel overlay pair and the (non-host) scrims inflate the count
851            // above, so also require the topmost non-inert overlay to be
852            // back-navigable — i.e. a non-host (menu) surface — before dismissing
853            // it.
854            let top_id = self
855                .overlay_manager
856                .stack
857                .iter()
858                .rev()
859                .find(|o| !o.inert)
860                .map(|o| o.id);
861            let top_is_back_navigable = top_id.is_some_and(|id| !self.overlay_is_host_surface(id));
862            if nested_menu_overlays > 1
863                && top_is_back_navigable
864                && let Some(top_id) = top_id
865            {
866                let mark = self.dismissal_mark();
867                let (content_ids, focus_restore) =
868                    self.overlay_manager.dismiss_with_focus_restore_because(
869                        top_id,
870                        crate::overlay::DismissReason::Escape,
871                    );
872                self.dormant_dismissed_content(&content_ids, &mut *ops);
873                self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
874                return;
875            }
876        }
877
878        // Escape retires any shown tooltip first, and does **not** stop there —
879        // see `tooltip_escape_pressed`. Ordered before the stack walk below so
880        // that walk can no longer pick a tooltip as the thing to dismiss, which
881        // is what used to spend the key on a tip nobody was reading while the
882        // editor / menu / dialog the user meant stayed open.
883        if let WidgetEvent::KeyDown {
884            key: Key::Escape, ..
885        } = &event
886        {
887            self.tooltip_escape_pressed();
888            // The same rule for every overlay nothing can be done in: it goes,
889            // and the key goes on. See `inert_overlays_escape_pressed`.
890            self.inert_overlays_escape_pressed(&mut *ops);
891        }
892
893        if let WidgetEvent::KeyDown {
894            key: Key::Escape, ..
895        } = &event
896            && !self.overlay_manager.is_empty()
897            && (self.escape_taken_inside_top_overlay(&event, &mut *ops)
898                || self.dismiss_top_on_escape(&mut *ops))
899        {
900            return;
901        }
902
903        // Outside-press overlay dismissal. Two shapes, chosen by the pointer:
904        // an indirect one dismisses on the press and falls through, exactly as
905        // it always has; a direct one *arms* on the press and commits on the
906        // release. See `arm_outside_press_dismissal`.
907        match &event {
908            WidgetEvent::PointerDown {
909                position, button, ..
910            } => {
911                if self.arm_outside_press_dismissal(*position, *button, &mut *ops) {
912                    return;
913                }
914            }
915            WidgetEvent::PointerUp { position, .. } => {
916                if self.commit_outside_press_dismissal(*position, &mut *ops) {
917                    return;
918                }
919            }
920            WidgetEvent::PointerCancel { pointer, .. } => {
921                // A revoked press dismisses nothing. The arming Down was never
922                // delivered beneath either, so the whole gesture leaves no
923                // trace — which is the point of deferring to the release.
924                self.overlay_manager.abort_dismiss(pointer.id);
925            }
926            _ => {}
927        }
928
929        // Key-capture mode: if a callback is armed (via
930        // `WidgetTree::begin_key_capture`), the next KeyDown bypasses
931        // shortcut resolution entirely and runs the callback with
932        // mutable access to the registry AND an `EventContext` so
933        // rebind handlers can also emit commands, send intents,
934        // dismiss overlays, etc. The capture is one-shot; its slot
935        // is emptied before the callback runs so a re-entrant
936        // `begin_key_capture` call from inside the callback arms a
937        // fresh session (rather than competing with the in-flight
938        // one).
939        if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
940            && let Some(callback) = self.take_key_capture()
941        {
942            let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
943            let mut cap_ctx = self.make_event_context(&mut *ops);
944            callback(keystroke, self.shortcut_registry_mut(), &mut cap_ctx);
945            // Route side effects of the callback through the
946            // focused widget (or the intent anchor if no focus).
947            let anchor = self.focused.or_else(|| self.intent_anchor());
948            if let Some(anchor_id) = anchor {
949                self.collect_from_ctx(cap_ctx, anchor_id);
950                self.drain_pending_intents(&mut *ops);
951            }
952            return;
953        }
954
955        // Keyboard-capture surfaces (a terminal, a game viewport) opt out
956        // of shortcut resolution entirely while focused: they want every
957        // keystroke delivered raw so a host-app `Ctrl+C` shortcut can't
958        // steal the SIGINT the child process needs. The Escape / overlay
959        // back-navigation handled above still runs first, so an open
960        // overlay is still dismissable. Only a KeyDown is affected; KeyUp
961        // and IME already bypass the shortcut path.
962        let focus_captures_keys = matches!(&event, WidgetEvent::KeyDown { .. })
963            && self.focused.is_some_and(|f| self.is_keyboard_capture(f));
964
965        // Shortcut → intent → action dispatch. A KeyDown whose chord
966        // matches a registered enabled `Shortcut` whose scope contains
967        // the focused widget is consumed here: the shortcut's
968        // `on_activate` runs (producing an `Intent`), its ctx side
969        // effects are collected, and the intent walks source-widget →
970        // root firing any matching `Action`. An intent nothing here takes
971        // is kept for the application tree when that tree takes it.
972        // Otherwise the focused widget sees the raw KeyDown below.
973        //
974        // Two-phase: the registry is inspected first (immutable read)
975        // to resolve `id / scope / propagate_when_disabled`. Only if
976        // scope matches the current focus do we take a mutable borrow
977        // to invoke `on_activate` — this way a scope mismatch cannot
978        // drop side effects the closure put into its ctx, because
979        // the closure never runs.
980        if !focus_captures_keys && let WidgetEvent::KeyDown { key, modifiers, .. } = &event {
981            let keystroke = crate::shortcut::KeyStroke::new(*key, *modifiers);
982            // Gather every same-chord candidate (owned fields) before any
983            // mutable borrow of the registry, then pick the one whose scope
984            // actually applies to the current focus. `find_by_keystroke`
985            // alone yields only the first by `(category, id)` order, which
986            // can be a `Scoped` binding outside focus shadowing an
987            // applicable `Global` one — or a `Global` binding that should
988            // yield to an in-focus `Scoped` one. Selection needs focus +
989            // the tree, so it happens here, not in the registry.
990            let candidates: Vec<(&'static str, crate::shortcut::ShortcutScope, bool)> = self
991                .shortcut_registry
992                .matches_by_keystroke(keystroke)
993                .map(|eff| {
994                    (
995                        eff.shortcut.id,
996                        eff.shortcut.scope,
997                        eff.shortcut.propagate_when_disabled,
998                    )
999                })
1000                .collect();
1001            // The focus region commands (F6, Shift+F6) yield: an application
1002            // shortcut on the same chord that applies wins whatever its
1003            // category, and with no region to go to and no action (in the
1004            // window or the application tree) taking the intent they are not
1005            // candidates, so the key reaches the focused widget as before.
1006            let (region_commands, others): (Vec<_>, Vec<_>) = candidates
1007                .into_iter()
1008                .partition(|(id, _, _)| crate::shortcut::region_direction(id).is_some());
1009            let lookup = self.select_shortcut_for_focus(&others).or_else(|| {
1010                let source = self.focus_or_modal().or_else(|| self.intent_anchor());
1011                let live: Vec<_> = region_commands
1012                    .into_iter()
1013                    .filter(|(id, _, _)| {
1014                        crate::shortcut::region_direction(id).is_some_and(|forward| {
1015                            self.region_cycle_target_from(source, forward).is_some()
1016                                || source.is_some_and(|source| self.can_handle_from(source, id))
1017                        })
1018                    })
1019                    .collect();
1020                self.select_shortcut_for_focus(&live)
1021            });
1022            if let Some((id, scope, propagate_when_disabled)) = lookup {
1023                let anchor = match scope {
1024                    // Global shortcuts fire regardless of focus. If no
1025                    // widget is currently focused, anchor the intent
1026                    // walk in the topmost modal, or at the intent anchor so
1027                    // actions registered at the top of the app's tree still
1028                    // see the intent.
1029                    crate::shortcut::ShortcutScope::Global => {
1030                        self.focus_or_modal().or_else(|| self.intent_anchor())
1031                    }
1032                    crate::shortcut::ShortcutScope::Scoped(scope_id) => self
1033                        .focus_or_modal()
1034                        .filter(|f| self.scope_distance_from_focus(*f, scope_id).is_some()),
1035                };
1036                if let Some(anchor_id) = anchor {
1037                    let mut act_ctx = self.make_event_context(&mut *ops);
1038                    if let Some(intent) =
1039                        self.shortcut_registry
1040                            .invoke_on_activate(id, keystroke, &mut act_ctx)
1041                    {
1042                        self.collect_from_ctx(act_ctx, anchor_id);
1043                        // Intents `on_activate` sent through its ctx go
1044                        // first, as they did when everything shared one
1045                        // queue.
1046                        self.drain_pending_intents(&mut *ops);
1047                        // Tag shortcut origin so analytics can
1048                        // distinguish keyboard-driven activations from
1049                        // button / menu / programmatic ones.
1050                        let intent = self.sent_here(
1051                            intent.with_source(crate::telemetry::IntentSource::Shortcut),
1052                        );
1053                        let keep = self.keeps_unclaimed(&intent).then(|| intent.clone());
1054                        let consumed = self.dispatch_intent(
1055                            anchor_id,
1056                            intent,
1057                            propagate_when_disabled,
1058                            &mut *ops,
1059                        );
1060                        self.drain_pending_intents(&mut *ops);
1061                        if consumed {
1062                            return;
1063                        }
1064                        // Nothing in this tree took it, but the application
1065                        // tree does: kept for it, and the key is used.
1066                        if let Some(intent) = keep {
1067                            self.keep_unclaimed(intent, &mut *ops);
1068                            return;
1069                        }
1070                        // No action took the intent: the key is the focused
1071                        // widget's after all, delivered below as if no
1072                        // shortcut had matched. The standard edit shortcuts
1073                        // depend on this: Ctrl+C is bound in every tree, and
1074                        // a widget that copies from its own `on_key` must
1075                        // still see it.
1076                    }
1077                }
1078                // Chosen candidate had no anchor after all (e.g. a Global
1079                // match while nothing is focused and the tree has no
1080                // roots) — fall through to normal KeyDown dispatch.
1081                // `on_activate` was never called, so nothing to clean up.
1082            }
1083            // `lookup` is `None` when every same-chord candidate was a
1084            // scoped binding outside the focused subtree — fall through.
1085        }
1086
1087        // Escape during an OS drag we escalated. There is no `active_drag`
1088        // any more — `try_escalate_to_os_drag` took it when the platform
1089        // accepted the hand-off — so this cannot live in the block below, but
1090        // it is the same user gesture and belongs on the same path rather than
1091        // being special-cased in the event loop of whichever backend needs it.
1092        if self.outbound_drag_source.is_some()
1093            && let WidgetEvent::KeyDown {
1094                key: Key::Escape, ..
1095            } = &event
1096        {
1097            ops.cancel_os_drag();
1098            // Deliberately no `return`: the backend answers asynchronously with
1099            // a terminal `DragEnded`, which is what actually tears the session
1100            // down via `handle_os_drag_ended`. Swallowing the key here would
1101            // also stop Escape from closing whatever else is open.
1102        }
1103
1104        // --- Active drag session handling ---
1105        //
1106        // Our own OS drag re-entered into this window is driven by the OS drag
1107        // events (`update_external_drag`, `end_external_drag`), never by this
1108        // window's pointer samples. On X11 winit keeps its implicit grab
1109        // through the XDND drag, so the press that handed the drag over still
1110        // sends this window its moves and its release; taking that release as
1111        // the drop would land it here, lose its in-app outcome (the session has
1112        // no source) and leave the XDND drop with nothing to complete. A move
1113        // is dropped, and the release goes on to the router below, which
1114        // swallows it as the `Up` of a press cancelled by the handover. Only
1115        // the dragging pointer's: another pointer (a pen hovering, a second
1116        // finger) goes on to the router as it would with no drag at all.
1117        if self.os_drag_reentered
1118            && matches!(
1119                event,
1120                WidgetEvent::PointerMove { .. } | WidgetEvent::PointerUp { .. }
1121            )
1122        {
1123            let dragging = self.current_pointer_id();
1124            let own = self
1125                .active_drag
1126                .as_ref()
1127                .is_some_and(|d| d.pointer.id == dragging);
1128            if own && matches!(event, WidgetEvent::PointerMove { .. }) {
1129                return;
1130            }
1131        } else if self.active_drag.is_some() {
1132            match &event {
1133                WidgetEvent::PointerMove { position, .. } => {
1134                    self.handle_drag_sample(*position, &mut *ops);
1135                    return;
1136                }
1137                WidgetEvent::PointerUp { position, .. } => {
1138                    self.handle_drag_drop(*position, &mut *ops);
1139                    return;
1140                }
1141                WidgetEvent::KeyDown {
1142                    key: Key::Escape, ..
1143                } => {
1144                    self.cancel_active_drag(&mut *ops);
1145                    return;
1146                }
1147                WidgetEvent::Scroll { .. } => {
1148                    // Route the wheel to the current drop target so users
1149                    // can scroll the list/tree beneath the drag. Then
1150                    // synthesise a hover at the stationary pointer so
1151                    // feedback, drop-index math and the preview overlay
1152                    // all reflect the new scroll offset.
1153                    let target_and_pos = self
1154                        .active_drag
1155                        .as_ref()
1156                        .and_then(|d| d.current_target.map(|t| (t, d.current_position)));
1157                    if let Some((target, _pos)) = target_and_pos {
1158                        self.dispatch_to_widget(target, &event, &mut *ops);
1159                    }
1160                    if let Some((_, pos)) = target_and_pos
1161                        && self.active_drag.is_some()
1162                    {
1163                        self.handle_drag_move(pos, &mut *ops);
1164                    }
1165                    return;
1166                }
1167                _ => {}
1168            }
1169        }
1170
1171        // A keyboard route to the context menu, reserved at the dispatcher so
1172        // every widget with a `.context_menu(..)` gets one without opting in.
1173        //
1174        // It has to be here rather than in a widget, and it cannot be a
1175        // `Shortcut`: shortcut resolution runs above this point, so a global
1176        // binding would fire while the user was typing in a modal. Sitting
1177        // below it means an application that deliberately binds Shift+F10 to
1178        // something else still wins.
1179        if let WidgetEvent::KeyDown { key, modifiers, .. } = &event
1180            && is_context_menu_chord(*key, *modifiers)
1181            && self.open_context_menu_from_keyboard(&mut *ops)
1182        {
1183            return;
1184        }
1185
1186        match &event {
1187            WidgetEvent::PointerMove { position, .. } => {
1188                // Every sample: re-check the sequence's members against the
1189                // arena, and record where the pointer now is so each threshold
1190                // reads one number.
1191                self.note_sequence_position(*position);
1192                self.revalidate_sequence(&mut *ops);
1193                // Timers before positional thresholds, and before the move
1194                // reaches any recognizer — see `tick_sequence_timers`.
1195                self.tick_sequence_timers();
1196                // The multi-contact and palm layers see every sample, decided
1197                // or not: a pinch is arbitrated by contact count rather than by
1198                // the press arbitration, and the palm watch has to know whether
1199                // this contact ever moved.
1200                self.note_palm_sample(*position);
1201                self.feed_pinch(super::pan_arbiter::PinchFeed::Move, *position, &mut *ops);
1202                if let Some(captured) = self.current_pointer_capture() {
1203                    self.dispatch_to_widget(captured, &event, &mut *ops);
1204                    // Advance the arbitration so an ancestor drag can still
1205                    // begin while a descendant tap holds the capture. Once a
1206                    // drag latches, `active_drag` takes over and the capture
1207                    // branch above is bypassed.
1208                    if self.active_drag.is_none() {
1209                        self.advance_sequence(&event, &mut *ops);
1210                    }
1211                } else {
1212                    self.handle_pointer_move(&event, *position, &mut *ops);
1213                    // No capture: for a mouse there is nothing enrolled (a
1214                    // gesture member is only enrolled *through* a capture), so
1215                    // this is a no-op. A contact panning from empty space has
1216                    // its pan claimants here.
1217                    if self.active_drag.is_none() {
1218                        self.advance_sequence(&event, &mut *ops);
1219                    }
1220                }
1221                // After the arbitration, so a claim taken on *this* sample
1222                // already delivers its own movement rather than waiting a frame.
1223                self.advance_pan(*position, &mut *ops);
1224                // …and after that, so the press visual answers to a claim taken
1225                // on this very sample rather than surviving it by one move.
1226                self.update_press(*position);
1227                // A tree-owned hold survives only while the contact holds
1228                // still; past the tap boundary this is a pan or a drag.
1229                self.touch_route_moved(self.current_pointer_id());
1230                // Hover-owner-only, exactly as `handle_pointer_move` is. The
1231                // `DismissBehavior::PointerLeave` grace is a *hover* dismissal:
1232                // it asks "has the pointer left this overlay and its trigger",
1233                // a question only a pointer that hovers is entitled to answer.
1234                // Ungated, any contact's move anywhere started the 150 ms grace
1235                // on every such overlay — so a submenu a finger had just tapped
1236                // open was closed by the frame pass with no further input, and
1237                // no mouse test could see it because a mouse *is* the hover
1238                // owner.
1239                if self.pointers.hover_owner_id() == Some(self.current_pointer_id()) {
1240                    self.update_pointer_leave_overlays(*position, &mut *ops);
1241                }
1242            }
1243            WidgetEvent::PointerDown {
1244                position, button, ..
1245            } => {
1246                // A new press is a new interaction: whatever took the previous
1247                // one away has nothing to say about this one.
1248                let pressed = self.current_pointer_id();
1249                self.cancelled_pointers.retain(|p| *p != pressed);
1250                // The user has acted — a tooltip that has not yet appeared is
1251                // now answering a question nobody is asking any more, and one
1252                // already up is covering the thing being clicked. Cancel the
1253                // pending dwell and retire any shown non-sticky tip, the way
1254                // Windows and GTK both do. Runs before hit-testing so it fires
1255                // even for a press that lands on nothing.
1256                self.tooltip_pointer_press(Some(*position));
1257                // Routed for the pointer that is actually pressing: a finger
1258                // gets its grip outsets and its miss-only slop, a mouse gets the
1259                // exact test it has always had.
1260                let pressing = self.current_input.pointer;
1261                if let Some(target) = self.hit_test_for(*position, &pressing) {
1262                    if *button == PointerButton::Secondary
1263                        && self.show_context_menu_for(
1264                            target,
1265                            *position,
1266                            crate::widget_builder::ContextMenuTrigger::Pointer,
1267                            &mut *ops,
1268                        )
1269                    {
1270                        return;
1271                    }
1272                    // Open the arbitration BEFORE any handler runs: the frozen
1273                    // `TouchAction` has to be readable from `ctx.touch_action()`
1274                    // inside the press handler, and an explicit
1275                    // `capture_pointer()` made there needs a sequence to enrol
1276                    // into.
1277                    self.begin_sequence(target, *position);
1278                    // Straight after the sequence, so the frozen `TouchAction`
1279                    // and the enrolled pan members are already in hand — and so
1280                    // a press on a coasting list catches it before anything
1281                    // else runs.
1282                    let modifiers = match &event {
1283                        WidgetEvent::PointerDown { modifiers, .. } => *modifiers,
1284                        _ => crate::event::Modifiers::NONE,
1285                    };
1286                    self.begin_pan(target, *position, modifiers);
1287                    self.begin_palm_watch(*position);
1288                    self.feed_pinch(super::pan_arbiter::PinchFeed::Down, *position, &mut *ops);
1289                    // Open the press record before the dispatch, so a handler
1290                    // asking `ctx.press_pending()` on its own `PointerDown`
1291                    // gets the answer the router already knows.
1292                    let focusable = self.find_focusable_at_or_above(target);
1293                    self.begin_press(focusable);
1294                    // An indirect pointer focuses on press, as it always has.
1295                    // A direct one waits for the release: a finger that lands
1296                    // on a control and slides away has not chosen it, and
1297                    // moving focus at touch-down would leave the ring — and
1298                    // the caret — on a control the user never activated. See
1299                    // `focus_on_release`. A widget built with
1300                    // `focus_on_click(false)` takes no focus from either, and
1301                    // has the focused field commit on the release that
1302                    // completes the click, for either.
1303                    if !pressing.kind.is_direct()
1304                        && let Some(focusable) = focusable
1305                        && self.click_may_focus(focusable)
1306                    {
1307                        self.focus_with_origin_ops(
1308                            focusable,
1309                            crate::focus::FocusOrigin::Pointer(pressing.kind),
1310                            &mut *ops,
1311                        );
1312                    }
1313                    self.dispatch_to_widget(target, &event, &mut *ops);
1314                    // Enrol the competitors that only become knowable once the
1315                    // press has been dispatched: the drag-capable ancestors of
1316                    // whoever took the capture (tap-vs-drag across the hit
1317                    // path).
1318                    if self.active_drag.is_none() {
1319                        self.enrol_sequence_members(&event, &mut *ops);
1320                    }
1321                    // The arena has now claimed the press, so the node whose
1322                    // visual this record drives is known — and whether the
1323                    // button that opened it is one that node can act on.
1324                    self.adopt_press_owner(*button);
1325                    // Last: the tree-owned hold. It has to see the enrolment
1326                    // (a deferred grab spends the hold) and it has to see the
1327                    // handlers a press-time build may have installed, so it is
1328                    // resolved after both. See `super::touch_route`.
1329                    self.arm_touch_route(target, *position);
1330                }
1331            }
1332            WidgetEvent::PointerUp {
1333                position, button, ..
1334            } => {
1335                // A `PointerCancel` is terminal. If this pointer's press was
1336                // revoked, the `Up` that follows completes nothing — the widget
1337                // has already been told to let go, and delivering the release
1338                // would hand it back an interaction the system took away. Drop
1339                // it, and forget the cancel: the pointer is free again.
1340                let released = self.current_pointer_id();
1341                // The hold is over whatever else this release does, and it is
1342                // cleared before any of it so a handler that runs below cannot
1343                // see a route that will never fire.
1344                self.cancel_touch_route(released);
1345                if let Some(index) = self.cancelled_pointers.iter().position(|p| *p == released) {
1346                    self.cancelled_pointers.swap_remove(index);
1347                    crate::trace_input!(
1348                        Samples,
1349                        "swallowing the Up for {released:?}: its press was cancelled"
1350                    );
1351                    return;
1352                }
1353                // The palm verdict, before anything else acts on the release:
1354                // a contact the heuristic rejects must fire no tap at all, and
1355                // the only way to guarantee that is to take the cancel funnel
1356                // instead of the release path. Judged on the `Up` and never
1357                // earlier — a contact is not revoked while the user might still
1358                // be doing something with it.
1359                self.feed_pinch(super::pan_arbiter::PinchFeed::Up, *position, &mut *ops);
1360                if self.take_palm_verdict(released) {
1361                    crate::trace_input!(
1362                        Samples,
1363                        "{released:?} released as a palm: large, and it never moved"
1364                    );
1365                    self.cancel_pointer(
1366                        released,
1367                        crate::pointer::CancelReason::PalmRejected,
1368                        &mut *ops,
1369                    );
1370                    return;
1371                }
1372                // A pan hands its release velocity to the fling driver here,
1373                // and closes its session either way.
1374                self.end_pan(*position, &mut *ops);
1375                // The pointer sequence ends here — the release sweep feeds the
1376                // `Up` to every member still following the press so its
1377                // recognizer clears the press origin it recorded. Without this,
1378                // a press that an interactive descendant captured (a card's
1379                // editor, a row's button) leaves the ancestor's DragRecognizer
1380                // armed, and the next hover move starts a phantom drag.
1381                self.note_sequence_position(*position);
1382                // Whether this release completes a tap on a control that
1383                // takes no focus is read from the sequence, so before it ends.
1384                let taps_unfocusable = self.release_taps_unfocusable(*position, *button);
1385                self.end_sequence(&event, &mut *ops);
1386                // A direct pointer's focus lands here, before the release is
1387                // dispatched, so a handler activating on the `Up` runs with the
1388                // focus its own press earned; so does the commit a click on a
1389                // control that takes no focus makes, for every pointer.
1390                self.focus_on_release(*position, taps_unfocusable, &mut *ops);
1391                if let Some(captured) = self.current_pointer_capture() {
1392                    self.dispatch_to_widget(captured, &event, &mut *ops);
1393                    // Per pointer: this Up releases *this* pointer's capture and
1394                    // leaves every other contact's alone.
1395                    self.set_current_pointer_capture(None);
1396                } else {
1397                    let releasing = self.current_input.pointer;
1398                    if let Some(target) = self.hit_test_for(*position, &releasing) {
1399                        self.dispatch_to_widget(target, &event, &mut *ops);
1400                    }
1401                }
1402                // The press is over. Any arena still following this contact saw
1403                // its `Down` but not its `Up` — see `release_arenas_following`.
1404                let released = self.current_pointer_id();
1405                self.release_arenas_following(released);
1406                // The visual goes with it. After the dispatch, so a release
1407                // handler reading `ctx.is_pressed()` still sees the press it is
1408                // completing.
1409                self.end_press(released);
1410            }
1411            WidgetEvent::PointerCancel {
1412                reason, pointer, ..
1413            } => {
1414                // A hand-built `PointerCancel` — a caller reaching the legacy
1415                // door with one, a test — means the same thing a producer does,
1416                // so it takes the same funnel rather than a second teardown of
1417                // its own. Queued behind this dispatch, like every cancel.
1418                let (reason, pointer) = (*reason, pointer.id);
1419                self.cancel_pointer(pointer, reason, &mut *ops);
1420            }
1421            WidgetEvent::Scroll {
1422                window_position: position,
1423                ..
1424            } => {
1425                use super::pan_arbiter::ScrollDelivery;
1426                match ScrollDelivery::for_source(self.current_input.scroll_source) {
1427                    // A synthesised pan (and the coast that follows it) walks
1428                    // the pan claimants and nothing else — see
1429                    // `widget_tree::pan_arbiter` for why the generic bubble
1430                    // would be wrong here.
1431                    ScrollDelivery::ClaimantChain => {
1432                        let position = *position;
1433                        self.route_scroll_along_chain(&event, position, &mut *ops);
1434                    }
1435                    // A positioned scroll routes by hit test; a positionless one
1436                    // keeps the historical hover-then-focus fallback. A mouse wheel
1437                    // is positionless, so this is a no-op for it — the change
1438                    // exists for a pan synthesised from a direct pointer, which
1439                    // never writes hover and would otherwise route nowhere.
1440                    ScrollDelivery::Bubble => {
1441                        let scrolling = self.current_input.pointer;
1442                        let target = match position {
1443                            Some(p) => self.hit_test_for(*p, &scrolling),
1444                            None => self.hovered_id().or(self.focused),
1445                        };
1446                        if let Some(target) = target {
1447                            self.dispatch_to_widget(target, &event, &mut *ops);
1448                        }
1449                    }
1450                }
1451            }
1452            WidgetEvent::KeyDown { key, modifiers, .. } => {
1453                if *key == Key::Tab {
1454                    // Ctrl+Tab / Ctrl+Shift+Tab always leave a keyboard-capture
1455                    // surface (WCAG 2.1.2). A capture node exists precisely to
1456                    // swallow every keystroke — a terminal encodes Tab as `\t`
1457                    // and Shift+Tab as CSI Z — so the ordinary "dispatch first,
1458                    // cycle only when unhandled" rule below can never move focus
1459                    // out of one. Reserving this one chord at the dispatcher, not
1460                    // in each capture widget, is what makes the escape a property
1461                    // of `keyboard_capture` itself rather than a promise every
1462                    // future capture-surface author has to remember to keep.
1463                    //
1464                    // Literal `ctrl()`, not `command()`: Ctrl+Tab is Ctrl+Tab on
1465                    // macOS too — ⌘⇥ is the application switcher and never
1466                    // reaches an app at all. Same reading as `TableView`'s
1467                    // cell-grid escape and `RichTextEditor`'s `tab_escape`.
1468                    let captured_focus = self
1469                        .focused
1470                        .is_some_and(|focused| self.is_keyboard_capture(focused));
1471                    if captured_focus && modifiers.ctrl() {
1472                        self.cycle_focus(modifiers.shift(), &mut *ops);
1473                        return;
1474                    }
1475                    // Dispatch Tab to the focused widget first so
1476                    // ancestors (e.g. an open overlay that wants to
1477                    // close instead of moving focus out through its
1478                    // content) get a chance to intercept. Fall back to
1479                    // built-in focus cycling only when no handler
1480                    // returns `EventResponse::Handled`.
1481                    if let Some(focused) = self.focused {
1482                        self.note_key_press(focused);
1483                    }
1484                    let handled = self
1485                        .focused
1486                        .map(|focused| {
1487                            self.dispatch_to_widget_returning_handled(focused, &event, &mut *ops)
1488                        })
1489                        .unwrap_or(false);
1490                    if !handled {
1491                        self.cycle_focus(modifiers.shift(), &mut *ops);
1492                    }
1493                } else if let Some(focused) = self.focused {
1494                    self.note_key_press(focused);
1495                    self.dispatch_to_widget(focused, &event, &mut *ops);
1496                }
1497            }
1498            WidgetEvent::KeyUp { key, .. } => {
1499                self.dispatch_key_release(*key, &event, &mut *ops);
1500            }
1501            WidgetEvent::ImeComposition { .. } | WidgetEvent::ImeCommit { .. } => {
1502                if let Some(focused) = self.focused {
1503                    self.dispatch_to_widget(focused, &event, &mut *ops);
1504                }
1505            }
1506            WidgetEvent::AccessAction {
1507                target,
1508                action,
1509                target_node,
1510                ..
1511            } => {
1512                // An AT action (e.g. VoiceOver's VO+Space → `Action::Click`)
1513                // always names the node it targets — the element under the
1514                // assistive-technology cursor. It must be delivered to THAT
1515                // node, never to whatever happens to hold keyboard focus.
1516                // Falling back to `self.focused` would make VO+Space fire the
1517                // focused control instead of the cursored one, and would mask
1518                // a stale/inactive target by silently activating something
1519                // else. If the target is missing or no longer active, drop the
1520                // action rather than redirecting it.
1521                //
1522                // Drop it too when a modal is up and the target is behind it
1523                // (`is_behind_modal`). The scrim keeps the pointer off the page
1524                // and the Tab cycle stays in the modal, but the page stays in
1525                // the tree a screen reader walks, and its request names a node
1526                // there directly. Delivered, a click behind "Save changes?"
1527                // opened a second box over it, and a focus request put the
1528                // keyboard on a control the box covers.
1529                if let Some(id) =
1530                    target.filter(|id| self.arena.is_active(*id) && !self.is_behind_modal(*id))
1531                {
1532                    if *action == accesskit::Action::Focus {
1533                        // Land where the keys go. A composite publishes one AT
1534                        // node on a root that is not itself focusable (a
1535                        // `ComboBox` or a `DateEdit` keeps focus on an inner
1536                        // leaf), and `ctx.request_focus` has always walked
1537                        // into the subtree for exactly that reason. The AT path
1538                        // must too: focusing the root parks `self.focused` on a
1539                        // node that takes no keystrokes, and because
1540                        // `on_key_preview` fires only on *strict* ancestors of
1541                        // the focused node, it also disarms the composite's own
1542                        // stepping keys. `first_focusable_descendant` returns the
1543                        // node itself when it is focusable, so every leaf control
1544                        // is unchanged.
1545                        //
1546                        // The walk is gated on the node actually offering
1547                        // `Action::Focus`, which is what makes the sentence
1548                        // above true of composites and only of them. Walking
1549                        // from *any* non-focusable node meant an AT `Focus` on a
1550                        // `Panel`, a `GroupBox`, a landmark or a label moved the
1551                        // keyboard onto the first control inside it, a node the
1552                        // assistive technology could have named itself and did
1553                        // not, and reported success. A node that offers no
1554                        // `Focus` reports the action unhandled instead, which is
1555                        // the honest answer, and so does one whose walk finds
1556                        // nothing focusable: the keyboard is never parked on a
1557                        // node that takes no keys.
1558                        //
1559                        // A node that stands for a focusable composite (an
1560                        // editor's text body, see `Widget::accessibility_proxy`)
1561                        // is the composite to the assistive technology, so the
1562                        // keyboard goes to the composite.
1563                        //
1564                        // A node that is not itself the focus but an item the
1565                        // focus points at (a table cell, a list row, a link in
1566                        // a viewer, a chart's mark: a widget's
1567                        // `active_descendant`) is first offered to the widget
1568                        // that publishes it, which moves its own cursor there
1569                        // and gives itself keyboard focus, as an arrow key
1570                        // would. See `offer_item_focus` for who is asked.
1571                        let synthetic =
1572                            *target_node != crate::accessibility::widget_id_to_node_id(id);
1573                        let takes_focus_itself = !synthetic
1574                            && (self
1575                                .arena
1576                                .get(id)
1577                                .is_some_and(|node| self.is_node_focusable(node))
1578                                || self.focusable_composite_behind(id).is_some());
1579                        // A composite with a Tab stop inside (a `DateEdit`
1580                        // in a card) is no item: the walk below lands on its
1581                        // stop, and no ancestor is asked. A synthetic node is
1582                        // offered only when it advertises `Focus` itself, in
1583                        // the tree the assistive technology was handed.
1584                        let offered = !takes_focus_itself
1585                            && if synthetic {
1586                                self.published_node_advertises_focus(*target_node)
1587                            } else {
1588                                self.advertises_focus_action(id)
1589                                    && self.first_focusable_descendant(id).is_none()
1590                            };
1591                        let focused_before = self.focused;
1592                        let answered = if offered {
1593                            self.offer_item_focus(id, synthetic, &event, &mut *ops)
1594                        } else {
1595                            None
1596                        };
1597                        if let Some(answerer) = answered {
1598                            // Success only when the keyboard is where the
1599                            // item's owner keeps it: focus moved, or it was
1600                            // already on the widget that answered (or inside
1601                            // it), or the item answered for itself, as an
1602                            // option of an open list does while the field it
1603                            // belongs to keeps focus. A handler that answered
1604                            // and moved nothing is no success, and lights no
1605                            // ring on a field the mouse focused.
1606                            let landed = self.focused.is_some_and(|focused| {
1607                                Some(focused) != focused_before
1608                                    || focused == answerer
1609                                    || self.is_descendant_of(focused, answerer)
1610                                    || answerer == id
1611                            });
1612                            if landed {
1613                                // The handler's own `request_focus` declares
1614                                // no modality; the user is navigating, as for
1615                                // the branch below.
1616                                self.focus_origin = Some(crate::focus::FocusOrigin::Accessibility);
1617                                if let Some(visible) =
1618                                    crate::focus::FocusOrigin::Accessibility.focus_visible()
1619                                    && self.focus_visible.get() != visible
1620                                {
1621                                    self.focus_visible.set(visible);
1622                                }
1623                            }
1624                            self.access_action_handled = landed;
1625                        } else if self.advertises_focus_action(id) {
1626                            let target = self
1627                                .focusable_composite_behind(id)
1628                                .or_else(|| self.first_focusable_descendant(id))
1629                                .unwrap_or(id);
1630                            if self.arena.is_enabled(target)
1631                                && self
1632                                    .arena
1633                                    .get(target)
1634                                    .is_some_and(|node| self.is_node_focusable(node))
1635                            {
1636                                // An assistive move, not a scripted one: the
1637                                // user is navigating, so the focus ring appears
1638                                // exactly as it would for a Tab. `Programmatic`,
1639                                // what this used to pass, declares no modality
1640                                // and left a screen-reader user with an
1641                                // invisible focus after any click.
1642                                self.focus_with_origin_ops(
1643                                    target,
1644                                    crate::focus::FocusOrigin::Accessibility,
1645                                    &mut *ops,
1646                                );
1647                                // Focus is serviced here rather than by the
1648                                // widget, so "handled" means the focus actually
1649                                // landed.
1650                                self.access_action_handled = self.focused == Some(target);
1651                            } else {
1652                                // An item whose owner refused it (a day the
1653                                // arrows cannot reach, a row in a disabled
1654                                // view), or a disabled widget: nothing takes
1655                                // keys there.
1656                                self.access_action_handled = false;
1657                            }
1658                        } else {
1659                            self.access_action_handled = false;
1660                        }
1661                    } else if *action == accesskit::Action::ShowContextMenu {
1662                        // A "show context menu" AT action — a screen reader's
1663                        // menu key, or an automation `right_click` /
1664                        // `invoke_action(node, "show_context_menu")` — first
1665                        // offers itself to the node's own `on_access_action`
1666                        // handlers. If none consume it, fall through to the very
1667                        // same machinery a Secondary `PointerDown` drives, so a
1668                        // widget's `.context_menu(..)` factory opens without the
1669                        // caller having to synthesise a right-click. The AT
1670                        // action carries no point, so anchor the menu at the
1671                        // node's centre. Without this, the AT action was a silent
1672                        // no-op for every widget that wires its menu through the
1673                        // factory (i.e. all of them) — see `show_context_menu_for`.
1674                        // Handled = the widget consumed it, or the factory
1675                        // fallback actually opened a menu. A node with neither
1676                        // reports unhandled rather than a silent success.
1677                        self.access_action_handled =
1678                            if self.dispatch_to_widget_returning_handled(id, &event, &mut *ops) {
1679                                true
1680                            } else {
1681                                // Where the keyboard's menu key would start:
1682                                // a data view's row, or the cell or delegate
1683                                // inside it that carries the app's menu.
1684                                let target = self.resolve_context_menu_target(id);
1685                                let position = self.arena.bounds(target).center();
1686                                self.show_context_menu_for(
1687                                    target,
1688                                    position,
1689                                    crate::widget_builder::ContextMenuTrigger::Accessibility,
1690                                    &mut *ops,
1691                                )
1692                            };
1693                    } else {
1694                        // A Click on a control a click does not focus
1695                        // (`focus_on_click(false)`, or nothing focusable up to
1696                        // the root) blurs nothing, as the pointer's does not:
1697                        // the focused field commits what was typed first, so
1698                        // the command sees it. Unless the control is part of
1699                        // that field, or scroll chrome, as for the pointer.
1700                        if *action == accesskit::Action::Click
1701                            && !self.in_scroll_chrome(id)
1702                            && match self.find_focusable_at_or_above(id) {
1703                                Some(focusable) => !self.click_may_focus(focusable),
1704                                None => !self.belongs_to_focused(id),
1705                            }
1706                        {
1707                            self.commit_pending_edit(&mut *ops);
1708                        }
1709                        self.access_action_handled =
1710                            self.dispatch_to_widget_returning_handled(id, &event, &mut *ops);
1711                    }
1712                }
1713            }
1714            WidgetEvent::Gesture { .. } => {
1715                if let Some(target) = self.hovered_id().or(self.focused) {
1716                    self.dispatch_to_widget(target, &event, &mut *ops);
1717                }
1718            }
1719            WidgetEvent::ScrollIntoView { .. }
1720            | WidgetEvent::PointerEnter { .. }
1721            | WidgetEvent::PointerLeave { .. }
1722            | WidgetEvent::FocusGained { .. }
1723            | WidgetEvent::FocusLost => {}
1724        }
1725        // Any intents queued by handlers via `ctx.send_intent(...)`
1726        // are dispatched after the raw event has been handled but
1727        // before commands are flushed, so commands emitted from
1728        // action handlers land on the same tick.
1729        self.drain_pending_intents(&mut *ops);
1730    }
1731
1732    /// Offer an assistive `Focus` on `id`, an item a widget may name as its
1733    /// active descendant, to the widget that owns it; report whether one took
1734    /// it.
1735    ///
1736    /// Addressed, not bubbled: the item's own node, then each ancestor up to
1737    /// and including the first focusable one, which is where a view, a menu or
1738    /// a calendar that keeps keyboard focus sits. An ancestor above that owns
1739    /// nothing in the item, and a catch-all `Focus` handler there (a card
1740    /// selecting itself) must not take it.
1741    ///
1742    /// A greyed item (a menu row the arrows still highlight) takes no input of
1743    /// its own, so the offer starts at its parent. An item inside a disabled
1744    /// container is offered to no one: its owner is disabled with it.
1745    ///
1746    /// Only a widget item is greyed that way. For a synthetic node `id` is its
1747    /// owner (a chart, a web view), and a disabled owner refuses; a disabled
1748    /// composite (a `DateEdit` with its segments) is no item either.
1749    ///
1750    /// Returns the node that answered.
1751    fn offer_item_focus(
1752        &mut self,
1753        id: WidgetId,
1754        synthetic: bool,
1755        event: &WidgetEvent,
1756        ops: &mut dyn crate::window::WindowOps,
1757    ) -> Option<WidgetId> {
1758        let start = if self.arena.is_enabled(id) {
1759            Some(id)
1760        } else {
1761            let greyed_item = !synthetic
1762                && self
1763                    .arena
1764                    .get(id)
1765                    .and_then(|node| node.enabled_state.as_ref())
1766                    .is_some_and(|state| !state.get())
1767                && !self.has_focusable_descendant(id);
1768            self.arena
1769                .parent(id)
1770                .filter(|&parent| greyed_item && self.arena.is_enabled(parent))
1771        };
1772        let mut at = start;
1773        while let Some(node) = at {
1774            if self.dispatch_to_widget_direct_returning_handled(node, event, &mut *ops) {
1775                return Some(node);
1776            }
1777            if self
1778                .arena
1779                .get(node)
1780                .is_some_and(|n| self.is_node_focusable(n))
1781            {
1782                return None;
1783            }
1784            at = self.arena.parent(node);
1785        }
1786        None
1787    }
1788
1789    /// Whether any node below `id` is focusable, Tab stop or not, enabled or
1790    /// not.
1791    fn has_focusable_descendant(&self, id: WidgetId) -> bool {
1792        let mut stack: Vec<WidgetId> = self.arena.children(id).to_vec();
1793        while let Some(node) = stack.pop() {
1794            if self
1795                .arena
1796                .get(node)
1797                .is_some_and(|n| self.is_node_focusable(n))
1798            {
1799                return true;
1800            }
1801            stack.extend_from_slice(self.arena.children(node));
1802        }
1803        false
1804    }
1805
1806    /// Whether the node `node_id` advertises `Focus` in the accessibility
1807    /// tree last built, the one an assistive technology acts on. For a
1808    /// synthetic node, which no widget can be asked about directly.
1809    fn published_node_advertises_focus(&self, node_id: accesskit::NodeId) -> bool {
1810        self.cached_a11y.as_ref().is_some_and(|update| {
1811            update
1812                .nodes
1813                .iter()
1814                .any(|(id, node)| *id == node_id && node.supports_action(accesskit::Action::Focus))
1815        })
1816    }
1817
1818    /// Open the context menu the keyboard just asked for, and report whether
1819    /// one appeared.
1820    ///
1821    /// Targets the focused widget, or whatever its
1822    /// [`context_menu_key_target`](crate::widget::Widget::context_menu_key_target)
1823    /// nominates instead — for a data view, the selected row. Anchors the menu
1824    /// at the target's own bounds rather than at the last pointer position,
1825    /// which may be anywhere on screen or nowhere at all.
1826    ///
1827    /// Returns `false` when nothing on the ancestor chain owns a factory, so
1828    /// the key falls through to normal dispatch and a widget that wants to
1829    /// handle it itself still can.
1830    fn open_context_menu_from_keyboard(&mut self, ops: &mut dyn crate::window::WindowOps) -> bool {
1831        let Some(focused) = self.focused else {
1832            return false;
1833        };
1834        let target = self.resolve_context_menu_target(focused);
1835
1836        // The menu belongs where the thing it is about is. A keyboard user has
1837        // no pointer position, and the stale one is worse than useless: it
1838        // would put the menu over an unrelated part of the window.
1839        let bounds = self.bounds(target);
1840        let anchor = Point {
1841            x: bounds.x + bounds.width / 2.0,
1842            y: bounds.y + bounds.height / 2.0,
1843        };
1844        self.show_context_menu_for(
1845            target,
1846            anchor,
1847            crate::widget_builder::ContextMenuTrigger::Keyboard,
1848            ops,
1849        )
1850    }
1851
1852    /// Follow [`context_menu_key_target`](crate::widget::Widget::context_menu_key_target)
1853    /// from `start` while it names an active node: a view names its current
1854    /// row, the row the delegate (or the cell) inside it, so each widget
1855    /// states only its own next hop. Stops after a few hops, or at a node
1856    /// already passed, on the last good one (a cycle is a widget bug, which
1857    /// a debug build reports).
1858    ///
1859    /// Asked for every node on every accessibility walk (to advertise the
1860    /// action), so it allocates nothing.
1861    pub(super) fn resolve_context_menu_target(&self, start: WidgetId) -> WidgetId {
1862        const MAX_HOPS: usize = 8;
1863        let next_of = |id: WidgetId| {
1864            self.arena
1865                .get(id)
1866                .and_then(|node| node.widget.context_menu_key_target())
1867                .filter(|next| self.arena.is_active(*next))
1868        };
1869        let mut passed = [start; MAX_HOPS + 1];
1870        let mut target = start;
1871        for hop in 0..MAX_HOPS {
1872            let Some(next) = next_of(target) else {
1873                break;
1874            };
1875            if passed[..=hop].contains(&next) {
1876                debug_assert!(
1877                    false,
1878                    "context_menu_key_target cycle through {next:?}: a widget names an ancestor's target"
1879                );
1880                break;
1881            }
1882            passed[hop + 1] = next;
1883            target = next;
1884        }
1885        target
1886    }
1887
1888    /// Ask the factories from `target` up for a menu, and mount the first one
1889    /// offered at `position`.
1890    ///
1891    /// The closest claimer wins: at each node the factory is asked first,
1892    /// and a node whose factory declines (or that has none) but carries a
1893    /// [contribution](crate::widget_builder::ContextMenuContribution) claims
1894    /// the menu with that section alone. Then the contributions on the owner
1895    /// and above it, up to (and excluding) the first focusable node above it,
1896    /// are appended to the menu, innermost first, unless the owner is itself
1897    /// a focusable control. So a contribution on a data view itself (which is
1898    /// focusable) is never merged into its rows' menus.
1899    ///
1900    /// `trigger` reaches each factory through
1901    /// [`EventContext::context_menu_trigger`](crate::widget::EventContext::context_menu_trigger):
1902    /// only a pointer's `position` is a place the user chose, and a factory
1903    /// that moves a caret to it on a keyboard request writes the menu's Paste
1904    /// into the middle of the text.
1905    pub(super) fn show_context_menu_for(
1906        &mut self,
1907        target: WidgetId,
1908        position: Point,
1909        trigger: crate::widget_builder::ContextMenuTrigger,
1910        ops: &mut dyn crate::window::WindowOps,
1911    ) -> bool {
1912        // Walks up the parent chain calling each factory in turn. A
1913        // factory returning `Some(menu)` claims the click and mounts;
1914        // a factory returning `None` declines and the walk continues.
1915        // No factory anywhere on the chain → fall through to whatever
1916        // the caller does with the unconsumed PointerDown.
1917        let mut ctx = self.make_event_context(&mut *ops);
1918        ctx.context_menu_trigger = Some(trigger);
1919        let mut walker = Some(target);
1920        // The owner, its menu, and whether a factory made it (rather than a
1921        // contribution standing in for one).
1922        let mut menu_decision: Option<(WidgetId, Box<dyn Widget>, bool)> = None;
1923        while let Some(id) = walker {
1924            let Some(node) = self.arena.get(id) else {
1925                break;
1926            };
1927            // Invoke the factory with the click position and a real
1928            // EventContext. The factory is `Fn`, so it is called through an
1929            // immutable borrow on the node; `ctx` is a local whose
1930            // `&mut WindowOps` is disjoint from `self.arena`.
1931            if let Some(factory) = node.context_menu_factory.as_ref()
1932                && let Some(menu) = factory(position, &mut ctx)
1933            {
1934                menu_decision = Some((id, menu, true));
1935                break;
1936            }
1937            // No factory claimed here: a contribution on this node is the
1938            // closest offer, mounted as the menu.
1939            if let Some(contribution) = node.context_menu_contribution.as_ref()
1940                && let Some(section) = contribution(&mut ctx)
1941            {
1942                menu_decision = Some((id, section, false));
1943                break;
1944            }
1945            walker = node.parent;
1946        }
1947
1948        // The sections the owner and the nodes above it add, innermost first,
1949        // up to the first focusable node above the owner (a data view: a view
1950        // nested in another view's row adds only its own). A menu a focusable
1951        // control owns (a text field, a cell editor) is about that control
1952        // and gains none.
1953        if let Some((owner_id, menu, by_factory)) = menu_decision.as_mut() {
1954            let sealed = *by_factory && self.is_focusable_control(*owner_id);
1955            if !sealed {
1956                // A contribution that claimed the menu already gave its
1957                // section; its node's is not asked again.
1958                let mut probe = if *by_factory {
1959                    Some(*owner_id)
1960                } else {
1961                    self.arena.get(*owner_id).and_then(|node| node.parent)
1962                };
1963                while let Some(id) = probe {
1964                    let Some(node) = self.arena.get(id) else {
1965                        break;
1966                    };
1967                    if id != *owner_id && self.is_focusable_control(id) {
1968                        break;
1969                    }
1970                    if let Some(contribution) = node.context_menu_contribution.as_ref()
1971                        && let Some(section) = contribution(&mut ctx)
1972                        && menu.append_context_menu_section(section).is_err()
1973                    {
1974                        note_refused_section(*owner_id);
1975                    }
1976                    probe = node.parent;
1977                }
1978            }
1979        }
1980        let menu_decision = menu_decision.map(|(owner, menu, _)| (owner, menu));
1981
1982        // Drain ctx side effects regardless of whether a menu showed —
1983        // a factory that returns `None` may still have queued intents,
1984        // updated signals, or requested a frame. With no menu, they start
1985        // where the menu was asked for, so every factory the walk called
1986        // (each one an ancestor of `target`) is on their way to the root.
1987        let drain_anchor = menu_decision.as_ref().map(|(id, _)| *id).unwrap_or(target);
1988        self.collect_from_ctx(ctx, drain_anchor);
1989
1990        let Some((owner_id, menu_widget)) = menu_decision else {
1991            return false;
1992        };
1993
1994        // Clear stale transient overlays (other menus / popovers) before mounting
1995        // the new menu, but KEEP any overlay that *contains* the right-clicked
1996        // widget — otherwise a right-click inside a modal editor would tear down
1997        // the modal it lives in (dismiss_all did exactly that). The context menu
1998        // then mounts on top of its host overlay.
1999        let keep: std::collections::HashSet<WidgetId> = self
2000            .overlay_manager
2001            .stack
2002            .iter()
2003            .map(|o| o.content_id)
2004            .filter(|&content_id| self.is_descendant_of(owner_id, content_id))
2005            .collect();
2006        let dismissed = self.overlay_manager.dismiss_except(&keep);
2007        self.dormant_dismissed_content(&dismissed, &mut *ops);
2008
2009        let content_id = self.add_boxed(menu_widget);
2010        let prev_focus = self.focused;
2011        // One branch, every menu: a coarse pointer gets a placement that keeps
2012        // clear of its own contact patch, everything else the historical
2013        // `AtPointer`. A point-anchored panel that puts its own corner under
2014        // the finger is the defect this exists to fix, and it is invisible from
2015        // a mouse — which is why the decision lives in
2016        // `OverlayPlacement::at_pointer_for` rather than at each call site.
2017        let placement =
2018            crate::overlay::OverlayPlacement::at_pointer_for(position, &self.current_input.pointer);
2019        // The factory built this menu for this opening alone: it is destroyed
2020        // once the menu closes, rather than parked forever.
2021        let mut request = crate::overlay::OverlayRequest::new(
2022            content_id,
2023            owner_id,
2024            placement,
2025            crate::overlay::DismissBehavior::EscapeOrClickOutside,
2026        );
2027        request.destroy_content_on_dismiss = true;
2028        self.overlay_manager.show(request);
2029        if let Some(focus_id) = prev_focus {
2030            self.record_top_focus_restore(focus_id);
2031        }
2032        // Opened without pointing (a key, a screen reader's action): the
2033        // keyboard drives the menu, so the modality is the keyboard's, and
2034        // a menu highlights its first row on arrival as it does from a key.
2035        if trigger != crate::widget_builder::ContextMenuTrigger::Pointer
2036            && !self.focus_visible.get()
2037        {
2038            self.focus_visible.set(true);
2039        }
2040        self.focus_ops(content_id, &mut *ops);
2041        // Flush intents the factory queued so they take effect on the
2042        // same dispatch tick as the menu mount. The caller's
2043        // PointerDown handler returns after we return `true`, skipping
2044        // its own drain — fire ours here.
2045        self.drain_pending_intents(&mut *ops);
2046        true
2047    }
2048
2049    /// Whether `id` is a control that takes focus itself: a focusable node,
2050    /// a composite whose root stands for a focusable inner leaf through its
2051    /// accessibility proxy (a `SpinBox`, a `DateEdit`, a `TimeEdit`, whose
2052    /// root is not focusable), or a body a focusable composite stands for.
2053    fn is_focusable_control(&self, id: WidgetId) -> bool {
2054        let focusable = |id: WidgetId| {
2055            self.arena
2056                .get(id)
2057                .is_some_and(|node| self.is_node_focusable(node))
2058        };
2059        if focusable(id) || self.focusable_composite_behind(id).is_some() {
2060            return true;
2061        }
2062        let mut current = id;
2063        for _ in 0..8 {
2064            let Some(proxy) = self.live_accessibility_proxy(current) else {
2065                return false;
2066            };
2067            if focusable(proxy) {
2068                return true;
2069            }
2070            current = proxy;
2071        }
2072        false
2073    }
2074
2075    // -----------------------------------------------------------------
2076    // Outside-press overlay dismissal
2077    // -----------------------------------------------------------------
2078
2079    /// Handle a press that lands outside one or more dismissable overlays.
2080    ///
2081    /// Returns `true` when the press is consumed and must not reach the tree.
2082    ///
2083    /// **An indirect pointer is unchanged.** It dismisses on the press and
2084    /// falls through, so one click still closes a menu and actuates the control
2085    /// beneath — deliberate behaviour for a cursor, which names one pixel that
2086    /// the user could see the whole time they were aiming at it.
2087    ///
2088    /// **A direct pointer arms instead.** A finger covers what it is about to
2089    /// actuate: the menu is the only thing the user was looking at, and the
2090    /// control underneath is one they never saw. So the `Down` is withheld from
2091    /// the tree, the dismissal waits for the release, and a press that is
2092    /// cancelled — or slid onto the very overlay it would have closed — leaves
2093    /// nothing behind at all. Both the arming `Down` and the committing `Up`
2094    /// are consumed, so nothing beneath ever sees half a press.
2095    fn arm_outside_press_dismissal(
2096        &mut self,
2097        position: Point,
2098        button: PointerButton,
2099        ops: &mut dyn crate::window::WindowOps,
2100    ) -> bool {
2101        self.prune_stale_dismiss_arms();
2102        let pressing = self.current_input.pointer;
2103        let busy = self.busy_press_points(pressing.id);
2104
2105        if pressing.kind.is_direct() {
2106            return self
2107                .overlay_manager
2108                .arm_dismiss(pressing.id, position, &busy)
2109                .suppress_beneath;
2110        }
2111
2112        let mark = self.dismissal_mark();
2113        let (dismissed, focus_restore, toggle_anchors) = self
2114            .overlay_manager
2115            .dismiss_outside_press_toggles(position, &busy);
2116        if dismissed.is_empty() {
2117            return false;
2118        }
2119        self.dormant_dismissed_content(&dismissed, &mut *ops);
2120        self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2121        // The press dismissed one or more overlays. By default it
2122        // now ALSO falls through to the widget under the cursor,
2123        // so a single click both closes the menu/popover and
2124        // activates the control beneath. The one case swallowed: a
2125        // press on the anchor of a click-opened overlay, with a
2126        // button the anchor's tap answers to, because that tap
2127        // would otherwise reopen the overlay this very press just
2128        // dismissed (click-the-trigger-to-close). A primary press
2129        // is always taken for the toggle, as it always was; any
2130        // other button only where the trigger opens on it, so a
2131        // right click on a left-click trigger still dismisses and
2132        // falls through to the trigger's context menu.
2133        toggle_anchors
2134            .iter()
2135            .any(|&toggle| self.press_reopens(toggle, position, button, &pressing))
2136    }
2137
2138    /// Whether a press at `position` that has just dismissed the overlay
2139    /// anchored on `toggle` would open it again, and so is taken for the
2140    /// toggle rather than falling through.
2141    ///
2142    /// A popover's anchor is its trigger: a press anywhere on it is taken, a
2143    /// primary one always and any other where the trigger opens on it. A
2144    /// modal's anchor is whatever presented it, which may be the whole page
2145    /// (an app that presents a card from its root), so a press inside it is
2146    /// taken only when the anchor is the first widget on its way up that
2147    /// answers the button: the dialog's own opener is, a list row on the
2148    /// page that presented a card is not, and gets the click.
2149    fn press_reopens(
2150        &self,
2151        toggle: crate::overlay::ToggleAnchor,
2152        position: Point,
2153        button: PointerButton,
2154        pointer: &crate::pointer::PointerInfo,
2155    ) -> bool {
2156        let anchor = toggle.anchor;
2157        if !self.arena.is_active(anchor) || !self.arena.bounds(anchor).contains(position) {
2158            return false;
2159        }
2160        if toggle.modal {
2161            return self.first_tapper(position, button, pointer) == Some(anchor);
2162        }
2163        button == PointerButton::Primary
2164            || self
2165                .anchor_tap_buttons(anchor, position, pointer)
2166                .contains(button)
2167    }
2168
2169    /// The first widget at or above the one hit at `position` whose tap
2170    /// answers `button`: the one a click there would activate.
2171    fn first_tapper(
2172        &self,
2173        position: Point,
2174        button: PointerButton,
2175        pointer: &crate::pointer::PointerInfo,
2176    ) -> Option<WidgetId> {
2177        let mut node = self.hit_test_for(position, pointer);
2178        while let Some(id) = node {
2179            if self.tap_buttons(id).contains(button) {
2180                return Some(id);
2181            }
2182            node = self.arena.parent(id);
2183        }
2184        None
2185    }
2186
2187    /// The buttons a tap at `position` would be recognized for on its way up
2188    /// to `anchor`: the union of the tap masks of every node from the one hit
2189    /// at `position` up to and including `anchor`.
2190    ///
2191    /// An anchor is often not the node carrying the tap. A popover anchors on
2192    /// its own wrapper, and the opener sits on the trigger inside it, or on a
2193    /// control inside that; any of them answering to the button means the
2194    /// press would reach an opener. [`ButtonMask::NONE`] when the node hit is
2195    /// not inside `anchor` at all.
2196    fn anchor_tap_buttons(
2197        &self,
2198        anchor: WidgetId,
2199        position: Point,
2200        pointer: &crate::pointer::PointerInfo,
2201    ) -> ButtonMask {
2202        let mut mask = ButtonMask::NONE;
2203        let mut node = self.hit_test_for(position, pointer);
2204        while let Some(id) = node {
2205            mask = mask.union(self.tap_buttons(id));
2206            if id == anchor {
2207                return mask;
2208            }
2209            node = self.arena.parent(id);
2210        }
2211        ButtonMask::NONE
2212    }
2213
2214    /// Complete a direct pointer's armed dismissal on its release.
2215    ///
2216    /// Returns `true` when this pointer held an arm — in which case the `Up` is
2217    /// consumed whether or not anything actually closed. Nothing beneath saw
2218    /// the `Down`, so delivering the `Up` alone would hand a widget the second
2219    /// half of a press it never started.
2220    fn commit_outside_press_dismissal(
2221        &mut self,
2222        position: Point,
2223        ops: &mut dyn crate::window::WindowOps,
2224    ) -> bool {
2225        let pointer = self.current_pointer_id();
2226        if !self.overlay_manager.has_armed_dismiss(pointer) {
2227            return false;
2228        }
2229        let mark = self.dismissal_mark();
2230        let (dismissed, focus_restore, _anchors) =
2231            self.overlay_manager.commit_dismiss(pointer, position);
2232        if !dismissed.is_empty() {
2233            self.dormant_dismissed_content(&dismissed, &mut *ops);
2234            self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
2235        }
2236        true
2237    }
2238
2239    /// Where every *other* live pointer is holding a press.
2240    ///
2241    /// A press is only "outside" relative to the overlays nobody else is
2242    /// working in — see
2243    /// [`OverlayManager::dismiss_outside_press`](crate::overlay::OverlayManager::dismiss_outside_press).
2244    /// Liveness is judged with
2245    /// [`press_is_revocable`](Self::press_is_revocable), the predicate the
2246    /// cancel funnel already uses, and for the same reason: a pointer with
2247    /// neither a sequence nor a capture has no interaction that could be taken
2248    /// away, so it has none to protect either. That also exempts a pointer
2249    /// inside its terminal `Up` — its sequence is still installed but already
2250    /// terminating — which is what lets a menu item's own handler close its
2251    /// menu while a second contact rests elsewhere.
2252    ///
2253    /// The *press* position, not the current one: a contact that grabbed a
2254    /// menu and dragged past its edge is still manipulating that menu.
2255    fn busy_press_points(&self, exclude: crate::pointer::PointerId) -> Vec<Point> {
2256        self.pointers
2257            .iter()
2258            .filter(|entry| entry.info.id != exclude)
2259            .filter(|entry| self.press_is_revocable(entry.info.id))
2260            .map(|entry| entry.down_position)
2261            .collect()
2262    }
2263
2264    /// Drop arms whose contact is gone.
2265    ///
2266    /// An arm is normally retired by its own `Up` or `Cancel`. A contact that
2267    /// disappears without either — a table sweep, a window losing its input —
2268    /// would otherwise leave one behind, and a later contact minted with the
2269    /// same id would inherit a dismissal it never asked for.
2270    fn prune_stale_dismiss_arms(&mut self) {
2271        let stale: Vec<crate::pointer::PointerId> = self
2272            .overlay_manager
2273            .armed_pointers()
2274            .into_iter()
2275            .filter(|id| self.pointers.get(*id).is_none())
2276            .collect();
2277        for id in stale {
2278            self.overlay_manager.abort_dismiss(id);
2279        }
2280    }
2281
2282    // -----------------------------------------------------------------
2283    // Press state
2284    // -----------------------------------------------------------------
2285
2286    /// Open the press record for the contact being dispatched.
2287    ///
2288    /// Called from the `PointerDown` arm before the press is dispatched, with
2289    /// the focusable the press landed on (which a direct pointer will hold
2290    /// until its release). The node whose visual the record drives is not
2291    /// known yet — the arena claims the press during the dispatch — so
2292    /// [`adopt_press_owner`](Self::adopt_press_owner) finishes the record
2293    /// afterwards.
2294    ///
2295    /// The press-feedback delay applies **only inside a pan claimant**: a
2296    /// finger resting on a list row must not flash the row before the pan has
2297    /// been ruled out, while a button that nothing can scroll has no ambiguity
2298    /// to wait out and highlights at once. `begin_pan` has already decided
2299    /// whether this press is inside one — a session exists only when a claimant
2300    /// along the hit path accepts this pointer kind — so this reads its answer
2301    /// rather than re-deriving it.
2302    fn begin_press(&mut self, focusable: Option<WidgetId>) {
2303        let pointer = self.current_pointer_id();
2304        let now = self.sequence_now();
2305        let delay = self
2306            .pan_session_open(pointer)
2307            .then(|| self.current_profile().press_feedback_delay)
2308            .filter(|d| !d.is_zero());
2309        self.presses.press(pointer, focusable, now, delay);
2310    }
2311
2312    /// Record the node whose gesture arena took this press, and publish its
2313    /// signal.
2314    ///
2315    /// The owner is the sequence's `pressed_owner` — the node holding the
2316    /// pointer capture once the `Down` has been dispatched, which is exactly
2317    /// the node whose `on_tap` would fire. A press no arena took owns no
2318    /// visual, and its record stays for the focus deferral alone.
2319    ///
2320    /// So does a press on a **button the owner cannot act on**. A press visual
2321    /// says "release here and this control acts", so it has to answer to the
2322    /// same buttons the activation does: the router opens a record for every
2323    /// button, but only [`press_buttons`](Self::press_buttons) — the union of
2324    /// the owner's own click-style [`ButtonMask`]s,
2325    /// `PRIMARY` unless the widget widened it — decides which of them may light
2326    /// the control up. Without the gate a middle-click, or a right-click on a
2327    /// node with no context menu, would raise a press that can never complete;
2328    /// with it, the visual and the activation agree by construction, exactly as
2329    /// they do at the tap boundary where one predicate fails the tap, fires
2330    /// `cancel_taps` and clears the visual.
2331    ///
2332    /// The record itself is untouched either way, so the focus a direct
2333    /// pointer deferred to its release is still there to be assigned.
2334    fn adopt_press_owner(&mut self, button: PointerButton) {
2335        let pointer = self.current_pointer_id();
2336        let Some(owner) = self.current_sequence().and_then(|s| s.pressed_owner()) else {
2337            return;
2338        };
2339        if !self.press_buttons(owner).contains(button) {
2340            crate::trace_input!(
2341                Samples,
2342                "no press visual for {owner:?}: {button:?} is not one of its accepted buttons"
2343            );
2344            return;
2345        }
2346        self.presses.set_owner(pointer, owner);
2347        self.publish_pressed(owner);
2348    }
2349
2350    /// Re-evaluate the press being dispatched against `position`.
2351    ///
2352    /// Three ways a press visual changes without a release:
2353    ///
2354    /// * the pointer left the press's [`TapBoundary`](crate::gesture::TapBoundary)
2355    ///   — the same predicate that fails the tap and fires `cancel_taps`, so
2356    ///   the visual and the activation are abandoned together;
2357    /// * it came back inside, which restores the visual: WCAG 2.2 SC 2.5.2's
2358    ///   abort gesture is reversible right up to the release;
2359    /// * the arbitration decided for somebody else — a pan claimant, an
2360    ///   ancestor drag — and the pressed control has lost the press without
2361    ///   ever seeing a release.
2362    fn update_press(&mut self, position: Point) {
2363        let pointer = self.current_pointer_id();
2364        if self.presses.get(pointer).is_none() {
2365            return;
2366        }
2367        // A peer claim ends the press outright: the node was never told, and
2368        // leaving its visual up would advertise an interaction it has lost.
2369        let claimed_elsewhere = self.current_sequence().is_some_and(|sequence| {
2370            sequence
2371                .winner()
2372                .is_some_and(|winner| Some(winner) != sequence.pressed_owner())
2373        });
2374        if claimed_elsewhere {
2375            self.end_press(pointer);
2376            return;
2377        }
2378        let profile = self.current_profile();
2379        let origin = self.current_sequence().map(|s| s.press_origin());
2380        let owner = self.presses.get(pointer).and_then(|p| p.owner);
2381        let inside = match (origin, owner) {
2382            (Some(origin), Some(owner)) => {
2383                let bounds = self
2384                    .arena
2385                    .is_active(owner)
2386                    .then(|| self.arena.bounds(owner));
2387                !crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2388                    .left(origin, position, bounds, &profile)
2389            }
2390            // No sequence to measure from, or no owner to measure against:
2391            // there is no visual either way, so the flag is moot.
2392            _ => true,
2393        };
2394        let Some(press) = self.presses.get_mut(pointer) else {
2395            return;
2396        };
2397        if press.inside == inside {
2398            return;
2399        }
2400        press.inside = inside;
2401        if let Some(owner) = owner {
2402            self.publish_pressed(owner);
2403        }
2404    }
2405
2406    /// Close the press held by `pointer` and republish the node it drove.
2407    ///
2408    /// The one exit: a release, a cancel, and a peer claim all come through
2409    /// here, so a node can never be left painted as pressed by a path that
2410    /// forgot to clear it.
2411    pub(crate) fn end_press(&mut self, pointer: crate::pointer::PointerId) {
2412        if let Some(owner) = self.presses.release(pointer) {
2413            self.publish_pressed(owner);
2414        }
2415    }
2416
2417    /// Resolve every elapsed press-feedback delay and publish the visuals that
2418    /// just appeared. Driven by the same tick that advances the long press.
2419    pub(crate) fn resolve_press_delays(&mut self, now: crate::pointer::EventTime) {
2420        if self.presses.is_empty() {
2421            return;
2422        }
2423        for id in self.presses.resolve_delays(now) {
2424            self.publish_pressed(id);
2425            self.arena.mark_needs_paint(id);
2426        }
2427    }
2428
2429    /// The earliest instant a pending press wants the event loop back, so a
2430    /// finger that lands and does not move still gets its highlight.
2431    pub(crate) fn next_press_deadline(&self) -> Option<std::time::Instant> {
2432        self.presses.next_deadline().map(|t| self.instant_for(t))
2433    }
2434
2435    /// The click about to be dispatched (by a pointer, or an
2436    /// assistive-technology Click) lands on a control that takes no
2437    /// focus from it (built with `focus_on_click(false)`, or not focusable at
2438    /// all), so it blurs nothing: have the focused widget, and each
2439    /// of its ancestors, commit the edit it holds
2440    /// ([`on_commit_edit`](crate::widget_builder::HandlerSet::on_commit_edit)),
2441    /// innermost first, as a blur would have made it, so that the command
2442    /// the click runs sees the value typed (AppKit's `commitEditing`).
2443    ///
2444    /// The ancestors are those of an intent's walk from the focused widget
2445    /// ([`intent_walk`](Self::intent_walk)): from a popup on to the widget it
2446    /// is attached to, and no further than an open modal. The widgets that
2447    /// commit are the ones whose actions the command can reach; a form whose
2448    /// field is edited in a popover attached to it commits, and the page
2449    /// behind a dialog does not.
2450    pub(crate) fn commit_pending_edit(&mut self, ops: &mut dyn crate::window::WindowOps) {
2451        let Some(focus) = self.focused.filter(|id| self.arena.is_active(*id)) else {
2452            return;
2453        };
2454        let (chain, _, _) = self.intent_walk(focus, &[]);
2455        for id in chain {
2456            for external in [true, false] {
2457                let handler = self.arena.get_mut(id).and_then(|node| {
2458                    let handlers = if external {
2459                        &mut node.external_handlers
2460                    } else {
2461                        &mut node.handlers
2462                    };
2463                    handlers.on_commit_edit.take()
2464                });
2465                let Some(mut handler) = handler else { continue };
2466                let mut ctx = self.make_event_context(&mut *ops).with_dispatch_node(id);
2467                handler(&mut ctx);
2468                if let Some(node) = self.arena.get_mut(id) {
2469                    let handlers = if external {
2470                        &mut node.external_handlers
2471                    } else {
2472                        &mut node.handlers
2473                    };
2474                    // A rebuild the commit caused installed a new one: keep it.
2475                    if handlers.on_commit_edit.is_none() {
2476                        handlers.on_commit_edit = Some(handler);
2477                    }
2478                }
2479                self.collect_from_ctx(ctx, id);
2480            }
2481        }
2482    }
2483
2484    /// Whether the release at `position` with `button` completes a tap on a
2485    /// control that takes no focus at all, from the press to the root (a
2486    /// `focusable(false)` icon, a plain `on_tap` node): the click runs that
2487    /// control's command, and the focused field commits first, as for a
2488    /// control built with `focus_on_click(false)`.
2489    ///
2490    /// The press must have been taken by a node whose `on_tap` answers the
2491    /// button, and must end inside the tap boundary the tap recognizer
2492    /// measures: a hover, a scroll, a press on empty background (no node took
2493    /// it, nothing runs, focus does not move, so nothing has to be committed,
2494    /// as a native line edit commits only when focus leaves it or a command
2495    /// runs), a drag or a swipe that took the press, and a press dragged off
2496    /// commit nothing. Nor does a tap on the focused widget's own parts
2497    /// ([`belongs_to_focused`](Self::belongs_to_focused)): its scroll bar, or
2498    /// a popup attached to it, act within the edit. Nor one on scroll chrome
2499    /// ([`in_scroll_chrome`](Self::in_scroll_chrome)): another view's scroll
2500    /// bar moves that view and runs no command.
2501    ///
2502    /// Read before the sequence ends, since the tap boundary is measured from
2503    /// the press origin it holds.
2504    fn release_taps_unfocusable(&self, position: Point, button: PointerButton) -> bool {
2505        let Some(press) = self.presses.get(self.current_pointer_id()) else {
2506            return false;
2507        };
2508        let Some(owner) = press.owner.filter(|_| press.focusable.is_none()) else {
2509            return false;
2510        };
2511        if !self.arena.is_active(owner) || !self.tap_buttons(owner).contains(button) {
2512            return false;
2513        }
2514        let Some(sequence) = self.current_sequence() else {
2515            return false;
2516        };
2517        // Any winner ends the tap: a peer that claimed the press (an
2518        // ancestor's drag, a pan), or the owner's own drag or swipe. An
2519        // explicit capture the owner made (a raw member) decides without
2520        // recognizing anything, and its tap may still follow.
2521        if let Some(winner) = sequence.winner()
2522            && (winner != owner
2523                || sequence.members().iter().any(|member| {
2524                    member.id == winner
2525                        && matches!(member.role, crate::gesture::MemberRole::Gesture)
2526                }))
2527        {
2528            return false;
2529        }
2530        let profile = self.current_profile();
2531        let left = crate::gesture::TapBoundary::for_pointer(&self.current_input.pointer, &profile)
2532            .left(
2533                sequence.press_origin(),
2534                position,
2535                Some(self.arena.bounds(owner)),
2536                &profile,
2537            );
2538        !left && !self.in_scroll_chrome(owner) && !self.belongs_to_focused(owner)
2539    }
2540
2541    /// Whether `id` is in a subtree marked
2542    /// [`scroll_chrome`](crate::widget_builder::HandlerSet::scroll_chrome): a
2543    /// scroll bar, a tab strip's scroll arrows. A click there scrolls and runs
2544    /// no command, so the focused field commits nothing for it.
2545    fn in_scroll_chrome(&self, id: WidgetId) -> bool {
2546        let mut current = Some(id);
2547        while let Some(node) = current {
2548            if self.arena.get(node).is_some_and(|n| n.scroll_chrome) {
2549                return true;
2550            }
2551            current = self.arena.parent(node);
2552        }
2553        false
2554    }
2555
2556    /// Whether `id` is part of the focused widget: in its subtree, or in a
2557    /// popup attached to it or to a widget of its subtree (its completion or
2558    /// candidate list, its context menu), which an intent's walk from `id`
2559    /// crosses back to it ([`intent_walk`](Self::intent_walk)).
2560    fn belongs_to_focused(&self, id: WidgetId) -> bool {
2561        self.focused
2562            .is_some_and(|focus| self.intent_walk(id, &[]).0.contains(&focus))
2563    }
2564
2565    /// Whether a click or a tap may move focus to `focusable`, the focusable
2566    /// widget it landed on or in: false when that widget was built with
2567    /// [`focus_on_click(false)`](crate::widget_builder::HandlerSet::focus_on_click),
2568    /// which a pointer activates without focusing. Asked of that one node, as
2569    /// GTK's `focus-on-click` is, so a text field inside a toolbar built with
2570    /// the flag still takes focus; a composite whose focusable node is a child
2571    /// of its own forwards the flag to it.
2572    fn click_may_focus(&self, focusable: WidgetId) -> bool {
2573        self.arena
2574            .get(focusable)
2575            .is_none_or(|node| node.focus_on_click)
2576    }
2577
2578    /// Assign the focus a direct pointer's press deferred, if the release
2579    /// earned it.
2580    ///
2581    /// The guard is "the release landed on the same focusable as the press".
2582    /// A finger that presses a button, slides onto its neighbour and lifts has
2583    /// activated nothing and must move focus nowhere — the same rule the tap
2584    /// recognizer applies to activation, applied to focus so the two cannot
2585    /// disagree. A press that found no focusable defers nothing.
2586    ///
2587    /// For a widget built with `focus_on_click(false)` it moves no focus and
2588    /// has the focused widget commit its pending edit instead
2589    /// ([`commit_pending_edit`](Self::commit_pending_edit)), under the same
2590    /// guard and for every pointer, a mouse included: a press dragged off or
2591    /// cancelled has run no command, so nothing is committed for it, and
2592    /// neither is a click on scroll chrome
2593    /// ([`in_scroll_chrome`](Self::in_scroll_chrome)), which only scrolls.
2594    /// Otherwise a no-op for an indirect pointer, which focused at press.
2595    ///
2596    /// A press that found no focusable at all commits the same way when
2597    /// `taps_unfocusable` says its release completes a tap
2598    /// ([`release_taps_unfocusable`](Self::release_taps_unfocusable)): a
2599    /// toolbar icon built without focus runs its command as one built with
2600    /// `focus_on_click(false)` does.
2601    fn focus_on_release(
2602        &mut self,
2603        position: Point,
2604        taps_unfocusable: bool,
2605        ops: &mut dyn crate::window::WindowOps,
2606    ) {
2607        if taps_unfocusable {
2608            self.commit_pending_edit(&mut *ops);
2609            return;
2610        }
2611        let releasing = self.current_input.pointer;
2612        let pointer = self.current_pointer_id();
2613        let Some(pressed) = self.presses.get(pointer).and_then(|p| p.focusable) else {
2614            return;
2615        };
2616        if !releasing.kind.is_direct() && self.click_may_focus(pressed) {
2617            return;
2618        }
2619        let Some(target) = self.hit_test_for(position, &releasing) else {
2620            return;
2621        };
2622        if self.find_focusable_at_or_above(target) != Some(pressed) {
2623            return;
2624        }
2625        if !self.click_may_focus(pressed) {
2626            // Scroll chrome built this way (a tab strip's scroll arrows)
2627            // scrolls and runs no command: nothing to commit for.
2628            if !self.in_scroll_chrome(target) {
2629                self.commit_pending_edit(&mut *ops);
2630            }
2631        } else {
2632            self.focus_with_origin_ops(
2633                pressed,
2634                crate::focus::FocusOrigin::Pointer(releasing.kind),
2635                &mut *ops,
2636            );
2637        }
2638    }
2639
2640    /// The hover walk for one move: hit-test, run the enter/leave transitions,
2641    /// then deliver `event` to whatever the pointer is now over.
2642    ///
2643    /// `event` is the `PointerMove` being dispatched and is forwarded
2644    /// **verbatim** — its `modifiers` (a Shift or Ctrl pressed mid-drag) and its
2645    /// `pointer` are the producer's own, not a copy assembled here. `position`
2646    /// is that event's position, passed separately only because every step below
2647    /// needs it.
2648    ///
2649    /// Which is the rule for the whole router: a producer's event reaches the
2650    /// widget carrying what the producer said, while an event the *tree*
2651    /// synthesizes — the enter/leave pair below — carries the tree's own view of
2652    /// who is pointing, read from the pointer table. The two differ only for a
2653    /// hand-built legacy event, which names no time and is stamped from the tree
2654    /// clock into the snapshot [`EventContext::pointer`] reports (see
2655    /// [`dispatch_event_with_ops`](Self::dispatch_event_with_ops)) without that
2656    /// stamp being written back onto the event.
2657    ///
2658    /// [`EventContext::pointer`]: crate::widget::EventContext::pointer
2659    fn handle_pointer_move(
2660        &mut self,
2661        event: &WidgetEvent,
2662        position: Point,
2663        ops: &mut dyn crate::window::WindowOps,
2664    ) {
2665        // A contact routes its move by hit test but writes **no hover**: a
2666        // finger has no hover state, so a second finger arriving beside a
2667        // hovering mouse must leave enter/leave, the cursor, tooltip dwell and
2668        // every `on_hover` handler exactly where they were.
2669        let moving = self.current_input.pointer;
2670        if self.pointers.hover_owner_id() != Some(self.current_pointer_id()) {
2671            if let Some(target) = self.hit_test_for(position, &moving) {
2672                self.dispatch_to_widget(target, event, &mut *ops);
2673            }
2674            return;
2675        }
2676        let target = self.hit_test_for(position, &moving);
2677
2678        if target != self.hovered_id() {
2679            // Past the gate above, so the mover *is* the hover owner.
2680            let hover = self.hover_transition_pointer();
2681            let previously_hovered = self.hovered_id();
2682            if let Some(old) = previously_hovered {
2683                self.dispatch_to_widget(
2684                    old,
2685                    &WidgetEvent::PointerLeave { pointer: hover },
2686                    &mut *ops,
2687                );
2688                self.tooltip_pointer_leave(old, &mut *ops);
2689            }
2690            if let Some(new) = target {
2691                self.dispatch_to_widget(
2692                    new,
2693                    &WidgetEvent::PointerEnter { pointer: hover },
2694                    &mut *ops,
2695                );
2696                self.tooltip_pointer_enter(new);
2697            }
2698            // A pointer that goes to another widget, or to nothing, has left
2699            // the one the press answered for: its next hover is a new one.
2700            self.tooltip_dwell_held = false;
2701            self.set_hovered(target);
2702            self.update_hover_within_signals(previously_hovered, target);
2703        } else if let Some(target) = target {
2704            // Same hover target — restart pending tooltip timers if the
2705            // pointer is still moving beyond the stationary slop.
2706            self.tooltip_pointer_moved(target, position);
2707        }
2708
2709        if let Some(target) = target {
2710            self.dispatch_to_widget(target, event, &mut *ops);
2711        }
2712    }
2713
2714    pub(super) fn dispatch_to_widget(
2715        &mut self,
2716        target: WidgetId,
2717        event: &WidgetEvent,
2718        ops: &mut dyn crate::window::WindowOps,
2719    ) {
2720        self.dispatch_to_widget_returning_handled(target, event, ops);
2721    }
2722
2723    /// Rebuild `event` with any pointer position converted into `id`'s
2724    /// **widget-local** space. Returns `None` for events that carry no
2725    /// position, so the caller keeps the original event.
2726    ///
2727    /// This is the single point where the framework localizes pointer
2728    /// coordinates. It runs once per node in both the preview and bubble
2729    /// passes, and because both `on_pointer_event` and the gesture arena
2730    /// read the position out of `event`, localizing it here makes
2731    /// `on_tap` / `on_double_tap` / `on_long_press` / `on_drag` and
2732    /// `on_pointer_event` all receive widget-local coordinates uniformly.
2733    /// See [`WidgetArena::local_pointer_position`]. `frame` is the node's,
2734    /// from [`WidgetArena::local_frame`], computed once per node so every
2735    /// position in the event is converted by the same walk — and the same
2736    /// frame is handed to the node's handlers as
2737    /// [`EventContext::to_local`](crate::widget::EventContext::to_local).
2738    pub(super) fn localize_event(
2739        frame: &crate::arena::LocalFrame,
2740        event: &WidgetEvent,
2741    ) -> Option<WidgetEvent> {
2742        match event {
2743            WidgetEvent::PointerDown {
2744                position,
2745                button,
2746                modifiers,
2747                pointer,
2748            } => Some(WidgetEvent::PointerDown {
2749                position: frame.to_local(*position),
2750                button: *button,
2751                modifiers: *modifiers,
2752                pointer: *pointer,
2753            }),
2754            WidgetEvent::PointerUp {
2755                position,
2756                button,
2757                modifiers,
2758                pointer,
2759            } => Some(WidgetEvent::PointerUp {
2760                position: frame.to_local(*position),
2761                button: *button,
2762                modifiers: *modifiers,
2763                pointer: *pointer,
2764            }),
2765            WidgetEvent::PointerMove {
2766                position,
2767                modifiers,
2768                pointer,
2769            } => Some(WidgetEvent::PointerMove {
2770                position: frame.to_local(*position),
2771                modifiers: *modifiers,
2772                pointer: *pointer,
2773            }),
2774            WidgetEvent::Gesture { gesture } => Some(WidgetEvent::Gesture {
2775                gesture: Self::localize_gesture(frame, gesture),
2776            }),
2777            // Deliberately no `Scroll` or `PointerCancel` arm. Both name their
2778            // position `window_position` precisely because it stays in window
2779            // space — see the fields' own docs for why routing and velocity
2780            // need it there — and adding an arm here would silently change the
2781            // frame every reader of those two fields works in.
2782            _ => None,
2783        }
2784    }
2785
2786    /// Convert every position / center field of a pre-recognized
2787    /// [`GestureEvent`] into `id`'s widget-local space (`DragMoved.delta`
2788    /// is relative and left untouched). Used for the platform gesture
2789    /// path; arena-recognized gestures are already local because the
2790    /// `RawPointerEvent` feeding the arena was localized by
2791    /// [`Self::localize_event`].
2792    fn localize_gesture(frame: &crate::arena::LocalFrame, gesture: &GestureEvent) -> GestureEvent {
2793        let loc = |p: teksilo_canvas::Point| frame.to_local(p);
2794        let tap = |t: &TapEvent| {
2795            TapEvent::new(loc(t.position), t.button, t.modifiers).with_pointer(t.pointer)
2796        };
2797        match gesture {
2798            GestureEvent::Tap(t) => GestureEvent::Tap(tap(t)),
2799            GestureEvent::DoubleTap(t) => GestureEvent::DoubleTap(tap(t)),
2800            GestureEvent::TripleTap(t) => GestureEvent::TripleTap(tap(t)),
2801            GestureEvent::LongPress(t) => GestureEvent::LongPress(tap(t)),
2802            GestureEvent::DragStarted {
2803                position,
2804                button,
2805                pointer,
2806            } => GestureEvent::DragStarted {
2807                position: loc(*position),
2808                button: *button,
2809                pointer: *pointer,
2810            },
2811            GestureEvent::DragMoved {
2812                position,
2813                delta,
2814                pointer,
2815            } => GestureEvent::DragMoved {
2816                position: loc(*position),
2817                delta: *delta,
2818                pointer: *pointer,
2819            },
2820            GestureEvent::DragEnded { position, pointer } => GestureEvent::DragEnded {
2821                position: loc(*position),
2822                pointer: *pointer,
2823            },
2824            GestureEvent::DragCancelled {
2825                position,
2826                pointer,
2827                reason,
2828            } => GestureEvent::DragCancelled {
2829                position: loc(*position),
2830                pointer: *pointer,
2831                reason: *reason,
2832            },
2833            GestureEvent::PinchStarted { center } => GestureEvent::PinchStarted {
2834                center: loc(*center),
2835            },
2836            GestureEvent::PinchChanged {
2837                center,
2838                scale,
2839                rotation,
2840            } => GestureEvent::PinchChanged {
2841                center: loc(*center),
2842                scale: *scale,
2843                rotation: *rotation,
2844            },
2845            GestureEvent::PinchEnded => GestureEvent::PinchEnded,
2846            GestureEvent::PinchCancelled { reason } => {
2847                GestureEvent::PinchCancelled { reason: *reason }
2848            }
2849            GestureEvent::Swipe {
2850                direction,
2851                velocity,
2852            } => GestureEvent::Swipe {
2853                direction: *direction,
2854                velocity: *velocity,
2855            },
2856        }
2857    }
2858
2859    /// Same as `dispatch_to_widget` but returns `true` when any
2860    /// preview or bubble handler consumed the event. Used for keyboard
2861    /// events the framework wants to consume by default (Tab focus
2862    /// navigation): callers can dispatch first, then fall back to
2863    /// built-in behavior only when no widget claimed it.
2864    pub(super) fn dispatch_to_widget_returning_handled(
2865        &mut self,
2866        target: WidgetId,
2867        event: &WidgetEvent,
2868        ops: &mut dyn crate::window::WindowOps,
2869    ) -> bool {
2870        // A disabled widget takes no input, but `FocusLost` is not input: it
2871        // tells a widget that held focus that it no longer does, and one that
2872        // loses focus because it was disabled must still hear it (Qt sends
2873        // FocusOut to the widget it disables), or a button keeps its focused
2874        // look and a text field never commits its edit.
2875        if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
2876            return false;
2877        }
2878
2879        let mut ancestors = Vec::new();
2880        let mut current = self.arena.parent(target);
2881        while let Some(id) = current {
2882            ancestors.push(id);
2883            current = self.arena.parent(id);
2884        }
2885        ancestors.reverse();
2886
2887        // For a pointer press, find the innermost tap-owning node at-or-above
2888        // the hit target (a chevron / checkbox / inline button). A row or
2889        // container that selects on press consults
2890        // `ctx.press_claimed_by_interactive_child()` to skip selecting when this
2891        // owner is a strict descendant of it — the press belongs to the inner
2892        // control, not the row. Tap-like handlers only; drag/swipe are excluded
2893        // so a draggable row still selects itself on press.
2894        //
2895        // **`on_tap` / `on_long_press` only — never `on_double_tap` alone.**
2896        // The question this answers is "does a descendant own *this press*",
2897        // and a widget that wired only a multi-tap handler does not: the first
2898        // click of a double-click is not its business. Counting it meant a
2899        // table cell could not carry double-click-to-edit without also
2900        // silently stopping its row from selecting on a plain click — while
2901        // every file manager selects a row on the first click of the
2902        // double-click that opens it. A node that wants the press still has
2903        // `on_tap` (a real `Button`, a checkbox), and those are unaffected.
2904        let tap_owner: Option<WidgetId> = if matches!(
2905            event,
2906            WidgetEvent::PointerDown { .. } | WidgetEvent::PointerUp { .. }
2907        ) {
2908            let mut owner = None;
2909            let mut cur = Some(target);
2910            while let Some(id) = cur {
2911                if self.arena.get(id).is_some_and(|n| {
2912                    n.any_handler(|h| h.on_tap.is_some() || h.on_long_press.is_some())
2913                }) {
2914                    owner = Some(id);
2915                    break;
2916                }
2917                cur = self.arena.parent(id);
2918            }
2919            owner
2920        } else {
2921            None
2922        };
2923
2924        for &id in &ancestors {
2925            let frame = self.arena.local_frame(id);
2926            let mut ctx = self
2927                .make_event_context(&mut *ops)
2928                .with_dispatch_node(id)
2929                .with_dispatch_target(target)
2930                .with_local_frame(frame);
2931            ctx.press_claimed_by_interactive_child =
2932                tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2933            // Convert any pointer position into this node's widget-local
2934            // space before its handlers see it (see `localize_event`).
2935            let localized = Self::localize_event(&frame, event);
2936            let event = localized.as_ref().unwrap_or(event);
2937            let response = if let Some(node) = self.arena.get_mut(id) {
2938                Self::try_handler_preview(node, event, &mut ctx).unwrap_or(EventResponse::Ignored)
2939            } else {
2940                EventResponse::Ignored
2941            };
2942            self.collect_from_ctx(ctx, id);
2943            if response == EventResponse::Handled {
2944                self.arena.mark_needs_paint(id);
2945                // Step 1 of the decision procedure: the raw-preview pass runs
2946                // FIRST and keeps its root-first order, and the first `Handled`
2947                // claims the press. Deliberately not folded into the
2948                // innermost-first member order — `rich_text/mouse.rs` documents
2949                // relying on an outer wrapper seeing a press before an inner
2950                // one, and reordering it would silently invert a precedence
2951                // real widgets depend on.
2952                if matches!(event, WidgetEvent::PointerDown { .. }) {
2953                    self.note_preview_claim(id);
2954                }
2955                return true;
2956            }
2957        }
2958
2959        let needs_layout_on_handle = matches!(
2960            event,
2961            WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
2962        );
2963        let mut current = Some(target);
2964        let mut is_target = true;
2965        while let Some(id) = current {
2966            let frame = self.arena.local_frame(id);
2967            let mut ctx = self
2968                .make_event_context(&mut *ops)
2969                .with_dispatch_node(id)
2970                .with_dispatch_target(target)
2971                .with_local_frame(frame);
2972            ctx.press_claimed_by_interactive_child =
2973                tap_owner.is_some_and(|owner| owner != id && self.is_descendant_of(owner, id));
2974            // Convert any pointer position into this node's widget-local
2975            // space before its handlers (and its gesture arena) see it.
2976            let localized = Self::localize_event(&frame, event);
2977            let gesture_cx = self.recognizer_context(id);
2978            // A member that lost the arbitration keeps its handlers and loses
2979            // only its recognizers — see `sequence_blocks_arena`.
2980            let arena_blocked = self.sequence_blocks_arena(id);
2981            let WidgetTree {
2982                arena,
2983                gesture_owners,
2984                ..
2985            } = self;
2986            let event = localized.as_ref().unwrap_or(event);
2987            let response = if let Some(node) = arena.get_mut(id) {
2988                Self::try_handler_bubble(
2989                    node,
2990                    event,
2991                    &mut ctx,
2992                    BubbleGates {
2993                        fire_on_pointer_event: is_target,
2994                        arena_blocked,
2995                    },
2996                    id,
2997                    gesture_owners,
2998                    gesture_cx,
2999                )
3000                .unwrap_or(EventResponse::Ignored)
3001            } else {
3002                EventResponse::Ignored
3003            };
3004            self.collect_from_ctx(ctx, id);
3005            if response == EventResponse::Handled {
3006                if needs_layout_on_handle {
3007                    self.arena.mark_needs_layout(id);
3008                } else {
3009                    self.arena.mark_needs_paint(id);
3010                }
3011                self.note_pointer_acceptance(id, event);
3012                // **Hover transitions are notifications, and every ancestor is
3013                // entitled to one.** Stopping the bubble here left a container
3014                // stuck hovered whenever the pointer left it *through* an
3015                // interactive child: the child's own `on_hover` handled the
3016                // `PointerLeave`, the bubble stopped, and the row went on believing
3017                // the pointer was still over it. A search result whose controls
3018                // appear on hover then kept them after the pointer had gone.
3019                //
3020                // The preview pass already refuses to let an ancestor swallow a
3021                // descendant's Enter/Leave; this is that rule in the other
3022                // direction, and it is what makes a container's hover mean "the
3023                // pointer is somewhere inside me" rather than "the pointer is on my
3024                // own background". Every other event still stops at its handler,
3025                // which is what makes handling one mean anything.
3026                if !matches!(
3027                    event,
3028                    WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. }
3029                ) {
3030                    return true;
3031                }
3032            }
3033            is_target = false;
3034            current = self.arena.parent(id);
3035        }
3036        false
3037    }
3038
3039    pub(super) fn dispatch_to_widget_direct(
3040        &mut self,
3041        target: WidgetId,
3042        event: &WidgetEvent,
3043        ops: &mut dyn crate::window::WindowOps,
3044    ) {
3045        self.dispatch_to_widget_direct_returning_handled(target, event, ops);
3046    }
3047
3048    /// [`dispatch_to_widget_direct`](Self::dispatch_to_widget_direct), reporting
3049    /// whether the node consumed the event.
3050    ///
3051    /// The claimant chain needs the answer: `Handled` means the container
3052    /// absorbed some of the delta and the walk stops, `Ignored` means it is at
3053    /// a boundary and the same whole event goes to the next container outward.
3054    /// Addressed rather than bubbled, which is exactly what a claimant chain is
3055    /// — a list of named recipients, like the one the cancel funnel delivers
3056    /// to.
3057    pub(super) fn dispatch_to_widget_direct_returning_handled(
3058        &mut self,
3059        target: WidgetId,
3060        event: &WidgetEvent,
3061        ops: &mut dyn crate::window::WindowOps,
3062    ) -> bool {
3063        // `FocusLost` reaches a disabled widget too: see
3064        // `dispatch_to_widget_returning_handled`.
3065        if !self.arena.is_enabled(target) && !matches!(event, WidgetEvent::FocusLost) {
3066            return false;
3067        }
3068
3069        let frame = self.arena.local_frame(target);
3070        let mut ctx = self
3071            .make_event_context(&mut *ops)
3072            .with_dispatch_node(target)
3073            .with_dispatch_target(target)
3074            .with_local_frame(frame);
3075        let gesture_cx = self.recognizer_context(target);
3076        let arena_blocked = self.sequence_blocks_arena(target);
3077        let WidgetTree {
3078            arena,
3079            gesture_owners,
3080            ..
3081        } = self;
3082        let response = if let Some(node) = arena.get_mut(target) {
3083            Self::try_handler_bubble(
3084                node,
3085                event,
3086                &mut ctx,
3087                BubbleGates {
3088                    fire_on_pointer_event: true,
3089                    arena_blocked,
3090                },
3091                target,
3092                gesture_owners,
3093                gesture_cx,
3094            )
3095            .unwrap_or(EventResponse::Ignored)
3096        } else {
3097            EventResponse::Ignored
3098        };
3099        self.collect_from_ctx(ctx, target);
3100
3101        if response == EventResponse::Handled {
3102            // A scroll changes geometry, so it earns a layout pass rather than
3103            // a repaint — the same distinction the bubble path makes.
3104            if matches!(
3105                event,
3106                WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. }
3107            ) {
3108                self.arena.mark_needs_layout(target);
3109            } else {
3110                self.arena.mark_needs_paint(target);
3111            }
3112            self.note_pointer_acceptance(target, event);
3113        }
3114        response == EventResponse::Handled
3115    }
3116
3117    /// Remember that `target` answered `Handled` to one of the current
3118    /// pointer's positional events.
3119    ///
3120    /// Read only by the cancel funnel, as the recipient of last resort when a
3121    /// revoked pointer holds no capture. Restricted to the three positional
3122    /// phases: a key, an accessibility action or a focus change is not "an
3123    /// event from this pointer", and letting one of those set the anchor would
3124    /// address the cancel to a widget the pointer never touched.
3125    pub(super) fn note_pointer_acceptance(&mut self, target: WidgetId, event: &WidgetEvent) {
3126        if !matches!(
3127            event,
3128            WidgetEvent::PointerDown { .. }
3129                | WidgetEvent::PointerMove { .. }
3130                | WidgetEvent::PointerUp { .. }
3131        ) {
3132            return;
3133        }
3134        let pointer = self.current_pointer_id();
3135        if let Some(entry) = self.pointers.get_mut(pointer) {
3136            entry.last_accepted = Some(target);
3137        }
3138    }
3139
3140    fn try_handler_preview(
3141        node: &mut crate::arena::WidgetNode,
3142        event: &WidgetEvent,
3143        ctx: &mut EventContext,
3144    ) -> Option<EventResponse> {
3145        match event {
3146            // Key + IME events fire `on_key_preview` on each strict
3147            // ancestor of the focused widget (root → parent-of-target).
3148            // Mirrors how `on_pointer_event` previews on the pointer
3149            // side; the focused widget itself does NOT see its own
3150            // `on_key_preview` (the dispatch loop builds an ancestors
3151            // list that excludes the target, so this is enforced by
3152            // the caller, not here).
3153            WidgetEvent::KeyDown { .. }
3154            | WidgetEvent::KeyUp { .. }
3155            | WidgetEvent::ImeComposition { .. }
3156            | WidgetEvent::ImeCommit { .. } => {
3157                let has = node.external_handlers.on_key_preview.is_some()
3158                    || node.handlers.on_key_preview.is_some();
3159                if !has {
3160                    return None;
3161                }
3162                Some(fire_event_handler_both(
3163                    &mut node.external_handlers.on_key_preview,
3164                    &mut node.handlers.on_key_preview,
3165                    event,
3166                    ctx,
3167                ))
3168            }
3169            // `PointerEnter` / `PointerLeave` are per-node hover transitions
3170            // synthesized by `handle_pointer_move`, not part of the raw pointer
3171            // stream. Running them through the ancestor preview pass would let
3172            // a drag-detecting ancestor whose `on_pointer_event` returns
3173            // `Handled` silently swallow a descendant's hover (its cursor and
3174            // `on_hover` would never fire). They are delivered to their target
3175            // directly via the bubble pass (where Enter/Leave fire `on_hover`),
3176            // so they have no business in preview. `PointerMove`/`Down`/`Up`
3177            // and `Scroll` still preview through the catch-all below — the
3178            // tab-bar wheel-remap (`tab_widget/bar.rs`) and the split-view /
3179            // rich-text drag guards depend on that.
3180            WidgetEvent::PointerEnter { .. } | WidgetEvent::PointerLeave { .. } => None,
3181            _ => {
3182                let has = node.external_handlers.on_pointer_event.is_some()
3183                    || node.handlers.on_pointer_event.is_some();
3184                if !has {
3185                    return None;
3186                }
3187                Some(fire_event_handler_both(
3188                    &mut node.external_handlers.on_pointer_event,
3189                    &mut node.handlers.on_pointer_event,
3190                    event,
3191                    ctx,
3192                ))
3193            }
3194        }
3195    }
3196
3197    fn try_handler_bubble(
3198        node: &mut crate::arena::WidgetNode,
3199        event: &WidgetEvent,
3200        ctx: &mut EventContext,
3201        gates: BubbleGates,
3202        node_id: WidgetId,
3203        gesture_owners: &mut std::collections::HashSet<WidgetId>,
3204        gesture_cx: crate::gesture::RecognizerContext<'_>,
3205    ) -> Option<EventResponse> {
3206        let BubbleGates {
3207            fire_on_pointer_event,
3208            arena_blocked,
3209        } = gates;
3210        match event {
3211            WidgetEvent::PointerEnter { .. } => {
3212                if let Some(cursor) = node.node_cursor {
3213                    // The declared channel, not `set_cursor`: a handler
3214                    // overriding the cursor in the same dispatch must not
3215                    // erase the tree's record of what the node asked for —
3216                    // that record is what `release_cursor` hands back to.
3217                    ctx.declared_cursor_request = Some(cursor);
3218                }
3219                let mut fired = false;
3220                if let Some(h) = node.external_handlers.on_hover.as_mut() {
3221                    h(true, ctx);
3222                    fired = true;
3223                }
3224                if let Some(h) = node.handlers.on_hover.as_mut() {
3225                    h(true, ctx);
3226                    fired = true;
3227                }
3228                if fired {
3229                    Some(EventResponse::Handled)
3230                } else {
3231                    node.node_cursor.map(|_| EventResponse::Handled)
3232                }
3233            }
3234            WidgetEvent::PointerLeave { .. } => {
3235                if node.node_cursor.is_some() {
3236                    ctx.declared_cursor_request = Some(crate::widget::CursorIcon::Default);
3237                }
3238                let mut fired = false;
3239                if let Some(h) = node.external_handlers.on_hover.as_mut() {
3240                    h(false, ctx);
3241                    fired = true;
3242                }
3243                if let Some(h) = node.handlers.on_hover.as_mut() {
3244                    h(false, ctx);
3245                    fired = true;
3246                }
3247                if fired {
3248                    Some(EventResponse::Handled)
3249                } else {
3250                    node.node_cursor.map(|_| EventResponse::Handled)
3251                }
3252            }
3253            WidgetEvent::FocusGained { origin } => {
3254                // The handler signature is a bare `bool`; the origin rides on
3255                // the context so a widget can tell a rebuild's restore from a
3256                // real arrival (`EventContext::focus_origin`).
3257                let previous_origin = ctx.focus_origin.replace(*origin);
3258                let mut fired = false;
3259                if let Some(h) = node.external_handlers.on_focus.as_mut() {
3260                    h(true, ctx);
3261                    fired = true;
3262                }
3263                if let Some(h) = node.handlers.on_focus.as_mut() {
3264                    h(true, ctx);
3265                    fired = true;
3266                }
3267                ctx.focus_origin = previous_origin;
3268                fired.then_some(EventResponse::Handled)
3269            }
3270            WidgetEvent::FocusLost => {
3271                let mut fired = false;
3272                if let Some(h) = node.external_handlers.on_focus.as_mut() {
3273                    h(false, ctx);
3274                    fired = true;
3275                }
3276                if let Some(h) = node.handlers.on_focus.as_mut() {
3277                    h(false, ctx);
3278                    fired = true;
3279                }
3280                fired.then_some(EventResponse::Handled)
3281            }
3282            WidgetEvent::KeyDown { .. }
3283            | WidgetEvent::KeyUp { .. }
3284            | WidgetEvent::ImeComposition { .. }
3285            | WidgetEvent::ImeCommit { .. } => {
3286                if node.external_handlers.on_key.is_some() || node.handlers.on_key.is_some() {
3287                    Some(fire_event_handler_both(
3288                        &mut node.external_handlers.on_key,
3289                        &mut node.handlers.on_key,
3290                        event,
3291                        ctx,
3292                    ))
3293                } else {
3294                    None
3295                }
3296            }
3297            WidgetEvent::Scroll { .. } | WidgetEvent::ScrollIntoView { .. } => {
3298                if node.external_handlers.on_scroll.is_some() || node.handlers.on_scroll.is_some() {
3299                    Some(fire_event_handler_both(
3300                        &mut node.external_handlers.on_scroll,
3301                        &mut node.handlers.on_scroll,
3302                        event,
3303                        ctx,
3304                    ))
3305                } else {
3306                    None
3307                }
3308            }
3309            WidgetEvent::AccessAction {
3310                action,
3311                target_node,
3312                data,
3313                ..
3314            } => {
3315                // Every installed slot fires — both payload shapes, and
3316                // within each shape both the external (app-installed
3317                // `.on_access_action*`) and the widget's own. Button (own)
3318                // and Dialog (external) layered together rely on that for a
3319                // single accesskit click.
3320                //
3321                // The two shapes are layered, not alternatives, because they
3322                // have different owners: `on_access_action_request` is what a
3323                // widget reaches for when it needs `target_node` or `data`
3324                // (`Slider`, `SpinBox`, `TextInputField`, `CodeEditor`,
3325                // `TabBar`), while `.on_access_action(..)` is the app's
3326                // builder-level hook. Preferring the payload shape when it was
3327                // set therefore did not choose between two handlers for the
3328                // same job — it silently disabled the app's handler on exactly
3329                // the widgets that had migrated, with nothing at the call site
3330                // to say so.
3331                //
3332                // Assistive-tech action paths run under the `Accessibility`
3333                // source label. Restored after the block.
3334                let saved_a11y_source = ctx
3335                    .current_source
3336                    .replace(crate::telemetry::IntentSource::Accessibility);
3337                let mut any_slot = false;
3338                let mut any_handled = false;
3339                if let Some(h) = node.external_handlers.on_access_action_request.as_mut() {
3340                    any_slot = true;
3341                    any_handled |=
3342                        h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3343                }
3344                if let Some(h) = node.handlers.on_access_action_request.as_mut() {
3345                    any_slot = true;
3346                    any_handled |=
3347                        h(*action, *target_node, data.clone(), ctx) == EventResponse::Handled;
3348                }
3349                if let Some(h) = node.external_handlers.on_access_action.as_mut() {
3350                    any_slot = true;
3351                    any_handled |= h(*action, ctx) == EventResponse::Handled;
3352                }
3353                if let Some(h) = node.handlers.on_access_action.as_mut() {
3354                    any_slot = true;
3355                    any_handled |= h(*action, ctx) == EventResponse::Handled;
3356                }
3357                let user_handled = any_slot.then_some(if any_handled {
3358                    EventResponse::Handled
3359                } else {
3360                    EventResponse::Ignored
3361                });
3362
3363                // Builder-level access_action / access_custom_action
3364                // callbacks. These layer on top of any user-installed
3365                // on_access_action / on_access_action_request — both
3366                // fire for the same dispatched event. Drives the
3367                // SwiftUI `.accessibilityAction(...)` parity.
3368                let mut override_handled = false;
3369                if let Some(ov) = node.access_overrides.as_deref_mut() {
3370                    if matches!(action, accesskit::Action::CustomAction) {
3371                        if let Some(accesskit::ActionData::CustomAction(idx)) = data
3372                            && let Some((_, cb)) = ov.custom_actions.get_mut(*idx as usize)
3373                        {
3374                            cb(ctx);
3375                            override_handled = true;
3376                        }
3377                    } else {
3378                        for (a, cb) in ov.actions.iter_mut() {
3379                            if *a == *action {
3380                                cb(ctx);
3381                                override_handled = true;
3382                            }
3383                        }
3384                    }
3385                }
3386
3387                ctx.current_source = saved_a11y_source;
3388                match (user_handled, override_handled) {
3389                    (Some(EventResponse::Handled), _) | (_, true) => Some(EventResponse::Handled),
3390                    (Some(EventResponse::Ignored), false) => Some(EventResponse::Ignored),
3391                    (None, false) => None,
3392                }
3393            }
3394            WidgetEvent::Gesture { gesture } => {
3395                // Pre-recognized gestures from the platform (OS trackpad
3396                // pinch/rotation, double-tap, …) bypass the gesture arena
3397                // and go straight to the matching handler. See §10.
3398                let matched = matches!(
3399                    gesture,
3400                    GestureEvent::PinchStarted { .. }
3401                        | GestureEvent::PinchChanged { .. }
3402                        | GestureEvent::PinchEnded
3403                        | GestureEvent::Swipe { .. }
3404                        | GestureEvent::DoubleTap { .. }
3405                        | GestureEvent::TripleTap { .. }
3406                ) && {
3407                    let has_handler = match gesture {
3408                        GestureEvent::PinchStarted { .. }
3409                        | GestureEvent::PinchChanged { .. }
3410                        | GestureEvent::PinchEnded => node.any_handler(|h| h.on_pinch.is_some()),
3411                        GestureEvent::Swipe { .. } => node.any_handler(|h| h.on_swipe.is_some()),
3412                        GestureEvent::DoubleTap { .. } => {
3413                            node.any_handler(|h| h.on_double_tap.is_some())
3414                        }
3415                        GestureEvent::TripleTap { .. } => {
3416                            node.any_handler(|h| h.on_triple_tap.is_some())
3417                        }
3418                        _ => false,
3419                    };
3420                    if has_handler {
3421                        Self::dispatch_recognized_gesture(node, *gesture, ctx);
3422                    }
3423                    has_handler
3424                };
3425                if matched {
3426                    Some(EventResponse::Handled)
3427                } else {
3428                    None
3429                }
3430            }
3431            WidgetEvent::PointerDown {
3432                position,
3433                button,
3434                modifiers,
3435                ..
3436            } => {
3437                // Raw pointer handler runs first so widgets can intercept
3438                // events that the gesture recognizers won't catch (e.g.
3439                // right-click → context menu). If it returns Handled the
3440                // gesture arena is skipped; otherwise we fall through.
3441                // Only fire for the target — ancestors already fired
3442                // on_pointer_event during the preview pass.
3443                if fire_on_pointer_event {
3444                    let r = fire_event_handler_both(
3445                        &mut node.external_handlers.on_pointer_event,
3446                        &mut node.handlers.on_pointer_event,
3447                        event,
3448                        ctx,
3449                    );
3450                    if r == EventResponse::Handled {
3451                        return Some(EventResponse::Handled);
3452                    }
3453                }
3454                if arena_blocked {
3455                    // This node lost the arbitration for the press: its
3456                    // recognizers stay out of it, and the event goes on
3457                    // bubbling as if the node carried none.
3458                    return None;
3459                }
3460                Self::ensure_gesture_arena(node, node_id, gesture_owners);
3461                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3462                    let cx = gesture_cx;
3463                    // Implicit capture for the Down..Up sequence so that
3464                    // moves leaving the widget bounds still reach the
3465                    // arena. Without this, a drag that starts inside the
3466                    // widget but crosses its edge before the recognizer
3467                    // latches would be hit-tested to another widget and
3468                    // the press-origin arena would never see a `Move`.
3469                    // Released unconditionally by the `PointerUp` branch
3470                    // in `dispatch_event`.
3471                    //
3472                    // **Implicit**: this is plumbing, not a claim. Routing it
3473                    // through the public `capture_pointer` would enrol every
3474                    // arena-bearing node as a `RawDrag` competitor and decide
3475                    // every mouse sequence at press. See
3476                    // `EventContext::capture_pointer_implicit`.
3477                    ctx.capture_pointer_implicit();
3478                    let result = arena.process(
3479                        &RawPointerEvent::Down {
3480                            position: *position,
3481                            button: *button,
3482                            modifiers: *modifiers,
3483                            pointer: cx.pointer,
3484                            time: cx.now,
3485                        },
3486                        &cx,
3487                    );
3488                    if let Some(gesture) = result {
3489                        Self::dispatch_recognized_gesture(node, gesture, ctx);
3490                    }
3491                    return Some(EventResponse::Handled);
3492                }
3493                None
3494            }
3495            WidgetEvent::PointerUp {
3496                position,
3497                button,
3498                modifiers,
3499                ..
3500            } => {
3501                if fire_on_pointer_event {
3502                    let r = fire_event_handler_both(
3503                        &mut node.external_handlers.on_pointer_event,
3504                        &mut node.handlers.on_pointer_event,
3505                        event,
3506                        ctx,
3507                    );
3508                    if r == EventResponse::Handled {
3509                        return Some(EventResponse::Handled);
3510                    }
3511                }
3512                if arena_blocked {
3513                    return None;
3514                }
3515                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3516                    let cx = gesture_cx;
3517                    let result = arena.process(
3518                        &RawPointerEvent::Up {
3519                            position: *position,
3520                            button: *button,
3521                            modifiers: *modifiers,
3522                            pointer: cx.pointer,
3523                            time: cx.now,
3524                        },
3525                        &cx,
3526                    );
3527                    if let Some(gesture) = result {
3528                        Self::dispatch_recognized_gesture(node, gesture, ctx);
3529                    }
3530                    return Some(EventResponse::Handled);
3531                }
3532                None
3533            }
3534            WidgetEvent::PointerMove { position, .. } => {
3535                if fire_on_pointer_event {
3536                    let r = fire_event_handler_both(
3537                        &mut node.external_handlers.on_pointer_event,
3538                        &mut node.handlers.on_pointer_event,
3539                        event,
3540                        ctx,
3541                    );
3542                    if r == EventResponse::Handled {
3543                        return Some(EventResponse::Handled);
3544                    }
3545                }
3546                if arena_blocked {
3547                    return None;
3548                }
3549                if let Some(arena) = node.handlers.gesture_arena.as_mut() {
3550                    let cx = gesture_cx;
3551                    let result = arena.process(
3552                        &RawPointerEvent::Move {
3553                            position: *position,
3554                            pointer: cx.pointer,
3555                            time: cx.now,
3556                        },
3557                        &cx,
3558                    );
3559                    if let Some(gesture) = result {
3560                        Self::dispatch_recognized_gesture(node, gesture, ctx);
3561                        // A recognized gesture (DragStarted / DragMoved / …)
3562                        // almost always changes visible state — return
3563                        // `Handled` so the bubble loop marks this widget
3564                        // `needs_paint`, which in turn makes
3565                        // `WidgetTree::needs_redraw()` return true and
3566                        // triggers a `request_redraw` for the next frame.
3567                        // Without this, state updates via bound signals are
3568                        // only observed on the *next* layout/render pass,
3569                        // which in turn is never scheduled because
3570                        // `teksilo-app::update_control_flow` only wakes up when
3571                        // `needs_redraw()` is true.
3572                        return Some(EventResponse::Handled);
3573                    }
3574                    return Some(EventResponse::Ignored);
3575                }
3576                None
3577            }
3578            WidgetEvent::PointerCancel {
3579                reason, pointer, ..
3580            } => {
3581                // Two hooks, and the dedicated one always runs. `on_pointer_cancel`
3582                // is a notification, not a route: a widget releasing what its
3583                // press latched has nothing to consume, and letting it report
3584                // `Handled` would make releasing state look like claiming the
3585                // event. The raw `on_pointer_event` hook keeps its ordinary
3586                // consuming semantics for widgets that drive the whole pointer
3587                // stream themselves.
3588                for slot in [
3589                    &mut node.external_handlers.on_pointer_cancel,
3590                    &mut node.handlers.on_pointer_cancel,
3591                ] {
3592                    if let Some(handler) = slot.as_mut() {
3593                        handler(pointer, *reason, ctx);
3594                    }
3595                }
3596                if fire_on_pointer_event {
3597                    let r = fire_event_handler_both(
3598                        &mut node.external_handlers.on_pointer_event,
3599                        &mut node.handlers.on_pointer_event,
3600                        event,
3601                        ctx,
3602                    );
3603                    if r == EventResponse::Handled {
3604                        return Some(EventResponse::Handled);
3605                    }
3606                }
3607                None
3608            }
3609        }
3610    }
3611
3612    pub(super) fn collect_from_ctx<'ops>(
3613        &mut self,
3614        mut ctx: EventContext<'ops>,
3615        source_widget: WidgetId,
3616    ) {
3617        // Take the ops handle out of ctx up front so we can freely
3618        // reborrow it inside the method without fighting the 'ops
3619        // lifetime propagation when other fields of `ctx` are moved.
3620        // When no ops is set (standalone trees / tests), fall back to
3621        // a stack NoopWindowOps.
3622        let local_ops = ctx.window_ops.take();
3623        let mut noop = crate::window::NoopWindowOps;
3624        let ops: &mut dyn crate::window::WindowOps = match local_ops {
3625            Some(o) => o,
3626            None => &mut noop,
3627        };
3628        if ctx.frame_requested {
3629            self.request_frame();
3630        }
3631        // Declared first, handler second — the order the two used to occur in
3632        // when both wrote the same slot, so a handler that speaks during an
3633        // enter still outranks the node it entered.
3634        if let Some(declared) = ctx.declared_cursor_request {
3635            self.node_declared_cursor = declared;
3636            self.current_cursor = declared;
3637        }
3638        match ctx.cursor_request {
3639            Some(crate::widget::CursorRequest::Set(cursor)) => {
3640                self.current_cursor = cursor;
3641            }
3642            // A withdrawal restores the node-declared cursor rather than
3643            // resetting to `Default`: the handler is stepping back, not
3644            // claiming the cursor is nothing.
3645            Some(crate::widget::CursorRequest::Release) => {
3646                self.current_cursor = self.node_declared_cursor;
3647            }
3648            None => {}
3649        }
3650        // Intents queued through `ctx.send_intent` are anchored at
3651        // the originating widget. Programmatic sends default to
3652        // `propagate_when_disabled = true` — there is no shortcut to
3653        // consult, and propagation is the safe, least-surprising
3654        // default.
3655        for intent in ctx.pending_intents {
3656            self.enqueue_intent(source_widget, intent, true);
3657        }
3658        // Key capture: process cancel before arm, matching the
3659        // handler's call order (the handler sets `cancel_key_capture`
3660        // when it calls `ctx.cancel_key_capture()`, and separately
3661        // stores `pending_key_capture` when it calls
3662        // `ctx.begin_key_capture(...)`). If the handler did both,
3663        // arm wins (whichever was called last on the ctx has
3664        // already overwritten the other field's effect via the
3665        // setter logic).
3666        if ctx.cancel_key_capture {
3667            self.cancel_key_capture();
3668        }
3669        if let Some(slot) = ctx.pending_key_capture {
3670            self.key_capture = Some(slot);
3671        }
3672        // Registry mutations queued by settings-UI buttons.
3673        for mutation in ctx.pending_shortcut_mutations {
3674            match mutation {
3675                crate::widget::ShortcutMutation::RebindPrimary { id, keystroke } => {
3676                    self.shortcut_registry.rebind_primary(id, keystroke);
3677                }
3678                crate::widget::ShortcutMutation::RebindSecondary { id, keystroke } => {
3679                    self.shortcut_registry.rebind_secondary(id, keystroke);
3680                }
3681                crate::widget::ShortcutMutation::ClearOverride { id } => {
3682                    self.shortcut_registry.clear_override(&id);
3683                }
3684            }
3685        }
3686        if ctx.close_request_to_modal {
3687            self.answer_close_request_with_modal(&mut *ops);
3688        }
3689        if ctx.close_window_requested {
3690            self.close_window_requested = true;
3691        }
3692        if ctx.force_close_requested {
3693            self.force_close_requested = true;
3694        }
3695        self.pending_modal_requests
3696            .extend(ctx.modal_requests.into_iter().map(|request| {
3697                crate::modal::QueuedModalRequest {
3698                    source_widget,
3699                    request,
3700                }
3701            }));
3702        // `send_intent_from_focus` starts from the widget focused after the
3703        // dismissals below: they give focus back to where the overlay took
3704        // it from (or the fallback chain's choice), so a row that closes its
3705        // popup and sends from focus reaches the editor the user was in. It
3706        // falls back to the handler's widget only when they leave nothing
3707        // focused (nothing was, when the overlay opened). Its walk is noted
3708        // before they close the overlay it may be in, so it still goes on
3709        // from the widget that overlay was attached to; and the modal it
3710        // stopped at, which, once closed, sends it to the content root.
3711        let focus_intents = std::mem::take(&mut ctx.pending_focus_intents);
3712        let (fallback_crossings, fallback_modal) = if focus_intents.is_empty() {
3713            (Vec::new(), None)
3714        } else {
3715            let modal = self.intent_walk(source_widget, &[]).2;
3716            (self.intent_route(source_widget, &[], true).crossings, modal)
3717        };
3718        if ctx.dismiss_modal && !self.dismiss_modal_for_source(source_widget, &mut *ops) {
3719            self.pending_modal_dismissal = true;
3720        }
3721        for callback in ctx.idle_callbacks {
3722            self.idle_queue.push_boxed(callback);
3723        }
3724        match ctx.dismiss_scope {
3725            Some(crate::widget::DismissScope::All) => {
3726                let mark = self.dismissal_mark();
3727                let dismissed = self.overlay_manager.dismiss_all();
3728                self.dormant_dismissed_content(&dismissed, &mut *ops);
3729                self.return_focus_after_dismissal(None, mark, &mut *ops);
3730            }
3731            Some(crate::widget::DismissScope::AllExceptHosts) => {
3732                self.dismiss_all_overlays_except_hosts(&mut *ops);
3733            }
3734            Some(crate::widget::DismissScope::SelfChain) => {
3735                self.dismiss_self_overlay_chain_for_source(source_widget, &mut *ops);
3736            }
3737            Some(crate::widget::DismissScope::Top) => {
3738                let mark = self.dismissal_mark();
3739                if let Some((_id, content_ids, focus_restore)) = self.overlay_manager.dismiss_top()
3740                {
3741                    self.dormant_dismissed_content(&content_ids, &mut *ops);
3742                    self.return_focus_after_dismissal(focus_restore, mark, &mut *ops);
3743                }
3744            }
3745            None => {
3746                for id in ctx.overlay_dismissals {
3747                    let mark = self.dismissal_mark();
3748                    let dismissed = self.overlay_manager.dismiss(id);
3749                    self.dormant_dismissed_content(&dismissed, &mut *ops);
3750                    self.return_focus_after_dismissal(None, mark, &mut *ops);
3751                }
3752            }
3753        }
3754        // Content-keyed dismissals (`dismiss_overlay_by_content`). Drained
3755        // unconditionally — independent of `dismiss_scope` and of the
3756        // pending delayed-overlay list — so a handler can retract a shown
3757        // reusable overlay it identifies only by content. Resolving the
3758        // id here (not at call time) is what lets the caller skip
3759        // tracking the `OverlayId`.
3760        for content_id in ctx.overlay_content_dismissals {
3761            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3762                let mark = self.dismissal_mark();
3763                let dismissed = self.overlay_manager.dismiss(overlay_id);
3764                self.dormant_dismissed_content(&dismissed, &mut *ops);
3765                self.return_focus_after_dismissal(None, mark, &mut *ops);
3766            }
3767        }
3768        // Apply pause/resume queue (ToastHost hover-pause). Drained
3769        // here so the handler-side `ctx.pause_overlay_auto_dismiss(id)`
3770        // is order-independent with `dismiss_overlay(id)` and the
3771        // scope-based dismissals: pause/resume on an overlay that
3772        // was concurrently dismissed is silently dropped (the find
3773        // inside the OverlayManager methods misses on the gone id).
3774        for (id, pause) in ctx.overlay_pause_requests {
3775            if pause {
3776                self.overlay_manager.pause_auto_dismiss(id);
3777            } else {
3778                self.overlay_manager.resume_auto_dismiss(id);
3779            }
3780        }
3781        for preserve_content in ctx.dismiss_descendant_overlays {
3782            self.dismiss_child_overlays_for_source(source_widget, preserve_content, &mut *ops);
3783        }
3784        // After every dismissal above, each of which may hand focus back to
3785        // what its overlay was opened over (a modal, a menu closing its
3786        // chain): `send_intent_from_focus` means that widget, not the row in
3787        // the closing menu that still held focus when the handler ran. With
3788        // nothing focused, it is the handler's widget, across the overlays
3789        // it was in; but a modal that has closed was the whole window to its
3790        // rows (a palette opened with nothing focused), so the intent starts
3791        // where one with no source does, at the content root.
3792        if !focus_intents.is_empty() {
3793            let modal_closed = fallback_modal
3794                .is_some_and(|content| self.overlay_manager.find_by_content(content).is_none());
3795            let start = match self.focused.filter(|id| self.arena.is_active(*id)) {
3796                Some(focus) => Some((focus, None)),
3797                None if modal_closed => self.intent_anchor().map(|root| (root, None)),
3798                // A handler outside the modal that is up (an app callback at
3799                // the content root, a native menu item): as if focus were in
3800                // the modal, which is in front of everything else.
3801                None if fallback_modal.is_none()
3802                    && self.overlay_manager.topmost_modal().is_some() =>
3803                {
3804                    self.focus_or_modal().map(|modal| (modal, None))
3805                }
3806                None => Some((source_widget, Some(fallback_crossings))),
3807            };
3808            if let Some((start, crossings)) = start {
3809                for intent in focus_intents {
3810                    match &crossings {
3811                        None => self.enqueue_intent(start, intent, true),
3812                        Some(crossings) => {
3813                            self.enqueue_intent_crossing(start, intent, true, crossings.clone())
3814                        }
3815                    }
3816                }
3817            }
3818        }
3819        let deferred_row_activations =
3820            self.apply_tree_mutations(std::mem::take(&mut ctx.tree_mutations));
3821        if ctx.request_a11y_update {
3822            self.a11y_dirty = true;
3823        }
3824        if let Some(visible) = ctx.soft_keyboard_request.take() {
3825            self.request_soft_keyboard(visible);
3826        }
3827        // Handed to the tree's own live regions, which schedule the two
3828        // accessibility syncs each message needs. See `crate::announcer`.
3829        for (message, politeness) in std::mem::take(&mut ctx.announcements) {
3830            self.announce_with(message, politeness);
3831        }
3832        for mut req in ctx.overlay_requests {
3833            if req.parent_overlay.is_none() {
3834                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3835            }
3836            if self
3837                .overlay_manager
3838                .find_by_content(req.content_id)
3839                .is_some()
3840            {
3841                continue;
3842            }
3843            let current_focus = self.focused;
3844            self.overlay_manager.show(req);
3845            // Overlay show changes the AT tree shape — mirror the
3846            // `WidgetTree::show_overlay` path. The dismissal sibling
3847            // (`dismiss_overlay_with_ops`) already flips this.
3848            self.a11y_dirty = true;
3849            if let Some(focus_id) = current_focus {
3850                self.record_top_focus_restore(focus_id);
3851            }
3852        }
3853        for (mut req, band) in ctx.overlay_band_requests {
3854            if req.parent_overlay.is_none() {
3855                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3856            }
3857            if self
3858                .overlay_manager
3859                .find_by_content(req.content_id)
3860                .is_some()
3861            {
3862                continue;
3863            }
3864            let content_id = req.content_id;
3865            self.overlay_manager.show_in_band(req, band);
3866            self.arena.activate(content_id);
3867            self.a11y_dirty = true;
3868            // Deliberately no `set_top_focus_restore`: the text-affordance band
3869            // never takes focus from the anchor, so there is nothing to give
3870            // back when it goes.
3871        }
3872        // After the shows, so a handler may raise an overlay and place it in
3873        // the same dispatch.
3874        for (content_id, placement) in ctx.overlay_placement_updates {
3875            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3876                self.overlay_manager.update_placement(overlay_id, placement);
3877            }
3878        }
3879        for (content_id, source) in ctx.overlay_placement_sources {
3880            if let Some(overlay_id) = self.overlay_manager.find_by_content(content_id) {
3881                self.overlay_manager
3882                    .set_placement_source(overlay_id, source);
3883            }
3884        }
3885        for (mut req, duration) in ctx.timed_overlay_requests {
3886            if req.parent_overlay.is_none() {
3887                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3888            }
3889            if self
3890                .overlay_manager
3891                .find_by_content(req.content_id)
3892                .is_some()
3893            {
3894                continue;
3895            }
3896            let current_focus = self.focused;
3897            let overlay_id = self.overlay_manager.show_for(req, duration);
3898            self.overlay_manager
3899                .set_shown_at_sim(overlay_id, self.sim_clock);
3900            self.a11y_dirty = true;
3901            if let Some(focus_id) = current_focus {
3902                self.record_top_focus_restore(focus_id);
3903            }
3904        }
3905        for (mut req, progress, duration) in ctx.reveal_overlay_requests {
3906            if req.parent_overlay.is_none() {
3907                req.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3908            }
3909            if self
3910                .overlay_manager
3911                .find_by_content(req.content_id)
3912                .is_some()
3913            {
3914                continue;
3915            }
3916            let content_id = req.content_id;
3917            let current_focus = self.focused;
3918            let overlay_id = self.overlay_manager.show(req);
3919            self.overlay_manager
3920                .set_shown_at_sim(overlay_id, self.sim_clock);
3921            self.a11y_dirty = true;
3922            if let Some(focus_id) = current_focus {
3923                self.record_top_focus_restore(focus_id);
3924            }
3925            // Drive the caller's progress signal 0 → 1, and register it
3926            // as the overlay's fade-state signal so every dismiss path
3927            // tweens it 1 → 0 and defers removal until it completes — the
3928            // same deferral machinery as `with_fade`, minus `set_opacity`
3929            // (the caller owns how `progress` paints).
3930            self.register_animated_signal(&progress, content_id);
3931            let _ = progress.try_animate_with_options(crate::animation::AnimationRequest {
3932                target: 1.0,
3933                duration,
3934                easing: teksilo_tokens::Easing::EaseOut,
3935                frame_interval: None,
3936                looping: false,
3937                epsilon: 0.0,
3938                max_duration: None,
3939            });
3940            self.overlay_manager
3941                .attach_fade(overlay_id, progress, duration);
3942        }
3943        if let Some((pointer, capture)) = ctx.pointer_capture {
3944            // Per pointer, and by default the pointer whose sample the handler
3945            // was serving — so a mouse call site means exactly what it meant
3946            // before, and two contacts on two widgets hold two captures.
3947            let named = pointer;
3948            let pointer = pointer.unwrap_or_else(|| self.current_pointer_id());
3949            self.set_pointer_capture(pointer, capture.then_some(source_widget));
3950            // An explicit `capture_pointer()` from a handler is an arbitration
3951            // act; the arena's and the drag pipeline's own captures are
3952            // plumbing and route through `capture_pointer_implicit`.
3953            if capture && ctx.explicit_capture && named.is_none() {
3954                self.note_explicit_capture(source_widget);
3955            }
3956        }
3957        if let Some(activation) = ctx.drag_activation_override.take() {
3958            // A press handler chose this node's drag activation for this press.
3959            // Onto the sequence, where it dies with the press — see
3960            // `EventContext::set_drag_activation`.
3961            self.note_drag_activation_override(source_widget, activation);
3962        }
3963        if ctx.recognized_owning_gesture {
3964            // A drag or a swipe recognized on this node owns the rest of the
3965            // press, however the recognizer was reached.
3966            self.note_gesture_recognized(source_widget);
3967        }
3968        if !ctx.gesture_acts.is_empty() {
3969            let acts = std::mem::take(&mut ctx.gesture_acts);
3970            self.apply_gesture_acts(&acts, source_widget);
3971        }
3972        if let Some(reason) = ctx.cancel_pointer_request {
3973            let pointer = self.current_pointer_id();
3974            self.cancel_pointer(pointer, reason, &mut *ops);
3975        }
3976        for (mut request, delay, focus_target, replace_siblings) in ctx.delayed_overlay_requests {
3977            if request.parent_overlay.is_none() {
3978                request.parent_overlay = self.overlay_ancestor_for_widget(source_widget);
3979            }
3980            if self
3981                .overlay_manager
3982                .find_by_content(request.content_id)
3983                .is_some()
3984            {
3985                continue;
3986            }
3987            let content_id = request.content_id;
3988            self.pending_delayed_overlays
3989                .retain(|pending| pending.request.content_id != content_id);
3990            self.pending_delayed_overlays.push(PendingDelayedOverlay {
3991                request,
3992                delay,
3993                focus_target,
3994                replace_siblings,
3995                real_requested_at: std::time::Instant::now(),
3996                sim_requested_at: self.sim_clock,
3997            });
3998            self.arena.mark_needs_paint(source_widget);
3999        }
4000        for content_id in ctx.cancel_delayed_overlays {
4001            self.pending_delayed_overlays
4002                .retain(|pending| pending.request.content_id != content_id);
4003        }
4004        // Apex = the last sample that was still over the anchor, which
4005        // for the intended caller (the anchor's own hover-leave) is the
4006        // point the diagonal starts from. Ops are applied per node as
4007        // its handler returns, so this lands before the next widget's
4008        // hover-enter and before the move's own pointer-leave
4009        // bookkeeping.
4010        for content_id in ctx.safe_region_arm_requests {
4011            if let Some(apex) = self
4012                .previous_pointer_position
4013                .or_else(|| self.hover_owner_position())
4014            {
4015                self.overlay_manager.arm_safe_region(
4016                    content_id,
4017                    apex,
4018                    std::time::Instant::now(),
4019                    self.sim_clock,
4020                );
4021            }
4022        }
4023        for id in ctx.repaint_requests {
4024            self.arena.mark_needs_paint(id);
4025        }
4026        for id in ctx.synthetic_clicks {
4027            // Over the caller's ops, never a standalone dispatch: the
4028            // tapped widget's own handler runs inside this nested
4029            // dispatch, so a standalone one would deny it the
4030            // multi-window API this dispatch already has in hand.
4031            self.synthesise_tap_with_ops(id, &mut *ops);
4032        }
4033        // `clear_focus` first: it emptied the two request lists as it was
4034        // queued, so anything still in them was requested after it.
4035        if ctx.clear_focus_requested {
4036            self.clear_focus_ops(&mut *ops);
4037        }
4038        // Both request kinds go through `request_focus_with_ops`, which holds
4039        // a request for a dormant node (a page this same handler is showing)
4040        // until the next layout pass has woken it, rather than focusing a node
4041        // the pass's revalidation would take focus straight back from.
4042        if let Some(&id) = ctx.focus_requests.last() {
4043            // If the requested widget is itself not focusable (e.g. a
4044            // composite like `TextInput` whose focus-handling lives on
4045            // an inner leaf), walk into the subtree and land on the
4046            // first focusable descendant in document order. This makes
4047            // `ctx.request_focus(some_composite)` Do The Right Thing
4048            // without every caller having to reach into private inner
4049            // ids. `first_focusable_descendant` returns the node itself
4050            // when it's focusable, so the usual leaf-target case is
4051            // still a no-op lookup.
4052            self.request_focus_with_ops(id, super::FocusRequestMode::SelfOrDescendant, &mut *ops);
4053        }
4054        if let Some(&id) = ctx.focus_into_requests.last() {
4055            // "Focus into" semantics: land on the first focusable descendant
4056            // and — unlike `focus_requests` above — do NOT fall back to the
4057            // container itself. A region with no focusable content (and not
4058            // focusable in its own right) leaves focus untouched rather than
4059            // trapping it on a non-interactive node. Drives Enter-on-a-tab →
4060            // into the tab panel.
4061            self.request_focus_with_ops(id, super::FocusRequestMode::Into, &mut *ops);
4062        }
4063
4064        // Rect-based "scroll this into view" requests (`ctx.ensure_visible`).
4065        // Walk outward from the widget whose handler queued the request and
4066        // reveal the rect inside every enclosing scroll container. Run after
4067        // focus so that if the same handler also moved focus, both follows
4068        // settle against the same (pre-relayout) bounds; each dispatch is
4069        // gated on the container not already showing the rect, so ordering is
4070        // harmless. The source widget itself is excluded from the walk — it
4071        // owns revealing an interior rect inside its own viewport.
4072        for req in ctx.scroll_into_view_requests {
4073            self.scroll_rect_into_view(
4074                // Whoever the rect belongs to — the source widget unless the caller
4075                // named another. See `EventContext::ensure_visible_from`.
4076                req.from.unwrap_or(source_widget),
4077                req.rect,
4078                req.margin,
4079                req.align,
4080                req.motion,
4081                &mut *ops,
4082            );
4083        }
4084        // Id-based `ctx.ensure_widget_visible`: resolve to the target's current
4085        // absolute bounds and walk *its* ancestors (skip if it was destroyed
4086        // before the drain). Walking from the target — not `source_widget` —
4087        // means the request reveals that widget wherever it sits, even when the
4088        // handler runs on a different node (a group's roving-key handler
4089        // revealing the child tile it just selected).
4090        for (id, margin) in ctx.scroll_widget_into_view_requests {
4091            if self.arena.get(id).is_some() {
4092                let bounds = self.arena.bounds(id);
4093                self.scroll_rect_into_view(
4094                    id,
4095                    bounds,
4096                    margin,
4097                    crate::event::ScrollAlign::Minimal,
4098                    crate::event::ScrollMotion::Instant,
4099                    &mut *ops,
4100                );
4101            }
4102        }
4103
4104        // Keyboard-highlight tooltip: surface the highlighted (menu) item's
4105        // tooltip immediately and dismiss the previously-highlighted one. Keyed
4106        // on the item id, NOT real focus (which stays on the menu panel for key
4107        // handling). Only the last request per handler is honoured.
4108        if let Some(&id) = ctx.highlight_tooltip_requests.last() {
4109            self.show_highlight_tooltip(id, &mut *ops);
4110        }
4111
4112        // --- Drag and drop ---
4113        if let Some((source_widget, payload, preview_widget)) = ctx.drag_start_request {
4114            let (preview_content_id, preview_overlay_id) = if let Some(preview) = preview_widget {
4115                // `add_boxed` — NOT `arena.insert` — runs the widget's
4116                // `build()` so composite previews (our `DragPreview`
4117                // wrapper in teksilo-widgets, or anything a user supplies)
4118                // actually instantiate their child subtree. Plain
4119                // `arena.insert` stops at the root node, leaves build
4120                // un-fired, and the overlay renders an empty widget.
4121                let content_id = self.add_boxed(preview);
4122                let overlay_id = self
4123                    .overlay_manager
4124                    .show(crate::overlay::OverlayRequest::new(
4125                        content_id,
4126                        source_widget,
4127                        crate::overlay::OverlayPlacement::AtPointer(teksilo_canvas::Point::ZERO),
4128                        crate::overlay::DismissBehavior::Manual,
4129                    ));
4130                // Force the next layout pass to run `position_overlays`
4131                // and `set_content_bounds` — otherwise the preview sits
4132                // at its initial (0, 0) placement forever.
4133                self.arena.mark_needs_layout(content_id);
4134                (Some(content_id), Some(overlay_id))
4135            } else {
4136                (None, None)
4137            };
4138            self.active_drag = Some(crate::drag_state::DragSession {
4139                payload,
4140                // The pointer that armed the drag. `start_drag` is always
4141                // reached from a handler serving a real sample, which is the
4142                // only place this is knowable — from here on the drag runs
4143                // through layout ticks and platform threads that have no
4144                // sample of their own. See `DragSession::pointer`.
4145                pointer: self.current_input.pointer,
4146                source_widget: Some(source_widget),
4147                is_external: false,
4148                current_position: teksilo_canvas::Point::ZERO,
4149                current_target: None,
4150                feedback: crate::drag_state::DropFeedback::NoFeedback,
4151                preview_content_id,
4152                preview_overlay_id,
4153            });
4154            self.set_current_pointer_capture(Some(source_widget));
4155            // Grabbing-hand cursor while the drag is in flight. Reset on
4156            // drop / cancel / source-destroyed below.
4157            self.current_cursor = crate::widget::CursorIcon::Grabbing;
4158        }
4159        if ctx.cancel_drag {
4160            self.cancel_active_drag(&mut *ops);
4161        }
4162
4163        // --- Environment changes (architecture §9.5) ---
4164        if let Some(theme) = ctx.theme_request {
4165            // Stored, not applied: the app layer routes this through
4166            // `WindowManager::set_theme` so every window re-themes, matching
4167            // the app-wide `set_locale` path below. Applying
4168            // `WidgetTree::set_theme` inline would re-theme only the
4169            // originating window.
4170            self.pending_theme_request = Some(theme);
4171        }
4172        if ctx.follow_system_request {
4173            // Stored, not applied: the app layer switches to
4174            // `ThemeMode::Native` and recomputes the theme from the current
4175            // OS colours, fanning it to every window.
4176            self.pending_follow_system_request = true;
4177        }
4178        if let Some(locale) = ctx.locale_request {
4179            // Stored, not applied: the app layer must route this through
4180            // `WindowManager::set_locale` so the `I18nManager`'s active
4181            // locale and direction stay in sync. Applying via
4182            // `WidgetTree::set_locale` alone would leave `tr!` bindings
4183            // reading the old translations.
4184            self.pending_locale_request = Some(locale);
4185        }
4186        if let Some(scale) = ctx.text_scale_request {
4187            // Stored, not applied: the app layer routes this through
4188            // `WindowManager::set_text_scale` so every window re-scales its
4189            // text. Applying `WidgetTree::set_user_text_scale` inline would
4190            // grow only the originating window.
4191            self.pending_text_scale_request = Some(scale);
4192        }
4193        // Last, and with a context of their own: `Space` on a data view's
4194        // focused row runs the row's published toggle, and a checkbox's toggle
4195        // fires the app's `on_change`, which may send an intent or open a
4196        // window. Running them here rather than inside the mutation drain is
4197        // what gives them an `EventContext`; the drain resolved which action to
4198        // run against the live tree and handed it back.
4199        if !deferred_row_activations.is_empty() {
4200            self.run_with_event_context(&mut *ops, move |ctx| {
4201                for action in deferred_row_activations {
4202                    action(ctx);
4203                }
4204            });
4205        }
4206    }
4207
4208    /// Returns the row activations it resolved but could not run: they need an
4209    /// [`EventContext`], and this method has no `ops` to build one from. The
4210    /// caller runs them once the drain is finished, the way
4211    /// [`WidgetTree::run_mount_actions`](crate::WidgetTree::run_mount_actions)
4212    /// does.
4213    #[must_use]
4214    fn apply_tree_mutations(
4215        &mut self,
4216        mutations: Vec<crate::widget::TreeMutation>,
4217    ) -> Vec<std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>> {
4218        let mut deferred_row_activations = Vec::new();
4219        use crate::binding::BindingLevel;
4220        use crate::widget::TreeMutation;
4221
4222        for mutation in mutations {
4223            match mutation {
4224                TreeMutation::SetDormant(id) => {
4225                    self.park_subtree(id);
4226                }
4227                TreeMutation::Activate(id) => self.arena.activate(id),
4228                TreeMutation::Destroy(id) => {
4229                    // Route through `destroy_subtree`, NOT the bare
4230                    // `arena.destroy`: the latter only unlinks nodes from
4231                    // the slotmap and leaks everything the widget owned —
4232                    // animation-scheduler entries (which hold strong
4233                    // `Signal<f32>` clones, so the widget keeps animating
4234                    // after it's gone), animated-quad slots, event-source
4235                    // subscriptions, registered shortcuts, bindings, and
4236                    // gesture ownership — and leaves `focused`/`hovered`
4237                    // dangling at a removed id. This mirrors the build-time
4238                    // `BuildContext::destroy_subtree`, including dismissing
4239                    // any overlay that still references the subtree so the
4240                    // manager doesn't retain a stale content reference.
4241                    if let Some(overlay_id) = self.overlay_manager().find_by_content(id) {
4242                        self.dismiss_overlay(overlay_id);
4243                    }
4244                    self.destroy_subtree(id);
4245                }
4246                // Build now, not next frame: the same handler is about to show
4247                // an overlay over this node and move focus into it, and both
4248                // read the subtree. See `EventContext::materialize_now`.
4249                TreeMutation::MaterializeNow(id) => {
4250                    if self.arena.get(id).is_some() {
4251                        self.rebuild_single_widget(id);
4252                    }
4253                }
4254                TreeMutation::RowSpaceActivate { row, fallback } => {
4255                    // Resolve against the *live* tree: a data view rebuilds its
4256                    // rows as they realize, so the row that was focused when
4257                    // the key arrived may have been rebuilt since.
4258                    //
4259                    // Resolve here, run later. The action carries an
4260                    // `EventContext` so a row's checkbox fires its `on_change`
4261                    // on this path exactly as it does under the pointer; this
4262                    // method has no `ops` to build one from, so the caller runs
4263                    // it after the drain.
4264                    deferred_row_activations.push(self.keyboard_toggle_in(row).unwrap_or(fallback));
4265                }
4266                TreeMutation::WithWidgetMut { id, dirty, apply } => {
4267                    // Run the typed mutation while `&mut arena` is live, then
4268                    // drop the borrow before dirty-marking (the `mark_*` calls
4269                    // re-borrow the arena). Only dirty-mark a live node so we
4270                    // never call `mark_ancestors_need_layout` on a destroyed id.
4271                    let existed = if let Some(any) =
4272                        self.arena.get_mut(id).and_then(|n| n.widget.as_any_mut())
4273                    {
4274                        apply(any);
4275                        true
4276                    } else {
4277                        false
4278                    };
4279                    if existed {
4280                        match dirty {
4281                            BindingLevel::RepaintOnly => self.arena.mark_needs_paint(id),
4282                            BindingLevel::SubtreeRepaint => self.arena.mark_subtree_needs_paint(id),
4283                            BindingLevel::Relayout => {
4284                                self.arena.mark_needs_layout(id);
4285                                self.arena.mark_ancestors_need_layout(id);
4286                            }
4287                            BindingLevel::Rebuild => {
4288                                self.arena.mark_needs_rebuild(id);
4289                                self.arena.mark_ancestors_need_layout(id);
4290                            }
4291                            BindingLevel::AccessibilityOnly => self.a11y_dirty = true,
4292                        }
4293                    }
4294                }
4295            }
4296        }
4297        deferred_row_activations
4298    }
4299
4300    /// Hit-test at a point for the **mouse, exactly** — the meaning this door
4301    /// has always had, and keeps.
4302    ///
4303    /// A mouse cursor's hot-spot is exact, so neither hit-targeting mechanism
4304    /// applies to it: the outset pre-pass sees zero insets and the miss-only
4305    /// slop pass short-circuits on a zero radius. A caller that holds a pointer
4306    /// should use [`hit_test_for`](Self::hit_test_for) instead, which is the
4307    /// same test for a mouse and the widened one for a finger or a stylus.
4308    pub fn hit_test(&self, point: Point) -> Option<WidgetId> {
4309        self.hit_test_excluding_overlay_and_widget(point, None, None)
4310    }
4311
4312    /// Hit-test at a point on behalf of a named pointer.
4313    ///
4314    /// Runs the exact pass with that pointer's `Widget::hit_outset`, then — only
4315    /// if the exact pass found nothing eligible — the miss-only slop pass. For
4316    /// [`PointerKind::Mouse`](teksilo_tokens::PointerKind::Mouse) this is
4317    /// exactly [`hit_test`](Self::hit_test).
4318    ///
4319    /// Candidates are restricted to the **topmost overlay layer the exact pass
4320    /// entered**: a press inside an open menu can be re-attributed to a menu
4321    /// row, never to a control on the page behind it.
4322    pub fn hit_test_for(
4323        &self,
4324        point: Point,
4325        pointer: &crate::pointer::PointerInfo,
4326    ) -> Option<WidgetId> {
4327        self.hit_test_for_excluding(point, pointer, None, None)
4328    }
4329
4330    /// [`hit_test_for`](Self::hit_test_for) with the drag-and-drop exclusions of
4331    /// [`hit_test_excluding_overlay_and_widget`](Self::hit_test_excluding_overlay_and_widget).
4332    pub fn hit_test_for_excluding(
4333        &self,
4334        point: Point,
4335        pointer: &crate::pointer::PointerInfo,
4336        exclude_overlay: Option<crate::overlay::OverlayId>,
4337        exclude_widget: Option<WidgetId>,
4338    ) -> Option<WidgetId> {
4339        let surfaces = self.text_surfaces();
4340        let read_only = |id: WidgetId| surfaces.is_read_only(id);
4341        let hit =
4342            crate::pointer::hit_slop::HitContext::new(pointer.kind, &self.effective_theme.input)
4343                .direction(self.layout_direction)
4344                .read_only_probe(&read_only);
4345        self.hit_test_with(point, exclude_overlay, exclude_widget, &hit)
4346    }
4347
4348    /// Hit-test at a point, excluding a specific overlay and widget from consideration.
4349    /// Used during drag-and-drop to exclude the preview overlay and its content widget,
4350    /// so they don't block hit-testing of the actual drop targets underneath.
4351    ///
4352    /// **Mouse, exact** — the pointer-aware twin is
4353    /// [`hit_test_for_excluding`](Self::hit_test_for_excluding).
4354    pub fn hit_test_excluding_overlay_and_widget(
4355        &self,
4356        point: Point,
4357        exclude_overlay: Option<crate::overlay::OverlayId>,
4358        exclude_widget: Option<WidgetId>,
4359    ) -> Option<WidgetId> {
4360        self.hit_test_with(
4361            point,
4362            exclude_overlay,
4363            exclude_widget,
4364            &crate::pointer::hit_slop::HitContext::mouse(),
4365        )
4366    }
4367
4368    /// The one hit-test body: overlay first, then the arena, under whichever
4369    /// [`HitContext`](crate::pointer::hit_slop::HitContext) the caller built.
4370    pub(crate) fn hit_test_with(
4371        &self,
4372        point: Point,
4373        exclude_overlay: Option<crate::overlay::OverlayId>,
4374        exclude_widget: Option<WidgetId>,
4375        hit: &crate::pointer::hit_slop::HitContext<'_>,
4376    ) -> Option<WidgetId> {
4377        if let Some(overlay_id) = self.overlay_manager.hit_test(point) {
4378            if Some(overlay_id) == exclude_overlay {
4379                // Skip this excluded overlay, fall through to widget tree
4380            } else if let Some(overlay) = self.overlay_manager.overlay(overlay_id) {
4381                // A modal's scrim dims the window's chrome but leaves it live.
4382                if self.overlay_manager.is_modal_scrim(overlay)
4383                    && let Some(chrome) = self.window_chrome_under_modal(point, exclude_widget, hit)
4384                {
4385                    return Some(chrome);
4386                }
4387                // Scoped to the overlay's content: this is what restricts the
4388                // slop pass's candidates to the topmost layer the exact pass
4389                // entered.
4390                let content_id = overlay.content_id;
4391                if let Some(found) =
4392                    self.arena
4393                        .hit_test_in_subtree_with_slop(content_id, point, exclude_widget, hit)
4394                {
4395                    return Some(found);
4396                }
4397                // The overlay was chosen by its **bounds** and its content
4398                // claimed nothing at this point. For every ordinary overlay
4399                // that is the end of the search — answering `None` is what
4400                // keeps the slop pass inside the one layer the exact pass
4401                // entered, so a near-miss on a menu row cannot be beaten by a
4402                // control behind the menu.
4403                //
4404                // An overlay whose content root declares `event_pass_through`
4405                // is the exception, because that flag already means "what I did
4406                // not claim belongs to whatever is behind me": the tree's
4407                // walker honours it for an ordinary node *after* its children
4408                // miss, and an overlay root is not an exception to it. Without
4409                // this, a viewport-sized pass-through layer — the placement the
4410                // text-affordance band was written for — stops the surface
4411                // under it taking presses at all. The widening is confined to
4412                // that flag: an overlay that does not set it still returns
4413                // here, so no ordinary overlay's candidate set changes.
4414                if !self
4415                    .arena
4416                    .get(content_id)
4417                    .is_some_and(|node| node.event_pass_through)
4418                {
4419                    return None;
4420                }
4421            }
4422        }
4423
4424        if self.overlay_manager.topmost_modal().is_some() {
4425            return self.window_chrome_under_modal(point, exclude_widget, hit);
4426        }
4427
4428        // The walk of the arena's roots honours event_pass_through and
4429        // clips_children. An overlay's content is one of those roots, and an
4430        // inert one is left out: the stack has stepped over it above, and a
4431        // point over it with nothing beneath it on the stack would otherwise
4432        // land in it here, wherever its root falls in the walk.
4433        let mut roots = self.arena.roots();
4434        let inert: Vec<WidgetId> = self
4435            .overlay_manager
4436            .stack
4437            .iter()
4438            .filter(|overlay| overlay.inert)
4439            .map(|overlay| overlay.content_id)
4440            .collect();
4441        if !inert.is_empty() {
4442            roots.retain(|root| !inert.contains(root));
4443        }
4444        self.arena
4445            .hit_test_roots_with_slop(roots, point, exclude_widget, hit)
4446    }
4447}
4448
4449impl WidgetTree {
4450    /// What a point on the page takes while a modal is open: only the
4451    /// window's own chrome (a title bar's drag surface, its window buttons,
4452    /// the resize edges; [`HandlerSet::window_chrome`]). The window system's
4453    /// frame stays live above a dialog in a window, so a window drawing its
4454    /// own frame keeps it live too, as libadwaita's dialogs leave the window
4455    /// draggable and WinUI's leave the caption buttons working.
4456    ///
4457    /// The closest chrome node above the page's own hit takes the point, so a
4458    /// press on app content a drag surface holds (a title) moves the window
4459    /// and never reaches that content. A gesture dead zone on the way, which
4460    /// is how an app puts a control in a title bar, means the point is the
4461    /// app's, and it goes nowhere: the app stays behind its modal.
4462    ///
4463    /// [`HandlerSet::window_chrome`]: crate::widget_builder::HandlerSet::window_chrome
4464    fn window_chrome_under_modal(
4465        &self,
4466        point: Point,
4467        exclude_widget: Option<WidgetId>,
4468        hit: &crate::pointer::hit_slop::HitContext<'_>,
4469    ) -> Option<WidgetId> {
4470        let overlays: Vec<WidgetId> = self
4471            .overlay_manager
4472            .stack
4473            .iter()
4474            .map(|overlay| overlay.content_id)
4475            .collect();
4476        let mut roots = self.arena.roots();
4477        roots.retain(|root| !overlays.contains(root));
4478        let found = self
4479            .arena
4480            .hit_test_roots_with_slop(roots, point, exclude_widget, hit)?;
4481        let mut node = Some(found);
4482        while let Some(id) = node {
4483            let current = self.arena.get(id)?;
4484            if current.window_chrome {
4485                return Some(id);
4486            }
4487            if current.gesture_dead_zone {
4488                return None;
4489            }
4490            node = self.arena.parent(id);
4491        }
4492        None
4493    }
4494}
4495
4496/// Whether this keystroke is one of the chords that ask for a context menu.
4497///
4498/// Three routes, because no single one exists on every platform:
4499///
4500/// * **The dedicated key.** `VK_APPS` on Windows, `keysyms::Menu` on X11 and
4501///   Wayland. `winit-0.30.13`'s AppKit backend references
4502///   `NamedKey::ContextMenu` zero times, so macOS never produces it.
4503/// * **Shift+F10.** The convention Windows, GTK and Qt all honour, and the one
4504///   thing a Windows or Linux keyboard without a Menu key can still reach.
4505/// * **Ctrl+Shift+M on macOS.** Neither of the above is available there: Mac
4506///   keyboards have no Menu key, and F10 is a media key under the default
4507///   "Use F1, F2 etc. as standard function keys = off" setting, so Shift+F10
4508///   may never arrive as F10 at all. Kept off the other platforms, where
4509///   Ctrl+Shift+M is a plausible application binding.
4510///
4511/// Modifiers are matched exactly. Shift+F10 with Ctrl held is a different
4512/// gesture and must reach the application unchanged.
4513fn is_context_menu_chord(key: Key, modifiers: Modifiers) -> bool {
4514    match key {
4515        Key::ContextMenu => modifiers == Modifiers::NONE,
4516        Key::F10 => modifiers == Modifiers::SHIFT,
4517        #[cfg(target_os = "macos")]
4518        Key::M => modifiers == Modifiers::CTRL | Modifiers::SHIFT,
4519        _ => false,
4520    }
4521}
4522
4523#[cfg(test)]
4524mod tests {
4525    use super::*;
4526    use crate::test_widgets::FillWidget;
4527    use crate::widget::CursorIcon;
4528    use crate::widget_builder::WidgetBuilder;
4529
4530    #[test]
4531    fn pointer_enter_leave_synthesized() {
4532        let mut tree = WidgetTree::new();
4533        let widget = tree.add(FillWidget::new());
4534        tree.layout(SizeProposal::exact(100.0, 50.0));
4535        tree.pointer_move(Point::new(50.0, 25.0));
4536        assert_eq!(tree.hovered(), Some(widget));
4537        tree.pointer_move(Point::new(200.0, 200.0));
4538        assert_eq!(tree.hovered(), None);
4539    }
4540
4541    #[test]
4542    fn pointer_hover_updates_current_cursor() {
4543        let mut tree = WidgetTree::new();
4544        tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4545        tree.layout(SizeProposal::exact(100.0, 50.0));
4546
4547        tree.pointer_move(Point::new(50.0, 25.0));
4548        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4549
4550        tree.pointer_move(Point::new(200.0, 200.0));
4551        assert_eq!(tree.current_cursor(), CursorIcon::Default);
4552    }
4553
4554    /// A leaf that declares its cursor from `build()` through
4555    /// `apply_self_handlers`, the route a composing widget takes.
4556    #[derive(Debug)]
4557    struct SelfDeclared(CursorIcon);
4558
4559    impl crate::widget::Widget for SelfDeclared {
4560        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
4561            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().cursor(self.0));
4562            Vec::new()
4563        }
4564
4565        fn layout_response(
4566            &self,
4567            proposal: SizeProposal,
4568            _ctx: &crate::widget::LayoutContext,
4569        ) -> crate::widget::LayoutResponse {
4570            proposal.resolve(100.0, 50.0).into()
4571        }
4572    }
4573
4574    /// A test reads the cursor two ways: `current_cursor` is the window's
4575    /// running answer and follows the hover, `declared_cursor` is what a node
4576    /// asks for and does not care where the pointer is. Both declaration
4577    /// routes (`.cursor(..)` and `apply_self_handlers`) land in the latter,
4578    /// and "declares nothing" is `None`, distinct from declaring `Default`.
4579    #[test]
4580    fn declared_cursor_reads_what_a_node_asks_for_and_current_cursor_follows_the_hover() {
4581        let over = Point::new(50.0, 25.0);
4582        let away = Point::new(200.0, 200.0);
4583
4584        // `.cursor(..)` through the builder.
4585        let mut tree = WidgetTree::new();
4586        let built = tree.add(FillWidget::new().cursor(CursorIcon::ColResize));
4587        tree.layout(SizeProposal::exact(100.0, 50.0));
4588        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4589        assert_eq!(
4590            tree.current_cursor(),
4591            CursorIcon::Default,
4592            "not hovered yet"
4593        );
4594        tree.pointer_move(over);
4595        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4596        assert_eq!(tree.declared_cursor(built), Some(CursorIcon::ColResize));
4597        tree.pointer_move(away);
4598        assert_eq!(tree.current_cursor(), CursorIcon::Default);
4599        assert_eq!(
4600            tree.declared_cursor(built),
4601            Some(CursorIcon::ColResize),
4602            "the declaration does not depend on the pointer",
4603        );
4604
4605        // `apply_self_handlers` in `build()`.
4606        let mut tree = WidgetTree::new();
4607        let own = tree.add(SelfDeclared(CursorIcon::Crosshair));
4608        tree.layout(SizeProposal::exact(100.0, 50.0));
4609        assert_eq!(tree.declared_cursor(own), Some(CursorIcon::Crosshair));
4610        tree.pointer_move(over);
4611        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4612        tree.pointer_move(away);
4613        assert_eq!(tree.current_cursor(), CursorIcon::Default);
4614
4615        // Nothing declared, and an explicit `Default`.
4616        let mut tree = WidgetTree::new();
4617        let plain = tree.add(FillWidget::new());
4618        tree.layout(SizeProposal::exact(100.0, 50.0));
4619        assert_eq!(tree.declared_cursor(plain), None);
4620        let mut tree = WidgetTree::new();
4621        let explicit = tree.add(FillWidget::new().cursor(CursorIcon::Default));
4622        tree.layout(SizeProposal::exact(100.0, 50.0));
4623        assert_eq!(
4624            tree.declared_cursor(explicit),
4625            Some(CursorIcon::Default),
4626            "declaring the default cursor is not declaring nothing",
4627        );
4628    }
4629
4630    /// A handler's `set_cursor` outlives its dispatch, so a handler that
4631    /// re-decides the cursor on every move needs a way to stop deciding.
4632    ///
4633    /// Going quiet does not do it: the cursor moves only when something writes
4634    /// to it, and the node-declared cursor is written on `PointerEnter` /
4635    /// `PointerLeave` alone — so while the pointer stays inside one node there
4636    /// is no second writer, and the handler's last word simply stands. That is
4637    /// what `release_cursor` withdraws, and it withdraws *to the node's own
4638    /// declaration*, not to `Default`.
4639    #[test]
4640    fn release_cursor_hands_the_cursor_back_to_the_node_that_declared_it() {
4641        // Overrides itself to `Crosshair` on the left third of the widget and
4642        // withdraws everywhere else — the shape of any handler that arbitrates
4643        // its own affordance against the node it sits on.
4644        let mut tree = WidgetTree::new();
4645        tree.add(
4646            FillWidget::new()
4647                .cursor(CursorIcon::ColResize)
4648                .on_pointer_event(|event, ctx| {
4649                    if let WidgetEvent::PointerMove { position, .. } = event {
4650                        if position.x < 30.0 {
4651                            ctx.set_cursor(CursorIcon::Crosshair);
4652                        } else {
4653                            ctx.release_cursor();
4654                        }
4655                    }
4656                    EventResponse::Ignored
4657                }),
4658        );
4659        tree.layout(SizeProposal::exact(100.0, 50.0));
4660
4661        tree.pointer_move(Point::new(10.0, 25.0));
4662        assert_eq!(
4663            tree.current_cursor(),
4664            CursorIcon::Crosshair,
4665            "the handler outranks the node it is attached to",
4666        );
4667        tree.pointer_move(Point::new(60.0, 25.0));
4668        assert_eq!(
4669            tree.current_cursor(),
4670            CursorIcon::ColResize,
4671            "and hands it back to the node, not to Default — no enter/leave \
4672             fires on a move within one node, so nothing else could",
4673        );
4674        tree.pointer_move(Point::new(10.0, 25.0));
4675        assert_eq!(
4676            tree.current_cursor(),
4677            CursorIcon::Crosshair,
4678            "and can take it again"
4679        );
4680        tree.pointer_move(Point::new(200.0, 200.0));
4681        assert_eq!(
4682            tree.current_cursor(),
4683            CursorIcon::Default,
4684            "leaving the node clears both the override and the declaration",
4685        );
4686    }
4687
4688    /// The withdrawal is a no-op for a handler that never spoke, and resolves
4689    /// to `Default` when the chain declares nothing — so a handler may call it
4690    /// unconditionally without having to remember whether it once set a cursor.
4691    #[test]
4692    fn release_cursor_is_a_no_op_with_nothing_to_undo() {
4693        let mut tree = WidgetTree::new();
4694        tree.add(
4695            FillWidget::new()
4696                .cursor(CursorIcon::ColResize)
4697                .on_pointer_event(|event, ctx| {
4698                    if matches!(event, WidgetEvent::PointerMove { .. }) {
4699                        ctx.release_cursor();
4700                    }
4701                    EventResponse::Ignored
4702                }),
4703        );
4704        tree.layout(SizeProposal::exact(100.0, 50.0));
4705        tree.pointer_move(Point::new(50.0, 25.0));
4706        assert_eq!(tree.current_cursor(), CursorIcon::ColResize);
4707
4708        // Same handler over a node that declares nothing.
4709        let mut tree = WidgetTree::new();
4710        tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
4711            if let WidgetEvent::PointerMove { position, .. } = event {
4712                if position.x < 30.0 {
4713                    ctx.set_cursor(CursorIcon::Crosshair);
4714                } else {
4715                    ctx.release_cursor();
4716                }
4717            }
4718            EventResponse::Ignored
4719        }));
4720        tree.layout(SizeProposal::exact(100.0, 50.0));
4721        tree.pointer_move(Point::new(10.0, 25.0));
4722        assert_eq!(tree.current_cursor(), CursorIcon::Crosshair);
4723        tree.pointer_move(Point::new(60.0, 25.0));
4724        assert_eq!(
4725            tree.current_cursor(),
4726            CursorIcon::Default,
4727            "nothing declared a cursor for this chain, so the hand-back \
4728             resolves to Default",
4729        );
4730    }
4731
4732    /// A handler that speaks during the `PointerEnter` itself still outranks
4733    /// the node it entered — and the node's declaration is remembered anyway,
4734    /// so the hand-back has somewhere to go.
4735    ///
4736    /// The two used to share one slot, where the handler's later write erased
4737    /// the declaration outright; they are separate channels now precisely so
4738    /// this case keeps both.
4739    #[test]
4740    fn an_on_hover_override_does_not_erase_the_declaration_it_outranks() {
4741        let mut tree = WidgetTree::new();
4742        tree.add(
4743            FillWidget::new()
4744                .cursor(CursorIcon::ColResize)
4745                .on_hover(|entered, ctx| {
4746                    if entered {
4747                        ctx.set_cursor(CursorIcon::Crosshair);
4748                    }
4749                })
4750                .on_pointer_event(|event, ctx| {
4751                    if let WidgetEvent::PointerMove { position, .. } = event
4752                        && position.x >= 30.0
4753                    {
4754                        ctx.release_cursor();
4755                    }
4756                    EventResponse::Ignored
4757                }),
4758        );
4759        tree.layout(SizeProposal::exact(100.0, 50.0));
4760
4761        tree.pointer_move(Point::new(10.0, 25.0));
4762        assert_eq!(
4763            tree.current_cursor(),
4764            CursorIcon::Crosshair,
4765            "on_hover runs after the node cursor is applied, so it wins",
4766        );
4767        tree.pointer_move(Point::new(60.0, 25.0));
4768        assert_eq!(
4769            tree.current_cursor(),
4770            CursorIcon::ColResize,
4771            "and the node's declaration survived being overridden",
4772        );
4773    }
4774
4775    // A leaf that opts into typed introspection, so `with_widget_mut` /
4776    // `widget_as_any(_mut)` can reach it (the default `as_any_mut` is `None`).
4777    #[derive(Debug)]
4778    struct Bumpable {
4779        value: i32,
4780    }
4781
4782    impl crate::widget::Widget for Bumpable {
4783        fn layout_response(
4784            &self,
4785            proposal: SizeProposal,
4786            _ctx: &crate::widget::LayoutContext,
4787        ) -> crate::widget::LayoutResponse {
4788            proposal.resolve(10.0, 10.0).into()
4789        }
4790        fn as_any(&self) -> Option<&dyn std::any::Any> {
4791            Some(self)
4792        }
4793        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4794            Some(self)
4795        }
4796    }
4797
4798    #[test]
4799    fn with_widget_mut_applies_and_dirty_marks() {
4800        let mut tree = WidgetTree::new();
4801        let id = tree.add(Bumpable { value: 0 });
4802        tree.layout(SizeProposal::exact(100.0, 100.0));
4803
4804        let mut ctx = EventContext::new();
4805        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::Relayout, |b| {
4806            b.value = 42;
4807        });
4808        tree.collect_from_ctx(ctx, id);
4809
4810        let value = tree
4811            .widget_as_any(id)
4812            .and_then(|a| a.downcast_ref::<Bumpable>())
4813            .map(|b| b.value);
4814        assert_eq!(
4815            value,
4816            Some(42),
4817            "the deferred closure must mutate the live widget"
4818        );
4819        assert!(
4820            tree.needs_layout(),
4821            "Relayout dirty level must mark the tree for relayout"
4822        );
4823    }
4824
4825    #[test]
4826    #[cfg(debug_assertions)]
4827    #[should_panic(expected = "not the requested type")]
4828    fn with_widget_mut_wrong_type_panics_in_debug() {
4829        struct Other;
4830        let mut tree = WidgetTree::new();
4831        let id = tree.add(Bumpable { value: 0 });
4832        let mut ctx = EventContext::new();
4833        ctx.with_widget_mut::<Other>(
4834            id,
4835            crate::binding::BindingLevel::RepaintOnly,
4836            |_o: &mut Other| {},
4837        );
4838        // Bumpable opts into as_any_mut, so the closure runs and the
4839        // wrong-type downcast trips the debug_assert.
4840        tree.collect_from_ctx(ctx, id);
4841    }
4842
4843    #[test]
4844    fn with_widget_mut_closure_may_fire_observed_signals() {
4845        // Reentrancy guard. The closure runs inside `apply_tree_mutations`
4846        // while the target arena node is mutably borrowed. If it fires a
4847        // `Signal` whose observer sets *another* signal — the exact
4848        // `SceneView` shape (`item_change_signal` → bump `reconcile_dirty`) —
4849        // nothing may double-borrow the arena. The arena borrow is scoped to
4850        // the closure call and dropped before dirty-marking; signal/observer
4851        // work touches the binding registry, not the arena.
4852        use crate::signal::Signal;
4853        let mut tree = WidgetTree::new();
4854        let id = tree.add(Bumpable { value: 0 });
4855        tree.layout(SizeProposal::exact(100.0, 100.0));
4856
4857        let trigger = Signal::new(0_u64);
4858        let echo = Signal::new(0_u64);
4859        let echo_for_obs = echo.clone();
4860        let _obs = trigger.observe(move |v| echo_for_obs.set(*v));
4861
4862        let trigger_in = trigger.clone();
4863        let mut ctx = EventContext::new();
4864        ctx.with_widget_mut::<Bumpable>(id, crate::binding::BindingLevel::RepaintOnly, move |b| {
4865            b.value = 7;
4866            // Fires `_obs` synchronously, mid-deferred-apply.
4867            trigger_in.set(99);
4868        });
4869        tree.collect_from_ctx(ctx, id); // must not panic / double-borrow
4870
4871        assert_eq!(
4872            echo.get(),
4873            99,
4874            "the observer ran during the deferred mutation"
4875        );
4876        let value = tree
4877            .widget_as_any(id)
4878            .and_then(|a| a.downcast_ref::<Bumpable>())
4879            .map(|b| b.value);
4880        assert_eq!(value, Some(7));
4881    }
4882
4883    #[test]
4884    fn request_accessibility_update_forces_rewalk() {
4885        let mut tree = WidgetTree::new();
4886        let id = tree.add(Bumpable { value: 0 });
4887        tree.layout(SizeProposal::exact(100.0, 100.0));
4888        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4889        assert!(
4890            !tree.a11y_dirty,
4891            "sync_accessibility should clear the dirty flag"
4892        );
4893
4894        let mut ctx = EventContext::new();
4895        ctx.request_accessibility_update();
4896        tree.collect_from_ctx(ctx, id);
4897        assert!(
4898            tree.a11y_dirty,
4899            "request_accessibility_update must force an AT re-walk"
4900        );
4901    }
4902
4903    #[test]
4904    fn rebuild_dirties_accessibility_tree() {
4905        // Regression for audit Blocker G1: every `BindingLevel::Rebuild`
4906        // consumer (ListView / TreeView / TableView / ComboBox / Calendar /
4907        // DockingLayout / ...) tears down and re-creates its subtree on an
4908        // ordinary model change, allocating fresh WidgetIds and changing the
4909        // AccessKit tree shape. That pass must dirty the cached AT snapshot,
4910        // or screen readers keep reading the pre-mutation tree indefinitely.
4911        let mut tree = WidgetTree::new();
4912        let id = tree.add(Bumpable { value: 0 });
4913        tree.layout(SizeProposal::exact(100.0, 100.0));
4914        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4915        assert!(
4916            !tree.a11y_dirty,
4917            "sync_accessibility should clear the dirty flag"
4918        );
4919
4920        // Marking for rebuild is exactly what a Rebuild-level binding does;
4921        // the following layout pass drains pending rebuilds.
4922        tree.arena_mark_needs_rebuild_for_testing(id);
4923        tree.layout(SizeProposal::exact(100.0, 100.0));
4924        assert!(
4925            tree.a11y_dirty,
4926            "a rebuild must dirty the AT tree so the next sync re-walks"
4927        );
4928    }
4929
4930    #[test]
4931    fn bound_access_label_change_dirties_accessibility_tree() {
4932        use crate::signal::Signal;
4933        use crate::test_widgets::FillWidget;
4934        use crate::widget_builder::WidgetBuilder;
4935
4936        // Regression for audit G15: a reactive `.access_label(signal)` (and
4937        // likewise description / value) must register at AccessibilityOnly so
4938        // changing the signal re-walks the AT tree and re-resolves the
4939        // announced name. Previously only `access_hidden` was registered, so
4940        // label / description / value updates were invisible to screen readers.
4941        let label = Signal::new("first".to_string());
4942        let mut tree = WidgetTree::new();
4943        let _id = tree.add(FillWidget::new().access_label(label.clone()));
4944        tree.layout(SizeProposal::exact(100.0, 100.0));
4945        let _ = tree.sync_accessibility(); // populate cache, clears a11y_dirty
4946        assert!(!tree.a11y_dirty, "sync_accessibility should clear the flag");
4947
4948        label.set("second".to_string());
4949        tree.layout(SizeProposal::exact(100.0, 100.0));
4950        assert!(
4951            tree.a11y_dirty,
4952            "changing a bound access_label must dirty the AT tree"
4953        );
4954    }
4955
4956    #[test]
4957    fn disabled_ancestor_blocks_event_to_descendant() {
4958        use crate::signal::Signal;
4959        use crate::test_widgets::StackWidget;
4960        use std::cell::Cell;
4961        use std::rc::Rc;
4962
4963        let tapped = Rc::new(Cell::new(false));
4964        let flag = tapped.clone();
4965        let enabled = Signal::new(true);
4966
4967        let mut tree = WidgetTree::new();
4968        let child = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
4969            flag.set(true);
4970        }));
4971        let parent = tree.add(StackWidget::new().child(child));
4972        tree.enabled_when(parent, enabled.clone());
4973        tree.layout(SizeProposal::exact(100.0, 50.0));
4974
4975        enabled.set(false);
4976        tree.click(child);
4977        assert!(
4978            !tapped.get(),
4979            "disabled ancestor should block descendant tap"
4980        );
4981
4982        enabled.set(true);
4983        tree.click(child);
4984        assert!(tapped.get(), "re-enabling should restore dispatch");
4985    }
4986
4987    #[test]
4988    fn pointer_positions_are_widget_local_at_nonzero_origin() {
4989        use crate::event::{Modifiers, PointerButton};
4990        use crate::test_widgets::InsetWidget;
4991        use std::cell::Cell;
4992        use std::rc::Rc;
4993
4994        // A 20px inset places the child at window origin (20, 20).
4995        let tap_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4996        let down_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4997        let drag_pos: Rc<Cell<Option<Point>>> = Rc::new(Cell::new(None));
4998        let (tp, dp, gp) = (tap_pos.clone(), down_pos.clone(), drag_pos.clone());
4999
5000        let mut tree = WidgetTree::new();
5001        let child = tree.add(
5002            FillWidget::new()
5003                .on_tap(move |ev, _ctx| tp.set(Some(ev.position)))
5004                .on_pointer_event(move |ev, _ctx| {
5005                    if let WidgetEvent::PointerDown { position, .. } = ev {
5006                        dp.set(Some(*position));
5007                    }
5008                    crate::event::EventResponse::Ignored
5009                })
5010                .on_drag(move |phase, _ctx| {
5011                    use crate::gesture::DragPhase;
5012                    match phase {
5013                        DragPhase::Started { position, .. }
5014                        | DragPhase::Moved { position, .. }
5015                        | DragPhase::Ended { position, .. } => gp.set(Some(position)),
5016                        _ => {}
5017                    }
5018                }),
5019        );
5020        let inset = tree.add(InsetWidget::new(20.0).set_child(child));
5021        let _ = inset;
5022        tree.layout(SizeProposal::exact(200.0, 200.0));
5023        assert_eq!(tree.bounds(child).origin(), Point::new(20.0, 20.0));
5024
5025        // A tap at window (50, 40) must reach the handler as local (30, 20).
5026        tree.dispatch_event(WidgetEvent::pointer_down(
5027            Point::new(50.0, 40.0),
5028            PointerButton::Primary,
5029            Modifiers::NONE,
5030        ));
5031        assert_eq!(
5032            down_pos.get(),
5033            Some(Point::new(30.0, 20.0)),
5034            "on_pointer_event PointerDown must be widget-local"
5035        );
5036        tree.dispatch_event(WidgetEvent::pointer_up(
5037            Point::new(50.0, 40.0),
5038            PointerButton::Primary,
5039            Modifiers::NONE,
5040        ));
5041        assert_eq!(
5042            tap_pos.get(),
5043            Some(Point::new(30.0, 20.0)),
5044            "on_tap position must be widget-local"
5045        );
5046
5047        // A drag (down then a move past the recognizer threshold) must
5048        // also deliver widget-local coordinates.
5049        tree.dispatch_event(WidgetEvent::pointer_down(
5050            Point::new(50.0, 40.0),
5051            PointerButton::Primary,
5052            Modifiers::NONE,
5053        ));
5054        // First move crosses the recognizer threshold (DragStarted);
5055        // the second reports a known DragMoved position.
5056        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(65.0, 55.0)));
5057        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(90.0, 70.0)));
5058        assert_eq!(
5059            drag_pos.get(),
5060            Some(Point::new(70.0, 50.0)),
5061            "on_drag position must be widget-local"
5062        );
5063    }
5064
5065    #[test]
5066    fn dormant_widget_not_hit_tested() {
5067        let mut tree = WidgetTree::new();
5068        let widget = tree.add(FillWidget::new());
5069        tree.layout(SizeProposal::exact(100.0, 50.0));
5070
5071        tree.pointer_move(Point::new(50.0, 25.0));
5072        assert_eq!(tree.hovered(), Some(widget));
5073
5074        tree.set_dormant(widget);
5075        tree.pointer_move(Point::new(200.0, 200.0));
5076        tree.pointer_move(Point::new(50.0, 25.0));
5077        assert_eq!(tree.hovered(), None);
5078    }
5079
5080    #[test]
5081    fn ancestor_pointer_handler_does_not_suppress_descendant_hover() {
5082        use crate::event::EventResponse;
5083        use crate::test_widgets::StackWidget;
5084        use std::cell::Cell;
5085        use std::rc::Rc;
5086
5087        // The child reports its own hover transitions via `on_hover`.
5088        let hovered = Rc::new(Cell::new(false));
5089        let h = hovered.clone();
5090
5091        let mut tree = WidgetTree::new();
5092        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| h.set(entered)));
5093        // An ancestor whose `on_pointer_event` greedily claims everything it
5094        // previews — exactly the "drag-detecting ancestor" footgun. Before the
5095        // fix it consumed the descendant's `PointerEnter`/`Leave` in the
5096        // preview pass and the child's hover never fired.
5097        tree.add(
5098            StackWidget::new()
5099                .child(child)
5100                .on_pointer_event(|_event, _ctx| EventResponse::Handled),
5101        );
5102        tree.layout(SizeProposal::exact(100.0, 50.0));
5103
5104        tree.pointer_move(Point::new(50.0, 25.0));
5105        assert!(
5106            hovered.get(),
5107            "a greedy ancestor on_pointer_event must NOT swallow the child's PointerEnter"
5108        );
5109
5110        tree.pointer_move(Point::new(500.0, 500.0));
5111        assert!(
5112            !hovered.get(),
5113            "PointerLeave must likewise reach the child despite the ancestor"
5114        );
5115    }
5116
5117    /// **The other direction: a child must not swallow its ancestor's hover.**
5118    ///
5119    /// A row that reveals controls on hover puts interactive children inside
5120    /// itself, and the pointer leaves the row *through* one of them. The child's
5121    /// own `on_hover` used to handle the `PointerLeave` and stop the bubble there,
5122    /// so the row went on believing the pointer was still over it and kept its
5123    /// controls showing after the pointer had gone.
5124    #[test]
5125    fn a_child_hover_handler_does_not_swallow_its_ancestors() {
5126        use crate::test_widgets::StackWidget;
5127        use std::cell::Cell;
5128        use std::rc::Rc;
5129
5130        let (row, button) = (Rc::new(Cell::new(false)), Rc::new(Cell::new(false)));
5131        let (r, b) = (row.clone(), button.clone());
5132
5133        let mut tree = WidgetTree::new();
5134        let child = tree.add(FillWidget::new().on_hover(move |entered, _ctx| b.set(entered)));
5135        tree.add(
5136            StackWidget::new()
5137                .child(child)
5138                .on_hover(move |entered, _ctx| r.set(entered)),
5139        );
5140        tree.layout(SizeProposal::exact(100.0, 50.0));
5141
5142        tree.pointer_move(Point::new(50.0, 25.0));
5143        assert!(button.get(), "the child is hovered");
5144        assert!(row.get(), "and so is the row it is inside");
5145
5146        tree.pointer_move(Point::new(500.0, 500.0));
5147        assert!(!button.get(), "the child heard the leave");
5148        assert!(
5149            !row.get(),
5150            "and so did the row — a container is not still hovered because the \
5151             pointer left it through a button"
5152        );
5153    }
5154
5155    // NOTE: legacy `shortcut_intercepts_before_widget` test removed with
5156    // the ShortcutMap dispatch path. The new shortcut→intent interception
5157    // is built on top of `ShortcutRegistry` + `Action`.
5158
5159    // ── on_key_preview ──────────────────────────────────────────
5160
5161    #[test]
5162    fn key_preview_consumes_before_focused_on_key() {
5163        // root → mid → leaf (focused). Root consumes Enter via
5164        // on_key_preview; the leaf's on_key must NOT fire.
5165        use crate::event::EventResponse;
5166        use crate::test_widgets::StackWidget;
5167        use std::cell::Cell;
5168        use std::rc::Rc;
5169
5170        let leaf_fired = Rc::new(Cell::new(false));
5171        let leaf_flag = leaf_fired.clone();
5172        let preview_fired = Rc::new(Cell::new(false));
5173        let preview_flag = preview_fired.clone();
5174
5175        let mut tree = WidgetTree::new();
5176        let leaf = tree.add(FillWidget::new().focusable().on_key(move |event, _c| {
5177            // Only count KeyDown so the trailing KeyUp from
5178            // press_key doesn't trigger us spuriously.
5179            if matches!(event, WidgetEvent::KeyDown { .. }) {
5180                leaf_flag.set(true);
5181            }
5182            EventResponse::Handled
5183        }));
5184        let mid = tree.add(StackWidget::new().child(leaf));
5185        let _root = tree.add(
5186            StackWidget::new()
5187                .child(mid)
5188                .on_key_preview(move |event, _c| match event {
5189                    WidgetEvent::KeyDown {
5190                        key: Key::Enter, ..
5191                    } => {
5192                        preview_flag.set(true);
5193                        EventResponse::Handled
5194                    }
5195                    _ => EventResponse::Ignored,
5196                }),
5197        );
5198
5199        tree.layout(SizeProposal::exact(100.0, 50.0));
5200        tree.focus(leaf);
5201        tree.press_key(Key::Enter, Modifiers::NONE);
5202
5203        assert!(
5204            preview_fired.get(),
5205            "ancestor on_key_preview must fire for KeyDown on a focused descendant"
5206        );
5207        assert!(
5208            !leaf_fired.get(),
5209            "consuming the event in preview must prevent the focused widget's on_key from running"
5210        );
5211    }
5212
5213    #[test]
5214    fn key_preview_falls_through_when_returning_ignored() {
5215        // Same shape; this time the preview returns Ignored, so
5216        // the leaf's on_key must still fire.
5217        use crate::event::EventResponse;
5218        use crate::test_widgets::StackWidget;
5219        use std::cell::Cell;
5220        use std::rc::Rc;
5221
5222        let leaf_fired = Rc::new(Cell::new(false));
5223        let leaf_flag = leaf_fired.clone();
5224        let preview_fired = Rc::new(Cell::new(false));
5225        let preview_flag = preview_fired.clone();
5226
5227        let mut tree = WidgetTree::new();
5228        let leaf = tree.add(FillWidget::new().focusable().on_key(move |_e, _c| {
5229            leaf_flag.set(true);
5230            EventResponse::Handled
5231        }));
5232        let mid = tree.add(StackWidget::new().child(leaf));
5233        let _root = tree.add(
5234            StackWidget::new()
5235                .child(mid)
5236                .on_key_preview(move |_event, _c| {
5237                    preview_flag.set(true);
5238                    EventResponse::Ignored
5239                }),
5240        );
5241
5242        tree.layout(SizeProposal::exact(100.0, 50.0));
5243        tree.focus(leaf);
5244        tree.press_key(Key::Enter, Modifiers::NONE);
5245
5246        assert!(preview_fired.get(), "preview must always be invoked");
5247        assert!(
5248            leaf_fired.get(),
5249            "preview returning Ignored must not block the focused widget's on_key"
5250        );
5251    }
5252
5253    #[test]
5254    fn key_preview_excludes_focused_target_itself() {
5255        // Strict-ancestors-only: the focused widget's own
5256        // on_key_preview must NOT fire — the preview pass walks
5257        // strict ancestors only.
5258        use crate::event::EventResponse;
5259        use std::cell::Cell;
5260        use std::rc::Rc;
5261
5262        let preview_on_target = Rc::new(Cell::new(false));
5263        let pf = preview_on_target.clone();
5264
5265        let mut tree = WidgetTree::new();
5266        let leaf = tree.add(FillWidget::new().focusable().on_key_preview(move |_e, _c| {
5267            pf.set(true);
5268            EventResponse::Handled
5269        }));
5270        tree.layout(SizeProposal::exact(100.0, 50.0));
5271        tree.focus(leaf);
5272        tree.press_key(Key::Enter, Modifiers::NONE);
5273
5274        assert!(
5275            !preview_on_target.get(),
5276            "the focused widget itself must not see its own on_key_preview"
5277        );
5278    }
5279
5280    #[test]
5281    fn key_preview_root_to_target_order() {
5282        // Two ancestors with on_key_preview attached. The outer
5283        // (root-side) one must fire first; the closer one (still
5284        // ancestor of the focused leaf) fires second.
5285        use crate::event::EventResponse;
5286        use crate::test_widgets::StackWidget;
5287        use std::cell::RefCell;
5288        use std::rc::Rc;
5289
5290        let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
5291        let outer_log = order.clone();
5292        let inner_log = order.clone();
5293
5294        let mut tree = WidgetTree::new();
5295        let leaf = tree.add(FillWidget::new().focusable());
5296        let inner = tree.add(
5297            StackWidget::new()
5298                .child(leaf)
5299                .on_key_preview(move |event, _c| {
5300                    if matches!(event, WidgetEvent::KeyDown { .. }) {
5301                        inner_log.borrow_mut().push("inner");
5302                    }
5303                    EventResponse::Ignored
5304                }),
5305        );
5306        let _outer = tree.add(
5307            StackWidget::new()
5308                .child(inner)
5309                .on_key_preview(move |event, _c| {
5310                    if matches!(event, WidgetEvent::KeyDown { .. }) {
5311                        outer_log.borrow_mut().push("outer");
5312                    }
5313                    EventResponse::Ignored
5314                }),
5315        );
5316
5317        tree.layout(SizeProposal::exact(100.0, 50.0));
5318        tree.focus(leaf);
5319        tree.dispatch_event(WidgetEvent::KeyDown {
5320            key: Key::Enter,
5321            modifiers: Modifiers::NONE,
5322            text: None,
5323        });
5324
5325        assert_eq!(
5326            *order.borrow(),
5327            vec!["outer", "inner"],
5328            "preview must walk root → parent-of-target"
5329        );
5330    }
5331
5332    #[test]
5333    fn access_action_routes_to_cursored_target_not_focus() {
5334        // VoiceOver's VO+Space targets the node under the AT cursor (`b`),
5335        // even when keyboard focus is on a different control (`a`). The action
5336        // must fire on `b`, never get redirected to the focused `a`.
5337        use crate::signal::Signal;
5338        let a_fired = Signal::new(false);
5339        let b_fired = Signal::new(false);
5340        let a_cb = a_fired.clone();
5341        let b_cb = b_fired.clone();
5342
5343        let mut tree = WidgetTree::new();
5344        let a = tree.add(
5345            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| a_cb.set(true)),
5346        );
5347        let b = tree.add(
5348            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| b_cb.set(true)),
5349        );
5350        tree.layout(SizeProposal::exact(200.0, 100.0));
5351
5352        tree.focus(a);
5353        tree.dispatch_event(WidgetEvent::AccessAction {
5354            action: accesskit::Action::Click,
5355            target: Some(b),
5356            target_node: crate::accessibility::widget_id_to_node_id(b),
5357            data: None,
5358        });
5359
5360        assert!(b_fired.get(), "the cursored target must receive the action");
5361        assert!(
5362            !a_fired.get(),
5363            "the keyboard-focused widget must NOT receive an action targeting another node"
5364        );
5365    }
5366
5367    #[test]
5368    fn access_action_without_target_is_dropped_not_redirected_to_focus() {
5369        // An action with no (or an inactive) target must be dropped — never
5370        // silently re-routed to whatever holds keyboard focus.
5371        use crate::signal::Signal;
5372        let fired = Signal::new(false);
5373        let cb = fired.clone();
5374
5375        let mut tree = WidgetTree::new();
5376        let widget = tree.add(
5377            FillWidget::new().access_action(accesskit::Action::Click, move |_ctx| cb.set(true)),
5378        );
5379        tree.layout(SizeProposal::exact(200.0, 100.0));
5380
5381        tree.focus(widget);
5382        tree.dispatch_event(WidgetEvent::AccessAction {
5383            action: accesskit::Action::Click,
5384            target: None,
5385            target_node: crate::accessibility::root_node_id(),
5386            data: None,
5387        });
5388
5389        assert!(
5390            !fired.get(),
5391            "a target-less action must not be redirected to the focused widget"
5392        );
5393    }
5394
5395    // NOTE: legacy `scoped_shortcut_fires_when_focused_in_subtree` test
5396    // removed along with the ShortcutMap dispatch path. Scope-aware
5397    // dispatch is handled by the new ShortcutRegistry.
5398
5399    // --- Intent / Action dispatch ------------------------------
5400
5401    #[test]
5402    fn shortcut_fires_matching_action_on_source_widget() {
5403        use crate::action::Action;
5404        use crate::shortcut::{KeyStroke, Shortcut};
5405        use std::cell::Cell;
5406        use std::rc::Rc;
5407
5408        let fired = Rc::new(Cell::new(false));
5409        let fired_flag = fired.clone();
5410
5411        let mut tree = WidgetTree::new();
5412        let widget = tree.add(FillWidget::new().focusable());
5413        tree.push_action(
5414            widget,
5415            Action::new("app.save").on_invoke(move |_intent, _ctx| {
5416                fired_flag.set(true);
5417            }),
5418        );
5419        tree.shortcut_registry_mut().register(
5420            Shortcut::new("app.save")
5421                .primary(KeyStroke::command(Key::S))
5422                .build(),
5423        );
5424
5425        tree.layout(SizeProposal::exact(100.0, 50.0));
5426        tree.focus(widget);
5427
5428        tree.press_key(Key::S, Modifiers::COMMAND);
5429        assert!(fired.get(), "matching action must fire on KeyDown");
5430    }
5431
5432    #[test]
5433    fn global_shortcut_fires_without_focused_widget() {
5434        use crate::action::Action;
5435        use crate::shortcut::{KeyStroke, Shortcut};
5436        use std::cell::Cell;
5437        use std::rc::Rc;
5438
5439        // Regression: a global shortcut must fire even when no widget
5440        // is focused. A root-registered action should still receive
5441        // the intent (anchored at the root as a fallback).
5442        let fired = Rc::new(Cell::new(false));
5443        let fired_flag = fired.clone();
5444
5445        let mut tree = WidgetTree::new();
5446        let root = tree.add(FillWidget::new());
5447        tree.push_action(
5448            root,
5449            Action::new("app.save").on_invoke(move |_intent, _ctx| {
5450                fired_flag.set(true);
5451            }),
5452        );
5453        tree.shortcut_registry_mut().register(
5454            Shortcut::new("app.save")
5455                .primary(KeyStroke::command(Key::S))
5456                .build(),
5457        );
5458
5459        tree.layout(SizeProposal::exact(100.0, 50.0));
5460        // Deliberately no focus() call.
5461
5462        tree.press_key(Key::S, Modifiers::COMMAND);
5463        assert!(
5464            fired.get(),
5465            "global shortcut must fire without a focused widget"
5466        );
5467    }
5468
5469    #[test]
5470    fn global_shortcut_fires_after_focused_widget_destroyed() {
5471        use crate::action::Action;
5472        use crate::shortcut::{KeyStroke, Shortcut};
5473        use std::cell::Cell;
5474        use std::rc::Rc;
5475
5476        // Regression: if the focused widget is destroyed (e.g. during a
5477        // rebuild after a settings-panel rebind), focus must be cleared
5478        // so the next global shortcut falls through to the root-anchor
5479        // path instead of dispatching from a stale, destroyed id.
5480        let fired = Rc::new(Cell::new(false));
5481        let fired_flag = fired.clone();
5482
5483        let mut tree = WidgetTree::new();
5484        let root = tree.add(FillWidget::new());
5485        let focusable = tree.add_child(root, FillWidget::new().focusable());
5486        tree.push_action(
5487            root,
5488            Action::new("app.save").on_invoke(move |_intent, _ctx| {
5489                fired_flag.set(true);
5490            }),
5491        );
5492        tree.shortcut_registry_mut().register(
5493            Shortcut::new("app.save")
5494                .primary(KeyStroke::command(Key::S))
5495                .build(),
5496        );
5497
5498        tree.layout(SizeProposal::exact(100.0, 50.0));
5499        tree.focus(focusable);
5500        assert_eq!(tree.focused(), Some(focusable));
5501
5502        // Destroy the focused subtree (simulates a rebuild that drops
5503        // the currently-focused Rebind button).
5504        tree.destroy_subtree(focusable);
5505        assert_eq!(tree.focused(), None, "focus must clear when destroyed");
5506
5507        tree.press_key(Key::S, Modifiers::COMMAND);
5508        assert!(
5509            fired.get(),
5510            "global shortcut must still fire after the focused widget is destroyed"
5511        );
5512    }
5513
5514    #[test]
5515    fn scoped_shortcut_matches_only_when_focus_in_scope() {
5516        use crate::action::Action;
5517        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5518        use std::cell::Cell;
5519        use std::rc::Rc;
5520
5521        let fired = Rc::new(Cell::new(0));
5522        let fired_flag = fired.clone();
5523
5524        let mut tree = WidgetTree::new();
5525        let scope_root = tree.add(FillWidget::new().focusable());
5526        let inside = tree.add_child(scope_root, FillWidget::new().focusable());
5527        let outside = tree.add(FillWidget::new().focusable());
5528
5529        tree.push_action(
5530            scope_root,
5531            Action::new("editor.find").on_invoke(move |_i, _c| {
5532                fired_flag.set(fired_flag.get() + 1);
5533            }),
5534        );
5535        tree.shortcut_registry_mut().register(
5536            Shortcut::new("editor.find")
5537                .primary(KeyStroke::command(Key::F))
5538                .scope(ShortcutScope::Scoped(scope_root))
5539                .build(),
5540        );
5541
5542        tree.layout(SizeProposal::exact(200.0, 100.0));
5543
5544        // Focus outside the scope: the shortcut does NOT activate.
5545        tree.focus(outside);
5546        tree.press_key(Key::F, Modifiers::COMMAND);
5547        assert_eq!(
5548            fired.get(),
5549            0,
5550            "scoped shortcut must not fire outside scope"
5551        );
5552
5553        // Focus inside the scope: it fires.
5554        tree.focus(inside);
5555        tree.press_key(Key::F, Modifiers::COMMAND);
5556        assert_eq!(
5557            fired.get(),
5558            1,
5559            "scoped shortcut must fire when focus in scope"
5560        );
5561    }
5562
5563    #[test]
5564    fn same_chord_scoped_first_falls_back_to_global_when_focus_outside() {
5565        // Defect 1: a Scoped binding that sorts first by id must NOT
5566        // shadow the slot when focus is outside its subtree — the
5567        // applicable Global binding fires instead. (`editor.saveBlock`
5568        // < `zzz.global.save`, so the scoped one wins the id-order race
5569        // that `find_by_keystroke` used to settle on.)
5570        use crate::action::Action;
5571        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5572        use std::cell::Cell;
5573        use std::rc::Rc;
5574
5575        let scoped_fired = Rc::new(Cell::new(0));
5576        let global_fired = Rc::new(Cell::new(0));
5577        let sf = scoped_fired.clone();
5578        let gf = global_fired.clone();
5579
5580        let mut tree = WidgetTree::new();
5581        let root = tree.add(FillWidget::new());
5582        let editor = tree.add_child(root, FillWidget::new().focusable());
5583        let _editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5584        let sidebar = tree.add_child(root, FillWidget::new().focusable());
5585
5586        tree.push_action(
5587            editor,
5588            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5589        );
5590        tree.push_action(
5591            root,
5592            Action::new("zzz.global.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5593        );
5594        tree.shortcut_registry_mut().register(
5595            Shortcut::new("editor.saveBlock")
5596                .primary(KeyStroke::command(Key::S))
5597                .scope(ShortcutScope::Scoped(editor))
5598                .build(),
5599        );
5600        tree.shortcut_registry_mut().register(
5601            Shortcut::new("zzz.global.save")
5602                .primary(KeyStroke::command(Key::S))
5603                .build(),
5604        );
5605
5606        tree.layout(SizeProposal::exact(200.0, 100.0));
5607        tree.focus(sidebar);
5608        tree.press_key(Key::S, Modifiers::COMMAND);
5609
5610        assert_eq!(global_fired.get(), 1, "applicable global must fire");
5611        assert_eq!(
5612            scoped_fired.get(),
5613            0,
5614            "inapplicable scoped binding must not eat the chord"
5615        );
5616    }
5617
5618    #[test]
5619    fn same_chord_global_first_yields_to_scoped_when_focus_inside() {
5620        // Defect 2: a Global binding that sorts first by id must yield to
5621        // an in-focus Scoped binding (most-specific-scope wins), then
5622        // reclaim the chord once focus leaves the scope. (`app.save` <
5623        // `editor.saveBlock`, so the global one wins id order.)
5624        use crate::action::Action;
5625        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
5626        use std::cell::Cell;
5627        use std::rc::Rc;
5628
5629        let scoped_fired = Rc::new(Cell::new(0));
5630        let global_fired = Rc::new(Cell::new(0));
5631        let sf = scoped_fired.clone();
5632        let gf = global_fired.clone();
5633
5634        let mut tree = WidgetTree::new();
5635        let root = tree.add(FillWidget::new());
5636        let editor = tree.add_child(root, FillWidget::new().focusable());
5637        let editor_inner = tree.add_child(editor, FillWidget::new().focusable());
5638        let sidebar = tree.add_child(root, FillWidget::new().focusable());
5639
5640        tree.push_action(
5641            editor,
5642            Action::new("editor.saveBlock").on_invoke(move |_i, _c| sf.set(sf.get() + 1)),
5643        );
5644        tree.push_action(
5645            root,
5646            Action::new("app.save").on_invoke(move |_i, _c| gf.set(gf.get() + 1)),
5647        );
5648        tree.shortcut_registry_mut().register(
5649            Shortcut::new("app.save")
5650                .primary(KeyStroke::command(Key::S))
5651                .build(),
5652        );
5653        tree.shortcut_registry_mut().register(
5654            Shortcut::new("editor.saveBlock")
5655                .primary(KeyStroke::command(Key::S))
5656                .scope(ShortcutScope::Scoped(editor))
5657                .build(),
5658        );
5659
5660        tree.layout(SizeProposal::exact(200.0, 100.0));
5661
5662        // Focus inside the editor: the scoped binding wins over global.
5663        tree.focus(editor_inner);
5664        tree.press_key(Key::S, Modifiers::COMMAND);
5665        assert_eq!(
5666            scoped_fired.get(),
5667            1,
5668            "in-focus scoped must win over global"
5669        );
5670        assert_eq!(
5671            global_fired.get(),
5672            0,
5673            "global must yield to the scoped binding"
5674        );
5675
5676        // Focus outside the editor: global reclaims the chord.
5677        tree.focus(sidebar);
5678        tree.press_key(Key::S, Modifiers::COMMAND);
5679        assert_eq!(scoped_fired.get(), 1, "scoped stays put outside its scope");
5680        assert_eq!(
5681            global_fired.get(),
5682            1,
5683            "global fires when focus leaves the scope"
5684        );
5685    }
5686
5687    #[test]
5688    fn propagated_action_lets_ancestor_handle() {
5689        use crate::action::Action;
5690        use crate::intent::IntentResponse;
5691        use crate::shortcut::{KeyStroke, Shortcut};
5692        use std::cell::Cell;
5693        use std::rc::Rc;
5694
5695        let inner_seen = Rc::new(Cell::new(false));
5696        let outer_seen = Rc::new(Cell::new(false));
5697        let inner_flag = inner_seen.clone();
5698        let outer_flag = outer_seen.clone();
5699
5700        let mut tree = WidgetTree::new();
5701        let outer = tree.add(FillWidget::new().focusable());
5702        let inner = tree.add_child(outer, FillWidget::new().focusable());
5703
5704        // Inner observes then propagates; outer consumes.
5705        tree.push_action(
5706            inner,
5707            Action::new("app.save").on_invoke_with_response(move |_i, _c| {
5708                inner_flag.set(true);
5709                IntentResponse::Propagated
5710            }),
5711        );
5712        tree.push_action(
5713            outer,
5714            Action::new("app.save").on_invoke(move |_i, _c| {
5715                outer_flag.set(true);
5716            }),
5717        );
5718        tree.shortcut_registry_mut().register(
5719            Shortcut::new("app.save")
5720                .primary(KeyStroke::command(Key::S))
5721                .build(),
5722        );
5723
5724        tree.layout(SizeProposal::exact(100.0, 50.0));
5725        tree.focus(inner);
5726
5727        tree.press_key(Key::S, Modifiers::COMMAND);
5728        assert!(inner_seen.get(), "inner action observed the intent");
5729        assert!(outer_seen.get(), "outer action reached after Propagated");
5730    }
5731
5732    #[test]
5733    fn handled_action_stops_propagation() {
5734        use crate::action::Action;
5735        use crate::shortcut::{KeyStroke, Shortcut};
5736        use std::cell::Cell;
5737        use std::rc::Rc;
5738
5739        let inner_seen = Rc::new(Cell::new(false));
5740        let outer_seen = Rc::new(Cell::new(false));
5741        let inner_flag = inner_seen.clone();
5742        let outer_flag = outer_seen.clone();
5743
5744        let mut tree = WidgetTree::new();
5745        let outer = tree.add(FillWidget::new().focusable());
5746        let inner = tree.add_child(outer, FillWidget::new().focusable());
5747
5748        tree.push_action(
5749            inner,
5750            Action::new("app.save").on_invoke(move |_i, _c| {
5751                inner_flag.set(true);
5752            }),
5753        );
5754        tree.push_action(
5755            outer,
5756            Action::new("app.save").on_invoke(move |_i, _c| {
5757                outer_flag.set(true);
5758            }),
5759        );
5760        tree.shortcut_registry_mut().register(
5761            Shortcut::new("app.save")
5762                .primary(KeyStroke::command(Key::S))
5763                .build(),
5764        );
5765
5766        tree.layout(SizeProposal::exact(100.0, 50.0));
5767        tree.focus(inner);
5768
5769        tree.press_key(Key::S, Modifiers::COMMAND);
5770        assert!(inner_seen.get());
5771        assert!(!outer_seen.get(), "Handled at inner must stop propagation");
5772    }
5773
5774    #[test]
5775    fn disabled_action_propagates_by_default() {
5776        use crate::action::Action;
5777        use crate::shortcut::{KeyStroke, Shortcut};
5778        use crate::signal::Signal;
5779        use std::cell::Cell;
5780        use std::rc::Rc;
5781
5782        let inner_seen = Rc::new(Cell::new(false));
5783        let outer_seen = Rc::new(Cell::new(false));
5784        let inner_flag = inner_seen.clone();
5785        let outer_flag = outer_seen.clone();
5786
5787        let mut tree = WidgetTree::new();
5788        let outer = tree.add(FillWidget::new().focusable());
5789        let inner = tree.add_child(outer, FillWidget::new().focusable());
5790
5791        let enabled = Signal::new(false);
5792        tree.push_action(
5793            inner,
5794            Action::new("app.save")
5795                .enabled_when(enabled.clone())
5796                .on_invoke(move |_i, _c| {
5797                    inner_flag.set(true);
5798                }),
5799        );
5800        tree.push_action(
5801            outer,
5802            Action::new("app.save").on_invoke(move |_i, _c| {
5803                outer_flag.set(true);
5804            }),
5805        );
5806        tree.shortcut_registry_mut().register(
5807            Shortcut::new("app.save")
5808                .primary(KeyStroke::command(Key::S))
5809                .build(),
5810        );
5811
5812        tree.layout(SizeProposal::exact(100.0, 50.0));
5813        tree.focus(inner);
5814
5815        tree.press_key(Key::S, Modifiers::COMMAND);
5816        assert!(!inner_seen.get(), "disabled inner must not run");
5817        assert!(
5818            outer_seen.get(),
5819            "intent must propagate past disabled inner"
5820        );
5821    }
5822
5823    #[test]
5824    fn disabled_action_with_non_propagating_shortcut_consumes() {
5825        use crate::action::Action;
5826        use crate::shortcut::{KeyStroke, Shortcut};
5827        use crate::signal::Signal;
5828        use std::cell::Cell;
5829        use std::rc::Rc;
5830
5831        let inner_seen = Rc::new(Cell::new(false));
5832        let outer_seen = Rc::new(Cell::new(false));
5833        let inner_flag = inner_seen.clone();
5834        let outer_flag = outer_seen.clone();
5835
5836        let mut tree = WidgetTree::new();
5837        let outer = tree.add(FillWidget::new().focusable());
5838        let inner = tree.add_child(outer, FillWidget::new().focusable());
5839
5840        let enabled = Signal::new(false);
5841        tree.push_action(
5842            inner,
5843            Action::new("app.save")
5844                .enabled_when(enabled.clone())
5845                .on_invoke(move |_i, _c| {
5846                    inner_flag.set(true);
5847                }),
5848        );
5849        tree.push_action(
5850            outer,
5851            Action::new("app.save").on_invoke(move |_i, _c| {
5852                outer_flag.set(true);
5853            }),
5854        );
5855        tree.shortcut_registry_mut().register(
5856            Shortcut::new("app.save")
5857                .primary(KeyStroke::command(Key::S))
5858                .propagate_when_disabled(false)
5859                .build(),
5860        );
5861
5862        tree.layout(SizeProposal::exact(100.0, 50.0));
5863        tree.focus(inner);
5864
5865        tree.press_key(Key::S, Modifiers::COMMAND);
5866        assert!(!inner_seen.get(), "disabled inner still does not run");
5867        assert!(
5868            !outer_seen.get(),
5869            "intent must NOT propagate when shortcut disallows it"
5870        );
5871    }
5872
5873    #[test]
5874    fn send_intent_from_handler_reaches_ancestor_action() {
5875        use crate::action::Action;
5876        use crate::intent::Intent;
5877        use std::cell::Cell;
5878        use std::rc::Rc;
5879
5880        let save_seen = Rc::new(Cell::new(false));
5881        let save_flag = save_seen.clone();
5882
5883        let mut tree = WidgetTree::new();
5884        let root = tree.add(FillWidget::new());
5885        let button = tree.add_child(
5886            root,
5887            FillWidget::new().on_tap(|_pos, ctx| {
5888                ctx.send_intent(Intent::new("app.save"));
5889            }),
5890        );
5891        tree.push_action(
5892            root,
5893            Action::new("app.save").on_invoke(move |_i, _c| {
5894                save_flag.set(true);
5895            }),
5896        );
5897
5898        tree.layout(SizeProposal::exact(100.0, 50.0));
5899        tree.click(button);
5900        assert!(
5901            save_seen.get(),
5902            "ctx.send_intent must reach ancestor action"
5903        );
5904    }
5905
5906    #[test]
5907    fn widget_type_histogram_counts_distinct_types() {
5908        // The histogram surfaces concrete widget types
5909        // by std::any::type_name_of_val. Widgets become active
5910        // after the first layout pass, so we run that before
5911        // checking the histogram.
5912        let mut tree = WidgetTree::new();
5913        let _ = tree.add(FillWidget::new());
5914        let _ = tree.add(FillWidget::new());
5915        let _ = tree.add(FillWidget::new());
5916        tree.layout(SizeProposal::exact(100.0, 100.0));
5917        let histogram = tree.widget_type_histogram();
5918        let total: u32 = histogram.values().sum();
5919        assert!(
5920            total >= 3,
5921            "expected at least 3 active widgets, got {total}: {histogram:?}"
5922        );
5923        let fillwidget_entries: u32 = histogram
5924            .iter()
5925            .filter(|(k, _)| k.contains("FillWidget"))
5926            .map(|(_, v)| *v)
5927            .sum();
5928        assert!(
5929            fillwidget_entries >= 3,
5930            "expected ≥3 FillWidget instances; histogram = {histogram:?}"
5931        );
5932        assert_eq!(tree.active_widget_count() as u32, total);
5933    }
5934
5935    #[test]
5936    fn intent_source_tagged_handler_for_tap_activation() {
5937        // A tap-driven `ctx.send_intent` must surface as
5938        // `IntentSource::Handler` to ancestor actions, not the
5939        // `Programmatic` default of `Intent::new`.
5940        use crate::action::Action;
5941        use crate::intent::Intent;
5942        use crate::telemetry::IntentSource;
5943        use std::cell::Cell;
5944        use std::rc::Rc;
5945        let captured = Rc::new(Cell::new(IntentSource::Unknown));
5946        let captured_for_action = captured.clone();
5947
5948        let mut tree = WidgetTree::new();
5949        let root = tree.add(FillWidget::new());
5950        let button = tree.add_child(
5951            root,
5952            FillWidget::new().on_tap(|_pos, ctx| {
5953                ctx.send_intent(Intent::new("app.save"));
5954            }),
5955        );
5956        tree.push_action(
5957            root,
5958            Action::new("app.save").on_invoke(move |intent, _c| {
5959                captured_for_action.set(intent.source);
5960            }),
5961        );
5962
5963        tree.layout(SizeProposal::exact(100.0, 50.0));
5964        tree.click(button);
5965        assert_eq!(
5966            captured.get(),
5967            IntentSource::Handler,
5968            "tap-driven intent must tag IntentSource::Handler"
5969        );
5970    }
5971
5972    #[test]
5973    fn intent_source_programmatic_when_no_handler_active() {
5974        use crate::intent::Intent;
5975        use crate::telemetry::IntentSource;
5976        let intent = Intent::new("app.demo");
5977        assert_eq!(intent.source, IntentSource::Programmatic);
5978
5979        // ctx.send_intent without a handler scope keeps it Programmatic.
5980        let mut ctx = EventContext::new();
5981        ctx.send_intent(Intent::new("app.demo"));
5982        let queued = ctx.pending_intents.first().expect("intent queued");
5983        assert_eq!(queued.source, IntentSource::Programmatic);
5984    }
5985
5986    #[test]
5987    fn with_intent_source_overrides_for_managed_widgets() {
5988        use crate::intent::Intent;
5989        use crate::telemetry::IntentSource;
5990        let mut ctx = EventContext::new();
5991        ctx.with_intent_source(IntentSource::Menu, |ctx| {
5992            ctx.send_intent(Intent::new("app.demo"));
5993        });
5994        let queued = ctx.pending_intents.first().expect("intent queued");
5995        assert_eq!(
5996            queued.source,
5997            IntentSource::Menu,
5998            "with_intent_source(Menu) must tag the dispatched intent"
5999        );
6000
6001        // After the closure returns, current_source is restored —
6002        // a follow-up send_intent without a wrapping closure goes
6003        // back to the default (no override).
6004        ctx.send_intent(Intent::new("app.next"));
6005        let next = ctx.pending_intents.last().expect("second intent");
6006        assert_eq!(next.source, IntentSource::Programmatic);
6007    }
6008
6009    #[test]
6010    fn disabled_shortcut_falls_through_to_focused_widget() {
6011        use crate::action::Action;
6012        use crate::shortcut::{KeyStroke, Shortcut};
6013        use crate::signal::Signal;
6014        use std::cell::Cell;
6015        use std::rc::Rc;
6016
6017        let action_fired = Rc::new(Cell::new(false));
6018        let on_key_fired = Rc::new(Cell::new(false));
6019        let af = action_fired.clone();
6020        let kf = on_key_fired.clone();
6021
6022        let enabled = Signal::new(false);
6023
6024        let mut tree = WidgetTree::new();
6025        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
6026            if matches!(
6027                event,
6028                WidgetEvent::KeyDown {
6029                    key: Key::S,
6030                    modifiers,
6031                    ..
6032                } if modifiers.command()
6033            ) {
6034                kf.set(true);
6035                return EventResponse::Handled;
6036            }
6037            EventResponse::Ignored
6038        }));
6039        tree.push_action(
6040            widget,
6041            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6042        );
6043        tree.shortcut_registry_mut().register(
6044            Shortcut::new("app.save")
6045                .primary(KeyStroke::command(Key::S))
6046                .enabled_when(enabled.clone())
6047                .build(),
6048        );
6049
6050        tree.layout(SizeProposal::exact(100.0, 50.0));
6051        tree.focus(widget);
6052
6053        // Disabled: keystroke falls through to on_key.
6054        tree.press_key(Key::S, Modifiers::COMMAND);
6055        assert!(
6056            !action_fired.get(),
6057            "disabled shortcut must not invoke its action"
6058        );
6059        assert!(
6060            on_key_fired.get(),
6061            "disabled shortcut must let KeyDown reach the focused widget"
6062        );
6063
6064        // Re-enable → action fires, on_key does not.
6065        on_key_fired.set(false);
6066        enabled.set(true);
6067        tree.press_key(Key::S, Modifiers::COMMAND);
6068        assert!(action_fired.get(), "re-enabled shortcut must dispatch");
6069        assert!(
6070            !on_key_fired.get(),
6071            "enabled shortcut must consume the KeyDown"
6072        );
6073    }
6074
6075    #[test]
6076    fn keyboard_capture_bypasses_shortcut() {
6077        use crate::action::Action;
6078        use crate::shortcut::{KeyStroke, Shortcut};
6079        use std::cell::Cell;
6080        use std::rc::Rc;
6081
6082        // A focused keyboard-capture surface (e.g. a terminal) must receive
6083        // the accelerator chord itself (⌘S on macOS, Ctrl+S elsewhere), even
6084        // though an ENABLED global shortcut binds it — the whole point of
6085        // GAP 1. A non-capturing widget must yield to the shortcut (the
6086        // control case).
6087        fn run(capture: bool) -> (bool, bool) {
6088            let action_fired = Rc::new(Cell::new(false));
6089            let on_key_fired = Rc::new(Cell::new(false));
6090            let af = action_fired.clone();
6091            let kf = on_key_fired.clone();
6092
6093            let mut tree = WidgetTree::new();
6094            let widget = tree.add(
6095                FillWidget::new()
6096                    .focusable()
6097                    .keyboard_capture(capture)
6098                    .on_key(move |event, _ctx| {
6099                        if matches!(
6100                            event,
6101                            WidgetEvent::KeyDown { key: Key::S, modifiers, .. } if modifiers.command()
6102                        ) {
6103                            kf.set(true);
6104                            return EventResponse::Handled;
6105                        }
6106                        EventResponse::Ignored
6107                    }),
6108            );
6109            tree.push_action(
6110                widget,
6111                Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6112            );
6113            tree.shortcut_registry_mut().register(
6114                Shortcut::new("app.save")
6115                    .primary(KeyStroke::command(Key::S))
6116                    .build(),
6117            );
6118
6119            tree.layout(SizeProposal::exact(100.0, 50.0));
6120            tree.focus(widget);
6121            tree.press_key(Key::S, Modifiers::COMMAND);
6122            (action_fired.get(), on_key_fired.get())
6123        }
6124
6125        // Capture on: the shortcut is bypassed, the widget sees the key.
6126        let (action, on_key) = run(true);
6127        assert!(
6128            !action,
6129            "keyboard_capture must suppress the shortcut action"
6130        );
6131        assert!(on_key, "keyboard_capture must deliver the raw KeyDown");
6132
6133        // Capture off (control): the shortcut consumes the key.
6134        let (action, on_key) = run(false);
6135        assert!(action, "without capture the shortcut must fire");
6136        assert!(!on_key, "without capture the widget must not see the key");
6137    }
6138
6139    /// A focused widget that records every `KeyDown` it is handed.
6140    fn key_recorder(
6141        tree: &mut WidgetTree,
6142    ) -> (WidgetId, std::rc::Rc<std::cell::RefCell<Vec<Key>>>) {
6143        let seen = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
6144        let log = seen.clone();
6145        let widget = tree.add(FillWidget::new().focusable().on_key(move |event, _ctx| {
6146            if let WidgetEvent::KeyDown { key, .. } = event {
6147                log.borrow_mut().push(*key);
6148            }
6149            EventResponse::Ignored
6150        }));
6151        tree.layout(SizeProposal::exact(100.0, 50.0));
6152        tree.focus(widget);
6153        (widget, seen)
6154    }
6155
6156    #[test]
6157    fn a_shortcut_no_action_takes_leaves_its_key_to_the_focused_widget() {
6158        use crate::action::Action;
6159        use crate::intent::IntentResponse;
6160        use crate::shortcut::{KeyStroke, Shortcut};
6161
6162        let mut tree = WidgetTree::new();
6163        let (widget, seen) = key_recorder(&mut tree);
6164        tree.shortcut_registry_mut().register(
6165            Shortcut::new("app.save")
6166                .primary(KeyStroke::command(Key::S))
6167                .build(),
6168        );
6169
6170        // Nothing handles `app.save`.
6171        tree.press_key(Key::S, Modifiers::COMMAND);
6172        assert_eq!(
6173            *seen.borrow(),
6174            vec![Key::S],
6175            "an unhandled intent must not eat the key"
6176        );
6177
6178        // An action that runs and passes the intent on has not consumed it.
6179        tree.push_action(
6180            widget,
6181            Action::new("app.save").on_invoke_with_response(|_i, _c| IntentResponse::Propagated),
6182        );
6183        tree.press_key(Key::S, Modifiers::COMMAND);
6184        assert_eq!(
6185            seen.borrow().len(),
6186            2,
6187            "a propagated intent must leave the key too"
6188        );
6189
6190        // Deleting the fall-through makes both of the above go red; this is
6191        // the control: a consuming action still takes the key.
6192        let mut tree = WidgetTree::new();
6193        let (widget, seen) = key_recorder(&mut tree);
6194        tree.shortcut_registry_mut().register(
6195            Shortcut::new("app.save")
6196                .primary(KeyStroke::command(Key::S))
6197                .build(),
6198        );
6199        tree.push_action(widget, Action::new("app.save").on_invoke(|_i, _c| {}));
6200        tree.press_key(Key::S, Modifiers::COMMAND);
6201        assert!(
6202            seen.borrow().is_empty(),
6203            "a handled shortcut consumes its key"
6204        );
6205    }
6206
6207    #[test]
6208    fn standard_edit_chords_follow_a_rebind() {
6209        use crate::action::Action;
6210        use crate::shortcut::{KeyStroke, StandardEdit};
6211        use std::cell::Cell;
6212        use std::rc::Rc;
6213
6214        let mut tree = WidgetTree::new();
6215        let (widget, seen) = key_recorder(&mut tree);
6216        let copies = Rc::new(Cell::new(0));
6217        let count = copies.clone();
6218        tree.push_action(
6219            widget,
6220            Action::new(StandardEdit::Copy.id())
6221                .on_invoke(move |_i, _c| count.set(count.get() + 1)),
6222        );
6223
6224        // Registered by `WidgetTree::new`, with no help from the app.
6225        tree.press_key(Key::C, Modifiers::COMMAND);
6226        assert_eq!(copies.get(), 1, "Ctrl+C reaches the edit.copy action");
6227        assert!(seen.borrow().is_empty());
6228
6229        // Move Copy elsewhere: the old chord no longer copies (it reaches the
6230        // widget as a plain key), the new one does.
6231        let elsewhere = KeyStroke::new(Key::Character('à'), Modifiers::CTRL);
6232        tree.shortcut_registry_mut()
6233            .rebind_primary(StandardEdit::Copy.id(), Some(elsewhere));
6234        tree.press_key(Key::C, Modifiers::COMMAND);
6235        assert_eq!(copies.get(), 1, "the replaced chord must not copy");
6236        assert_eq!(*seen.borrow(), vec![Key::C]);
6237        tree.press_key(Key::Character('à'), Modifiers::CTRL);
6238        assert_eq!(copies.get(), 2, "the new chord copies");
6239    }
6240
6241    #[test]
6242    fn ctrl_tab_always_escapes_a_keyboard_capture_surface() {
6243        use std::cell::Cell;
6244        use std::rc::Rc;
6245
6246        // WCAG 2.1.2. A capture surface answers `Handled` to every key —
6247        // that is what it is for — so the "cycle focus only when the focused
6248        // widget did not handle Tab" rule can never get focus out of one.
6249        // Ctrl+Tab / Ctrl+Shift+Tab are therefore reserved by the dispatcher
6250        // and never reach the widget at all.
6251        let saw_key = Rc::new(Cell::new(false));
6252        let sk = saw_key.clone();
6253
6254        let mut tree = WidgetTree::new();
6255        let capture = tree.add(
6256            FillWidget::new()
6257                .focusable()
6258                .keyboard_capture(true)
6259                // The greediest possible handler: everything is consumed.
6260                .on_key(move |_event, _ctx| {
6261                    sk.set(true);
6262                    EventResponse::Handled
6263                }),
6264        );
6265        let neighbour = tree.add(FillWidget::new().focusable());
6266        tree.layout(SizeProposal::exact(100.0, 50.0));
6267
6268        // Plain Tab stays inside: the widget consumed it (a terminal writes
6269        // it to the child as `\t`).
6270        tree.focus(capture);
6271        tree.press_key(Key::Tab, Modifiers::NONE);
6272        assert!(saw_key.get(), "plain Tab must reach the capture surface");
6273        assert_eq!(
6274            tree.focused(),
6275            Some(capture),
6276            "plain Tab must not move focus off a capture surface"
6277        );
6278
6279        // Ctrl+Tab escapes forward, without the widget ever seeing it.
6280        saw_key.set(false);
6281        tree.press_key(Key::Tab, Modifiers::CTRL);
6282        assert!(
6283            !saw_key.get(),
6284            "Ctrl+Tab is reserved and must not reach the capture surface"
6285        );
6286        assert_eq!(
6287            tree.focused(),
6288            Some(neighbour),
6289            "Ctrl+Tab must move focus out of a capture surface"
6290        );
6291
6292        // And backwards.
6293        tree.focus(capture);
6294        tree.press_key(Key::Tab, Modifiers::CTRL | Modifiers::SHIFT);
6295        assert_eq!(
6296            tree.focused(),
6297            Some(neighbour),
6298            "Ctrl+Shift+Tab must move focus out of a capture surface"
6299        );
6300    }
6301
6302    #[test]
6303    fn scope_mismatch_does_not_invoke_on_activate() {
6304        use crate::intent::Intent;
6305        use crate::shortcut::{KeyStroke, Shortcut, ShortcutScope};
6306        use std::cell::Cell;
6307        use std::rc::Rc;
6308
6309        // Regression: before the find/invoke split, `on_activate` ran
6310        // even when the focused widget was outside the shortcut's
6311        // scope, and any side effects on its ctx were silently
6312        // dropped. The closure must now only run when the scope
6313        // check has already passed.
6314        let activated = Rc::new(Cell::new(false));
6315        let activated_flag = activated.clone();
6316
6317        let mut tree = WidgetTree::new();
6318        let scope_root = tree.add(FillWidget::new().focusable());
6319        let outside = tree.add(FillWidget::new().focusable());
6320
6321        tree.shortcut_registry_mut().register(
6322            Shortcut::new("editor.find")
6323                .primary(KeyStroke::command(Key::F))
6324                .scope(ShortcutScope::Scoped(scope_root))
6325                .on_activate(move |_ks, _ctx| {
6326                    activated_flag.set(true);
6327                    Intent::new("editor.find")
6328                })
6329                .build(),
6330        );
6331
6332        tree.layout(SizeProposal::exact(200.0, 100.0));
6333        tree.focus(outside);
6334
6335        tree.press_key(Key::F, Modifiers::COMMAND);
6336        assert!(
6337            !activated.get(),
6338            "on_activate must not run when focus is outside the shortcut's scope"
6339        );
6340    }
6341
6342    #[test]
6343    fn key_capture_runs_callback_and_bypasses_registry() {
6344        use crate::action::Action;
6345        use crate::shortcut::{KeyStroke, Shortcut};
6346        use std::cell::Cell;
6347        use std::rc::Rc;
6348
6349        let action_fired = Rc::new(Cell::new(false));
6350        let af = action_fired.clone();
6351        let captured = Rc::new(Cell::new(None));
6352        let cf = captured.clone();
6353
6354        let mut tree = WidgetTree::new();
6355        let widget = tree.add(FillWidget::new().focusable());
6356        tree.push_action(
6357            widget,
6358            Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6359        );
6360        tree.shortcut_registry_mut().register(
6361            Shortcut::new("app.save")
6362                .primary(KeyStroke::command(Key::S))
6363                .build(),
6364        );
6365
6366        tree.layout(SizeProposal::exact(100.0, 50.0));
6367        tree.focus(widget);
6368
6369        let handle = tree.begin_key_capture(move |ks, _reg, _ctx| cf.set(Some(ks)));
6370        assert!(tree.is_capturing_keys());
6371
6372        tree.press_key(Key::S, Modifiers::COMMAND);
6373        assert_eq!(
6374            captured.get(),
6375            Some(KeyStroke::command(Key::S)),
6376            "capture callback must receive the chord"
6377        );
6378        assert!(
6379            !action_fired.get(),
6380            "shortcut action must not fire while capture is armed"
6381        );
6382        assert!(
6383            !tree.is_capturing_keys(),
6384            "capture is one-shot; next KeyDown flows normally"
6385        );
6386        drop(handle);
6387    }
6388
6389    #[test]
6390    fn key_capture_can_rebind_through_registry() {
6391        use crate::shortcut::{KeyStroke, Shortcut};
6392
6393        let mut tree = WidgetTree::new();
6394        let widget = tree.add(FillWidget::new().focusable());
6395        tree.shortcut_registry_mut().register(
6396            Shortcut::new("app.save")
6397                .primary(KeyStroke::command(Key::S))
6398                .build(),
6399        );
6400
6401        tree.layout(SizeProposal::exact(100.0, 50.0));
6402        tree.focus(widget);
6403
6404        // Arm capture: whatever chord comes next, rebind app.save to it.
6405        let _h = tree.begin_key_capture(|ks, reg, _ctx| {
6406            reg.rebind_primary("app.save", Some(ks));
6407        });
6408
6409        tree.press_key(Key::B, Modifiers::COMMAND | Modifiers::SHIFT);
6410        assert_eq!(
6411            tree.shortcut_registry()
6412                .effective("app.save")
6413                .unwrap()
6414                .primary,
6415            Some(KeyStroke::command_shift(Key::B))
6416        );
6417    }
6418
6419    #[test]
6420    fn dropping_capture_handle_cancels_capture() {
6421        use crate::shortcut::{KeyStroke, Shortcut};
6422        use std::cell::Cell;
6423        use std::rc::Rc;
6424
6425        let action_fired = Rc::new(Cell::new(false));
6426        let af = action_fired.clone();
6427        let capture_fired = Rc::new(Cell::new(false));
6428        let cf = capture_fired.clone();
6429
6430        let mut tree = WidgetTree::new();
6431        let widget = tree.add(FillWidget::new().focusable());
6432        tree.push_action(
6433            widget,
6434            crate::action::Action::new("app.save").on_invoke(move |_i, _c| af.set(true)),
6435        );
6436        tree.shortcut_registry_mut().register(
6437            Shortcut::new("app.save")
6438                .primary(KeyStroke::command(Key::S))
6439                .build(),
6440        );
6441        tree.layout(SizeProposal::exact(100.0, 50.0));
6442        tree.focus(widget);
6443
6444        // Arm capture in a scope, then drop the handle before any key
6445        // is pressed. The next KeyDown must fall through to the normal
6446        // shortcut path, firing the action — not the cancelled capture.
6447        {
6448            let _h = tree.begin_key_capture(move |_ks, _reg, _ctx| cf.set(true));
6449            assert!(tree.is_capturing_keys());
6450            // `_h` drops here → cancel.
6451        }
6452        assert!(
6453            !tree.is_capturing_keys(),
6454            "dropping the handle must cancel the capture"
6455        );
6456
6457        tree.press_key(Key::S, Modifiers::COMMAND);
6458        assert!(!capture_fired.get(), "cancelled capture must not fire");
6459        assert!(
6460            action_fired.get(),
6461            "shortcut action runs after capture was cancelled"
6462        );
6463    }
6464
6465    #[test]
6466    fn second_begin_key_capture_does_not_racecancel_first() {
6467        use std::cell::Cell;
6468        use std::rc::Rc;
6469
6470        let first = Rc::new(Cell::new(false));
6471        let second = Rc::new(Cell::new(false));
6472        let f = first.clone();
6473        let s = second.clone();
6474
6475        let mut tree = WidgetTree::new();
6476        let widget = tree.add(FillWidget::new().focusable());
6477        tree.layout(SizeProposal::exact(100.0, 50.0));
6478        tree.focus(widget);
6479
6480        // Arm #1 then replace with #2. #1's handle is later dropped,
6481        // which would have cancelled the active capture under the old
6482        // `Option<Box<FnOnce>>` design — CaptureHandle now ties each
6483        // session to its own slot, so the drop only clears #1's
6484        // (orphaned) slot, not #2.
6485        let h1 = tree.begin_key_capture(move |_ks, _reg, _ctx| f.set(true));
6486        let _h2 = tree.begin_key_capture(move |_ks, _reg, _ctx| s.set(true));
6487        drop(h1);
6488
6489        assert!(
6490            tree.is_capturing_keys(),
6491            "dropping the older handle must not cancel the active capture"
6492        );
6493        tree.press_key(Key::K, Modifiers::COMMAND);
6494        assert!(!first.get());
6495        assert!(second.get(), "newest capture wins");
6496    }
6497
6498    #[test]
6499    fn capture_callback_can_send_intent() {
6500        use crate::action::Action;
6501        use crate::intent::Intent;
6502
6503        use std::cell::Cell;
6504        use std::rc::Rc;
6505
6506        let ran = Rc::new(Cell::new(false));
6507        let flag = ran.clone();
6508
6509        let mut tree = WidgetTree::new();
6510        let widget = tree.add(FillWidget::new().focusable());
6511        tree.push_action(
6512            widget,
6513            Action::new("app.save").on_invoke(move |_i, _c| flag.set(true)),
6514        );
6515        tree.layout(SizeProposal::exact(100.0, 50.0));
6516        tree.focus(widget);
6517
6518        let _h = tree.begin_key_capture(|_ks, _reg, ctx| {
6519            ctx.send_intent(Intent::new("app.save"));
6520        });
6521        tree.press_key(Key::X, Modifiers::COMMAND);
6522        assert!(
6523            ran.get(),
6524            "intent queued from capture callback must dispatch"
6525        );
6526    }
6527
6528    #[test]
6529    fn binding_registry_does_not_accumulate_across_rebuilds() {
6530        use crate::binding::BindingLevel;
6531        use crate::signal::Signal;
6532
6533        #[derive(Debug)]
6534        struct BoundLeaf {
6535            tick: Signal<u64>,
6536        }
6537        impl crate::widget::Widget for BoundLeaf {
6538            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6539                self.tick.bind_to(
6540                    ctx.self_id(),
6541                    ctx.binding_registry(),
6542                    BindingLevel::Relayout,
6543                );
6544                Vec::new()
6545            }
6546            fn layout_response(
6547                &self,
6548                proposal: SizeProposal,
6549                _ctx: &crate::widget::LayoutContext,
6550            ) -> crate::widget::LayoutResponse {
6551                proposal.resolve(10.0, 10.0).into()
6552            }
6553        }
6554
6555        let mut tree = WidgetTree::new();
6556        let tick = Signal::new(0_u64);
6557        let widget = tree.add(BoundLeaf { tick: tick.clone() });
6558        tree.layout(SizeProposal::exact(200.0, 200.0));
6559        let after_first_build = tree.binding_registry().len();
6560        assert!(after_first_build >= 1);
6561
6562        // Force rebuild a handful of times and verify the binding
6563        // count does not keep growing. Pre-fix: each rebuild pushed
6564        // a new entry for the same (widget, signal) pair.
6565        for _ in 0..5 {
6566            tree.arena.mark_needs_rebuild(widget);
6567            tree.layout(SizeProposal::exact(200.0, 200.0));
6568        }
6569        assert_eq!(
6570            tree.binding_registry().len(),
6571            after_first_build,
6572            "bindings must be cleared on rebuild"
6573        );
6574
6575        tree.destroy_subtree(widget);
6576        assert_eq!(
6577            tree.binding_registry().len(),
6578            0,
6579            "bindings must be cleared on destroy"
6580        );
6581        // Silence unused-variable warning for the signal.
6582        let _ = tick;
6583    }
6584
6585    #[test]
6586    fn ctx_destroy_cancels_animations_and_bindings_via_deferred_path() {
6587        // Regression: `EventContext::destroy` queues
6588        // `TreeMutation::Destroy`, which used to be applied with the
6589        // bare `arena.destroy` — unlinking the node but leaking the
6590        // animation-scheduler entry (it holds a strong `Signal<f32>`
6591        // clone, so the widget kept animating after destruction) and
6592        // the widget's bindings. It must route through
6593        // `destroy_subtree` like every other destroy path does.
6594        use crate::binding::BindingLevel;
6595        use crate::signal::Signal;
6596        use std::time::{Duration, Instant};
6597        use teksilo_tokens::Easing;
6598
6599        #[derive(Debug)]
6600        struct BoundLeaf {
6601            tick: Signal<u64>,
6602        }
6603        impl crate::widget::Widget for BoundLeaf {
6604            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6605                self.tick.bind_to(
6606                    ctx.self_id(),
6607                    ctx.binding_registry(),
6608                    BindingLevel::Relayout,
6609                );
6610                Vec::new()
6611            }
6612            fn layout_response(
6613                &self,
6614                proposal: SizeProposal,
6615                _ctx: &crate::widget::LayoutContext,
6616            ) -> crate::widget::LayoutResponse {
6617                proposal.resolve(10.0, 10.0).into()
6618            }
6619        }
6620
6621        let mut tree = WidgetTree::new();
6622        let widget = tree.add(BoundLeaf {
6623            tick: Signal::new(0_u64),
6624        });
6625        tree.layout(SizeProposal::exact(200.0, 200.0));
6626        assert!(tree.binding_registry().len() >= 1);
6627
6628        // Seed an animation owned by the widget — exactly the strong
6629        // `Signal<f32>` clone the scheduler outlives the widget with.
6630        let anim = Signal::<f32>::new_animated(0.0);
6631        tree.animation_scheduler.animate(
6632            &anim,
6633            widget,
6634            1.0,
6635            Duration::from_secs(10),
6636            Easing::Linear,
6637            Instant::now(),
6638        );
6639        assert_eq!(tree.animation_scheduler.active_count(), 1);
6640
6641        // Destroy via the deferred handler-time path.
6642        let mut noop = crate::window::NoopWindowOps;
6643        tree.run_with_event_context(&mut noop, |ctx| ctx.destroy(widget));
6644
6645        assert_eq!(
6646            tree.animation_scheduler.active_count(),
6647            0,
6648            "ctx.destroy must cancel animations owned by the destroyed widget"
6649        );
6650        assert_eq!(
6651            tree.binding_registry().len(),
6652            0,
6653            "ctx.destroy must unregister the destroyed widget's bindings"
6654        );
6655        assert!(
6656            tree.arena.get(widget).is_none(),
6657            "node must be removed from the arena"
6658        );
6659    }
6660
6661    #[test]
6662    fn clear_shortcut_override_via_event_context_restores_default() {
6663        use crate::shortcut::{KeyStroke, Shortcut};
6664
6665        let mut tree = WidgetTree::new();
6666        tree.shortcut_registry_mut().register(
6667            Shortcut::new("app.save")
6668                .primary(KeyStroke::command(Key::S))
6669                .build(),
6670        );
6671        tree.shortcut_registry_mut()
6672            .rebind_primary("app.save", Some(KeyStroke::alt(Key::S)));
6673
6674        let source = tree.add(FillWidget::new());
6675        let mut ctx = EventContext::new();
6676        ctx.clear_shortcut_override("app.save");
6677        tree.collect_from_ctx(ctx, source);
6678
6679        assert_eq!(
6680            tree.shortcut_registry()
6681                .effective("app.save")
6682                .unwrap()
6683                .primary,
6684            Some(KeyStroke::command(Key::S))
6685        );
6686    }
6687
6688    #[test]
6689    fn rebind_shortcut_primary_via_event_context() {
6690        use crate::shortcut::{KeyStroke, Shortcut};
6691
6692        let mut tree = WidgetTree::new();
6693        tree.shortcut_registry_mut().register(
6694            Shortcut::new("app.save")
6695                .primary(KeyStroke::command(Key::S))
6696                .build(),
6697        );
6698        let source = tree.add(FillWidget::new());
6699
6700        let mut ctx = EventContext::new();
6701        ctx.rebind_shortcut_primary("app.save", Some(KeyStroke::alt(Key::S)));
6702        tree.collect_from_ctx(ctx, source);
6703
6704        assert_eq!(
6705            tree.shortcut_registry()
6706                .effective("app.save")
6707                .unwrap()
6708                .primary,
6709            Some(KeyStroke::alt(Key::S))
6710        );
6711    }
6712
6713    #[test]
6714    fn unregister_all_for_owner_called_on_destroy() {
6715        use crate::shortcut::{KeyStroke, Shortcut};
6716
6717        let mut tree = WidgetTree::new();
6718        let widget = tree.add(FillWidget::new());
6719        let widget_owner = widget;
6720        tree.shortcut_registry_mut().register_owned(
6721            Shortcut::new("scoped.thing")
6722                .primary(KeyStroke::command(Key::K))
6723                .build(),
6724            widget_owner,
6725        );
6726        assert!(
6727            tree.shortcut_registry()
6728                .get_default("scoped.thing")
6729                .is_some()
6730        );
6731
6732        tree.destroy_subtree(widget);
6733        assert!(
6734            tree.shortcut_registry()
6735                .get_default("scoped.thing")
6736                .is_none(),
6737            "destroying the owner must unregister its shortcut"
6738        );
6739    }
6740
6741    /// A global action fires for an intent dispatched from a widget in a
6742    /// completely unrelated subtree — proving it is a position-independent
6743    /// fallback (the menu-bar-vs-content case).
6744    #[test]
6745    fn global_action_reached_from_unrelated_source() {
6746        use crate::action::Action;
6747        use crate::intent::Intent;
6748        use std::cell::Cell;
6749        use std::rc::Rc;
6750
6751        #[derive(Debug)]
6752        struct Registrar(Rc<Cell<bool>>);
6753        impl crate::widget::Widget for Registrar {
6754            fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6755                let flag = self.0.clone();
6756                ctx.register_action_global(
6757                    Action::new("test.global").on_invoke(move |_i, _c| flag.set(true)),
6758                );
6759                vec![]
6760            }
6761            fn layout_response(
6762                &self,
6763                _p: teksilo_canvas::SizeProposal,
6764                _c: &crate::widget::LayoutContext,
6765            ) -> crate::widget::LayoutResponse {
6766                teksilo_canvas::Size::new(0.0, 0.0).into()
6767            }
6768        }
6769
6770        let mut tree = WidgetTree::new();
6771        let fired = Rc::new(Cell::new(false));
6772        let registrar = tree.add(Registrar(fired.clone()));
6773        let source = tree.add(FillWidget::new()); // unrelated sibling root
6774        let mut ops = crate::window::NoopWindowOps;
6775
6776        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6777        assert!(
6778            fired.get(),
6779            "global action must fire from an unrelated source"
6780        );
6781
6782        // And it is torn down with its owner.
6783        fired.set(false);
6784        tree.destroy_subtree(registrar);
6785        tree.dispatch_intent(source, Intent::new("test.global"), true, &mut ops);
6786        assert!(
6787            !fired.get(),
6788            "destroying the owner must remove its global action"
6789        );
6790    }
6791
6792    // --- Transform-aware hit-testing -------------------------------------
6793    //
6794    // `set_transform` scopes are paint-only: the renderer pushes the
6795    // transform around the subtree, so the visually-displayed area is
6796    // shifted relative to `arena.bounds(id)`. Hit-testing must inverse-
6797    // transform the screen-space input point as it descends through each
6798    // transform scope so that a click on the visually-rendered area lands
6799    // on the correct widget. Pre-fix, screen-space `bounds.contains(point)`
6800    // returned the *pre-transform* widget for in-bounds-pre-transform
6801    // points and missed the visually-shifted hit area entirely.
6802
6803    #[test]
6804    fn hit_test_through_translate_scope() {
6805        use crate::test_widgets::StackWidget;
6806        let mut tree = WidgetTree::new();
6807        let child = tree.add(FillWidget::new());
6808        let parent = tree.add(StackWidget::new().child(child));
6809        // Visually shift the entire subtree right by 100px.
6810        tree.set_transform(parent, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6811        tree.layout(SizeProposal::exact(100.0, 50.0));
6812
6813        // (50, 25) is inside the *pre-transform* bounds but the widget is
6814        // visually painted at x=100..200; a click at (50, 25) lands on
6815        // empty space.
6816        assert_eq!(
6817            tree.hit_test(Point::new(50.0, 25.0)),
6818            None,
6819            "pre-transform area is not visually populated and must not hit"
6820        );
6821        // (150, 25) is inside the visually-rendered area (post-translate).
6822        assert_eq!(
6823            tree.hit_test(Point::new(150.0, 25.0)),
6824            Some(child),
6825            "visually-rendered area must hit the child"
6826        );
6827        // Off everything.
6828        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6829    }
6830
6831    #[test]
6832    fn hit_test_through_scale_scope() {
6833        use crate::test_widgets::StackWidget;
6834        let mut tree = WidgetTree::new();
6835        let child = tree.add(FillWidget::new());
6836        let parent = tree.add(StackWidget::new().child(child));
6837        // Halve the visual size: pre-transform bounds (0,0,100,50) →
6838        // visually (0,0,50,25).
6839        tree.set_transform(parent, teksilo_canvas::Transform2D::scale(0.5, 0.5));
6840        tree.layout(SizeProposal::exact(100.0, 50.0));
6841
6842        // Inside the visual area.
6843        assert_eq!(tree.hit_test(Point::new(25.0, 12.0)), Some(child));
6844        // Outside the visual area but inside the pre-transform bounds.
6845        // Without the fix this would (incorrectly) hit the child.
6846        assert_eq!(
6847            tree.hit_test(Point::new(75.0, 25.0)),
6848            None,
6849            "scaled-out region must not hit"
6850        );
6851    }
6852
6853    #[test]
6854    fn hit_test_through_nested_transforms_compose() {
6855        use crate::test_widgets::StackWidget;
6856        let mut tree = WidgetTree::new();
6857        let leaf = tree.add(FillWidget::new());
6858        let inner = tree.add(StackWidget::new().child(leaf));
6859        let outer = tree.add(StackWidget::new().child(inner));
6860        // Outer translates by (100, 0); inner additionally scales by 2.
6861        // Effective at leaf = scale(2,2).then(translate(100,0)) — the
6862        // renderer composes deepest-first (see `effective_transform`).
6863        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6864        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6865        tree.layout(SizeProposal::exact(50.0, 25.0));
6866
6867        // Leaf-local (0, 0) → scale → (0, 0) → translate → (100, 0).
6868        // Leaf-local (50, 25) → scale → (100, 50) → translate → (200, 50).
6869        // So the visual hit area is x in [100, 200], y in [0, 50].
6870        assert_eq!(tree.hit_test(Point::new(150.0, 25.0)), Some(leaf));
6871        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), None);
6872        assert_eq!(tree.hit_test(Point::new(250.0, 25.0)), None);
6873    }
6874
6875    #[test]
6876    fn hit_test_identity_transform_unchanged() {
6877        // Sanity: an identity transform must not perturb the existing
6878        // hit-test behavior. Guards against accidental over-application
6879        // of inversion on the hot path.
6880        let mut tree = WidgetTree::new();
6881        let widget = tree.add(FillWidget::new());
6882        tree.set_transform(widget, teksilo_canvas::Transform2D::IDENTITY);
6883        tree.layout(SizeProposal::exact(100.0, 50.0));
6884        assert_eq!(tree.hit_test(Point::new(50.0, 25.0)), Some(widget));
6885    }
6886
6887    #[test]
6888    fn arena_effective_transform_composes_ancestors() {
6889        // `arena.effective_transform(id)` must equal the renderer's
6890        // transform-stack top by the time it begins painting `id` —
6891        // i.e. mapping `id`'s pre-transform local point to screen space.
6892        // The renderer's `PushTransform` handler composes as
6893        // `device_t.then(prev_top)` (see `teksilo-render/src/renderer.rs`),
6894        // so the *innermost* transform applies first to a local point.
6895        // For ancestors [outer, inner] both with transforms, this means
6896        // effective = inner.then(outer), NOT outer.then(inner).
6897        // teksilo-scene relies on this to project scene-coord bounds to
6898        // screen space when emitting AT nodes for view-transformed items.
6899        use crate::test_widgets::StackWidget;
6900        let mut tree = WidgetTree::new();
6901        let leaf = tree.add(FillWidget::new());
6902        let inner = tree.add(StackWidget::new().child(leaf));
6903        let outer = tree.add(StackWidget::new().child(inner));
6904        tree.set_transform(outer, teksilo_canvas::Transform2D::translate(100.0, 0.0));
6905        tree.set_transform(inner, teksilo_canvas::Transform2D::scale(2.0, 2.0));
6906        tree.layout(SizeProposal::exact(50.0, 25.0));
6907
6908        let eff = tree.arena.effective_transform(leaf);
6909        let expected = teksilo_canvas::Transform2D::scale(2.0, 2.0)
6910            .then(&teksilo_canvas::Transform2D::translate(100.0, 0.0));
6911        for (a, b) in eff.m.iter().zip(expected.m.iter()) {
6912            assert!(
6913                (a - b).abs() < 1e-5,
6914                "effective_transform mismatch: got {:?}, want {:?}",
6915                eff.m,
6916                expected.m
6917            );
6918        }
6919
6920        // Concrete-point check that pins the composition order without
6921        // relying on matrix equality alone: a leaf-local point at the
6922        // bounds origin (0, 0) should land at screen (100, 0) — scale
6923        // first (still (0,0)), then translate by 100 in x. With the
6924        // wrong composition order it would land at (200, 0).
6925        let screen_origin = eff.apply_point(Point::new(0.0, 0.0));
6926        assert!((screen_origin.x - 100.0).abs() < 1e-5);
6927        assert!((screen_origin.y - 0.0).abs() < 1e-5);
6928        // Far corner: leaf-local (50, 25) → scale → (100, 50) → translate
6929        // by 100 in x → (200, 50).
6930        let screen_corner = eff.apply_point(Point::new(50.0, 25.0));
6931        assert!((screen_corner.x - 200.0).abs() < 1e-5);
6932        assert!((screen_corner.y - 50.0).abs() < 1e-5);
6933    }
6934
6935    // ─── Context-menu factory: position, ctx, None fall-through ─────────
6936
6937    /// A throwaway content widget the factory mounts. We never paint
6938    /// it — the test only checks that it lands in the overlay manager.
6939    #[derive(Debug)]
6940    struct StubMenu;
6941    impl crate::widget::Widget for StubMenu {
6942        fn layout_response(
6943            &self,
6944            _proposal: SizeProposal,
6945            _ctx: &crate::widget::LayoutContext,
6946        ) -> crate::widget::LayoutResponse {
6947            teksilo_canvas::Size::new(100.0, 40.0).into()
6948        }
6949    }
6950
6951    // The keyboard route to a context menu.
6952    //
6953    // Until this existed there was none at all: no `Key::ContextMenu`, no
6954    // Shift+F10, and `Action::ShowContextMenu` appears in zero of the three
6955    // AccessKit adapters, so the assistive-technology route is dead on every
6956    // platform too. A menu reachable only by right-click is a menu a keyboard
6957    // user does not have.
6958
6959    /// A widget that hands the keyboard a different target than itself, the way
6960    /// every data view does: the container has focus, the row is what the menu
6961    /// is about.
6962    #[derive(Debug)]
6963    struct NominatingWidget {
6964        row: std::cell::Cell<Option<WidgetId>>,
6965    }
6966
6967    impl crate::widget::Widget for NominatingWidget {
6968        fn layout_response(
6969            &self,
6970            proposal: SizeProposal,
6971            _ctx: &LayoutContext,
6972        ) -> crate::widget::LayoutResponse {
6973            proposal.resolve(50.0, 20.0).into()
6974        }
6975
6976        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
6977            ctx.apply_self_handlers(crate::widget_builder::HandlerSet::new().focusable(true));
6978            Vec::new()
6979        }
6980
6981        fn context_menu_key_target(&self) -> Option<WidgetId> {
6982            self.row.get()
6983        }
6984    }
6985
6986    fn press(tree: &mut WidgetTree, key: Key, modifiers: Modifiers) {
6987        tree.dispatch_event(WidgetEvent::KeyDown {
6988            key,
6989            modifiers,
6990            text: None,
6991        });
6992    }
6993
6994    #[test]
6995    fn the_context_menu_key_opens_the_focused_widget_menu() {
6996        use std::cell::Cell;
6997        use std::rc::Rc;
6998
6999        let opened = Rc::new(Cell::new(false));
7000        let flag = opened.clone();
7001        let mut tree = WidgetTree::new();
7002        let widget = tree.add(
7003            FillWidget::new()
7004                .focusable()
7005                .context_menu(move |_pos, _ctx| {
7006                    flag.set(true);
7007                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7008                }),
7009        );
7010        tree.layout(SizeProposal::exact(200.0, 100.0));
7011        tree.focus(widget);
7012
7013        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7014        assert!(opened.get(), "the dedicated Menu key must open the menu");
7015    }
7016
7017    /// The chord every Windows and Linux keyboard can reach, including the many
7018    /// that have no dedicated Menu key at all.
7019    #[test]
7020    fn shift_f10_opens_the_focused_widget_menu() {
7021        use std::cell::Cell;
7022        use std::rc::Rc;
7023
7024        let opened = Rc::new(Cell::new(false));
7025        let flag = opened.clone();
7026        let mut tree = WidgetTree::new();
7027        let widget = tree.add(
7028            FillWidget::new()
7029                .focusable()
7030                .context_menu(move |_pos, _ctx| {
7031                    flag.set(true);
7032                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7033                }),
7034        );
7035        tree.layout(SizeProposal::exact(200.0, 100.0));
7036        tree.focus(widget);
7037
7038        press(&mut tree, Key::F10, Modifiers::SHIFT);
7039        assert!(opened.get(), "Shift+F10 must open the menu");
7040    }
7041
7042    /// Modifiers are matched exactly. Ctrl+Shift+F10 is a different gesture and
7043    /// belongs to the application.
7044    #[test]
7045    fn a_near_miss_chord_is_not_a_context_menu_request() {
7046        use std::cell::Cell;
7047        use std::rc::Rc;
7048
7049        let opened = Rc::new(Cell::new(false));
7050        let flag = opened.clone();
7051        let mut tree = WidgetTree::new();
7052        let widget = tree.add(
7053            FillWidget::new()
7054                .focusable()
7055                .context_menu(move |_pos, _ctx| {
7056                    flag.set(true);
7057                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7058                }),
7059        );
7060        tree.layout(SizeProposal::exact(200.0, 100.0));
7061        tree.focus(widget);
7062
7063        press(&mut tree, Key::F10, Modifiers::SHIFT | Modifiers::CTRL);
7064        press(&mut tree, Key::F10, Modifiers::NONE);
7065        assert!(!opened.get(), "only Shift+F10 exactly asks for a menu");
7066    }
7067
7068    /// The correction the design needed. A data view is focusable and its rows
7069    /// are not, so "the focused widget" is the list, and the menu a user asked
7070    /// for on row 4 would have been the list's own.
7071    #[test]
7072    fn the_keyboard_target_can_be_a_row_rather_than_the_focused_container() {
7073        use std::cell::Cell;
7074        use std::rc::Rc;
7075
7076        let menu_owner = Rc::new(Cell::new(None::<&'static str>));
7077
7078        let row_flag = menu_owner.clone();
7079        let container_flag = menu_owner.clone();
7080
7081        let mut tree = WidgetTree::new();
7082        let row = tree.add(FillWidget::new().context_menu(move |_pos, _ctx| {
7083            row_flag.set(Some("row"));
7084            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7085        }));
7086        let container = tree.add(
7087            crate::test_widgets::StackWidget::new()
7088                .child(row)
7089                .context_menu(move |_pos, _ctx| {
7090                    container_flag.set(Some("container"));
7091                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7092                }),
7093        );
7094        tree.layout(SizeProposal::exact(200.0, 100.0));
7095
7096        // The container is focused, and nominates the row.
7097        let nominator = tree.add(NominatingWidget {
7098            row: std::cell::Cell::new(Some(row)),
7099        });
7100        tree.layout(SizeProposal::exact(200.0, 100.0));
7101        tree.focus(nominator);
7102        let _ = container;
7103
7104        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7105        assert_eq!(
7106            menu_owner.get(),
7107            Some("row"),
7108            "the nominated row's factory must be the one that runs"
7109        );
7110    }
7111
7112    /// Nothing on the chain owns a factory, so the framework must not swallow
7113    /// the key: a widget that wants to handle Shift+F10 itself still can.
7114    #[test]
7115    fn the_chord_falls_through_when_there_is_no_menu_to_show() {
7116        use std::cell::Cell;
7117        use std::rc::Rc;
7118
7119        let saw_key = Rc::new(Cell::new(false));
7120        let flag = saw_key.clone();
7121        let mut tree = WidgetTree::new();
7122        let widget = tree.add(FillWidget::new().focusable().on_key(move |ev, _ctx| {
7123            if matches!(ev, WidgetEvent::KeyDown { key: Key::F10, .. }) {
7124                flag.set(true);
7125            }
7126            crate::event::EventResponse::Ignored
7127        }));
7128        tree.layout(SizeProposal::exact(200.0, 100.0));
7129        tree.focus(widget);
7130
7131        press(&mut tree, Key::F10, Modifiers::SHIFT);
7132        assert!(
7133            saw_key.get(),
7134            "with no factory anywhere, the key must reach the widget"
7135        );
7136    }
7137
7138    /// Every route to a menu tells the factory what asked for it, because only
7139    /// a pointer's point is a place the user chose. A text field's factory
7140    /// moves the caret to its point for a right-click; told nothing, it did the
7141    /// same for Shift+F10, and the menu's Paste wrote into the middle of the
7142    /// field.
7143    #[test]
7144    fn each_route_to_a_menu_tells_the_factory_what_asked_for_it() {
7145        use crate::widget_builder::ContextMenuTrigger;
7146        use std::cell::RefCell;
7147        use std::rc::Rc;
7148
7149        let seen = Rc::new(RefCell::new(Vec::new()));
7150        let log = seen.clone();
7151        let mut tree = WidgetTree::new();
7152        let widget = tree.add(
7153            FillWidget::new()
7154                .focusable()
7155                .context_menu(move |_pos, ctx| {
7156                    log.borrow_mut().push(ctx.context_menu_trigger());
7157                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7158                }),
7159        );
7160        tree.layout(SizeProposal::exact(200.0, 100.0));
7161        tree.focus(widget);
7162        let inside = tree.bounds(widget).center();
7163
7164        press(&mut tree, Key::F10, Modifiers::SHIFT);
7165        press(&mut tree, Key::Escape, Modifiers::NONE);
7166        press(&mut tree, Key::ContextMenu, Modifiers::NONE);
7167        press(&mut tree, Key::Escape, Modifiers::NONE);
7168        tree.dispatch_event(WidgetEvent::pointer_down(
7169            inside,
7170            crate::event::PointerButton::Secondary,
7171            Modifiers::NONE,
7172        ));
7173        tree.dispatch_event(WidgetEvent::pointer_up(
7174            inside,
7175            crate::event::PointerButton::Secondary,
7176            Modifiers::NONE,
7177        ));
7178        press(&mut tree, Key::Escape, Modifiers::NONE);
7179        tree.dispatch_event(WidgetEvent::AccessAction {
7180            action: accesskit::Action::ShowContextMenu,
7181            target: Some(widget),
7182            target_node: crate::accessibility::widget_id_to_node_id(widget),
7183            data: None,
7184        });
7185        press(&mut tree, Key::Escape, Modifiers::NONE);
7186        tree.long_press_at(teksilo_tokens::PointerKind::Touch, inside);
7187
7188        assert_eq!(
7189            *seen.borrow(),
7190            vec![
7191                Some(ContextMenuTrigger::Keyboard),
7192                Some(ContextMenuTrigger::Keyboard),
7193                Some(ContextMenuTrigger::Pointer),
7194                Some(ContextMenuTrigger::Accessibility),
7195                Some(ContextMenuTrigger::Pointer),
7196            ],
7197            "Shift+F10, the Menu key, a right-click, the assistive action and a \
7198             hold, in that order"
7199        );
7200    }
7201
7202    #[test]
7203    fn context_menu_factory_receives_click_position() {
7204        use crate::event::{Modifiers, PointerButton};
7205        use std::cell::Cell;
7206        use std::rc::Rc;
7207
7208        let captured_position = Rc::new(Cell::new(None::<Point>));
7209        let cap = captured_position.clone();
7210        let mut tree = WidgetTree::new();
7211        let widget = tree.add(FillWidget::new().context_menu(move |pos, _ctx| {
7212            cap.set(Some(pos));
7213            Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7214        }));
7215        tree.layout(SizeProposal::exact(200.0, 100.0));
7216
7217        let click = Point::new(73.0, 42.0);
7218        tree.dispatch_event(WidgetEvent::pointer_down(
7219            click,
7220            PointerButton::Secondary,
7221            Modifiers::NONE,
7222        ));
7223
7224        let got = captured_position.get();
7225        assert_eq!(
7226            got,
7227            Some(click),
7228            "factory must receive the click position; got {:?}",
7229            got
7230        );
7231        let _ = widget;
7232    }
7233
7234    #[test]
7235    fn context_menu_factory_returning_none_falls_through_to_parent() {
7236        use crate::event::{Modifiers, PointerButton};
7237        use crate::test_widgets::StackWidget;
7238        use std::cell::Cell;
7239        use std::rc::Rc;
7240
7241        // Outer factory always returns Some(StubMenu); inner factory
7242        // returns None. Right-click should walk past the inner and
7243        // mount the outer's menu.
7244        let outer_called = Rc::new(Cell::new(0_u32));
7245        let outer_flag = outer_called.clone();
7246        let mut tree = WidgetTree::new();
7247        let inner = tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7248        let _outer = tree.add(
7249            StackWidget::new()
7250                .child(inner)
7251                .context_menu(move |_pos, _ctx| {
7252                    outer_flag.set(outer_flag.get() + 1);
7253                    Some(Box::new(StubMenu) as Box<dyn crate::widget::Widget>)
7254                }),
7255        );
7256        tree.layout(SizeProposal::exact(200.0, 100.0));
7257
7258        tree.dispatch_event(WidgetEvent::pointer_down(
7259            Point::new(50.0, 25.0),
7260            PointerButton::Secondary,
7261            Modifiers::NONE,
7262        ));
7263
7264        assert_eq!(
7265            outer_called.get(),
7266            1,
7267            "inner returning None must fall through to the outer factory"
7268        );
7269    }
7270
7271    #[test]
7272    fn context_menu_factory_none_throughout_chain_does_not_show_overlay() {
7273        use crate::event::{Modifiers, PointerButton};
7274
7275        // Single factory returning None → no overlay shown, no panic.
7276        let mut tree = WidgetTree::new();
7277        tree.add(FillWidget::new().context_menu(|_pos, _ctx| None));
7278        tree.layout(SizeProposal::exact(200.0, 100.0));
7279
7280        let overlay_count_before = tree.overlay_manager.len();
7281        tree.dispatch_event(WidgetEvent::pointer_down(
7282            Point::new(50.0, 25.0),
7283            PointerButton::Secondary,
7284            Modifiers::NONE,
7285        ));
7286        let overlay_count_after = tree.overlay_manager.len();
7287        assert_eq!(
7288            overlay_count_before, overlay_count_after,
7289            "a factory returning None must not mount any overlay"
7290        );
7291    }
7292
7293    // ---- Reconcile-on-rebuild (`preserves_children_on_rebuild`) ----------
7294    //
7295    // These pin the contract that the preserve path RECONCILES: it keeps the
7296    // children a rebuild re-attaches (and any subtree re-parented into the new
7297    // tree) while reaping the ones it drops — so memoizing widgets are both
7298    // stateful and leak-free. Regression guard for the orphan-leak the old
7299    // "preserve = destroy nothing" behaviour caused.
7300
7301    /// `build()` mints a fresh child every time and returns only it, abandoning
7302    /// the previous one. Used to prove dropped children are reaped, not leaked.
7303    #[derive(Debug)]
7304    struct FreshChildHost {
7305        preserve: bool,
7306    }
7307    impl Widget for FreshChildHost {
7308        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7309            vec![ctx.add(FillWidget::new())]
7310        }
7311        fn layout_response(
7312            &self,
7313            p: SizeProposal,
7314            _c: &LayoutContext,
7315        ) -> crate::widget::LayoutResponse {
7316            p.resolve(10.0, 10.0).into()
7317        }
7318        fn preserves_children_on_rebuild(&self) -> bool {
7319            self.preserve
7320        }
7321    }
7322
7323    #[test]
7324    fn reconcile_reaps_dropped_children_no_leak() {
7325        // preserve=false (destroy-all) and preserve=true (reconcile) must BOTH
7326        // keep the arena bounded when a rebuild drops its old child. Before the
7327        // reconcile fix, preserve=true grew the arena (and the active set) by
7328        // one stranded orphan per rebuild.
7329        for preserve in [false, true] {
7330            let mut tree = WidgetTree::new();
7331            let host = tree.add(FreshChildHost { preserve });
7332            tree.layout(SizeProposal::exact(100.0, 100.0));
7333            let total0 = tree.arena.len();
7334            let active0 = tree.active_widget_count();
7335            for _ in 0..5 {
7336                tree.arena_mark_needs_rebuild_for_testing(host);
7337                tree.layout(SizeProposal::exact(100.0, 100.0));
7338            }
7339            assert_eq!(
7340                tree.arena.len(),
7341                total0,
7342                "preserve={preserve}: dropped children must be reaped, not leaked"
7343            );
7344            assert_eq!(
7345                tree.active_widget_count(),
7346                active0,
7347                "preserve={preserve}: no stranded still-active orphans"
7348            );
7349        }
7350    }
7351
7352    /// `build()` mints one **detached** node every time — the shape of every
7353    /// pre-built popup in the widget crate (a dropdown, a calendar, a
7354    /// tooltip's cascade children): parked dormant, shown later through an
7355    /// overlay, and deliberately not a child, since activation and paint both
7356    /// descend through `children`.
7357    #[derive(Debug)]
7358    struct DetachedContentHost {
7359        preserve: bool,
7360    }
7361    impl Widget for DetachedContentHost {
7362        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7363            let popup = ctx.add_detached(FillWidget::new());
7364            ctx.set_dormant(popup);
7365            vec![ctx.add(FillWidget::new())]
7366        }
7367        fn layout_response(
7368            &self,
7369            p: SizeProposal,
7370            _c: &LayoutContext,
7371        ) -> crate::widget::LayoutResponse {
7372            p.resolve(10.0, 10.0).into()
7373        }
7374        fn preserves_children_on_rebuild(&self) -> bool {
7375            self.preserve
7376        }
7377    }
7378
7379    #[test]
7380    fn rebuilding_reaps_detached_content_no_leak() {
7381        // A parentless node is reachable from no walk at all — not the child
7382        // teardown, not the accessibility tree, not `active_widget_count`. Held
7383        // by a bare `ctx.add` it simply accumulated: one stranded popup per
7384        // rebuild, for the lifetime of the process. `add_detached` records the
7385        // ownership edge that makes it reapable.
7386        for preserve in [false, true] {
7387            let mut tree = WidgetTree::new();
7388            let host = tree.add(DetachedContentHost { preserve });
7389            tree.layout(SizeProposal::exact(100.0, 100.0));
7390            let total0 = tree.arena.len();
7391            for _ in 0..5 {
7392                tree.arena_mark_needs_rebuild_for_testing(host);
7393                tree.layout(SizeProposal::exact(100.0, 100.0));
7394            }
7395            assert_eq!(
7396                tree.arena.len(),
7397                total0,
7398                "preserve={preserve}: the previous build's detached content must be reaped"
7399            );
7400        }
7401    }
7402
7403    #[test]
7404    fn destroying_a_host_reaps_its_detached_content() {
7405        let mut tree = WidgetTree::new();
7406        let outer = tree.add(FillWidget::new());
7407        tree.layout(SizeProposal::exact(100.0, 100.0));
7408        let empty = tree.arena.len();
7409
7410        let host = tree.add_child(outer, DetachedContentHost { preserve: false });
7411        tree.layout(SizeProposal::exact(100.0, 100.0));
7412        assert!(tree.arena.len() > empty);
7413
7414        tree.destroy_subtree(host);
7415        assert_eq!(
7416            tree.arena.len(),
7417            empty,
7418            "the popup must die with the widget that built it"
7419        );
7420    }
7421
7422    /// Memoizes one child and re-attaches the same id every build.
7423    #[derive(Debug)]
7424    struct StableChildHost {
7425        child: Option<WidgetId>,
7426        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7427    }
7428    impl Widget for StableChildHost {
7429        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7430            let id = match self.child {
7431                Some(id) => id,
7432                None => {
7433                    let id = ctx.add(FillWidget::new());
7434                    self.child = Some(id);
7435                    self.probe.set(Some(id));
7436                    id
7437                }
7438            };
7439            vec![id]
7440        }
7441        fn layout_response(
7442            &self,
7443            p: SizeProposal,
7444            _c: &LayoutContext,
7445        ) -> crate::widget::LayoutResponse {
7446            p.resolve(10.0, 10.0).into()
7447        }
7448        fn preserves_children_on_rebuild(&self) -> bool {
7449            true
7450        }
7451    }
7452
7453    #[test]
7454    fn reconcile_preserves_reattached_child() {
7455        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7456        let mut tree = WidgetTree::new();
7457        let host = tree.add(StableChildHost {
7458            child: None,
7459            probe: probe.clone(),
7460        });
7461        tree.layout(SizeProposal::exact(100.0, 100.0));
7462        let child = probe.get().expect("child mounted");
7463        let total0 = tree.arena.len();
7464        for _ in 0..5 {
7465            tree.arena_mark_needs_rebuild_for_testing(host);
7466            tree.layout(SizeProposal::exact(100.0, 100.0));
7467        }
7468        assert!(
7469            tree.arena.is_active(child),
7470            "the re-attached child must survive every rebuild"
7471        );
7472        assert_eq!(tree.arena.len(), total0, "no growth — same child reused");
7473    }
7474
7475    /// Re-homes a node returned from its `build()` under itself.
7476    #[derive(Debug)]
7477    struct Wrapper {
7478        child: WidgetId,
7479    }
7480    impl Widget for Wrapper {
7481        fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7482            vec![self.child]
7483        }
7484        fn layout_response(
7485            &self,
7486            p: SizeProposal,
7487            _c: &LayoutContext,
7488        ) -> crate::widget::LayoutResponse {
7489            p.resolve(10.0, 10.0).into()
7490        }
7491    }
7492
7493    /// Memoizes a body, then wraps it in a FRESH `Wrapper` each build —
7494    /// re-parenting the body out of the previous (now dropped) wrapper. This is
7495    /// the TabWidget / CompositeTooltip pattern in miniature.
7496    #[derive(Debug)]
7497    struct ReparentHost {
7498        body: Option<WidgetId>,
7499        probe: std::rc::Rc<std::cell::Cell<Option<WidgetId>>>,
7500    }
7501    impl Widget for ReparentHost {
7502        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
7503            let body = match self.body {
7504                Some(id) => id,
7505                None => {
7506                    let id = ctx.add(FillWidget::new());
7507                    self.body = Some(id);
7508                    self.probe.set(Some(id));
7509                    id
7510                }
7511            };
7512            vec![ctx.add(Wrapper { child: body })]
7513        }
7514        fn layout_response(
7515            &self,
7516            p: SizeProposal,
7517            _c: &LayoutContext,
7518        ) -> crate::widget::LayoutResponse {
7519            p.resolve(10.0, 10.0).into()
7520        }
7521        fn preserves_children_on_rebuild(&self) -> bool {
7522            true
7523        }
7524    }
7525
7526    #[test]
7527    fn reconcile_spares_reparented_survivor() {
7528        // The memoized body is re-parented into a fresh wrapper each rebuild;
7529        // the old wrapper is dropped. The body must survive (it is re-homed),
7530        // and the old wrappers must be reaped (no leak). This is the exact
7531        // failure that destroyed TabWidget's static panel before the fix: the
7532        // parent-authoritative recursion + single-node arena removal spare the
7533        // re-homed body while still reaping the dropped wrapper subtree.
7534        let probe = std::rc::Rc::new(std::cell::Cell::new(None));
7535        let mut tree = WidgetTree::new();
7536        let host = tree.add(ReparentHost {
7537            body: None,
7538            probe: probe.clone(),
7539        });
7540        tree.layout(SizeProposal::exact(100.0, 100.0));
7541        let body = probe.get().expect("body mounted");
7542        let total0 = tree.arena.len();
7543        for _ in 0..5 {
7544            tree.arena_mark_needs_rebuild_for_testing(host);
7545            tree.layout(SizeProposal::exact(100.0, 100.0));
7546        }
7547        assert!(
7548            tree.arena.is_active(body),
7549            "the re-parented body must survive — it was moved into the new tree, \
7550             not swept with the dropped wrapper"
7551        );
7552        assert_eq!(
7553            tree.arena.len(),
7554            total0,
7555            "dropped wrappers reaped — no per-rebuild leak"
7556        );
7557    }
7558
7559    // -----------------------------------------------------------------
7560    // EventContext::ensure_visible / ensure_widget_visible — the
7561    // rect/id-based outer-scroll chase drained in `collect_from_ctx`.
7562    // -----------------------------------------------------------------
7563
7564    /// A `clips_children` container that places its single child at a fixed
7565    /// vertical offset — used to give a child arena bounds *outside* the
7566    /// container's viewport so the id-based `ensure_widget_visible` walk has a
7567    /// reason to dispatch `ScrollIntoView`.
7568    #[derive(Debug)]
7569    struct BelowContainer {
7570        child: Option<WidgetId>,
7571        offset: f32,
7572    }
7573
7574    impl crate::widget::Widget for BelowContainer {
7575        fn layout_response(
7576            &self,
7577            proposal: SizeProposal,
7578            _ctx: &crate::widget::LayoutContext,
7579        ) -> crate::widget::LayoutResponse {
7580            proposal.resolve(0.0, 0.0).into()
7581        }
7582        fn place_children(
7583            &self,
7584            bounds: Rect,
7585            _proposal: SizeProposal,
7586            children: &mut [crate::widget::WidgetPlacement],
7587            _ctx: &crate::widget::LayoutContext,
7588        ) {
7589            for c in children.iter_mut() {
7590                c.origin = Point::new(bounds.x, bounds.y + self.offset);
7591                c.size = bounds.size();
7592            }
7593        }
7594        fn children(&self) -> Vec<WidgetId> {
7595            self.child.into_iter().collect()
7596        }
7597    }
7598
7599    /// A `clips_children` container that records the `ScrollIntoView` it
7600    /// receives, so a test can assert what the framework dispatched to it.
7601    fn recording_scroll_container(
7602        tree: &mut WidgetTree,
7603        child: WidgetId,
7604        recorded: std::rc::Rc<std::cell::Cell<Option<Rect>>>,
7605    ) -> WidgetId {
7606        use crate::test_widgets::StackWidget;
7607        tree.add(
7608            StackWidget::new()
7609                .child(child)
7610                .on_scroll(move |ev, _ctx| match ev {
7611                    WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7612                        recorded.set(Some(*target_bounds));
7613                        EventResponse::Handled
7614                    }
7615                    _ => EventResponse::Ignored,
7616                })
7617                .clips_children(true),
7618        )
7619    }
7620
7621    #[test]
7622    fn ensure_visible_dispatches_scroll_into_view_to_clipping_ancestor() {
7623        use std::cell::Cell;
7624        use std::rc::Rc;
7625        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7626        let mut tree = WidgetTree::new();
7627        let actor = tree.add(FillWidget::new());
7628        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7629        tree.layout(SizeProposal::exact(100.0, 100.0));
7630
7631        // A rect well below the 100px viewport — the container must be asked to
7632        // reveal it.
7633        let target = Rect::new(10.0, 500.0, 20.0, 15.0);
7634        let mut ctx = EventContext::new();
7635        ctx.ensure_visible(target);
7636        tree.collect_from_ctx(ctx, actor);
7637
7638        assert_eq!(
7639            recorded.get(),
7640            Some(target),
7641            "ensure_visible(rect) must dispatch ScrollIntoView with the exact rect \
7642             to the clips_children ancestor"
7643        );
7644    }
7645
7646    #[test]
7647    fn ensure_visible_is_noop_when_rect_already_visible() {
7648        use std::cell::Cell;
7649        use std::rc::Rc;
7650        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7651        let mut tree = WidgetTree::new();
7652        let actor = tree.add(FillWidget::new());
7653        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7654        tree.layout(SizeProposal::exact(100.0, 100.0));
7655
7656        // Fully inside the viewport → the ancestor already shows it, so no
7657        // ScrollIntoView is dispatched.
7658        let mut ctx = EventContext::new();
7659        ctx.ensure_visible(Rect::new(10.0, 10.0, 20.0, 15.0));
7660        tree.collect_from_ctx(ctx, actor);
7661
7662        assert_eq!(
7663            recorded.get(),
7664            None,
7665            "a rect already inside the viewport must not trigger a scroll"
7666        );
7667    }
7668
7669    #[test]
7670    fn ensure_visible_margin_forces_scroll_near_edge() {
7671        use std::cell::Cell;
7672        use std::rc::Rc;
7673        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7674        let mut tree = WidgetTree::new();
7675        let actor = tree.add(FillWidget::new());
7676        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7677        tree.layout(SizeProposal::exact(100.0, 100.0));
7678
7679        // Rect at y=95..99 is visible at margin 0, but with a 10px margin its
7680        // padded bottom (109) spills past the 100px viewport → scroll.
7681        let rect = Rect::new(10.0, 95.0, 20.0, 4.0);
7682        let mut ctx = EventContext::new();
7683        ctx.ensure_visible_with_margin(rect, 10.0);
7684        tree.collect_from_ctx(ctx, actor);
7685
7686        assert_eq!(
7687            recorded.get(),
7688            Some(rect),
7689            "the margin must widen the visibility test so a near-edge rect scrolls"
7690        );
7691    }
7692
7693    /// A `clips_children` container that records the alignment and motion of the
7694    /// `ScrollIntoView` it receives.
7695    fn recording_align_container(
7696        tree: &mut WidgetTree,
7697        child: WidgetId,
7698        recorded: std::rc::Rc<
7699            std::cell::Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>,
7700        >,
7701    ) -> WidgetId {
7702        use crate::test_widgets::StackWidget;
7703        tree.add(
7704            StackWidget::new()
7705                .child(child)
7706                .on_scroll(move |ev, _ctx| match ev {
7707                    WidgetEvent::ScrollIntoView { align, motion, .. } => {
7708                        recorded.set(Some((*align, *motion)));
7709                        EventResponse::Handled
7710                    }
7711                    _ => EventResponse::Ignored,
7712                })
7713                .clips_children(true),
7714        )
7715    }
7716
7717    #[test]
7718    fn ensure_visible_aligned_scrolls_even_when_already_visible() {
7719        use std::cell::Cell;
7720        use std::rc::Rc;
7721        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7722        let mut tree = WidgetTree::new();
7723        let actor = tree.add(FillWidget::new());
7724        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
7725        tree.layout(SizeProposal::exact(100.0, 100.0));
7726
7727        // Comfortably inside the viewport — a *minimal* reveal would decline
7728        // (see `ensure_visible_is_noop_when_rect_already_visible`). A pin must
7729        // still fire: re-asserting unconditionally is the whole difference
7730        // between "keep it on screen" and "hold it at this height".
7731        let target = Rect::new(10.0, 10.0, 20.0, 15.0);
7732        let mut ctx = EventContext::new();
7733        ctx.ensure_visible_aligned(target, 0.5, crate::event::ScrollMotion::Instant);
7734        tree.collect_from_ctx(ctx, actor);
7735
7736        assert_eq!(
7737            recorded.get(),
7738            Some(target),
7739            "an aligned reveal must dispatch even when the rect is already visible"
7740        );
7741    }
7742
7743    #[test]
7744    fn ensure_visible_aligned_forwards_fraction_and_motion() {
7745        use std::cell::Cell;
7746        use std::rc::Rc;
7747        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7748            Rc::new(Cell::new(None));
7749        let mut tree = WidgetTree::new();
7750        let actor = tree.add(FillWidget::new());
7751        let _container = recording_align_container(&mut tree, actor, recorded.clone());
7752        tree.layout(SizeProposal::exact(100.0, 100.0));
7753
7754        let mut ctx = EventContext::new();
7755        ctx.ensure_visible_aligned(
7756            Rect::new(10.0, 10.0, 20.0, 15.0),
7757            0.25,
7758            crate::event::ScrollMotion::Smooth,
7759        );
7760        tree.collect_from_ctx(ctx, actor);
7761
7762        assert_eq!(
7763            recorded.get(),
7764            Some((
7765                crate::event::ScrollAlign::Fraction(0.25),
7766                crate::event::ScrollMotion::Smooth
7767            )),
7768            "the container must receive the requested fraction and motion verbatim"
7769        );
7770    }
7771
7772    #[test]
7773    fn ensure_visible_aligned_clamps_the_fraction() {
7774        use std::cell::Cell;
7775        use std::rc::Rc;
7776        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7777            Rc::new(Cell::new(None));
7778        let mut tree = WidgetTree::new();
7779        let actor = tree.add(FillWidget::new());
7780        let _container = recording_align_container(&mut tree, actor, recorded.clone());
7781        tree.layout(SizeProposal::exact(100.0, 100.0));
7782
7783        let mut ctx = EventContext::new();
7784        ctx.ensure_visible_aligned(
7785            Rect::new(10.0, 10.0, 20.0, 15.0),
7786            4.2,
7787            crate::event::ScrollMotion::Instant,
7788        );
7789        tree.collect_from_ctx(ctx, actor);
7790
7791        assert_eq!(
7792            recorded.get().map(|(a, _)| a),
7793            Some(crate::event::ScrollAlign::Fraction(1.0)),
7794            "an out-of-range fraction must clamp rather than aim the pin off-screen"
7795        );
7796    }
7797
7798    #[test]
7799    fn plain_ensure_visible_requests_minimal_alignment() {
7800        use std::cell::Cell;
7801        use std::rc::Rc;
7802        let recorded: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7803            Rc::new(Cell::new(None));
7804        let mut tree = WidgetTree::new();
7805        let actor = tree.add(FillWidget::new());
7806        let _container = recording_align_container(&mut tree, actor, recorded.clone());
7807        tree.layout(SizeProposal::exact(100.0, 100.0));
7808
7809        let mut ctx = EventContext::new();
7810        ctx.ensure_visible(Rect::new(10.0, 500.0, 20.0, 15.0));
7811        tree.collect_from_ctx(ctx, actor);
7812
7813        assert_eq!(
7814            recorded.get(),
7815            Some((
7816                crate::event::ScrollAlign::Minimal,
7817                crate::event::ScrollMotion::Instant
7818            )),
7819            "the pre-existing reveal API must keep its exact semantics"
7820        );
7821    }
7822
7823    #[test]
7824    fn only_the_innermost_container_aligns() {
7825        use std::cell::Cell;
7826        use std::rc::Rc;
7827        let inner_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7828            Rc::new(Cell::new(None));
7829        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7830            Rc::new(Cell::new(None));
7831
7832        let mut tree = WidgetTree::new();
7833        let actor = tree.add(FillWidget::new());
7834        let inner = recording_align_container(&mut tree, actor, inner_rec.clone());
7835        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7836        tree.layout(SizeProposal::exact(100.0, 100.0));
7837
7838        // Off-screen, so the outer container is asked too (a `Minimal` request
7839        // is gated on visibility).
7840        let mut ctx = EventContext::new();
7841        ctx.ensure_visible_aligned(
7842            Rect::new(10.0, 500.0, 20.0, 15.0),
7843            0.5,
7844            crate::event::ScrollMotion::Instant,
7845        );
7846        tree.collect_from_ctx(ctx, actor);
7847
7848        assert_eq!(
7849            inner_rec.get().map(|(a, _)| a),
7850            Some(crate::event::ScrollAlign::Fraction(0.5)),
7851            "the innermost clipping ancestor owns the pin"
7852        );
7853        assert_eq!(
7854            outer_rec.get().map(|(a, _)| a),
7855            Some(crate::event::ScrollAlign::Minimal),
7856            "an outer container must only bring the inner viewport into view — a \
7857             fraction names a height in one viewport, not in every ancestor's"
7858        );
7859    }
7860
7861    #[test]
7862    fn a_clipper_that_does_not_scroll_passes_the_pin_on() {
7863        // The shape of every capped text column: the editor inside a `MaxSize`
7864        // (which clips once a cap is set, and has no `on_scroll`) inside the
7865        // page's scroll area. The wrapper cannot act on a `ScrollIntoView`, so
7866        // it must not be the one the pin is spent on. Otherwise the scroll
7867        // area is handed a plain reveal and typewriter scrolling silently
7868        // becomes ordinary caret-following.
7869        use crate::test_widgets::StackWidget;
7870        use std::cell::Cell;
7871        use std::rc::Rc;
7872        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7873            Rc::new(Cell::new(None));
7874
7875        let mut tree = WidgetTree::new();
7876        let actor = tree.add(FillWidget::new());
7877        let clipper = tree.add(StackWidget::new().child(actor).clips_children_on(true));
7878        let _scroller = recording_align_container(&mut tree, clipper, outer_rec.clone());
7879        tree.layout(SizeProposal::exact(100.0, 100.0));
7880
7881        // Inside the viewport, so only a pin (never a minimal reveal) reaches
7882        // the scroller at all.
7883        let mut ctx = EventContext::new();
7884        ctx.ensure_visible_aligned(
7885            Rect::new(10.0, 10.0, 20.0, 15.0),
7886            0.5,
7887            crate::event::ScrollMotion::Instant,
7888        );
7889        tree.collect_from_ctx(ctx, actor);
7890
7891        assert_eq!(
7892            outer_rec.get().map(|(a, _)| a),
7893            Some(crate::event::ScrollAlign::Fraction(0.5)),
7894            "a clipping wrapper with no scroll handler must pass the pin through \
7895             to the scroll container above it"
7896        );
7897    }
7898
7899    #[test]
7900    fn a_scroller_that_declines_to_move_still_holds_the_pin() {
7901        // What the scroller does with the pin is its own business: a `SceneView`
7902        // already holding the target where asked answers `Ignored`, since it
7903        // moved nothing. That is not an invitation for the next container out
7904        // to align a rectangle inside a viewport it does not own.
7905        use crate::test_widgets::StackWidget;
7906        use std::cell::Cell;
7907        use std::rc::Rc;
7908        let inner_rec: Rc<Cell<Option<crate::event::ScrollAlign>>> = Rc::new(Cell::new(None));
7909        let outer_rec: Rc<Cell<Option<(crate::event::ScrollAlign, crate::event::ScrollMotion)>>> =
7910            Rc::new(Cell::new(None));
7911
7912        let mut tree = WidgetTree::new();
7913        let actor = tree.add(FillWidget::new());
7914        let rec = inner_rec.clone();
7915        let inner = tree.add(
7916            StackWidget::new()
7917                .child(actor)
7918                .on_scroll(move |ev, _ctx| match ev {
7919                    WidgetEvent::ScrollIntoView { align, .. } => {
7920                        rec.set(Some(*align));
7921                        EventResponse::Ignored
7922                    }
7923                    _ => EventResponse::Ignored,
7924                })
7925                .clips_children(true),
7926        );
7927        let _outer = recording_align_container(&mut tree, inner, outer_rec.clone());
7928        tree.layout(SizeProposal::exact(100.0, 100.0));
7929
7930        // Off-screen, so the outer container is asked too.
7931        let mut ctx = EventContext::new();
7932        ctx.ensure_visible_aligned(
7933            Rect::new(10.0, 500.0, 20.0, 15.0),
7934            0.5,
7935            crate::event::ScrollMotion::Instant,
7936        );
7937        tree.collect_from_ctx(ctx, actor);
7938
7939        assert_eq!(
7940            inner_rec.get(),
7941            Some(crate::event::ScrollAlign::Fraction(0.5)),
7942            "the innermost scroller is offered the pin"
7943        );
7944        assert_eq!(
7945            outer_rec.get().map(|(a, _)| a),
7946            Some(crate::event::ScrollAlign::Minimal),
7947            "declining to move must not pass the pin outward"
7948        );
7949    }
7950
7951    #[test]
7952    fn ensure_widget_visible_uses_target_arena_bounds() {
7953        use std::cell::Cell;
7954        use std::rc::Rc;
7955        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
7956        let mut tree = WidgetTree::new();
7957        // Target lives 500px below the container's top — off the viewport.
7958        let target = tree.add(FillWidget::new());
7959        let rec = recorded.clone();
7960        let container = tree.add(
7961            BelowContainer {
7962                child: Some(target),
7963                offset: 500.0,
7964            }
7965            .on_scroll(move |ev, _ctx| match ev {
7966                WidgetEvent::ScrollIntoView { target_bounds, .. } => {
7967                    rec.set(Some(*target_bounds));
7968                    EventResponse::Handled
7969                }
7970                _ => EventResponse::Ignored,
7971            })
7972            .clips_children(true),
7973        );
7974        tree.layout(SizeProposal::exact(100.0, 100.0));
7975
7976        let expected = tree.bounds(target);
7977        assert!(
7978            expected.y > 100.0,
7979            "fixture sanity: the target must sit below the viewport (y={})",
7980            expected.y
7981        );
7982
7983        // The source widget is irrelevant for the id-based walk — it starts
7984        // from the *target's* parent — so pass the container itself.
7985        let mut ctx = EventContext::new();
7986        ctx.ensure_widget_visible(target);
7987        tree.collect_from_ctx(ctx, container);
7988
7989        assert_eq!(
7990            recorded.get(),
7991            Some(expected),
7992            "ensure_widget_visible(id) must dispatch ScrollIntoView with the \
7993             target's current arena bounds"
7994        );
7995    }
7996
7997    #[test]
7998    fn ensure_widget_visible_ignores_missing_widget() {
7999        // A never-mounted id must neither panic nor dispatch a spurious scroll.
8000        use std::cell::Cell;
8001        use std::rc::Rc;
8002        let recorded: Rc<Cell<Option<Rect>>> = Rc::new(Cell::new(None));
8003        let mut tree = WidgetTree::new();
8004        let actor = tree.add(FillWidget::new());
8005        let _container = recording_scroll_container(&mut tree, actor, recorded.clone());
8006        tree.layout(SizeProposal::exact(100.0, 100.0));
8007
8008        let mut ctx = EventContext::new();
8009        ctx.ensure_widget_visible(WidgetId::default());
8010        tree.collect_from_ctx(ctx, actor); // must not panic
8011
8012        assert_eq!(
8013            recorded.get(),
8014            None,
8015            "an unmounted id must not trigger a scroll"
8016        );
8017    }
8018
8019    #[test]
8020    fn context_menu_inside_a_modal_keeps_the_modal() {
8021        // Regression: right-clicking a widget that lives inside an open modal must
8022        // open its context menu WITHOUT tearing down the modal. `show_context_menu_for`
8023        // used to `dismiss_all()`, which closed the very overlay hosting the editor.
8024        use crate::event::{Modifiers, PointerButton, WidgetEvent};
8025        use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
8026        use crate::test_widgets::{FillWidget, StackWidget};
8027
8028        let mut tree = WidgetTree::new();
8029        // A container standing in for the modal's content subtree, with the editor
8030        // (a right-clickable widget) inside it.
8031        let modal_content = tree.add(StackWidget::new());
8032        let _editor = tree.add_child(
8033            modal_content,
8034            FillWidget::new()
8035                .context_menu(|_pos, _ctx| Some(Box::new(FillWidget::new()) as Box<dyn Widget>)),
8036        );
8037        tree.layout(SizeProposal::exact(200.0, 100.0));
8038
8039        let modal = tree.overlay_manager.show(OverlayRequest::new(
8040            modal_content,
8041            modal_content,
8042            OverlayPlacement::Centered,
8043            DismissBehavior::EscapeKey,
8044        ));
8045        // Give the overlay real bounds so the right-click hit-tests inside it.
8046        tree.overlay_manager
8047            .stack
8048            .iter_mut()
8049            .find(|o| o.id == modal)
8050            .unwrap()
8051            .bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
8052        assert_eq!(tree.overlay_manager.len(), 1);
8053
8054        // Right-click the editor inside the modal.
8055        tree.dispatch_event(WidgetEvent::pointer_down(
8056            Point::new(50.0, 25.0),
8057            PointerButton::Secondary,
8058            Modifiers::NONE,
8059        ));
8060
8061        assert!(
8062            tree.overlay_manager.active_ids().contains(&modal),
8063            "the modal must survive opening a context menu inside it"
8064        );
8065        assert_eq!(
8066            tree.overlay_manager.len(),
8067            2,
8068            "the context menu should now be open on top of the surviving modal"
8069        );
8070    }
8071}
8072
8073/// The stage-1 input ingress: the two sample doors, the scroll routing rule,
8074/// and the guarantee that a mouse still behaves exactly as it did.
8075#[cfg(test)]
8076mod input_ingress_tests {
8077    use super::*;
8078    use crate::event::{EventResponse, Modifiers, PointerButton, ScrollDelta};
8079    use crate::pointer::{
8080        EventTime, PointerId, PointerInfo, PointerPhase, PointerSample, ScrollPhase, ScrollSample,
8081        ScrollSource,
8082    };
8083    use crate::test_widgets::FillWidget;
8084    use crate::widget::{LayoutContext, WidgetPlacement};
8085    use crate::widget_builder::WidgetBuilder;
8086    use std::cell::RefCell;
8087    use std::rc::Rc;
8088
8089    /// A container that lays children out side by side across its bounds, so a
8090    /// hit test at a given x picks a specific child.
8091    ///
8092    /// Local rather than shared: the common `StackWidget` deliberately stacks
8093    /// its children at one origin, which is the opposite of what a routing test
8094    /// needs.
8095    #[derive(Debug)]
8096    struct RowWidget {
8097        children: Vec<WidgetId>,
8098    }
8099
8100    impl crate::widget::Widget for RowWidget {
8101        fn layout_response(
8102            &self,
8103            proposal: SizeProposal,
8104            _ctx: &LayoutContext,
8105        ) -> crate::widget::LayoutResponse {
8106            proposal.resolve(0.0, 0.0).into()
8107        }
8108
8109        fn place_children(
8110            &self,
8111            bounds: Rect,
8112            _proposal: SizeProposal,
8113            children: &mut [WidgetPlacement],
8114            _ctx: &LayoutContext,
8115        ) {
8116            let n = children.len().max(1) as f32;
8117            let w = bounds.width / n;
8118            for (i, child) in children.iter_mut().enumerate() {
8119                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
8120                child.size = teksilo_canvas::Size::new(w, bounds.height);
8121            }
8122        }
8123
8124        fn children(&self) -> Vec<WidgetId> {
8125            self.children.clone()
8126        }
8127    }
8128
8129    /// Everything a widget observes of a pointer interaction, as text, so two
8130    /// runs can be compared for exact equality rather than field by field.
8131    fn record(drive: impl FnOnce(&mut WidgetTree)) -> Vec<String> {
8132        let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
8133
8134        let mut tree = WidgetTree::new();
8135        let pointer_log = log.clone();
8136        let hover_log = log.clone();
8137        tree.add(
8138            FillWidget::new()
8139                .on_pointer_event(move |event, _ctx| {
8140                    pointer_log.borrow_mut().push(format!("{event:?}"));
8141                    EventResponse::Ignored
8142                })
8143                .on_hover(move |entered, _ctx| {
8144                    hover_log.borrow_mut().push(format!("hover({entered})"));
8145                }),
8146        );
8147        tree.layout(SizeProposal::exact(100.0, 100.0));
8148
8149        drive(&mut tree);
8150        log.borrow().clone()
8151    }
8152
8153    /// The load-bearing compatibility claim of this package: lowering a mouse
8154    /// `PointerSample` produces the *same* `WidgetEvent` stream, in the same
8155    /// order, as writing the events out by hand. If this ever diverges, the
8156    /// door has started meaning something different from the events it lowers
8157    /// to.
8158    #[test]
8159    fn a_mouse_sample_reproduces_todays_event_stream() {
8160        let inside = Point::new(40.0, 40.0);
8161        let moved = Point::new(60.0, 55.0);
8162        let outside = Point::new(400.0, 400.0);
8163
8164        let legacy = record(|tree| {
8165            tree.dispatch_event(WidgetEvent::pointer_move(inside));
8166            tree.dispatch_event(WidgetEvent::pointer_down(
8167                inside,
8168                PointerButton::Primary,
8169                Modifiers::NONE,
8170            ));
8171            tree.dispatch_event(WidgetEvent::pointer_move(moved));
8172            tree.dispatch_event(WidgetEvent::pointer_up(
8173                moved,
8174                PointerButton::Primary,
8175                Modifiers::NONE,
8176            ));
8177            tree.dispatch_event(WidgetEvent::pointer_move(outside));
8178        });
8179
8180        let sampled = record(|tree| {
8181            let t = EventTime::ZERO;
8182            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, inside, t));
8183            tree.dispatch_pointer(
8184                PointerSample::mouse(PointerPhase::Down, inside, t)
8185                    .with_button(PointerButton::Primary),
8186            );
8187            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, moved, t));
8188            tree.dispatch_pointer(
8189                PointerSample::mouse(PointerPhase::Up, moved, t)
8190                    .with_button(PointerButton::Primary),
8191            );
8192            tree.dispatch_pointer(PointerSample::mouse(PointerPhase::Move, outside, t));
8193        });
8194
8195        assert_eq!(legacy, sampled);
8196        assert!(
8197            legacy.contains(&"hover(true)".to_string())
8198                && legacy.contains(&"hover(false)".to_string()),
8199            "the fixture must actually exercise enter and leave: {legacy:?}"
8200        );
8201    }
8202
8203    /// A press with no button — what a bare direct-pointer contact reports —
8204    /// still reads as the primary press, because that is what a tap has always
8205    /// been.
8206    #[test]
8207    fn a_buttonless_press_lowers_to_primary() {
8208        let events = record(|tree| {
8209            tree.dispatch_pointer(PointerSample::mouse(
8210                PointerPhase::Down,
8211                Point::new(20.0, 20.0),
8212                EventTime::ZERO,
8213            ));
8214        });
8215        assert!(
8216            events.iter().any(|e| e.contains("button: Primary")),
8217            "{events:?}"
8218        );
8219    }
8220
8221    /// A mouse press and release delivered as samples, as a test writes them
8222    /// by hand with timestamps of its own, form a tap: the sample door reaches
8223    /// the tap recognizer, not only raw `on_pointer_event` handlers. A port
8224    /// review reported the opposite for 0.16.0 and it did not reproduce; this
8225    /// pins it, with a stamp unrelated to the tree's clock on purpose.
8226    #[test]
8227    fn a_sampled_press_and_release_form_a_tap() {
8228        let taps = Rc::new(std::cell::Cell::new(0));
8229        let seen = taps.clone();
8230        let mut tree = WidgetTree::new();
8231        tree.add(FillWidget::new().on_tap(move |_event, _ctx| seen.set(seen.get() + 1)));
8232        tree.layout(SizeProposal::exact(100.0, 100.0));
8233
8234        let at = Point::new(50.0, 50.0);
8235        tree.dispatch_pointer(PointerSample::mouse(
8236            PointerPhase::Down,
8237            at,
8238            EventTime::from_millis(0),
8239        ));
8240        tree.dispatch_pointer(PointerSample::mouse(
8241            PointerPhase::Up,
8242            at,
8243            EventTime::from_millis(5),
8244        ));
8245
8246        assert_eq!(taps.get(), 1);
8247    }
8248
8249    // --- scroll routing --------------------------------------------------
8250
8251    /// Two leaves side by side across a 200-wide tree: `top` owns x < 100,
8252    /// `bottom` owns x >= 100.
8253    fn scroll_fixture() -> (WidgetTree, Rc<RefCell<Vec<&'static str>>>) {
8254        let hits: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
8255        let mut tree = WidgetTree::new();
8256
8257        let leading_hits = hits.clone();
8258        let trailing_hits = hits.clone();
8259        let leading = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8260            leading_hits.borrow_mut().push("leading");
8261            EventResponse::Handled
8262        }));
8263        let trailing = tree.add(FillWidget::new().on_scroll(move |_event, _ctx| {
8264            trailing_hits.borrow_mut().push("trailing");
8265            EventResponse::Handled
8266        }));
8267        tree.add(RowWidget {
8268            children: vec![leading, trailing],
8269        });
8270        tree.layout(SizeProposal::exact(200.0, 100.0));
8271        (tree, hits)
8272    }
8273
8274    fn notch() -> ScrollDelta {
8275        ScrollDelta::Lines { x: 0.0, y: -1.0 }
8276    }
8277
8278    /// The mouse path: a wheel notch carries no position, so it routes by
8279    /// hover exactly as it always has. This is the no-op claim.
8280    #[test]
8281    fn a_positionless_scroll_routes_by_hover() {
8282        let (mut tree, hits) = scroll_fixture();
8283
8284        tree.pointer_move(Point::new(150.0, 50.0)); // hover the trailing leaf
8285        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8286        assert_eq!(*hits.borrow(), vec!["trailing"]);
8287
8288        tree.pointer_move(Point::new(50.0, 50.0)); // hover the leading leaf
8289        tree.dispatch_scroll(ScrollSample::wheel(
8290            notch(),
8291            Modifiers::NONE,
8292            EventTime::ZERO,
8293        ));
8294        assert_eq!(*hits.borrow(), vec!["trailing", "leading"]);
8295    }
8296
8297    /// A positioned scroll routes by hit test, *against* the hover. This is
8298    /// the only thing that makes a synthesised touch pan routable at all: a
8299    /// contact never writes hover, so hover would send the pan to whatever the
8300    /// mouse last touched — or nowhere.
8301    #[test]
8302    fn a_positioned_scroll_routes_by_hit_test() {
8303        let (mut tree, hits) = scroll_fixture();
8304        tree.pointer_move(Point::new(150.0, 50.0)); // hover the TRAILING leaf
8305
8306        tree.dispatch_event(WidgetEvent::scroll_at(
8307            notch(),
8308            Modifiers::NONE,
8309            Point::new(50.0, 50.0), // …but scroll over the LEADING one
8310        ));
8311        assert_eq!(*hits.borrow(), vec!["leading"]);
8312
8313        tree.dispatch_scroll(
8314            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8315                .at(Point::new(150.0, 50.0)),
8316        );
8317        assert_eq!(*hits.borrow(), vec!["leading", "trailing"]);
8318    }
8319
8320    /// With nothing hovered and nothing focused a positionless scroll goes
8321    /// nowhere — the pre-existing behaviour, pinned rather than left
8322    /// incidental.
8323    #[test]
8324    fn a_positionless_scroll_with_no_hover_goes_nowhere() {
8325        let (mut tree, hits) = scroll_fixture();
8326        tree.dispatch_event(WidgetEvent::scroll(notch(), Modifiers::NONE));
8327        assert!(hits.borrow().is_empty());
8328    }
8329
8330    // --- the per-dispatch snapshot ---------------------------------------
8331
8332    /// A handler can ask which pointer it is serving, and what phase and
8333    /// source a scroll had.
8334    #[test]
8335    fn a_handler_sees_the_sample_it_is_serving() {
8336        let seen: Rc<RefCell<Vec<(ScrollPhase, ScrollSource, Option<Point>)>>> =
8337            Rc::new(RefCell::new(Vec::new()));
8338        let sink = seen.clone();
8339
8340        let mut tree = WidgetTree::new();
8341        // A `ScrollSource::TouchPan` sample is delivered along the pan
8342        // claimants and nowhere else (see `widget_tree::pan_arbiter`), so the
8343        // fixture has to be a pan surface for the sample below to reach it at
8344        // all. Nothing about what this test *asserts* changes — only that the
8345        // widget it asserts against is now the kind of widget a touch pan is
8346        // addressed to.
8347        tree.add(
8348            FillWidget::new()
8349                .scroll_container(crate::pointer::touch_action::PanAxes::BOTH)
8350                .on_scroll(move |_event, ctx| {
8351                    sink.borrow_mut().push((
8352                        ctx.scroll_phase(),
8353                        ctx.scroll_source(),
8354                        ctx.pointer_position(),
8355                    ));
8356                    EventResponse::Handled
8357                }),
8358        );
8359        tree.layout(SizeProposal::exact(100.0, 100.0));
8360
8361        tree.dispatch_scroll(ScrollSample {
8362            delta: notch(),
8363            position: Some(Point::new(50.0, 50.0)),
8364            phase: ScrollPhase::Momentum,
8365            source: ScrollSource::TouchPan,
8366            pointer: PointerInfo::mouse(EventTime::from_millis(12)),
8367            modifiers: Modifiers::NONE,
8368        });
8369
8370        assert_eq!(
8371            *seen.borrow(),
8372            vec![(
8373                ScrollPhase::Momentum,
8374                ScrollSource::TouchPan,
8375                Some(Point::new(50.0, 50.0))
8376            )]
8377        );
8378    }
8379
8380    /// Outside a pointer dispatch a handler sees the default mouse — the same
8381    /// answer every such handler got before pointers were distinguishable.
8382    #[test]
8383    fn a_legacy_event_reports_the_mouse_at_the_epoch() {
8384        let seen: Rc<RefCell<Option<(PointerId, ScrollPhase)>>> = Rc::new(RefCell::new(None));
8385        let sink = seen.clone();
8386
8387        let mut tree = WidgetTree::new();
8388        tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
8389            *sink.borrow_mut() = Some((ctx.pointer().id, ctx.scroll_phase()));
8390            EventResponse::Ignored
8391        }));
8392        tree.layout(SizeProposal::exact(100.0, 100.0));
8393        tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 50.0)));
8394
8395        assert_eq!(
8396            *seen.borrow(),
8397            Some((PointerId::MOUSE, ScrollPhase::Discrete))
8398        );
8399    }
8400
8401    /// The snapshot is saved and restored around a dispatch, so a nested one
8402    /// (a synthetic click, a scroll-into-view walk) does not strand the outer
8403    /// sample's view of the world.
8404    #[test]
8405    fn the_snapshot_is_restored_after_a_dispatch() {
8406        let mut tree = WidgetTree::new();
8407        tree.add(FillWidget::new());
8408        tree.layout(SizeProposal::exact(100.0, 100.0));
8409
8410        let before = tree.current_input.clone();
8411        tree.dispatch_scroll(
8412            ScrollSample::wheel(notch(), Modifiers::NONE, EventTime::ZERO)
8413                .at(Point::new(10.0, 10.0)),
8414        );
8415        assert_eq!(tree.current_input, before);
8416    }
8417}
8418
8419#[cfg(test)]
8420mod press_and_focus_tests {
8421    //! The framework press, focus-on-release for direct pointers, and the one
8422    //! `focus_visible` signal — driven through the real ingress doors against
8423    //! real trees.
8424
8425    use std::cell::{Cell, RefCell};
8426    use std::rc::Rc;
8427    use teksilo_canvas::{Point, SizeProposal};
8428
8429    use crate::WidgetId;
8430    use crate::event::{EventResponse, Key, Modifiers, PointerButton, WidgetEvent};
8431    use crate::focus::FocusOrigin;
8432    use crate::pointer::clock::ManualClock;
8433    use crate::pointer::touch_action::PanClaim;
8434    use crate::pointer::{
8435        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
8436        PointerSample,
8437    };
8438    use crate::test_widgets::{FillWidget, StackWidget};
8439    use crate::widget_builder::WidgetBuilder;
8440    use crate::widget_tree::WidgetTree;
8441
8442    // -----------------------------------------------------------------
8443    // Fixtures
8444    // -----------------------------------------------------------------
8445
8446    /// A fresh contact identity, minted through the real allocator.
8447    fn contact_id() -> PointerId {
8448        use std::sync::atomic::{AtomicU64, Ordering};
8449        static NEXT: AtomicU64 = AtomicU64::new(1);
8450        PointerIdAllocator::global().begin(
8451            BackendDeviceKey::new(0x0B11),
8452            NEXT.fetch_add(1, Ordering::Relaxed),
8453        )
8454    }
8455
8456    fn contact(id: PointerId, phase: PointerPhase, at: Point, t: EventTime) -> PointerSample {
8457        PointerSample {
8458            pointer: PointerInfo::touch(id, t),
8459            phase,
8460            position: at,
8461            button: None,
8462            modifiers: Modifiers::NONE,
8463            coalesced: Vec::new(),
8464        }
8465    }
8466
8467    /// A tree on a clock the test drives, so every deadline in these tests is
8468    /// virtual.
8469    fn tree_on_a_clock() -> (WidgetTree, Rc<ManualClock>) {
8470        let mut tree = WidgetTree::new();
8471        let clock = Rc::new(ManualClock::new(EventTime::ZERO));
8472        tree.set_input_clock(clock.clone());
8473        (tree, clock)
8474    }
8475
8476    /// A tappable leaf with a framework press signal installed on it, plus the
8477    /// signal itself.
8478    ///
8479    /// `on_tap` is what gives the node a gesture arena, which is what makes it
8480    /// the press owner — the same thing a `Button` gets from its own tap
8481    /// handler.
8482    fn tappable(tree: &mut WidgetTree) -> (WidgetId, crate::signal::Signal<bool>) {
8483        let id = tree.add(FillWidget::new().focusable().on_tap(|_e, _c| {}));
8484        let signal = tree.pressed_signal(id);
8485        (id, signal)
8486    }
8487
8488    // -----------------------------------------------------------------
8489    // The enum
8490    // -----------------------------------------------------------------
8491
8492    /// `FocusOrigin` now names the device behind a pointer focus, and the two
8493    /// accessors that replaced `== FocusOrigin::Pointer` answer for every arm.
8494    #[test]
8495    fn a_pointer_origin_names_its_device() {
8496        use teksilo_tokens::{PenKind, PointerKind};
8497
8498        assert!(FocusOrigin::Pointer(PointerKind::Touch).is_pointer());
8499        assert!(FocusOrigin::Pointer(PointerKind::Pen(PenKind::Pen)).is_pointer());
8500        assert!(FocusOrigin::POINTER.is_pointer());
8501        assert!(!FocusOrigin::Keyboard.is_pointer());
8502        assert!(!FocusOrigin::Programmatic.is_pointer());
8503        assert!(!FocusOrigin::Accessibility.is_pointer());
8504
8505        assert_eq!(
8506            FocusOrigin::Pointer(PointerKind::Touch).pointer_kind(),
8507            Some(PointerKind::Touch),
8508        );
8509        assert_eq!(FocusOrigin::Keyboard.pointer_kind(), None);
8510        assert_eq!(
8511            FocusOrigin::POINTER.pointer_kind(),
8512            Some(PointerKind::Unknown),
8513            "a widget deriving its own origin says so rather than naming a device it never saw",
8514        );
8515    }
8516
8517    /// The `:focus-visible` verdict, per origin. `Programmatic` abstains — a
8518    /// scripted focus declares no modality, so the ring stays where the user's
8519    /// last real interaction left it.
8520    #[test]
8521    fn only_a_real_navigation_declares_a_modality() {
8522        use teksilo_tokens::PointerKind;
8523
8524        assert_eq!(FocusOrigin::Keyboard.focus_visible(), Some(true));
8525        assert_eq!(FocusOrigin::Accessibility.focus_visible(), Some(true));
8526        assert_eq!(
8527            FocusOrigin::Pointer(PointerKind::Touch).focus_visible(),
8528            Some(false),
8529        );
8530        assert_eq!(FocusOrigin::Programmatic.focus_visible(), None);
8531    }
8532
8533    /// The three Tier-3 configs still carry `Signal<Option<FocusOrigin>>`, and
8534    /// a style reading one still gets what it was written against.
8535    #[test]
8536    fn the_tier_three_configs_are_unchanged() {
8537        let origin: crate::signal::Signal<Option<FocusOrigin>> =
8538            crate::signal::Signal::new(Some(FocusOrigin::Keyboard));
8539        let slider: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8540        let splitter: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8541        let segmented: crate::signal::Signal<Option<FocusOrigin>> = origin.clone();
8542        for field in [slider, splitter, segmented] {
8543            assert_eq!(field.get(), Some(FocusOrigin::Keyboard));
8544        }
8545        origin.set(Some(FocusOrigin::POINTER));
8546        assert_ne!(
8547            origin.get(),
8548            Some(FocusOrigin::Keyboard),
8549            "the `== Some(Keyboard)` test every consumer makes still discriminates",
8550        );
8551    }
8552
8553    // -----------------------------------------------------------------
8554    // focus-visible per kind
8555    // -----------------------------------------------------------------
8556
8557    /// Keyboard focus, then a touch tap, leaves no ring.
8558    ///
8559    /// The behaviour predates this package; what is new is that the focus the
8560    /// tap installs lands on the **release**, so this pins that moving the
8561    /// assignment did not leave a keyboard ring standing over a control the
8562    /// finger just took.
8563    #[test]
8564    fn a_touch_tap_after_keyboard_focus_leaves_no_ring() {
8565        let (mut tree, _clock) = tree_on_a_clock();
8566        let a = tree.add(FillWidget::new().focusable());
8567        let b = tree.add(FillWidget::new().focusable());
8568        let root = tree.add(SideBySide {
8569            children: vec![a, b],
8570        });
8571        tree.layout(SizeProposal::exact(200.0, 100.0));
8572        let _ = root;
8573
8574        let visible = tree.focus_visible_signal();
8575        tree.press_key(Key::Tab, Modifiers::NONE);
8576        assert_eq!(tree.focused(), Some(a));
8577        assert!(visible.get(), "keyboard navigation reveals the ring");
8578
8579        let id = contact_id();
8580        let at = tree.bounds(b).center();
8581        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
8582        tree.dispatch_pointer(contact(
8583            id,
8584            PointerPhase::Up,
8585            at,
8586            EventTime::from_millis(30),
8587        ));
8588
8589        assert_eq!(tree.focused(), Some(b), "the release moved focus");
8590        assert!(!visible.get(), "and the ring did not come with it");
8591        assert_eq!(
8592            tree.focus_origin(),
8593            Some(FocusOrigin::Pointer(teksilo_tokens::PointerKind::Touch)),
8594        );
8595    }
8596
8597    /// An assistive `Action::Focus` reveals the ring. The user is navigating —
8598    /// they are simply not doing it with a key — and this used to route through
8599    /// `Programmatic`, which declares nothing and left a screen-reader user
8600    /// with an invisible focus after any click.
8601    #[test]
8602    fn an_assistive_focus_reveals_the_ring() {
8603        let mut tree = WidgetTree::new();
8604        let a = tree.add(FillWidget::new().focusable());
8605        let b = tree.add(FillWidget::new().focusable());
8606        let root = tree.add(SideBySide {
8607            children: vec![a, b],
8608        });
8609        tree.layout(SizeProposal::exact(200.0, 50.0));
8610        let _ = root;
8611        let visible = tree.focus_visible_signal();
8612
8613        tree.click(a);
8614        assert_eq!(tree.focused(), Some(a));
8615        assert!(!visible.get(), "the click hid it");
8616
8617        tree.dispatch_event(WidgetEvent::AccessAction {
8618            target: Some(b),
8619            action: accesskit::Action::Focus,
8620            target_node: crate::accessibility::widget_id_to_node_id(b),
8621            data: None,
8622        });
8623        assert_eq!(tree.focused(), Some(b));
8624        assert!(visible.get(), "an assistive move is a navigation");
8625        assert_eq!(tree.focus_origin(), Some(FocusOrigin::Accessibility));
8626    }
8627
8628    /// A view-like owner of two items it can point at, inside a focusable
8629    /// ancestor whose handler takes any `Focus` (a card selecting itself), the
8630    /// two beside another focusable control.
8631    struct ItemFocusFixture {
8632        tree: WidgetTree,
8633        other: WidgetId,
8634        /// A container around the owner that takes no focus.
8635        group: WidgetId,
8636        owner: WidgetId,
8637        /// The owner refuses this one, as a calendar refuses a day the arrows
8638        /// cannot reach.
8639        refused: WidgetId,
8640        taken: WidgetId,
8641        /// A plain label, advertising no `Focus`.
8642        label: WidgetId,
8643        cursor: Rc<Cell<Option<accesskit::NodeId>>>,
8644        catch_all: Rc<Cell<u32>>,
8645    }
8646
8647    fn item_focus_fixture() -> ItemFocusFixture {
8648        let advertises_focus = |b: &mut crate::accessibility::AccessNodeBuilder| {
8649            b.add_action(accesskit::Action::Focus)
8650        };
8651        let mut tree = WidgetTree::new();
8652        let other = tree.add(FillWidget::new().focusable());
8653        let refused = tree.add(FillWidget::new().access_customize(advertises_focus));
8654        let taken = tree.add(FillWidget::new().access_customize(advertises_focus));
8655        let label = tree.add(FillWidget::new());
8656        let cursor: Rc<Cell<Option<accesskit::NodeId>>> = Rc::new(Cell::new(None));
8657        let owner_slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
8658        let taken_node = crate::accessibility::widget_id_to_node_id(taken);
8659        let owner = tree.add(
8660            SideBySide {
8661                children: vec![refused, taken],
8662            }
8663            .focusable(true)
8664            .on_access_action_request({
8665                let cursor = cursor.clone();
8666                let owner_slot = owner_slot.clone();
8667                move |action, node, _data, ctx| {
8668                    if action != accesskit::Action::Focus || node != taken_node {
8669                        return EventResponse::Ignored;
8670                    }
8671                    cursor.set(Some(node));
8672                    if let Some(owner) = owner_slot.get() {
8673                        ctx.request_focus(owner);
8674                    }
8675                    EventResponse::Handled
8676                }
8677            }),
8678        );
8679        owner_slot.set(Some(owner));
8680        // A container that is not focusable, as a `GroupBox` is.
8681        let group = tree.add(SideBySide {
8682            children: vec![owner],
8683        });
8684        let catch_all = Rc::new(Cell::new(0));
8685        let card = tree.add(
8686            SideBySide {
8687                children: vec![group, label],
8688            }
8689            .focusable(true)
8690            .on_access_action({
8691                let catch_all = catch_all.clone();
8692                move |action, _ctx| {
8693                    if action != accesskit::Action::Focus {
8694                        return EventResponse::Ignored;
8695                    }
8696                    catch_all.set(catch_all.get() + 1);
8697                    EventResponse::Handled
8698                }
8699            }),
8700        );
8701        tree.add(SideBySide {
8702            children: vec![other, card],
8703        });
8704        tree.layout(SizeProposal::exact(400.0, 50.0));
8705        ItemFocusFixture {
8706            tree,
8707            other,
8708            group,
8709            owner,
8710            refused,
8711            taken,
8712            label,
8713            cursor,
8714            catch_all,
8715        }
8716    }
8717
8718    fn assistive_focus(tree: &mut WidgetTree, id: WidgetId) -> bool {
8719        tree.dispatch_access_action(
8720            crate::accessibility::widget_id_to_node_id(id),
8721            accesskit::Action::Focus,
8722            None,
8723            &mut crate::window::NoopWindowOps,
8724        )
8725    }
8726
8727    /// An assistive `Focus` on an item a widget points at through its
8728    /// `active_descendant` goes to that widget, which moves its cursor to
8729    /// the item and takes keyboard focus itself, as an arrow key would. The
8730    /// item takes no keys: focusing its own node left the widget naming no
8731    /// descendant and the keys going nowhere useful. The ancestor above the
8732    /// owner, which takes any `Focus`, is not asked.
8733    #[test]
8734    fn an_assistive_focus_on_an_item_goes_to_the_widget_that_points_at_it() {
8735        let mut f = item_focus_fixture();
8736        let visible = f.tree.focus_visible_signal();
8737        f.tree.click(f.other);
8738        assert!(!visible.get());
8739
8740        assert!(
8741            assistive_focus(&mut f.tree, f.taken),
8742            "the widget that points at the item took it"
8743        );
8744        let item = crate::accessibility::widget_id_to_node_id(f.taken);
8745        assert_eq!(f.cursor.get(), Some(item), "its cursor moved to the item");
8746        assert_eq!(
8747            f.tree.focused(),
8748            Some(f.owner),
8749            "the keyboard is on the widget, not on the item"
8750        );
8751        assert!(visible.get(), "an assistive move is a navigation");
8752        assert_eq!(f.tree.focus_origin(), Some(FocusOrigin::Accessibility));
8753        assert_eq!(
8754            f.catch_all.get(),
8755            0,
8756            "no ancestor above the owner was asked"
8757        );
8758    }
8759
8760    /// An item its owner refuses gets no focus: the keyboard is not parked on
8761    /// a node that takes no keys, no ancestor above the owner takes it in its
8762    /// place, and the action is reported unhandled.
8763    #[test]
8764    fn an_item_its_owner_refuses_takes_no_assistive_focus() {
8765        let mut f = item_focus_fixture();
8766        f.tree.click(f.other);
8767        assert!(!assistive_focus(&mut f.tree, f.refused));
8768        assert_eq!(f.tree.focused(), Some(f.other), "focus did not move");
8769        assert_eq!(f.cursor.get(), None);
8770        assert_eq!(f.catch_all.get(), 0);
8771    }
8772
8773    /// A node that advertises no `Focus` (a label) is offered to no one and
8774    /// reports the action unhandled, whatever an ancestor's handler takes.
8775    #[test]
8776    fn a_node_that_offers_no_focus_takes_no_assistive_focus() {
8777        let mut f = item_focus_fixture();
8778        f.tree.click(f.other);
8779        assert!(!assistive_focus(&mut f.tree, f.label));
8780        assert_eq!(f.tree.focused(), Some(f.other));
8781        assert_eq!(f.catch_all.get(), 0);
8782    }
8783
8784    /// A greyed item (a menu row the arrows still highlight) is still offered
8785    /// to its owner; an item in a disabled container is offered to no one,
8786    /// not to an ancestor above the container either.
8787    #[test]
8788    fn a_greyed_item_reaches_its_owner_and_a_disabled_container_takes_nothing() {
8789        let mut f = item_focus_fixture();
8790        let item_enabled = crate::signal::Signal::new(false);
8791        f.tree.enabled_when(f.taken, item_enabled.clone());
8792        f.tree.layout(SizeProposal::exact(400.0, 50.0));
8793        f.tree.click(f.other);
8794        assert!(assistive_focus(&mut f.tree, f.taken), "a greyed item");
8795        assert_eq!(f.tree.focused(), Some(f.owner));
8796
8797        let mut f = item_focus_fixture();
8798        f.tree
8799            .enabled_when(f.group, crate::signal::Signal::new(false));
8800        f.tree.layout(SizeProposal::exact(400.0, 50.0));
8801        f.tree.click(f.other);
8802        assert!(
8803            !assistive_focus(&mut f.tree, f.taken),
8804            "a disabled container"
8805        );
8806        assert_eq!(f.tree.focused(), Some(f.other));
8807        assert_eq!(f.cursor.get(), None);
8808        assert_eq!(f.catch_all.get(), 0);
8809    }
8810
8811    /// A composite with a Tab stop inside (a `DateEdit` root that advertises
8812    /// `Focus`) is no item: the focus lands on its stop, and the ancestor that
8813    /// takes any `Focus` is not asked.
8814    #[test]
8815    fn a_composite_in_a_catch_all_ancestor_gets_its_stop() {
8816        let mut tree = WidgetTree::new();
8817        let other = tree.add(FillWidget::new().focusable());
8818        let leaf = tree.add(FillWidget::new().focusable());
8819        let composite = tree.add(
8820            SideBySide {
8821                children: vec![leaf],
8822            }
8823            .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8824        );
8825        let catch_all = Rc::new(Cell::new(0));
8826        let card = tree.add(
8827            SideBySide {
8828                children: vec![composite],
8829            }
8830            .focusable(true)
8831            .on_access_action({
8832                let catch_all = catch_all.clone();
8833                move |action, _ctx| {
8834                    if action == accesskit::Action::Focus {
8835                        catch_all.set(catch_all.get() + 1);
8836                        return EventResponse::Handled;
8837                    }
8838                    EventResponse::Ignored
8839                }
8840            }),
8841        );
8842        tree.add(SideBySide {
8843            children: vec![other, card],
8844        });
8845        tree.layout(SizeProposal::exact(400.0, 50.0));
8846        tree.click(other);
8847        assert!(assistive_focus(&mut tree, composite));
8848        assert_eq!(tree.focused(), Some(leaf), "the keyboard is on its stop");
8849        assert_eq!(catch_all.get(), 0);
8850    }
8851
8852    /// A focusable widget that publishes one synthetic child, advertising
8853    /// `Focus` or not.
8854    #[derive(Debug)]
8855    struct SyntheticOwner {
8856        advertises: bool,
8857    }
8858
8859    impl crate::widget::Widget for SyntheticOwner {
8860        fn layout_response(
8861            &self,
8862            proposal: SizeProposal,
8863            _ctx: &crate::widget::LayoutContext,
8864        ) -> crate::widget::LayoutResponse {
8865            proposal.resolve(0.0, 0.0).into()
8866        }
8867
8868        fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
8869            let advertises = self.advertises;
8870            builder.push_scene_child(1, crate::accessibility::SyntheticKind::ChartMark, |child| {
8871                child.set_role(accesskit::Role::GraphicsObject);
8872                if advertises {
8873                    child.add_action(accesskit::Action::Focus);
8874                }
8875            });
8876        }
8877    }
8878
8879    /// A focusable ancestor that takes any `Focus`, counting them.
8880    fn catch_all_card(tree: &mut WidgetTree, child: WidgetId) -> (WidgetId, Rc<Cell<u32>>) {
8881        let catch_all = Rc::new(Cell::new(0));
8882        let card = tree.add(
8883            SideBySide {
8884                children: vec![child],
8885            }
8886            .focusable(true)
8887            .on_access_action({
8888                let catch_all = catch_all.clone();
8889                move |action, _ctx| {
8890                    if action == accesskit::Action::Focus {
8891                        catch_all.set(catch_all.get() + 1);
8892                        return EventResponse::Handled;
8893                    }
8894                    EventResponse::Ignored
8895                }
8896            }),
8897        );
8898        (card, catch_all)
8899    }
8900
8901    /// A disabled owner of synthetic items, or a disabled composite, is no
8902    /// greyed item: a `Focus` aimed at it is offered to no ancestor, and
8903    /// nothing takes it.
8904    #[test]
8905    fn a_disabled_synthetic_owner_or_composite_offers_nothing_upward() {
8906        let mut tree = WidgetTree::new();
8907        let other = tree.add(FillWidget::new().focusable());
8908        let owner = tree.add(SyntheticOwner { advertises: true }.focusable(true));
8909        let leaf = tree.add(FillWidget::new().focusable());
8910        let composite = tree.add(
8911            SideBySide {
8912                children: vec![leaf],
8913            }
8914            .access_customize(|b| b.add_action(accesskit::Action::Focus)),
8915        );
8916        let group = tree.add(SideBySide {
8917            children: vec![owner, composite],
8918        });
8919        let (card, catch_all) = catch_all_card(&mut tree, group);
8920        tree.add(SideBySide {
8921            children: vec![other, card],
8922        });
8923        tree.layout(SizeProposal::exact(400.0, 50.0));
8924        let _ = tree.sync_accessibility();
8925        tree.enabled_when(owner, crate::signal::Signal::new(false));
8926        tree.enabled_when(composite, crate::signal::Signal::new(false));
8927        tree.layout(SizeProposal::exact(400.0, 50.0));
8928        tree.click(other);
8929
8930        let mark = crate::accessibility::synthetic_node_id(
8931            owner,
8932            1,
8933            crate::accessibility::SyntheticKind::ChartMark,
8934        );
8935        let handled = tree.dispatch_access_action(
8936            mark,
8937            accesskit::Action::Focus,
8938            None,
8939            &mut crate::window::NoopWindowOps,
8940        );
8941        assert!(!handled, "a disabled owner's item");
8942        assert!(
8943            !assistive_focus(&mut tree, composite),
8944            "a disabled composite"
8945        );
8946        assert_eq!(tree.focused(), Some(other));
8947        assert_eq!(catch_all.get(), 0, "no ancestor was asked");
8948    }
8949
8950    /// A synthetic node that advertises no `Focus` is offered to no one; the
8951    /// framework focuses its owner, as it always did.
8952    #[test]
8953    fn a_synthetic_node_that_offers_no_focus_is_offered_to_no_one() {
8954        let mut tree = WidgetTree::new();
8955        let other = tree.add(FillWidget::new().focusable());
8956        // Not focusable itself, so the offer would climb to the card.
8957        let owner = tree.add(SyntheticOwner { advertises: false });
8958        let (card, catch_all) = catch_all_card(&mut tree, owner);
8959        tree.add(SideBySide {
8960            children: vec![other, card],
8961        });
8962        tree.layout(SizeProposal::exact(400.0, 50.0));
8963        let _ = tree.sync_accessibility();
8964        tree.click(other);
8965        let mark = crate::accessibility::synthetic_node_id(
8966            owner,
8967            1,
8968            crate::accessibility::SyntheticKind::ChartMark,
8969        );
8970        let _ = tree.dispatch_access_action(
8971            mark,
8972            accesskit::Action::Focus,
8973            None,
8974            &mut crate::window::NoopWindowOps,
8975        );
8976        assert_eq!(catch_all.get(), 0, "no handler was asked");
8977    }
8978
8979    /// A handler that answers `Focus` and moves nothing, with the keyboard on
8980    /// another control, is no success: the action is reported unhandled and
8981    /// the mouse-focused control gets no ring.
8982    #[test]
8983    fn an_answer_that_moves_no_focus_is_no_success() {
8984        let mut tree = WidgetTree::new();
8985        let other = tree.add(FillWidget::new().focusable());
8986        let item = tree
8987            .add(FillWidget::new().access_customize(|b| b.add_action(accesskit::Action::Focus)));
8988        let owner = tree.add(
8989            SideBySide {
8990                children: vec![item],
8991            }
8992            .focusable(true)
8993            .on_access_action(|action, _ctx| {
8994                if action == accesskit::Action::Focus {
8995                    EventResponse::Handled
8996                } else {
8997                    EventResponse::Ignored
8998                }
8999            }),
9000        );
9001        tree.add(SideBySide {
9002            children: vec![other, owner],
9003        });
9004        tree.layout(SizeProposal::exact(400.0, 50.0));
9005        let visible = tree.focus_visible_signal();
9006        tree.click(other);
9007        assert!(!assistive_focus(&mut tree, item), "reported unhandled");
9008        assert_eq!(tree.focused(), Some(other));
9009        assert!(!visible.get(), "no ring on the mouse-focused control");
9010    }
9011
9012    /// A programmatic focus abstains: it declares no modality, so the ring
9013    /// stays exactly where the last real interaction left it. This is what
9014    /// `Button` / `Checkbox`'s pre-existing focus-ring tests pin, and what
9015    /// browsers do for `element.focus()`.
9016    #[test]
9017    fn a_programmatic_focus_leaves_the_modality_alone() {
9018        let mut tree = WidgetTree::new();
9019        let a = tree.add(FillWidget::new().focusable());
9020        let b = tree.add(FillWidget::new().focusable());
9021        let root = tree.add(SideBySide {
9022            children: vec![a, b],
9023        });
9024        tree.layout(SizeProposal::exact(200.0, 100.0));
9025        let _ = root;
9026        let visible = tree.focus_visible_signal();
9027
9028        tree.press_key(Key::Tab, Modifiers::NONE);
9029        assert_eq!(tree.focused(), Some(a));
9030        assert!(visible.get());
9031        tree.focus(b);
9032        assert!(
9033            visible.get(),
9034            "a scripted focus does not hide a keyboard ring"
9035        );
9036
9037        tree.click(a);
9038        assert!(!visible.get());
9039        tree.focus(b);
9040        assert!(
9041            !visible.get(),
9042            "and does not reveal one after a click either",
9043        );
9044    }
9045
9046    // -----------------------------------------------------------------
9047    // Activation on release
9048    // -----------------------------------------------------------------
9049
9050    /// A mouse focuses on press, exactly as it always has.
9051    #[test]
9052    fn a_mouse_still_focuses_on_press() {
9053        let mut tree = WidgetTree::new();
9054        let w = tree.add(FillWidget::new().focusable());
9055        tree.layout(SizeProposal::exact(100.0, 50.0));
9056        let center = tree.bounds(w).center();
9057
9058        tree.dispatch_event(WidgetEvent::pointer_down(
9059            center,
9060            PointerButton::Primary,
9061            Modifiers::NONE,
9062        ));
9063        assert_eq!(
9064            tree.focused(),
9065            Some(w),
9066            "focus lands on the press for an indirect pointer",
9067        );
9068    }
9069
9070    /// A finger's focus waits for the release.
9071    #[test]
9072    fn a_finger_focuses_on_release() {
9073        let (mut tree, _clock) = tree_on_a_clock();
9074        let w = tree.add(FillWidget::new().focusable());
9075        tree.layout(SizeProposal::exact(100.0, 50.0));
9076        let center = tree.bounds(w).center();
9077
9078        let id = contact_id();
9079        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9080        assert_eq!(
9081            tree.focused(),
9082            None,
9083            "a finger that has only landed has chosen nothing yet",
9084        );
9085
9086        tree.dispatch_pointer(contact(
9087            id,
9088            PointerPhase::Up,
9089            center,
9090            EventTime::from_millis(40),
9091        ));
9092        assert_eq!(tree.focused(), Some(w));
9093        assert_eq!(
9094            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9095            Some(teksilo_tokens::PointerKind::Touch),
9096            "the origin names the device that delivered it",
9097        );
9098    }
9099
9100    /// …and only when the release lands back on the same focusable. A finger
9101    /// that presses one control, slides onto its neighbour and lifts has
9102    /// activated nothing and must move focus nowhere.
9103    #[test]
9104    fn a_release_on_a_different_focusable_moves_no_focus() {
9105        let (mut tree, _clock) = tree_on_a_clock();
9106        let a = tree.add(FillWidget::new().focusable());
9107        let b = tree.add(FillWidget::new().focusable());
9108        let root = tree.add(SideBySide {
9109            children: vec![a, b],
9110        });
9111        tree.layout(SizeProposal::exact(200.0, 50.0));
9112        let _ = root;
9113
9114        let on_a = tree.bounds(a).center();
9115        let on_b = tree.bounds(b).center();
9116        let id = contact_id();
9117        tree.dispatch_pointer(contact(id, PointerPhase::Down, on_a, EventTime::ZERO));
9118        tree.dispatch_pointer(contact(
9119            id,
9120            PointerPhase::Move,
9121            on_b,
9122            EventTime::from_millis(20),
9123        ));
9124        tree.dispatch_pointer(contact(
9125            id,
9126            PointerPhase::Up,
9127            on_b,
9128            EventTime::from_millis(40),
9129        ));
9130
9131        assert_eq!(
9132            tree.focused(),
9133            None,
9134            "the guard is `the release landed on the same focusable as the press`",
9135        );
9136    }
9137
9138    // -----------------------------------------------------------------
9139    // The press visual
9140    // -----------------------------------------------------------------
9141
9142    /// A mouse press lights the visual at once and the release clears it —
9143    /// the pre-touch rule, and no press-feedback delay anywhere near it.
9144    #[test]
9145    fn a_mouse_press_visual_is_unchanged() {
9146        let mut tree = WidgetTree::new();
9147        let (w, pressed) = tappable(&mut tree);
9148        tree.layout(SizeProposal::exact(100.0, 50.0));
9149        let center = tree.bounds(w).center();
9150
9151        assert!(!pressed.get(), "not pressed at rest");
9152        tree.dispatch_event(WidgetEvent::pointer_down(
9153            center,
9154            PointerButton::Primary,
9155            Modifiers::NONE,
9156        ));
9157        assert!(pressed.get(), "an indirect pointer lights up on the press");
9158        assert!(
9159            !tree.press_pending(w),
9160            "and never waits: a mouse opens no pan session, so there is no \
9161             ambiguity to wait out",
9162        );
9163        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9164
9165        tree.dispatch_event(WidgetEvent::pointer_up(
9166            center,
9167            PointerButton::Primary,
9168            Modifiers::NONE,
9169        ));
9170        assert!(!pressed.get(), "the release clears it");
9171        assert_eq!(tree.pressed_by(w), None);
9172    }
9173
9174    /// A slide off the target clears the visual, and sliding back on restores
9175    /// it. WCAG 2.2 SC 2.5.2's abort gesture, and reversible right up to the
9176    /// release.
9177    #[test]
9178    fn a_slide_off_clears_the_visual_and_re_entry_restores_it() {
9179        let (mut tree, _clock) = tree_on_a_clock();
9180        let (w, pressed) = tappable(&mut tree);
9181        tree.layout(SizeProposal::exact(100.0, 200.0));
9182        let inside = tree.bounds(w).center();
9183        let outside = Point::new(inside.x, inside.y + 400.0);
9184
9185        let id = contact_id();
9186        tree.dispatch_pointer(contact(id, PointerPhase::Down, inside, EventTime::ZERO));
9187        assert!(
9188            pressed.get(),
9189            "nothing here claims a pan, so no delay applies"
9190        );
9191
9192        tree.dispatch_pointer(contact(
9193            id,
9194            PointerPhase::Move,
9195            outside,
9196            EventTime::from_millis(20),
9197        ));
9198        assert!(!pressed.get(), "the press has left its target");
9199        assert!(!tree.press_is_inside(w));
9200        assert_eq!(
9201            tree.pressed_by(w),
9202            Some(id),
9203            "the contact still holds the press — it is the *visual* that is off",
9204        );
9205
9206        tree.dispatch_pointer(contact(
9207            id,
9208            PointerPhase::Move,
9209            inside,
9210            EventTime::from_millis(40),
9211        ));
9212        assert!(pressed.get(), "sliding back on restores it");
9213
9214        tree.dispatch_pointer(contact(
9215            id,
9216            PointerPhase::Up,
9217            inside,
9218            EventTime::from_millis(60),
9219        ));
9220        assert!(!pressed.get());
9221    }
9222
9223    /// A second contact cannot clear the first's visual. Its own release
9224    /// removes only the press it owns.
9225    #[test]
9226    fn a_second_contact_cannot_clear_the_first_visual() {
9227        let (mut tree, _clock) = tree_on_a_clock();
9228        let (w, pressed) = tappable(&mut tree);
9229        tree.layout(SizeProposal::exact(100.0, 50.0));
9230        let bounds = tree.bounds(w);
9231        let first_at = Point::new(bounds.x + 20.0, bounds.y + 25.0);
9232        let second_at = Point::new(bounds.x + 70.0, bounds.y + 25.0);
9233
9234        let first = contact_id();
9235        let second = contact_id();
9236        tree.dispatch_pointer(contact(
9237            first,
9238            PointerPhase::Down,
9239            first_at,
9240            EventTime::ZERO,
9241        ));
9242        assert!(pressed.get());
9243        assert_eq!(tree.pressed_by(w), Some(first));
9244
9245        tree.dispatch_pointer(contact(
9246            second,
9247            PointerPhase::Down,
9248            second_at,
9249            EventTime::from_millis(10),
9250        ));
9251        assert_eq!(
9252            tree.pressed_by(w),
9253            Some(first),
9254            "under `MultiContact::First` the second contact is terminated before \
9255             it reaches the arena at all",
9256        );
9257
9258        tree.dispatch_pointer(contact(
9259            second,
9260            PointerPhase::Up,
9261            second_at,
9262            EventTime::from_millis(20),
9263        ));
9264        assert!(
9265            pressed.get(),
9266            "so the second contact's release cannot clear a visual it never owned",
9267        );
9268        assert_eq!(tree.pressed_by(w), Some(first));
9269
9270        tree.dispatch_pointer(contact(
9271            first,
9272            PointerPhase::Up,
9273            first_at,
9274            EventTime::from_millis(30),
9275        ));
9276        assert!(!pressed.get(), "the owner's release does clear it");
9277    }
9278
9279    /// A cancel clears the visual. The node is never sent an `Up` to clear it
9280    /// from, so nothing else could.
9281    #[test]
9282    fn a_cancel_clears_the_visual() {
9283        let (mut tree, _clock) = tree_on_a_clock();
9284        let (w, pressed) = tappable(&mut tree);
9285        tree.layout(SizeProposal::exact(100.0, 50.0));
9286        let center = tree.bounds(w).center();
9287
9288        let id = contact_id();
9289        tree.dispatch_pointer(contact(id, PointerPhase::Down, center, EventTime::ZERO));
9290        assert!(pressed.get());
9291
9292        tree.dispatch_pointer(contact(
9293            id,
9294            PointerPhase::Cancel,
9295            center,
9296            EventTime::from_millis(20),
9297        ));
9298        assert!(!pressed.get(), "a press that was taken away is not painted");
9299        assert_eq!(tree.pressed_by(w), None);
9300    }
9301
9302    /// A peer winning the arbitration clears the visual. The pressed control is
9303    /// never told; only the router knows it has lost the press.
9304    #[test]
9305    fn a_pan_claim_clears_the_visual() {
9306        let (mut tree, clock) = tree_on_a_clock();
9307        let (row, pressed) = tappable(&mut tree);
9308        let list = tree.add(
9309            StackWidget::new()
9310                .child(row)
9311                .pan_claim(PanClaim::vertical())
9312                .on_scroll(|_e, _c| EventResponse::Handled),
9313        );
9314        tree.layout(SizeProposal::exact(200.0, 400.0));
9315        let _ = list;
9316        let start = Point::new(100.0, 200.0);
9317
9318        let id = contact_id();
9319        tree.dispatch_pointer(contact(id, PointerPhase::Down, start, EventTime::ZERO));
9320        // Inside a claimant, so the visual waits.
9321        assert!(tree.press_pending(row));
9322        clock.set(EventTime::from_millis(100));
9323        tree.tick_gestures(std::time::Instant::now());
9324        assert!(pressed.get(), "…and appears once the delay has elapsed");
9325
9326        // Past the touch profile's 36 dp pan slop: the list claims.
9327        tree.dispatch_pointer(contact(
9328            id,
9329            PointerPhase::Move,
9330            Point::new(100.0, 260.0),
9331            EventTime::from_millis(120),
9332        ));
9333        assert!(
9334            !pressed.get(),
9335            "the row lost the press to the list and must stop advertising it",
9336        );
9337        assert_eq!(tree.pressed_by(row), None);
9338    }
9339
9340    // -----------------------------------------------------------------
9341    // Which button may raise the visual
9342    // -----------------------------------------------------------------
9343
9344    /// The visual answers to the same buttons the activation does. A node
9345    /// carrying a plain `on_tap` accepts `PRIMARY` and nothing else, so a
9346    /// middle, back or forward press must light nothing up — the press it
9347    /// would be advertising can never complete.
9348    #[test]
9349    fn a_button_the_control_cannot_act_on_raises_no_visual() {
9350        let taps = Rc::new(Cell::new(0u32));
9351        let count = taps.clone();
9352        let mut tree = WidgetTree::new();
9353        let w = tree.add(FillWidget::new().focusable().on_tap(move |_e, _c| {
9354            count.set(count.get() + 1);
9355        }));
9356        let pressed = tree.pressed_signal(w);
9357        tree.layout(SizeProposal::exact(100.0, 50.0));
9358        let center = tree.bounds(w).center();
9359
9360        for button in [
9361            PointerButton::Middle,
9362            PointerButton::Back,
9363            PointerButton::Forward,
9364        ] {
9365            tree.dispatch_event(WidgetEvent::pointer_down(center, button, Modifiers::NONE));
9366            assert!(
9367                !pressed.get(),
9368                "{button:?} cannot activate an `on_tap` node, so it must not light one up",
9369            );
9370            assert_eq!(tree.pressed_by(w), None, "and owns no visual to clear");
9371            tree.dispatch_event(WidgetEvent::pointer_up(center, button, Modifiers::NONE));
9372            assert_eq!(
9373                taps.get(),
9374                0,
9375                "the tap recognizer refuses {button:?} too — that is the point",
9376            );
9377        }
9378
9379        // …and the button it does act on is untouched.
9380        tree.dispatch_event(WidgetEvent::pointer_down(
9381            center,
9382            PointerButton::Primary,
9383            Modifiers::NONE,
9384        ));
9385        assert!(pressed.get(), "a primary press lights up as it always has");
9386        assert_eq!(tree.pressed_by(w), Some(PointerId::MOUSE));
9387        tree.dispatch_event(WidgetEvent::pointer_up(
9388            center,
9389            PointerButton::Primary,
9390            Modifiers::NONE,
9391        ));
9392        assert!(!pressed.get());
9393        assert_eq!(taps.get(), 1);
9394    }
9395
9396    /// The case a real user hits: a right-click on a control with no context
9397    /// menu. The secondary arm finds nothing to open and falls through to the
9398    /// ordinary press path, which must still raise nothing.
9399    #[test]
9400    fn a_secondary_press_with_no_context_menu_raises_no_visual() {
9401        let mut tree = WidgetTree::new();
9402        let (w, pressed) = tappable(&mut tree);
9403        tree.layout(SizeProposal::exact(100.0, 50.0));
9404        let center = tree.bounds(w).center();
9405
9406        tree.dispatch_event(WidgetEvent::pointer_down(
9407            center,
9408            PointerButton::Secondary,
9409            Modifiers::NONE,
9410        ));
9411        assert!(
9412            !pressed.get(),
9413            "nothing opened, and nothing may look pressed either",
9414        );
9415        assert_eq!(tree.pressed_by(w), None);
9416        assert!(!tree.press_is_inside(w));
9417    }
9418
9419    /// A widget that widened its own mask keeps the visual on the buttons it
9420    /// widened to: the gate reads the node's declared acceptance, it does not
9421    /// hardcode `PRIMARY`.
9422    #[test]
9423    fn a_widened_mask_widens_the_visual_with_it() {
9424        use crate::event::ButtonMask;
9425
9426        let mut tree = WidgetTree::new();
9427        let w = tree.add(
9428            FillWidget::new()
9429                .on_tap(|_e, _c| {})
9430                .accept_tap_buttons(ButtonMask::PRIMARY | ButtonMask::MIDDLE),
9431        );
9432        let pressed = tree.pressed_signal(w);
9433        tree.layout(SizeProposal::exact(100.0, 50.0));
9434        let center = tree.bounds(w).center();
9435
9436        tree.dispatch_event(WidgetEvent::pointer_down(
9437            center,
9438            PointerButton::Middle,
9439            Modifiers::NONE,
9440        ));
9441        assert!(pressed.get(), "this node really does act on a middle-click");
9442
9443        tree.dispatch_event(WidgetEvent::pointer_up(
9444            center,
9445            PointerButton::Middle,
9446            Modifiers::NONE,
9447        ));
9448        assert!(!pressed.get());
9449    }
9450
9451    /// A press that raises no visual still records the focus a direct pointer
9452    /// defers to its release. A stylus barrel button reports `Secondary`, and
9453    /// a pen that presses a control and lifts on it has chosen that control
9454    /// whether or not the button lit it up — the record exists for the focus,
9455    /// not only for the visual.
9456    #[test]
9457    fn a_press_that_raises_no_visual_still_defers_its_focus() {
9458        use teksilo_tokens::{PenKind, PointerKind};
9459
9460        let (mut tree, _clock) = tree_on_a_clock();
9461        let (w, pressed) = tappable(&mut tree);
9462        tree.layout(SizeProposal::exact(100.0, 50.0));
9463        let center = tree.bounds(w).center();
9464
9465        let id = contact_id();
9466        let barrel = |phase, t| PointerSample {
9467            pointer: PointerInfo {
9468                kind: PointerKind::Pen(PenKind::Pen),
9469                ..PointerInfo::touch(id, t)
9470            },
9471            phase,
9472            position: center,
9473            button: Some(PointerButton::Secondary),
9474            modifiers: Modifiers::NONE,
9475            coalesced: Vec::new(),
9476        };
9477
9478        tree.dispatch_pointer(barrel(PointerPhase::Down, EventTime::ZERO));
9479        assert!(!pressed.get(), "the barrel button activates nothing here");
9480        assert_eq!(tree.pressed_by(w), None);
9481        assert_eq!(
9482            tree.focused(),
9483            None,
9484            "a direct pointer has chosen nothing until it lifts",
9485        );
9486
9487        tree.dispatch_pointer(barrel(PointerPhase::Up, EventTime::from_millis(40)));
9488        assert_eq!(
9489            tree.focused(),
9490            Some(w),
9491            "the deferral is not button-gated: the release landed where the press did",
9492        );
9493        assert_eq!(
9494            tree.focus_origin().and_then(FocusOrigin::pointer_kind),
9495            Some(PointerKind::Pen(PenKind::Pen)),
9496        );
9497    }
9498
9499    // -----------------------------------------------------------------
9500    // The press-feedback delay
9501    // -----------------------------------------------------------------
9502
9503    /// The delay applies only inside a pan claimant — a control nothing can
9504    /// scroll out from under has no ambiguity to wait out.
9505    #[test]
9506    fn the_feedback_delay_applies_only_inside_a_claimant() {
9507        // Outside a claimant: immediate.
9508        let (mut tree, _clock) = tree_on_a_clock();
9509        let (w, pressed) = tappable(&mut tree);
9510        tree.layout(SizeProposal::exact(200.0, 400.0));
9511        let id = contact_id();
9512        let at = tree.bounds(w).center();
9513        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9514        assert!(
9515            !tree.press_pending(w),
9516            "no claimant above it, so nothing to rule out",
9517        );
9518        assert!(pressed.get());
9519
9520        // Inside one: withheld, then released by the delay.
9521        let (mut tree, clock) = tree_on_a_clock();
9522        let (row, pressed) = tappable(&mut tree);
9523        let list = tree.add(
9524            StackWidget::new()
9525                .child(row)
9526                .pan_claim(PanClaim::vertical())
9527                .on_scroll(|_e, _c| EventResponse::Handled),
9528        );
9529        tree.layout(SizeProposal::exact(200.0, 400.0));
9530        let _ = list;
9531        let id = contact_id();
9532        let at = Point::new(100.0, 200.0);
9533        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9534        assert!(tree.press_pending(row), "a finger might be about to scroll");
9535        assert!(!pressed.get(), "so the row does not flash");
9536        assert!(
9537            tree.press_is_inside(row),
9538            "the press is real; only its visual is being withheld",
9539        );
9540
9541        clock.set(EventTime::from_millis(99));
9542        tree.tick_gestures(std::time::Instant::now());
9543        assert!(!pressed.get(), "99 ms is under the 100 ms delay");
9544
9545        clock.set(EventTime::from_millis(100));
9546        tree.tick_gestures(std::time::Instant::now());
9547        assert!(pressed.get(), "and 100 ms is it");
9548        assert!(!tree.press_pending(row));
9549    }
9550
9551    /// A mouse pressing inside the very same claimant waits for nothing: an
9552    /// indirect pointer opens no pan session, so the delay never reaches it.
9553    #[test]
9554    fn a_mouse_inside_a_claimant_never_waits() {
9555        let mut tree = WidgetTree::new();
9556        let (row, pressed) = tappable(&mut tree);
9557        let list = tree.add(
9558            StackWidget::new()
9559                .child(row)
9560                .pan_claim(PanClaim::vertical())
9561                .on_scroll(|_e, _c| EventResponse::Handled),
9562        );
9563        tree.layout(SizeProposal::exact(200.0, 400.0));
9564        let _ = list;
9565
9566        tree.dispatch_event(WidgetEvent::pointer_down(
9567            Point::new(100.0, 200.0),
9568            PointerButton::Primary,
9569            Modifiers::NONE,
9570        ));
9571        assert!(!tree.press_pending(row));
9572        assert!(pressed.get(), "Compact with a mouse is exactly as it was");
9573    }
9574
9575    // -----------------------------------------------------------------
9576    // The EventContext queries
9577    // -----------------------------------------------------------------
9578
9579    /// The three queries `teksilo-widgets`' `common/interaction.rs` consumes,
9580    /// read from inside a real handler.
9581    #[test]
9582    fn a_handler_can_read_the_press_it_is_inside() {
9583        let (mut tree, _clock) = tree_on_a_clock();
9584        let seen: Rc<RefCell<Vec<(bool, bool, bool)>>> = Rc::new(RefCell::new(Vec::new()));
9585        let log = seen.clone();
9586        let row = tree.add(FillWidget::new().on_tap(|_e, _c| {}).on_pointer_event(
9587            move |event, ctx| {
9588                if matches!(event, WidgetEvent::PointerUp { .. }) {
9589                    log.borrow_mut().push((
9590                        ctx.is_pressed(),
9591                        ctx.press_is_inside(),
9592                        ctx.press_pending(),
9593                    ));
9594                }
9595                EventResponse::Ignored
9596            },
9597        ));
9598        let list = tree.add(
9599            StackWidget::new()
9600                .child(row)
9601                .pan_claim(PanClaim::vertical())
9602                .on_scroll(|_e, _c| EventResponse::Handled),
9603        );
9604        tree.layout(SizeProposal::exact(200.0, 400.0));
9605        let _ = list;
9606
9607        let id = contact_id();
9608        let at = Point::new(100.0, 200.0);
9609        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9610        tree.dispatch_pointer(contact(
9611            id,
9612            PointerPhase::Up,
9613            at,
9614            EventTime::from_millis(30),
9615        ));
9616
9617        assert_eq!(
9618            seen.borrow().as_slice(),
9619            &[(false, true, true)],
9620            "released inside the claimant before the delay elapsed: inside, \
9621             pending, and therefore not yet showing",
9622        );
9623    }
9624
9625    /// A handler outside any press reads three falses rather than a panic or a
9626    /// stale answer.
9627    #[test]
9628    fn a_handler_outside_a_press_reads_nothing() {
9629        let mut tree = WidgetTree::new();
9630        let asked = Rc::new(Cell::new(false));
9631        let flag = asked.clone();
9632        let w = tree.add(FillWidget::new().focusable().on_key(move |_e, ctx| {
9633            flag.set(ctx.is_pressed() || ctx.press_is_inside() || ctx.press_pending());
9634            EventResponse::Ignored
9635        }));
9636        tree.layout(SizeProposal::exact(100.0, 50.0));
9637        tree.focus(w);
9638        tree.press_key(Key::ArrowDown, Modifiers::NONE);
9639        assert!(!asked.get());
9640    }
9641
9642    /// A completed interaction leaves no press behind, and the leak detector
9643    /// says so — it now reads the press table, which it could not before this
9644    /// package landed one.
9645    #[test]
9646    fn a_completed_press_leaves_the_detector_clean() {
9647        let (mut tree, _clock) = tree_on_a_clock();
9648        let (w, pressed) = tappable(&mut tree);
9649        tree.layout(SizeProposal::exact(100.0, 50.0));
9650        let at = tree.bounds(w).center();
9651
9652        let id = contact_id();
9653        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9654        assert!(pressed.get());
9655        tree.dispatch_pointer(contact(
9656            id,
9657            PointerPhase::Up,
9658            at,
9659            EventTime::from_millis(30),
9660        ));
9661        tree.assert_no_leaked_pointer_state();
9662    }
9663
9664    /// …and it would have caught the opposite. Driven by holding the press open
9665    /// rather than by faking state, so the assertion is about the real exit
9666    /// path.
9667    #[test]
9668    fn the_detector_reports_a_press_still_held() {
9669        let (mut tree, _clock) = tree_on_a_clock();
9670        let (w, _pressed) = tappable(&mut tree);
9671        tree.layout(SizeProposal::exact(100.0, 50.0));
9672        let at = tree.bounds(w).center();
9673
9674        let id = contact_id();
9675        tree.dispatch_pointer(contact(id, PointerPhase::Down, at, EventTime::ZERO));
9676        let held = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
9677            tree.assert_no_leaked_pointer_state()
9678        }));
9679        let message = *held
9680            .expect_err("a live press is leaked state")
9681            .downcast::<String>()
9682            .expect("the detector panics with a message");
9683        assert!(
9684            message.contains("is still pressed by"),
9685            "the press is named in the report, got: {message}",
9686        );
9687    }
9688
9689    /// A container that lays its children out side by side, so a hit test at a
9690    /// given x picks a specific child. `StackWidget` deliberately stacks at one
9691    /// origin, which is the opposite of what a release-elsewhere test needs.
9692    #[derive(Debug)]
9693    struct SideBySide {
9694        children: Vec<WidgetId>,
9695    }
9696
9697    impl crate::widget::Widget for SideBySide {
9698        fn layout_response(
9699            &self,
9700            proposal: SizeProposal,
9701            _ctx: &crate::widget::LayoutContext,
9702        ) -> crate::widget::LayoutResponse {
9703            proposal.resolve(0.0, 0.0).into()
9704        }
9705
9706        fn place_children(
9707            &self,
9708            bounds: teksilo_canvas::Rect,
9709            _proposal: SizeProposal,
9710            children: &mut [crate::widget::WidgetPlacement],
9711            _ctx: &crate::widget::LayoutContext,
9712        ) {
9713            let n = children.len().max(1) as f32;
9714            let w = bounds.width / n;
9715            for (i, child) in children.iter_mut().enumerate() {
9716                child.origin = Point::new(bounds.x + w * i as f32, bounds.y);
9717                child.size = teksilo_canvas::Size::new(w, bounds.height);
9718            }
9719        }
9720
9721        fn children(&self) -> Vec<WidgetId> {
9722            self.children.clone()
9723        }
9724    }
9725}
9726
9727#[cfg(test)]
9728mod overlay_release_dismissal_tests {
9729    //! Outside-press overlay dismissal, driven through the real ingress door.
9730    //!
9731    //! The defect: `handle_click_outside` ran on the `PointerDown` and then
9732    //! *fell through*, so one press both closed a menu and actuated whatever
9733    //! the menu was covering. With a cursor that is defensible — the user aimed
9734    //! at a pixel they could see the whole time. With a finger it is not: the
9735    //! menu is the only thing they were looking at, and the control underneath
9736    //! is one they never saw.
9737
9738    use std::cell::Cell;
9739    use std::rc::Rc;
9740
9741    use teksilo_canvas::{Point, Rect, Size, SizeProposal};
9742
9743    use crate::WidgetId;
9744    use crate::event::{Modifiers, PointerButton, WidgetEvent};
9745    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
9746    use crate::pointer::{
9747        BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
9748        PointerSample,
9749    };
9750    use crate::test_widgets::FillWidget;
9751    use crate::widget::{LayoutContext, LayoutResponse, Widget};
9752    use crate::widget_builder::WidgetBuilder;
9753    use crate::widget_tree::WidgetTree;
9754
9755    // -----------------------------------------------------------------
9756    // Fixtures
9757    // -----------------------------------------------------------------
9758
9759    /// A leaf with an intrinsic size, so an overlay hung off it gets real
9760    /// bounds out of `position_overlays` instead of a zero rect.
9761    #[derive(Debug)]
9762    struct Panel(Size);
9763
9764    impl Widget for Panel {
9765        fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9766            self.0.into()
9767        }
9768    }
9769
9770    /// A root that puts its first child over the whole window and its second in
9771    /// a 10 dp corner.
9772    ///
9773    /// Two bare roots would both be laid out at the window's full size — the
9774    /// trigger would then contain every press point, and the pre-existing
9775    /// "a press on a click-opened overlay's own anchor is consumed" rule would
9776    /// swallow the very presses these tests are about.
9777    #[derive(Debug)]
9778    struct PageAndTrigger {
9779        children: Vec<WidgetId>,
9780    }
9781
9782    impl Widget for PageAndTrigger {
9783        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
9784            proposal.resolve(0.0, 0.0).into()
9785        }
9786
9787        fn place_children(
9788            &self,
9789            bounds: Rect,
9790            _proposal: SizeProposal,
9791            children: &mut [crate::widget::WidgetPlacement],
9792            _ctx: &LayoutContext,
9793        ) {
9794            if let Some(page) = children.get_mut(0) {
9795                page.origin = bounds.origin();
9796                page.size = bounds.size();
9797            }
9798            if let Some(trigger) = children.get_mut(1) {
9799                trigger.origin = bounds.origin();
9800                trigger.size = Size::new(10.0, 10.0);
9801            }
9802        }
9803
9804        fn children(&self) -> Vec<WidgetId> {
9805            self.children.clone()
9806        }
9807    }
9808
9809    fn contact_id(n: u64) -> PointerId {
9810        PointerIdAllocator::global().begin(BackendDeviceKey::new(0x0FA2), n)
9811    }
9812
9813    fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
9814        PointerSample {
9815            pointer: PointerInfo::touch(id, EventTime::ZERO),
9816            phase,
9817            position: at,
9818            button: None,
9819            modifiers: Modifiers::NONE,
9820            coalesced: Vec::new(),
9821        }
9822    }
9823
9824    /// The scene every test below shares: a tappable page filling the window,
9825    /// and a menu overlay floating over part of it at (100, 100, 200, 200).
9826    ///
9827    /// The menu's content is a separate root, so a press outside it lands on
9828    /// the page and a press inside it lands on the menu — which is exactly the
9829    /// arrangement the dismissal rule is about.
9830    fn page_with_a_menu() -> (WidgetTree, WidgetId, Rc<Cell<u32>>) {
9831        let mut tree = WidgetTree::new();
9832        let taps = Rc::new(Cell::new(0u32));
9833        let counter = taps.clone();
9834        let page = tree.add(
9835            FillWidget::new()
9836                .focusable()
9837                .on_tap(move |_e, _c| counter.set(counter.get() + 1)),
9838        );
9839        // A small trigger in the corner, well clear of both press points — see
9840        // `PageAndTrigger` for why it cannot simply be a second root.
9841        let trigger = tree.add(Panel(Size::new(10.0, 10.0)));
9842        let _root = tree.add(PageAndTrigger {
9843            children: vec![page, trigger],
9844        });
9845        let menu = tree.add(Panel(Size::new(200.0, 200.0)));
9846        tree.show_overlay(OverlayRequest::new(
9847            menu,
9848            trigger,
9849            OverlayPlacement::AtPointer(Point::new(100.0, 100.0)),
9850            DismissBehavior::ClickOutside,
9851        ));
9852        tree.layout(SizeProposal::exact(800.0, 600.0));
9853        assert_eq!(tree.active_overlays().len(), 1, "the menu is open");
9854        (tree, page, taps)
9855    }
9856
9857    fn outside() -> Point {
9858        Point::new(600.0, 500.0)
9859    }
9860
9861    fn inside_menu() -> Point {
9862        Point::new(150.0, 150.0)
9863    }
9864
9865    // -----------------------------------------------------------------
9866    // The direct-pointer contract
9867    // -----------------------------------------------------------------
9868
9869    /// The arming press reaches nothing: no press record, no capture, and the
9870    /// menu is still up because the decision belongs to the release.
9871    #[test]
9872    fn the_suppressed_down_leaves_nothing_pressed_or_captured_beneath() {
9873        let (mut tree, page, taps) = page_with_a_menu();
9874        let finger = contact_id(1);
9875        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9876
9877        assert_eq!(
9878            tree.active_overlays().len(),
9879            1,
9880            "the press does not close the menu; the release does"
9881        );
9882        assert_eq!(tree.pressed_by(page), None, "nothing beneath is pressed");
9883        let entry = tree.pointers.get(finger).expect("the contact is live");
9884        assert_eq!(entry.captured_by, None, "nothing beneath captured it");
9885        assert!(entry.sequence.is_none(), "no arbitration was opened");
9886        assert_eq!(taps.get(), 0);
9887    }
9888
9889    /// …and the release closes the menu without actuating what it covered.
9890    #[test]
9891    fn a_touch_tap_outside_a_menu_closes_it_and_actuates_nothing() {
9892        let (mut tree, page, taps) = page_with_a_menu();
9893        let finger = contact_id(2);
9894        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9895        tree.dispatch_pointer(touch(finger, PointerPhase::Up, outside()));
9896
9897        assert!(tree.active_overlays().is_empty(), "the menu closed");
9898        assert_eq!(taps.get(), 0, "the page beneath was never tapped");
9899        assert_eq!(tree.pressed_by(page), None);
9900        tree.assert_no_leaked_pointer_state();
9901    }
9902
9903    /// A press that never completes delivers nothing at all — neither the
9904    /// dismissal it armed nor the press it withheld.
9905    #[test]
9906    fn a_cancelled_press_aborts_the_arm() {
9907        let (mut tree, page, taps) = page_with_a_menu();
9908        let finger = contact_id(3);
9909        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9910        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
9911
9912        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9913        assert_eq!(taps.get(), 0);
9914        assert_eq!(tree.pressed_by(page), None);
9915        tree.assert_no_leaked_pointer_state();
9916    }
9917
9918    /// Land beside the menu, drag onto it, lift there. The finger changed its
9919    /// mind: the menu stays, and the page under the arming press was never
9920    /// touched either.
9921    #[test]
9922    fn a_press_slid_onto_the_menu_dismisses_nothing() {
9923        let (mut tree, _page, taps) = page_with_a_menu();
9924        let finger = contact_id(4);
9925        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
9926        tree.dispatch_pointer(touch(finger, PointerPhase::Move, inside_menu()));
9927        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
9928
9929        assert_eq!(tree.active_overlays().len(), 1, "the menu survives");
9930        assert_eq!(taps.get(), 0);
9931        tree.assert_no_leaked_pointer_state();
9932    }
9933
9934    /// A finger working *inside* the menu is an interaction, and a second one
9935    /// landing on the page is not a reason to take it away mid-flight.
9936    #[test]
9937    fn a_second_contact_cannot_dismiss_what_the_first_is_manipulating() {
9938        let (mut tree, _page, _taps) = page_with_a_menu();
9939        let first = contact_id(5);
9940        let second = contact_id(6);
9941
9942        tree.dispatch_pointer(touch(first, PointerPhase::Down, inside_menu()));
9943        assert!(
9944            tree.pointers
9945                .get(first)
9946                .is_some_and(|e| e.sequence.is_some()),
9947            "the first contact holds a live press inside the menu"
9948        );
9949
9950        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9951        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9952        assert_eq!(
9953            tree.active_overlays().len(),
9954            1,
9955            "the menu the first finger is holding must not close under it"
9956        );
9957
9958        // Once the first contact's press is over there is nothing left to
9959        // revoke, so it protects nothing and the same tap closes the menu.
9960        tree.dispatch_pointer(touch(first, PointerPhase::Up, inside_menu()));
9961        let third = contact_id(7);
9962        tree.dispatch_pointer(touch(third, PointerPhase::Down, outside()));
9963        tree.dispatch_pointer(touch(third, PointerPhase::Up, outside()));
9964        assert!(tree.active_overlays().is_empty());
9965        tree.assert_no_leaked_pointer_state();
9966    }
9967
9968    /// A contact that is merely *holding an arm* has no sequence and no
9969    /// capture, so `press_is_revocable` says it has no press — and it must not
9970    /// block a second contact's dismissal the way a real press does.
9971    #[test]
9972    fn an_arm_is_not_itself_a_press_that_blocks_another_contact() {
9973        let (mut tree, _page, _taps) = page_with_a_menu();
9974        let first = contact_id(8);
9975        let second = contact_id(9);
9976
9977        tree.dispatch_pointer(touch(first, PointerPhase::Down, outside()));
9978        assert!(
9979            tree.overlay_manager().has_armed_dismiss(first),
9980            "the first contact armed"
9981        );
9982        // The first contact is live but holds nothing revocable.
9983        assert!(tree.busy_press_points(second).is_empty());
9984
9985        tree.dispatch_pointer(touch(second, PointerPhase::Down, outside()));
9986        assert!(tree.overlay_manager().has_armed_dismiss(second));
9987        tree.dispatch_pointer(touch(second, PointerPhase::Up, outside()));
9988        assert!(tree.active_overlays().is_empty());
9989
9990        // The first contact's arm now names an overlay that is gone; its own
9991        // release must be a quiet no-op rather than a panic.
9992        tree.dispatch_pointer(touch(first, PointerPhase::Up, outside()));
9993        tree.assert_no_leaked_pointer_state();
9994    }
9995
9996    /// A tap *inside* the menu is not an outside press, so nothing is armed and
9997    /// the menu's own content handles the press exactly as before.
9998    #[test]
9999    fn a_touch_inside_the_menu_arms_nothing() {
10000        let (mut tree, _page, _taps) = page_with_a_menu();
10001        let finger = contact_id(10);
10002        tree.dispatch_pointer(touch(finger, PointerPhase::Down, inside_menu()));
10003        assert!(!tree.overlay_manager().has_armed_dismiss(finger));
10004        tree.dispatch_pointer(touch(finger, PointerPhase::Up, inside_menu()));
10005        assert_eq!(tree.active_overlays().len(), 1);
10006        tree.assert_no_leaked_pointer_state();
10007    }
10008
10009    /// A contact whose press was never opened — it hit nothing, or its `Down`
10010    /// was suppressed by an arm — still ceases to exist when the platform
10011    /// revokes it.
10012    ///
10013    /// The cancel funnel returns early for a pointer with nothing revocable and
10014    /// so never reaches the step that drops the table entry. Before the release
10015    /// dismissal that shape was rare (a press on bare background); the arm makes
10016    /// it the ordinary case, so the ingress door applies the same
10017    /// contact-ceases-to-exist rule it applies to an `Up`.
10018    #[test]
10019    fn a_contact_with_no_press_still_ends_when_the_platform_revokes_it() {
10020        let mut tree = WidgetTree::new();
10021        tree.layout(SizeProposal::exact(800.0, 600.0));
10022        let finger = contact_id(99);
10023        tree.dispatch_pointer(touch(finger, PointerPhase::Down, outside()));
10024        tree.dispatch_pointer(touch(finger, PointerPhase::Cancel, outside()));
10025        tree.assert_no_leaked_pointer_state();
10026    }
10027
10028    // -----------------------------------------------------------------
10029    // The mouse, unchanged
10030    // -----------------------------------------------------------------
10031
10032    /// The press dismisses and falls through, exactly as before: one click both
10033    /// closes the menu and actuates the control beneath.
10034    #[test]
10035    fn a_mouse_click_outside_a_menu_dismisses_on_the_press_and_falls_through() {
10036        let (mut tree, _page, taps) = page_with_a_menu();
10037        tree.dispatch_event(WidgetEvent::pointer_down(
10038            outside(),
10039            PointerButton::Primary,
10040            Modifiers::NONE,
10041        ));
10042        assert!(
10043            tree.active_overlays().is_empty(),
10044            "the mouse still dismisses on the press"
10045        );
10046        assert!(
10047            tree.pointers
10048                .get(PointerId::MOUSE)
10049                .is_some_and(|e| e.sequence.is_some()),
10050            "and the press still reaches the page beneath"
10051        );
10052        tree.dispatch_event(WidgetEvent::pointer_up(
10053            outside(),
10054            PointerButton::Primary,
10055            Modifiers::NONE,
10056        ));
10057        assert_eq!(taps.get(), 1, "the control beneath activated");
10058        assert!(
10059            !tree.overlay_manager().has_armed_dismiss(PointerId::MOUSE),
10060            "a mouse never arms"
10061        );
10062        tree.assert_no_leaked_pointer_state();
10063    }
10064
10065    // -----------------------------------------------------------------
10066    // Contact avoidance
10067    // -----------------------------------------------------------------
10068
10069    /// A context menu raised by a finger keeps clear of the contact patch; the
10070    /// same menu raised by a mouse lands on the pixel, as it always has.
10071    #[test]
10072    fn a_context_menu_avoids_the_contact_that_opened_it() {
10073        fn menu_bounds(pointer: PointerInfo, at: Point) -> Rect {
10074            let mut tree = WidgetTree::new();
10075            let page = tree.add(
10076                FillWidget::new()
10077                    .focusable()
10078                    .context_menu(|_p, _ctx| Some(Box::new(Panel(Size::new(200.0, 160.0))))),
10079            );
10080            let _ = page;
10081            tree.layout(SizeProposal::exact(800.0, 600.0));
10082            tree.dispatch_pointer(PointerSample {
10083                pointer,
10084                phase: PointerPhase::Down,
10085                position: at,
10086                button: Some(PointerButton::Secondary),
10087                modifiers: Modifiers::NONE,
10088                coalesced: Vec::new(),
10089            });
10090            tree.layout(SizeProposal::exact(800.0, 600.0));
10091            let id = *tree
10092                .active_overlays()
10093                .first()
10094                .expect("the context menu opened");
10095            tree.overlay_manager().bounds_for(id).expect("bounds")
10096        }
10097
10098        let at = Point::new(400.0, 300.0);
10099        let finger = menu_bounds(PointerInfo::touch(contact_id(11), EventTime::ZERO), at);
10100        let contact = crate::overlay::rect_centred_on(at, crate::overlay::ASSUMED_CONTACT_PATCH);
10101        assert!(
10102            finger.x < contact.x && finger.right() <= contact.x,
10103            "a touch menu clears the contact patch: {finger:?} vs {contact:?}"
10104        );
10105
10106        let mouse = menu_bounds(PointerInfo::mouse(EventTime::ZERO), at);
10107        assert_eq!(
10108            (mouse.x, mouse.y),
10109            (at.x, at.y),
10110            "a mouse menu still opens with its corner on the pointer"
10111        );
10112    }
10113}
10114
10115/// An overlay is chosen by its **bounds**, and what happens when its content
10116/// then claims nothing at the point.
10117///
10118/// Two answers, and the flag on the content root is what picks between them.
10119/// An ordinary overlay ends the search inside itself — that is what keeps the
10120/// miss-only slop pass confined to the one layer the exact pass entered, so a
10121/// near-miss on a menu row can never be re-attributed to a control on the page
10122/// behind the menu. An overlay whose content root declares `event_pass_through`
10123/// is the exception: the flag already means "what I did not claim belongs to
10124/// whatever is behind me", and an overlay root is not an exception to it.
10125///
10126/// This is not a hypothetical. The touch text affordances were specified as a
10127/// viewport-sized pass-through layer, and under the first answer applied to
10128/// both, mounting one made the editor beneath stop taking presses entirely —
10129/// measured, as `hit_test` returning `None` at a point inside the field.
10130#[cfg(test)]
10131mod pass_through_overlay_tests {
10132    use teksilo_canvas::{Point, Rect, SizeProposal};
10133
10134    use crate::WidgetId;
10135    use crate::overlay::{DismissBehavior, OverlayPlacement, OverlayRequest};
10136    use crate::test_widgets::FillWidget;
10137    use crate::widget::{LayoutContext, LayoutResponse, Widget};
10138    use crate::widget_builder::WidgetBuilder;
10139    use crate::widget_tree::WidgetTree;
10140
10141    /// The affordance layer's shape: fills whatever it is given, and puts its
10142    /// one child — a selection handle — on a fixed rectangle inside it.
10143    #[derive(Debug)]
10144    struct HandleLayer {
10145        handle: WidgetId,
10146        at: Rect,
10147    }
10148
10149    impl Widget for HandleLayer {
10150        fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
10151            proposal.resolve(0.0, 0.0).into()
10152        }
10153
10154        fn place_children(
10155            &self,
10156            _bounds: Rect,
10157            _proposal: SizeProposal,
10158            children: &mut [crate::widget::WidgetPlacement],
10159            _ctx: &LayoutContext,
10160        ) {
10161            if let Some(child) = children.get_mut(0) {
10162                child.origin = self.at.origin();
10163                child.size = self.at.size();
10164            }
10165        }
10166
10167        fn children(&self) -> Vec<WidgetId> {
10168            vec![self.handle]
10169        }
10170    }
10171
10172    const HANDLE: Rect = Rect {
10173        x: 10.0,
10174        y: 10.0,
10175        width: 20.0,
10176        height: 20.0,
10177    };
10178
10179    /// Which flag the overlay's content root carries.
10180    #[derive(Clone, Copy, PartialEq)]
10181    enum RootFlag {
10182        /// The affordance layer's: not a target itself, children are.
10183        PassThrough,
10184        /// Neither the root nor its children are targets. Stronger, and
10185        /// deliberately **not** what the fall-through is gated on.
10186        HitTransparent,
10187    }
10188
10189    /// A field under a viewport-sized overlay carrying one handle.
10190    ///
10191    /// Both flags make the subtree answer `None` for a point no handle covers,
10192    /// which is the state the fall-through decides — so the two arms differ in
10193    /// nothing but the flag the router reads.
10194    fn field_under_a_layer(flag: RootFlag) -> (WidgetTree, WidgetId, WidgetId) {
10195        let mut tree = WidgetTree::new();
10196        let field = tree.add(FillWidget::new());
10197        let handle = tree.add(FillWidget::new());
10198        let layer = HandleLayer { handle, at: HANDLE };
10199        let layer = match flag {
10200            RootFlag::PassThrough => tree.add(layer.event_pass_through(true)),
10201            RootFlag::HitTransparent => tree.add(layer.hit_transparent(true)),
10202        };
10203        tree.show_overlay(OverlayRequest::new(
10204            layer,
10205            field,
10206            OverlayPlacement::FullViewport,
10207            DismissBehavior::Manual,
10208        ));
10209        tree.layout(SizeProposal::exact(400.0, 200.0));
10210        assert_eq!(tree.active_overlays().len(), 1, "the layer is up");
10211        (tree, field, handle)
10212    }
10213
10214    /// The door: the surface under a pass-through layer goes on taking presses.
10215    #[test]
10216    fn a_pass_through_layer_hands_back_what_it_did_not_claim() {
10217        let (tree, field, handle) = field_under_a_layer(RootFlag::PassThrough);
10218
10219        assert_eq!(
10220            tree.hit_test(Point::new(15.0, 15.0)),
10221            Some(handle),
10222            "the layer must still win the point its own child covers"
10223        );
10224        assert_eq!(
10225            tree.hit_test(Point::new(200.0, 100.0)),
10226            Some(field),
10227            "a press the layer did not claim must reach the surface beneath it"
10228        );
10229    }
10230
10231    /// `event_pass_through` removes a node from **hit-testing**, not from the
10232    /// **bubble path** of a descendant that was hit.
10233    ///
10234    /// The distinction is what lets a host mount its affordances at the full
10235    /// viewport and still answer the one press neither the layer nor the surface
10236    /// beneath can: the pass-through root is skipped when a point belongs to
10237    /// nobody in its subtree, and is still told about a press that landed on one
10238    /// of its children. The single-line text stack's `AffordanceHost` is exactly
10239    /// this — a cursor's click on a selection handle, which the handle refuses and
10240    /// the editor never sees.
10241    #[test]
10242    fn a_pass_through_root_still_hears_a_press_that_landed_on_its_child() {
10243        use std::cell::Cell;
10244        use std::rc::Rc;
10245
10246        let seen = Rc::new(Cell::new(0u32));
10247        let counter = seen.clone();
10248        let mut tree = WidgetTree::new();
10249        let field = tree.add(FillWidget::new());
10250        let handle = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
10251        let layer = tree.add(
10252            HandleLayer { handle, at: HANDLE }
10253                .event_pass_through(true)
10254                .on_pointer_event(move |_event, _ctx| {
10255                    counter.set(counter.get() + 1);
10256                    crate::event::EventResponse::Ignored
10257                }),
10258        );
10259        tree.show_overlay(OverlayRequest::new(
10260            layer,
10261            field,
10262            OverlayPlacement::FullViewport,
10263            DismissBehavior::Manual,
10264        ));
10265        tree.layout(SizeProposal::exact(400.0, 200.0));
10266
10267        tree.pointer_down_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10268        assert!(
10269            seen.get() > 0,
10270            "a pass-through root heard nothing about a press on its own child"
10271        );
10272
10273        let after_child = seen.get();
10274        tree.pointer_up_button(Point::new(15.0, 15.0), crate::event::PointerButton::Primary);
10275        // …and a press it did not contain a target for never arrives, because it
10276        // was never the hit.
10277        tree.pointer_down_button(
10278            Point::new(200.0, 100.0),
10279            crate::event::PointerButton::Primary,
10280        );
10281        assert_eq!(
10282            seen.get(),
10283            after_child + 1,
10284            "the press-and-lift on the child accounts for the count, and the \
10285             press on the surface beneath added nothing"
10286        );
10287    }
10288
10289    /// …and the widening stops at that one flag.
10290    ///
10291    /// `hit_transparent` is the discriminating case, and the only one available:
10292    /// it is the other way for an overlay's content to claim nothing at a point,
10293    /// so it is the fixture in which the gate — rather than the subtree's own
10294    /// answer — is what decides. A gate that read "the subtree claimed nothing"
10295    /// alone would fall through here too, and with it past every overlay whose
10296    /// content happens to miss, which is what confines the miss-only slop pass to
10297    /// the layer the exact pass entered.
10298    ///
10299    /// The exclusion is deliberate rather than an oversight: the one overlay that
10300    /// must be seen past regardless is the drag preview, and the hit-test already
10301    /// takes an explicit `exclude_overlay` for it.
10302    #[test]
10303    fn a_hit_transparent_root_does_not_get_the_fall_through() {
10304        let (tree, field, _handle) = field_under_a_layer(RootFlag::HitTransparent);
10305
10306        // Nothing in the overlay is a target, not even the handle.
10307        assert_eq!(
10308            tree.hit_test(Point::new(15.0, 15.0)),
10309            None,
10310            "hit_transparent must exclude the subtree, or this fixture is not \
10311             testing the gate"
10312        );
10313        assert_eq!(
10314            tree.hit_test(Point::new(200.0, 100.0)),
10315            None,
10316            "the fall-through reached past an overlay that did not ask for it"
10317        );
10318        let _ = field;
10319    }
10320}
10321
10322/// Say once per owner, in a debug build, that its menu refused a section a
10323/// node added to it (its widget does not implement
10324/// `Widget::append_context_menu_section`), so the section was left out.
10325fn note_refused_section(owner: WidgetId) {
10326    #[cfg(debug_assertions)]
10327    {
10328        thread_local! {
10329            static NOTED: std::cell::RefCell<std::collections::HashSet<WidgetId>> =
10330                std::cell::RefCell::new(std::collections::HashSet::new());
10331        }
10332        if NOTED.with(|noted| noted.borrow_mut().insert(owner)) {
10333            eprintln!(
10334                "teksilo: the context menu of {owner:?} takes no section \
10335                 (Widget::append_context_menu_section), so a contribution was left out"
10336            );
10337        }
10338    }
10339    #[cfg(not(debug_assertions))]
10340    let _ = owner;
10341}
10342
10343/// A key's identity for pairing a release with its press when the window
10344/// manager noted no physical key (tests, automation): the key itself, a
10345/// character folded to lower case so a Shift released first does not split
10346/// one key into two.
10347fn logical_key_identity(key: Key) -> u64 {
10348    use std::hash::{Hash, Hasher};
10349    let key = match key {
10350        Key::Character(c) => Key::Character(c.to_ascii_lowercase()),
10351        other => other,
10352    };
10353    let mut hasher = std::collections::hash_map::DefaultHasher::new();
10354    key.hash(&mut hasher);
10355    hasher.finish()
10356}