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