Skip to main content

teksilo_core/widget_tree/
pointer_cancel.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The cancel funnel: one queued path by which a pointer interaction is taken
5//! away, and the enumerated set of things allowed to take it.
6//!
7//! # Why a funnel
8//!
9//! Before this module the framework revoked interactions by *forgetting* them.
10//! A window that lost focus dropped every capture in the table, and a parked
11//! subtree simply stopped being hit-testable: the widget mid-drag was
12//! never told, so it kept its own half of the interaction — a latched
13//! selection, a grabbed divider, a highlighted drop target — with no event
14//! coming that could ever clear it. `PointerUp` cannot stand in, because "the
15//! user finished" and "the system took it away" call for opposite responses:
16//! an `Up` on a drag *drops*, and dropping a file because the window lost
17//! focus is a data-loss bug.
18//!
19//! So every revocation now goes through [`WidgetTree::cancel_pointer`], which
20//! tears the interaction down in one order and delivers exactly one
21//! [`WidgetEvent::PointerCancel`] carrying the [`CancelReason`] that names who
22//! took it.
23//!
24//! # Two granularities, and they are not the same act
25//!
26//! * [`cancel_pointer`](WidgetTree::cancel_pointer) revokes the **whole
27//!   pointer**. The sequence dies, the capture is given back, the table entry
28//!   goes (for a contact), and nothing more will be delivered for that
29//!   pointer.
30//! * [`revoke_sequence_member`](WidgetTree::revoke_sequence_member) revokes
31//!   **one competitor** of a sequence that is still alive. Its recognizers are
32//!   cancelled and it is told so, and the pointer carries on — its winner
33//!   keeps receiving moves and will still get its `Up`. This is what a peer
34//!   claim does: exactly one member wins, and every other member is told it
35//!   lost. Revoking a member is emphatically not cancelling the pointer, and
36//!   confusing the two would make an ancestor's losing drag kill the tap that
37//!   beat it.
38//!
39//! # Queued, always
40//!
41//! A cancel raised from inside a handler must not unwind the sample that
42//! handler is standing on. It therefore rides the same
43//! [`pending_dispatch`](WidgetTree::pending_dispatch) queue P07 built for
44//! nested dispatch, as a [`QueuedDispatch::Cancel`](super::pointer_router::QueuedDispatch::Cancel) entry rather than a second
45//! queue of its own: one queue means one order, and the cancel a handler
46//! raised lands after the event that provoked it rather than in the middle of
47//! it. At depth zero the queue is drained immediately, so a caller outside a
48//! dispatch — `set_window_active`, an overlay teardown — sees the cancel take
49//! effect before its own call returns.
50//!
51//! Reference: `docs/touch-and-pen.md` §3.3.
52
53use super::*;
54use crate::pointer::{CancelReason, PointerId};
55
56impl WidgetTree {
57    // -----------------------------------------------------------------
58    // The funnel
59    // -----------------------------------------------------------------
60
61    /// Revoke `pointer`'s interaction, for `reason`.
62    ///
63    /// **Always queued** behind the sample currently being dispatched, and a
64    /// **no-op if that interaction has already finished** by the time the
65    /// queue drains — a cancel raised from a handler must not fire against a
66    /// press the very same sample completed. "Finished" means the pointer is
67    /// no longer live, or holds no capture and has no sequence, or its
68    /// sequence is already inside its own terminal dispatch; all three say
69    /// there is nothing left to revoke.
70    ///
71    /// The teardown runs in one order, and the order is load-bearing: every
72    /// competitor's recognizer state goes first (so nothing can recognize on
73    /// the way out), then the arbitration, then the capture, then the drag
74    /// session, then the table entry, and the event is delivered last — to a
75    /// tree that has already forgotten the interaction, so a handler that
76    /// reacts by capturing or dragging starts from a clean slate rather than
77    /// racing the teardown.
78    ///
79    /// Two teardown steps named in the design are absent because their subject
80    /// does not exist yet: the framework press signal (P11) and the fling
81    /// driver (P13/P21) each clear at the marked point below.
82    pub fn cancel_pointer(
83        &mut self,
84        pointer: PointerId,
85        reason: CancelReason,
86        ops: &mut dyn crate::window::WindowOps,
87    ) {
88        self.cancel_pointer_to(pointer, reason, None, ops);
89    }
90
91    /// [`cancel_pointer`](Self::cancel_pointer), addressed to a widget the
92    /// caller names rather than to whoever the table says holds the pointer.
93    ///
94    /// For a producer whose own teardown has already given the capture back
95    /// before it raises the cancel — the OS-drag escalation hands the pointer
96    /// to the platform first — so the funnel would otherwise have nobody left
97    /// to tell. The named widget is used only while it is still there; a
98    /// recipient destroyed in the meantime falls back to the ordinary chain.
99    pub fn cancel_pointer_to(
100        &mut self,
101        pointer: PointerId,
102        reason: CancelReason,
103        recipient: Option<WidgetId>,
104        ops: &mut dyn crate::window::WindowOps,
105    ) {
106        self.enqueue_cancel(pointer, reason, recipient);
107        if self.dispatch_depth == 0 {
108            self.drain_pending_dispatch(ops);
109        }
110    }
111
112    /// Revoke every live pointer, for `reason`. The window went away under
113    /// them, a modal opened over them, the platform took the seat.
114    pub fn cancel_all_pointers(
115        &mut self,
116        reason: CancelReason,
117        ops: &mut dyn crate::window::WindowOps,
118    ) {
119        let live: Vec<PointerId> = self.pointers.iter().map(|e| e.info.id).collect();
120        for id in live {
121            self.enqueue_cancel(id, reason, None);
122        }
123        if self.dispatch_depth == 0 {
124            self.drain_pending_dispatch(ops);
125        }
126    }
127
128    /// Revoke every pointer whose interaction is **anchored inside** `root`.
129    ///
130    /// "Anchored inside" means the pointer's captor is `root` or a descendant
131    /// of it: that widget is the one about to stop existing, and the pointer
132    /// it holds would otherwise be stranded on it. A pointer merely passing
133    /// over the subtree is not anchored in it and is left alone.
134    ///
135    /// A pointer whose press is no longer revocable is skipped, which is what
136    /// makes the named exemption work: tapping a menu item whose own handler
137    /// closes its menu must complete the tap, not have it cancelled out from
138    /// under itself by the teardown it asked for.
139    pub fn cancel_pointers_in_subtree(
140        &mut self,
141        root: WidgetId,
142        reason: CancelReason,
143        ops: &mut dyn crate::window::WindowOps,
144    ) {
145        let anchored: Vec<PointerId> = self
146            .pointers
147            .iter()
148            .filter(|entry| {
149                entry
150                    .captured_by
151                    .is_some_and(|captor| captor == root || self.is_descendant_of(captor, root))
152            })
153            .map(|entry| entry.info.id)
154            .collect();
155        let mut queued = false;
156        for id in anchored {
157            if !self.press_is_revocable(id) {
158                crate::trace_input!(
159                    Gestures,
160                    "{id:?} is inside the parked subtree but its press has already ended: not cancelled"
161                );
162                continue;
163            }
164            self.enqueue_cancel(id, reason, None);
165            queued = true;
166        }
167        if queued && self.dispatch_depth == 0 {
168            self.drain_pending_dispatch(ops);
169        }
170    }
171
172    /// Park `root`'s subtree and cancel every pointer it was holding.
173    ///
174    /// The tree-level door onto [`WidgetArena::set_dormant`](crate::arena::WidgetArena::set_dormant):
175    /// parking is invisible to hit-testing and to dispatch, so a widget parked
176    /// mid-interaction would keep whatever the press latched and never receive
177    /// another event. Every caller that parks a subtree which could plausibly
178    /// contain a live pointer goes through here; the audit of the ones that do
179    /// not is in `docs/touch-and-pen.md` §3.3.
180    pub(crate) fn park_subtree(&mut self, root: WidgetId) {
181        let mut noop = crate::window::NoopWindowOps;
182        self.park_subtree_with_ops(root, &mut noop);
183    }
184
185    /// [`park_subtree`](Self::park_subtree) with the caller's
186    /// [`WindowOps`](crate::window::WindowOps).
187    ///
188    /// The cancel is raised **before** the subtree is parked, so the widget is
189    /// still active when it is told to let go — a `PointerCancel` delivered to
190    /// a node the dispatcher has just made dormant would be dropped, which is
191    /// precisely the silent teardown this replaces.
192    pub(crate) fn park_subtree_with_ops(
193        &mut self,
194        root: WidgetId,
195        ops: &mut dyn crate::window::WindowOps,
196    ) {
197        self.cancel_pointers_in_subtree(root, CancelReason::SubtreeParked, ops);
198        let _parked = self.arena.set_dormant(root);
199    }
200
201    /// Revoke one **member** of a live sequence, leaving the sequence, the
202    /// capture and the pointer itself alone.
203    ///
204    /// The member's recognizers are cancelled for this contact and the member
205    /// widget is told with a [`WidgetEvent::PointerCancel`]. Unlike
206    /// [`cancel_pointer`](Self::cancel_pointer) this is delivered **now**
207    /// rather than queued: it is raised from the arbitration itself, which
208    /// already runs at a point where the sequence is consistent, and a member
209    /// that lost must stop recognizing before the same sample reaches it
210    /// through the ordinary bubble.
211    ///
212    /// A member whose node has already been destroyed gets the recognizer
213    /// teardown and no event — there is nothing left to deliver to. A merely
214    /// *dormant* one still exists and is told directly, without a bubble,
215    /// exactly as a dormant node is told about a lost focus.
216    pub(super) fn revoke_sequence_member(
217        &mut self,
218        pointer: PointerId,
219        member: WidgetId,
220        reason: CancelReason,
221        ops: &mut dyn crate::window::WindowOps,
222    ) {
223        crate::trace_input!(
224            Gestures,
225            "member {member:?} of {pointer:?} revoked: {reason:?}"
226        );
227        self.cancel_member_arena(member, pointer);
228        if self.arena.get(member).is_none() {
229            return;
230        }
231        // The entry is still there on this path — a losing member's revoke does
232        // not end the pointer, only its own recognizers — so the event names the
233        // real device. A pointer that has gone anyway has no press left to
234        // announce, and the recognizer teardown above is the whole of what it
235        // needed.
236        let Some((info, at)) = self.pointers.get(pointer).map(|e| (e.info, e.position)) else {
237            return;
238        };
239        let event = Self::pointer_cancel_event(info, at, reason);
240        self.dispatch_to_widget_direct(member, &event, ops);
241    }
242
243    // -----------------------------------------------------------------
244    // Queue plumbing
245    // -----------------------------------------------------------------
246
247    /// Put a cancel on the shared dispatch queue, unless one for the same
248    /// pointer is already waiting there.
249    ///
250    /// The de-duplication is what keeps "exactly one cancel" true when two
251    /// producers fire on the same sample — an overlay dismissal that also
252    /// parks the subtree it lived in, say. The first reason wins, because it
253    /// is the one that describes what actually happened.
254    fn enqueue_cancel(
255        &mut self,
256        pointer: PointerId,
257        reason: CancelReason,
258        recipient: Option<WidgetId>,
259    ) {
260        if self.pending_dispatch.iter().any(|queued| {
261            matches!(queued, pointer_router::QueuedDispatch::Cancel { pointer: p, .. } if *p == pointer)
262        }) {
263            return;
264        }
265        crate::trace_input!(Samples, "cancel queued for {pointer:?}: {reason:?}");
266        // A revoked contact holds nothing, so its tree-owned hold is over. Done
267        // at *enqueue* rather than at dispatch: the queue can be drained a
268        // frame later, and a route that fires in between would be answering a
269        // press the system has already taken away.
270        self.cancel_touch_route(pointer);
271        self.pending_dispatch
272            .push_back(pointer_router::QueuedDispatch::Cancel {
273                pointer,
274                reason,
275                recipient,
276            });
277    }
278
279    /// Run one queued cancel, at dispatch depth zero.
280    pub(super) fn run_one_cancel(
281        &mut self,
282        pointer: PointerId,
283        reason: CancelReason,
284        recipient: Option<WidgetId>,
285        ops: &mut dyn crate::window::WindowOps,
286    ) {
287        if !self.press_is_revocable(pointer) {
288            crate::trace_input!(
289                Samples,
290                "cancel for {pointer:?} ({reason:?}) dropped: nothing left to revoke"
291            );
292            return;
293        }
294        crate::trace_input!(Samples, "cancelling {pointer:?}: {reason:?}");
295
296        // Read the pointer's identity ONCE, here, while its table entry is
297        // certainly present — `press_is_revocable` above answered false for an
298        // absent entry, so this cannot fail. Both the snapshot below and the
299        // `PointerCancel` delivered at the end of the teardown are built from
300        // this one read, which is what stops them disagreeing: step 6 of the
301        // teardown *removes* a contact's entry, so anything reading the table
302        // after it gets a fabricated answer.
303        let Some((info, at)) = self.pointers.get(pointer).map(|e| (e.info, e.position)) else {
304            return;
305        };
306
307        // Serve the teardown as *this* pointer's sample: every helper below
308        // that reads "the pointer being dispatched" — the recognizer context,
309        // the drag's capture release, `EventContext::pointer()` inside the
310        // handler — must answer with the pointer being cancelled and not with
311        // whatever the outer dispatch was serving. Restored on the way out, so
312        // a cancel drained after an outer sample leaves that sample's snapshot
313        // as it found it.
314        let snapshot = crate::pointer::InputSnapshot {
315            pointer: info,
316            position: Some(at),
317            ..Default::default()
318        };
319        let previous_input = std::mem::replace(&mut self.current_input, snapshot);
320        // Anything the cancel's own handler dispatches is queued behind it,
321        // exactly as it would be from inside an ordinary sample.
322        self.dispatch_depth += 1;
323        self.tear_down_cancelled_pointer(pointer, info, at, reason, recipient, ops);
324        self.dispatch_depth -= 1;
325        self.current_input = previous_input;
326    }
327
328    /// The ordered teardown itself, with `current_input` already serving
329    /// `pointer`. See [`cancel_pointer`](Self::cancel_pointer) for why the
330    /// order is what it is.
331    ///
332    /// `info` and `at` are the pointer's identity and last position, read by the
333    /// caller **before** any of this ran: step 6 removes a contact's table
334    /// entry, and the event delivered in step 7 has to name the pointer that
335    /// went away.
336    fn tear_down_cancelled_pointer(
337        &mut self,
338        pointer: PointerId,
339        info: crate::pointer::PointerInfo,
340        at: Point,
341        reason: CancelReason,
342        recipient: Option<WidgetId>,
343        ops: &mut dyn crate::window::WindowOps,
344    ) {
345        // 1. Every competitor's recognizer state, before anything else can
346        //    recognize on the way out.
347        let members: Vec<WidgetId> = self
348            .pointers
349            .get(pointer)
350            .and_then(|e| e.sequence.as_ref())
351            .map(|s| s.members().iter().map(|m| m.id).collect())
352            .unwrap_or_default();
353        for member in members {
354            self.cancel_member_arena(member, pointer);
355        }
356        // The captor's own arena is not necessarily a member (a plain tap owner
357        // never enrols), and it is the node most likely to be holding
358        // recognizer state for this contact.
359        let captor = self.pointers.get(pointer).and_then(|e| e.captured_by);
360        if let Some(captor) = captor {
361            self.cancel_member_arena(captor, pointer);
362        }
363        // …and anything else that saw the press but is neither: an ancestor
364        // that took the capture off the node whose arena is still following the
365        // contact. See `release_arenas_following`.
366        self.release_arenas_following(pointer);
367
368        // 2. The arbitration. Whoever it decided for is about to be told the
369        //    press it won has been taken away.
370        let recipient = recipient
371            .filter(|id| self.arena.get(*id).is_some())
372            .or_else(|| self.cancel_recipient(pointer));
373        if let Some(entry) = self.pointers.get_mut(pointer) {
374            entry.sequence = None;
375        }
376
377        // 3. The capture.
378        self.set_pointer_capture(pointer, None);
379
380        // 4. The drag session this pointer was driving, if it was driving one.
381        //    `cancel_active_drag` is the same teardown Escape runs: the current
382        //    drop target is told to clear its feedback and the source is told
383        //    the drag ended as `Cancelled`, so nothing is left highlighted and
384        //    no payload is silently dropped where the pointer happened to be.
385        if captor.is_some() && self.pointer_owns_active_drag_via(captor) {
386            self.cancel_active_drag(ops);
387        }
388
389        // 5. The touch-motion layer. The pan session goes without delivering a
390        //    release — there is no velocity to hand on from an interaction that
391        //    was taken away — and any coast the claimant chain is running stops
392        //    with it. The palm watch is dropped rather than judged: a cancel is
393        //    not a release, so there is nothing to be a palm *of*. The pinch is
394        //    told, because a handler that has been zooming since `PinchStarted`
395        //    must be given its `Cancelled` to unwind on. The framework press
396        //    visual goes with them: a press that was taken away must not stay
397        //    painted, and the node is never sent an `Up` to clear it from.
398        let pan_chain: Vec<WidgetId> = self.pan_chain_ids(pointer);
399        self.abandon_pan(pointer);
400        for id in pan_chain {
401            self.stop_fling(id);
402        }
403        self.forget_palm_watch(pointer);
404        self.end_press(pointer);
405        self.cancel_pinch(pointer, reason, ops);
406
407        // 6. The table entry, for a pointer that ceases to exist when it is
408        //    taken away. A hovering-capable pointer does not: a mouse whose
409        //    press was cancelled is still there, still hovering, and its entry
410        //    is what every singular accessor reads — the same rule
411        //    `dispatch_pointer_with_ops` applies to an `Up`.
412        let hovers = self
413            .pointers
414            .get(pointer)
415            .is_some_and(|e| e.info.kind.hovers());
416        if !hovers {
417            self.pointers.end(pointer);
418        } else if let Some(entry) = self.pointers.get_mut(pointer) {
419            // …and *hovering* is the whole of what it is now doing. The entry's
420            // button mask is what `PointerEntry::is_contacting` reads for a
421            // hovering-capable pointer, so leaving the press's mask on it
422            // leaves the pointer reading as held by an interaction the
423            // framework has just finished forgetting — for ever, since a Cancel
424            // sample never reaches `PointerTable::admit` and the next real
425            // sample is the earliest correction. The platform layer already
426            // builds its pen proximity-leave with an empty mask, intending
427            // exactly this; the intent was being discarded.
428            entry.info.buttons = crate::event::ButtonMask::NONE;
429        }
430
431        // A cancel is terminal: an `Up` that arrives for this pointer
432        // afterwards — a platform that sends both, a test that sends one by
433        // hand — must not complete the interaction that was taken away.
434        if !self.cancelled_pointers.contains(&pointer) {
435            self.cancelled_pointers.push(pointer);
436        }
437
438        // 7. The event, last, to a tree that has already let go — built from the
439        //    identity the caller captured, since step 6 may have taken the
440        //    entry away.
441        if let Some(recipient) = recipient {
442            let event = Self::pointer_cancel_event(info, at, reason);
443            self.dispatch_to_widget_direct(recipient, &event, ops);
444        }
445    }
446
447    // -----------------------------------------------------------------
448    // Predicates the producers and the funnel share
449    // -----------------------------------------------------------------
450
451    /// Whether `pointer` still has an interaction that can be taken away.
452    ///
453    /// Three ways the answer is no, and all three mean the same thing — there
454    /// is nothing left for a `PointerCancel` to revoke:
455    ///
456    /// * the pointer is not live at all (a contact that lifted);
457    /// * it holds no capture and has no sequence, so it is merely hovering;
458    /// * its sequence is inside its own terminal dispatch
459    ///   ([`PointerSequence::is_terminating`](crate::gesture::PointerSequence::is_terminating)),
460    ///   so the press has completed and only its epilogue is still running.
461    ///
462    /// The second case is what carries the design's named exemption. By the
463    /// time a menu item's `on_tap` runs, the release sweep has already closed
464    /// that pointer's sequence (`end_sequence` clears it before the `Up` is
465    /// delivered), so a cancel the handler's own overlay teardown queues finds
466    /// no press to revoke and the tap completes. The third case covers the
467    /// same window for any future producer that fires while a sequence is
468    /// installed but already terminating.
469    pub(super) fn press_is_revocable(&self, pointer: PointerId) -> bool {
470        let Some(entry) = self.pointers.get(pointer) else {
471            return false;
472        };
473        match entry.sequence.as_ref() {
474            Some(sequence) => !sequence.is_terminating(),
475            None => entry.captured_by.is_some(),
476        }
477    }
478
479    /// Who receives the `PointerCancel`: the widget holding the capture, and
480    /// failing that the last widget that accepted an event from this pointer.
481    ///
482    /// The first of the two that still *exists* wins. A captor destroyed by
483    /// the very rebuild that provoked the cancel cannot be told anything, and
484    /// falling through to the last acceptor is how the widget that was
485    /// actually interacting still hears about it.
486    fn cancel_recipient(&self, pointer: PointerId) -> Option<WidgetId> {
487        let entry = self.pointers.get(pointer)?;
488        [entry.captured_by, entry.last_accepted]
489            .into_iter()
490            .flatten()
491            .find(|id| self.arena.get(*id).is_some())
492    }
493
494    /// The `PointerCancel` announcing `pointer`, at the position it last
495    /// reported.
496    ///
497    /// Takes the identity rather than looking it up, and that is the point: the
498    /// teardown **removes a contact's table entry** before the event is
499    /// delivered (step 6, deliberately — a lifted or revoked contact stops
500    /// existing), so a builder that read the table here read the entry it had
501    /// just deleted and fell back to a fabricated mouse with no position. The
502    /// handler whose entire job is to know which pointer went away was told the
503    /// mouse cancelled a press only a finger had made. Every caller now reads
504    /// the entry while it is still there and hands the answer down, so the event
505    /// payload and `EventContext::pointer()` inside the handler cannot disagree
506    /// — which is how that defect stayed invisible, since the snapshot half was
507    /// always right.
508    fn pointer_cancel_event(
509        pointer: crate::pointer::PointerInfo,
510        window_position: Point,
511        reason: CancelReason,
512    ) -> WidgetEvent {
513        WidgetEvent::PointerCancel {
514            window_position: Some(window_position),
515            reason,
516            pointer,
517        }
518    }
519
520    /// Whether the in-flight drag belongs to the pointer whose capture is
521    /// `captor`.
522    ///
523    /// An internal drag captures the pointer it started from onto its own
524    /// source widget (`collect_from_ctx`'s drag-start arm), so holding that
525    /// capture *is* what it means to be driving the drag. An external (OS)
526    /// drag takes no capture and has no in-app source: it belongs to the
527    /// platform backend that began it, and no in-app pointer cancel may end
528    /// it.
529    fn pointer_owns_active_drag_via(&self, captor: Option<WidgetId>) -> bool {
530        self.active_drag
531            .as_ref()
532            .and_then(|drag| drag.source_widget)
533            .is_some_and(|source| captor == Some(source))
534    }
535}
536
537// -------------------------------------------------------------------------
538// P09: the cancel taxonomy — one funnel, an enumerated producer set
539// -------------------------------------------------------------------------
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544    use crate::event::{EventResponse, Modifiers, PointerButton, WidgetEvent};
545    use crate::pointer::{
546        BackendDeviceKey, EventTime, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
547    };
548    use crate::test_widgets::{FillWidget, StackWidget};
549    use crate::widget_builder::WidgetBuilder;
550    use std::cell::RefCell;
551    use std::rc::Rc;
552    use teksilo_canvas::{Point, SizeProposal};
553
554    /// Every cancel a test observed, in delivery order.
555    type Log = Rc<RefCell<Vec<(WidgetId, CancelReason)>>>;
556
557    fn log() -> Log {
558        Rc::new(RefCell::new(Vec::new()))
559    }
560
561    /// A widget that records the cancels it is told about.
562    fn recorder(log: &Log, id_slot: Rc<std::cell::Cell<Option<WidgetId>>>) -> impl Widget {
563        let log = log.clone();
564        FillWidget::new().on_pointer_cancel(move |_pointer, reason, _ctx| {
565            let id = id_slot.get().expect("the recorder's id was never recorded");
566            log.borrow_mut().push((id, reason));
567        })
568    }
569
570    fn press(tree: &mut WidgetTree, at: Point) {
571        tree.dispatch_event(WidgetEvent::pointer_down(
572            at,
573            PointerButton::Primary,
574            Modifiers::NONE,
575        ));
576    }
577
578    fn moved(tree: &mut WidgetTree, at: Point) {
579        tree.dispatch_event(WidgetEvent::pointer_move(at));
580    }
581
582    fn release(tree: &mut WidgetTree, at: Point) {
583        tree.dispatch_event(WidgetEvent::pointer_up(
584            at,
585            PointerButton::Primary,
586            Modifiers::NONE,
587        ));
588    }
589
590    /// A fresh contact: the platform mints a new id per press.
591    fn new_contact() -> PointerId {
592        use std::sync::atomic::{AtomicU64, Ordering};
593        static NEXT: AtomicU64 = AtomicU64::new(31_000);
594        PointerIdAllocator::global().begin(
595            BackendDeviceKey::DEFAULT,
596            NEXT.fetch_add(1, Ordering::Relaxed),
597        )
598    }
599
600    fn touch(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
601        PointerSample {
602            pointer: PointerInfo::touch(id, EventTime::ZERO),
603            phase,
604            position: at,
605            button: None,
606            modifiers: Modifiers::NONE,
607            coalesced: Vec::new(),
608        }
609    }
610
611    /// A leaf that takes the pointer on press and drives itself from moves —
612    /// the splitter-handle / column-grip shape, and the simplest thing that
613    /// has an interaction a cancel can take away.
614    fn grip(log: &Log, id_slot: Rc<std::cell::Cell<Option<WidgetId>>>) -> impl Widget {
615        let log = log.clone();
616        let slot = id_slot.clone();
617        FillWidget::new()
618            .on_pointer_event(|event, ctx| {
619                if matches!(event, WidgetEvent::PointerDown { .. }) {
620                    ctx.capture_pointer();
621                }
622                EventResponse::Ignored
623            })
624            .on_pointer_cancel(move |_pointer, reason, _ctx| {
625                let id = slot.get().expect("the grip's id was never recorded");
626                log.borrow_mut().push((id, reason));
627            })
628    }
629
630    /// Build a one-leaf tree whose leaf grips the pointer, and return it.
631    fn tree_with_grip(log: &Log) -> (WidgetTree, WidgetId) {
632        let slot = Rc::new(std::cell::Cell::new(None));
633        let mut tree = WidgetTree::new();
634        let id = tree.add(grip(log, slot.clone()));
635        slot.set(Some(id));
636        tree.layout(SizeProposal::exact(200.0, 100.0));
637        (tree, id)
638    }
639
640    // ---------------------------------------------------------------
641    // Producer: window deactivation
642    // ---------------------------------------------------------------
643
644    /// The producer that replaces the framework's oldest silent teardown.
645    /// Deactivating a window used to drop every capture without a word; now
646    /// the widget holding one is told, so it can let go of what the press
647    /// latched.
648    #[test]
649    fn deactivating_the_window_cancels_the_pointer_it_stranded() {
650        let log = log();
651        let (mut tree, id) = tree_with_grip(&log);
652
653        press(&mut tree, Point::new(20.0, 50.0));
654        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(id));
655
656        tree.set_window_active(false);
657
658        assert_eq!(
659            *log.borrow(),
660            vec![(id, CancelReason::WindowDeactivated)],
661            "the widget that captured the pointer is told the window took it away"
662        );
663        assert_eq!(
664            tree.captured_by(PointerId::MOUSE),
665            None,
666            "and the capture is released, as it always was"
667        );
668        tree.assert_no_leaked_pointer_state();
669    }
670
671    /// A window deactivated with nothing going on cancels nothing: there is no
672    /// interaction to revoke, and firing a `PointerCancel` at a widget the
673    /// mouse merely rests over would be noise.
674    #[test]
675    fn deactivating_the_window_with_no_live_press_cancels_nothing() {
676        let log = log();
677        let (mut tree, _id) = tree_with_grip(&log);
678
679        moved(&mut tree, Point::new(20.0, 50.0));
680        tree.set_window_active(false);
681
682        assert!(log.borrow().is_empty());
683        tree.assert_no_leaked_pointer_state();
684    }
685
686    /// The platform itself revoked the contact — a `wl_touch.cancel`, a
687    /// `WM_POINTERCAPTURECHANGED`, a compositor grab. It reaches the funnel
688    /// directly from the sample door rather than being lowered onto an event,
689    /// because lowering would first *admit* the pointer the sample revokes.
690    #[test]
691    fn a_platform_cancel_sample_tears_the_contact_down() {
692        let log = log();
693        let slot = Rc::new(std::cell::Cell::new(None));
694        let mut tree = WidgetTree::new();
695        let leaf = tree.add(grip(&log, slot.clone()));
696        slot.set(Some(leaf));
697        tree.layout(SizeProposal::exact(200.0, 100.0));
698
699        let contact = new_contact();
700        tree.dispatch_pointer(touch(contact, PointerPhase::Down, Point::new(20.0, 50.0)));
701        assert_eq!(tree.captured_by(contact), Some(leaf));
702
703        tree.dispatch_pointer(touch(contact, PointerPhase::Cancel, Point::new(20.0, 50.0)));
704
705        assert_eq!(*log.borrow(), vec![(leaf, CancelReason::Platform)]);
706        assert!(
707            tree.pointers.get(contact).is_none(),
708            "a revoked contact leaves the table, as a lifted one does"
709        );
710        tree.assert_no_leaked_pointer_state();
711    }
712
713    // ---------------------------------------------------------------
714    // The delivered payload names the pointer that went away
715    // ---------------------------------------------------------------
716
717    /// What a `PointerCancel` handler was told, per delivery.
718    type PayloadLog = Rc<RefCell<Vec<(CancelReason, PointerInfo, Option<Point>)>>>;
719
720    /// A leaf that grips the pointer and records the whole `PointerCancel`
721    /// payload it is handed — not just the reason.
722    fn payload_grip(log: &PayloadLog) -> impl Widget {
723        let log = log.clone();
724        FillWidget::new()
725            .on_pointer_event(|event, ctx| {
726                if matches!(event, WidgetEvent::PointerDown { .. }) {
727                    ctx.capture_pointer();
728                }
729                EventResponse::Ignored
730            })
731            .on_pointer_cancel(move |pointer, reason, ctx| {
732                log.borrow_mut()
733                    .push((reason, *pointer, ctx.pointer_position()));
734            })
735    }
736
737    /// A cancelled **contact** is announced as that contact.
738    ///
739    /// The handler whose whole job is to know which pointer went away was being
740    /// handed a fabricated mouse: the teardown removes a non-hovering pointer's
741    /// table entry before the event is built, and the builder read the entry it
742    /// had just deleted, so an app branching on `pointer.kind` — releasing a
743    /// per-contact grip, dropping a stroke, un-highlighting one finger's row —
744    /// was told the mouse cancelled a press only a finger had made, and given no
745    /// position to do it at.
746    #[test]
747    fn a_cancelled_contact_is_announced_as_that_contact() {
748        let log: PayloadLog = Rc::new(RefCell::new(Vec::new()));
749        let mut tree = WidgetTree::new();
750        let leaf = tree.add(payload_grip(&log));
751        tree.layout(SizeProposal::exact(200.0, 100.0));
752
753        let contact = new_contact();
754        let at = Point::new(20.0, 50.0);
755        tree.dispatch_pointer(touch(contact, PointerPhase::Down, at));
756        assert_eq!(tree.captured_by(contact), Some(leaf));
757
758        tree.dispatch_pointer(touch(contact, PointerPhase::Cancel, at));
759
760        let seen = log.borrow();
761        assert_eq!(seen.len(), 1, "one cancel, delivered once");
762        let (reason, pointer, position) = seen[0];
763        assert_eq!(reason, CancelReason::Platform);
764        assert_eq!(
765            pointer.kind,
766            teksilo_tokens::PointerKind::Touch,
767            "the event must name the device that was cancelled",
768        );
769        assert_eq!(
770            pointer.id, contact,
771            "and its own id, not the mouse's reserved one",
772        );
773        assert_eq!(
774            position,
775            Some(at),
776            "with the position the contact was last at",
777        );
778    }
779
780    /// The same for a mouse, so the fix above is a *translation* of whatever the
781    /// entry held and not a hardcoded touch.
782    #[test]
783    fn a_cancelled_mouse_press_is_still_announced_as_the_mouse() {
784        let log: PayloadLog = Rc::new(RefCell::new(Vec::new()));
785        let mut tree = WidgetTree::new();
786        tree.add(payload_grip(&log));
787        tree.layout(SizeProposal::exact(200.0, 100.0));
788
789        let at = Point::new(30.0, 40.0);
790        press(&mut tree, at);
791        tree.cancel_all_pointers(CancelReason::ModalOpened, &mut crate::window::NoopWindowOps);
792
793        let seen = log.borrow();
794        assert_eq!(seen.len(), 1);
795        let (reason, pointer, position) = seen[0];
796        assert_eq!(reason, CancelReason::ModalOpened);
797        assert_eq!(pointer.kind, teksilo_tokens::PointerKind::Mouse);
798        assert_eq!(pointer.id, PointerId::MOUSE);
799        assert_eq!(position, Some(at));
800    }
801
802    /// A second producer, with a different shape: the subtree holding the press
803    /// is parked, so the cancel is queued by `set_dormant` rather than raised
804    /// from the sample door, and the position it announces is the one carried by
805    /// a *later* move than the press. The payload must read the same — the
806    /// fallback the defect lived in sits below all fifteen producers, so one
807    /// producer being right is not evidence about the others.
808    #[test]
809    fn a_parked_subtree_announces_the_contact_it_stranded() {
810        let log: PayloadLog = Rc::new(RefCell::new(Vec::new()));
811        let mut tree = WidgetTree::new();
812        let leaf = tree.add(payload_grip(&log));
813        let branch = tree.add(StackWidget::new().child(leaf));
814        tree.layout(SizeProposal::exact(200.0, 100.0));
815
816        let contact = new_contact();
817        tree.dispatch_pointer(touch(contact, PointerPhase::Down, Point::new(20.0, 50.0)));
818        assert_eq!(tree.captured_by(contact), Some(leaf));
819        let moved_to = Point::new(24.0, 52.0);
820        tree.dispatch_pointer(touch(contact, PointerPhase::Move, moved_to));
821
822        tree.set_dormant(branch);
823
824        let seen = log.borrow();
825        assert_eq!(seen.len(), 1, "the parked leaf was told once");
826        let (reason, pointer, position) = seen[0];
827        assert_eq!(reason, CancelReason::SubtreeParked);
828        assert_eq!(pointer.kind, teksilo_tokens::PointerKind::Touch);
829        assert_eq!(pointer.id, contact);
830        assert_eq!(
831            position,
832            Some(moved_to),
833            "the position is where the contact last was, not where it pressed",
834        );
835    }
836
837    /// A cancel sample for a pointer the tree never saw must not *create* one.
838    /// Admitting it would leave an entry behind that nothing can ever remove.
839    #[test]
840    fn a_cancel_for_an_unknown_pointer_admits_nothing() {
841        let mut tree = WidgetTree::new();
842        tree.add(FillWidget::new());
843        tree.layout(SizeProposal::exact(200.0, 100.0));
844
845        let ghost = new_contact();
846        tree.dispatch_pointer(touch(ghost, PointerPhase::Cancel, Point::new(20.0, 50.0)));
847
848        assert!(tree.pointers.get(ghost).is_none());
849        tree.assert_no_leaked_pointer_state();
850    }
851
852    /// The recipient of last resort. A press that took no capture still has a
853    /// widget that was interacting with it — the last one that answered
854    /// `Handled` — and that is who must be told the press was taken away.
855    #[test]
856    fn a_cancel_without_a_capture_reaches_the_last_widget_that_accepted() {
857        let log = log();
858        let slot = Rc::new(std::cell::Cell::new(None));
859
860        let mut tree = WidgetTree::new();
861        let leaf = {
862            let l = log.clone();
863            let s = slot.clone();
864            tree.add(
865                FillWidget::new()
866                    // Handles the press without taking the pointer — the shape
867                    // of a widget that paints a press state and nothing else.
868                    .on_pointer_event(|event, _ctx| {
869                        if matches!(event, WidgetEvent::PointerDown { .. }) {
870                            return EventResponse::Handled;
871                        }
872                        EventResponse::Ignored
873                    })
874                    .on_pointer_cancel(move |_p, reason, _ctx| {
875                        let id = s.get().expect("the leaf's id was never recorded");
876                        l.borrow_mut().push((id, reason));
877                    }),
878            )
879        };
880        slot.set(Some(leaf));
881        tree.layout(SizeProposal::exact(200.0, 100.0));
882
883        press(&mut tree, Point::new(20.0, 50.0));
884        assert_eq!(
885            tree.captured_by(PointerId::MOUSE),
886            None,
887            "the widget took the press without taking the pointer"
888        );
889
890        tree.set_window_active(false);
891
892        assert_eq!(*log.borrow(), vec![(leaf, CancelReason::WindowDeactivated)]);
893        tree.assert_no_leaked_pointer_state();
894    }
895
896    // ---------------------------------------------------------------
897    // Producer: a modal opening
898    // ---------------------------------------------------------------
899
900    /// A modal opens over the press: the surface being worked on is now behind
901    /// a scrim, and the `Up` that would have completed the press will land on
902    /// the modal instead.
903    #[test]
904    fn opening_a_modal_cancels_every_live_pointer() {
905        let log = log();
906        let (mut tree, id) = tree_with_grip(&log);
907
908        press(&mut tree, Point::new(20.0, 50.0));
909
910        let modal_content = tree.add(FillWidget::new());
911        tree.show_overlay(crate::overlay::OverlayRequest {
912            content_id: modal_content,
913            anchor: id,
914            placement: crate::overlay::OverlayPlacement::Centered,
915            dismiss: crate::overlay::DismissBehavior::Manual,
916            layer: crate::overlay::OverlayLayer::InTree,
917            parent_overlay: None,
918            on_dismiss: None,
919            fade_duration: None,
920        });
921
922        assert_eq!(*log.borrow(), vec![(id, CancelReason::ModalOpened)]);
923        assert_eq!(tree.captured_by(PointerId::MOUSE), None);
924        tree.assert_no_leaked_pointer_state();
925    }
926
927    /// Only a modal. A menu, a popover, a tooltip or a drag preview opens over
928    /// an interaction that legitimately continues.
929    #[test]
930    fn opening_a_non_modal_overlay_cancels_nothing() {
931        let log = log();
932        let (mut tree, id) = tree_with_grip(&log);
933
934        press(&mut tree, Point::new(20.0, 50.0));
935
936        let popover = tree.add(FillWidget::new());
937        tree.show_overlay(crate::overlay::OverlayRequest {
938            content_id: popover,
939            anchor: id,
940            placement: crate::overlay::OverlayPlacement::Below,
941            dismiss: crate::overlay::DismissBehavior::Manual,
942            layer: crate::overlay::OverlayLayer::InTree,
943            parent_overlay: None,
944            on_dismiss: None,
945            fade_duration: None,
946        });
947
948        assert!(log.borrow().is_empty(), "a popover is not a modal");
949        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(id));
950        release(&mut tree, Point::new(20.0, 50.0));
951        tree.assert_no_leaked_pointer_state();
952    }
953
954    // ---------------------------------------------------------------
955    // Producer: a subtree going dormant
956    // ---------------------------------------------------------------
957
958    /// Parking a subtree is invisible to hit-testing and to dispatch, so a
959    /// widget parked mid-press would keep what the press latched with no event
960    /// left that could clear it.
961    #[test]
962    fn parking_a_subtree_cancels_the_pointer_inside_it() {
963        let log = log();
964        let slot = Rc::new(std::cell::Cell::new(None));
965        let mut tree = WidgetTree::new();
966        let leaf = tree.add(grip(&log, slot.clone()));
967        slot.set(Some(leaf));
968        let branch = tree.add(StackWidget::new().child(leaf));
969        tree.layout(SizeProposal::exact(200.0, 100.0));
970
971        press(&mut tree, Point::new(20.0, 50.0));
972        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(leaf));
973
974        tree.set_dormant(branch);
975
976        assert_eq!(*log.borrow(), vec![(leaf, CancelReason::SubtreeParked)]);
977        assert_eq!(tree.captured_by(PointerId::MOUSE), None);
978        tree.assert_no_leaked_pointer_state();
979    }
980
981    /// The parked-ids plumbing is what makes the producer above possible: a
982    /// caller that cannot see which nodes went to sleep cannot cancel the
983    /// pointers holding them. `set_dormant` reports the whole subtree, its
984    /// root first, and reports it once per node however deep the nesting.
985    #[test]
986    fn set_dormant_reports_the_whole_parked_subtree() {
987        let mut tree = WidgetTree::new();
988        let leaf_a = tree.add(FillWidget::new());
989        let leaf_b = tree.add(FillWidget::new());
990        let inner = tree.add(StackWidget::new().child(leaf_a).child(leaf_b));
991        let outer = tree.add(StackWidget::new().child(inner));
992        tree.layout(SizeProposal::exact(200.0, 100.0));
993
994        let parked = tree.arena.set_dormant(outer);
995
996        assert_eq!(parked.first(), Some(&outer), "the root is reported first");
997        let mut sorted = parked.clone();
998        sorted.sort();
999        sorted.dedup();
1000        assert_eq!(
1001            sorted.len(),
1002            parked.len(),
1003            "each node is reported exactly once"
1004        );
1005        for id in [outer, inner, leaf_a, leaf_b] {
1006            assert!(
1007                parked.contains(&id),
1008                "{id:?} is missing from the parked set"
1009            );
1010        }
1011    }
1012
1013    /// The other way a subtree parks: a `visible_when` gate flipping false,
1014    /// resolved by the layout pass rather than by an explicit call. It is the
1015    /// busiest `set_dormant` caller in the framework, and it goes through the
1016    /// same door.
1017    #[test]
1018    fn a_visible_when_gate_closing_cancels_the_pointer_inside_it() {
1019        let log = log();
1020        let slot = Rc::new(std::cell::Cell::new(None));
1021        let shown = crate::signal::Signal::new(true);
1022
1023        let mut tree = WidgetTree::new();
1024        let leaf = tree.add(grip(&log, slot.clone()));
1025        slot.set(Some(leaf));
1026        let branch = tree.add(StackWidget::new().child(leaf).visible_when(shown.clone()));
1027        let _root = tree.add(StackWidget::new().child(branch));
1028        tree.layout(SizeProposal::exact(200.0, 100.0));
1029
1030        let at = tree.bounds(leaf).center();
1031        press(&mut tree, at);
1032        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(leaf));
1033
1034        shown.set(false);
1035        tree.layout(SizeProposal::exact(200.0, 100.0));
1036
1037        assert_eq!(*log.borrow(), vec![(leaf, CancelReason::SubtreeParked)]);
1038        assert_eq!(tree.captured_by(PointerId::MOUSE), None);
1039        tree.assert_no_leaked_pointer_state();
1040    }
1041
1042    // ---------------------------------------------------------------
1043    // Producer: an overlay being dismissed, and its named exemption
1044    // ---------------------------------------------------------------
1045
1046    /// A pointer anchored inside an overlay that is torn down under it is
1047    /// stranded on a widget that no longer takes events.
1048    #[test]
1049    fn dismissing_an_overlay_cancels_a_pointer_anchored_inside_it() {
1050        let log = log();
1051        let slot = Rc::new(std::cell::Cell::new(None));
1052        let mut tree = WidgetTree::new();
1053        let anchor = tree.add(FillWidget::new());
1054        let content = tree.add(grip(&log, slot.clone()));
1055        slot.set(Some(content));
1056        tree.layout(SizeProposal::exact(200.0, 100.0));
1057
1058        let overlay = tree.show_overlay(crate::overlay::OverlayRequest {
1059            content_id: content,
1060            anchor,
1061            placement: crate::overlay::OverlayPlacement::Below,
1062            dismiss: crate::overlay::DismissBehavior::Manual,
1063            layer: crate::overlay::OverlayLayer::InTree,
1064            parent_overlay: None,
1065            on_dismiss: None,
1066            fade_duration: None,
1067        });
1068        tree.layout(SizeProposal::exact(200.0, 100.0));
1069
1070        // The press lands on the overlay's own content, which grips the
1071        // pointer — a scrollbar thumb inside a dropdown, say.
1072        tree.dispatch_event(WidgetEvent::pointer_down(
1073            tree.bounds(content).center(),
1074            PointerButton::Primary,
1075            Modifiers::NONE,
1076        ));
1077        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(content));
1078
1079        tree.dismiss_overlay(overlay);
1080
1081        assert_eq!(
1082            *log.borrow(),
1083            vec![(content, CancelReason::OverlayDismissed)]
1084        );
1085        tree.assert_no_leaked_pointer_state();
1086    }
1087
1088    /// **The named exemption.** Tapping a menu item whose own handler closes
1089    /// its menu must complete the tap. The overlay teardown the handler asks
1090    /// for happens while that pointer's press is already over — the release
1091    /// sweep ran before the `Up` was delivered — so there is nothing left for a
1092    /// cancel to revoke, and the item is never told its own activation was
1093    /// taken away.
1094    #[test]
1095    fn tapping_a_menu_item_that_closes_its_own_menu_completes_the_tap() {
1096        for kind in ["mouse", "touch"] {
1097            let log = log();
1098            let tapped = Rc::new(std::cell::Cell::new(0_u32));
1099            let overlay_slot = Rc::new(std::cell::Cell::new(None));
1100
1101            let mut tree = WidgetTree::new();
1102            let anchor = tree.add(FillWidget::new());
1103            let item_slot = Rc::new(std::cell::Cell::new(None));
1104            let item = {
1105                let t = tapped.clone();
1106                let o = overlay_slot.clone();
1107                let l = log.clone();
1108                let s = item_slot.clone();
1109                tree.add(
1110                    FillWidget::new()
1111                        .on_tap(move |_e, ctx| {
1112                            t.set(t.get() + 1);
1113                            // The menu item closes its own menu, exactly as a
1114                            // real one does.
1115                            if let Some(id) = o.get() {
1116                                ctx.dismiss_overlay(id);
1117                            }
1118                        })
1119                        .on_pointer_cancel(move |_p, reason, _ctx| {
1120                            let id = s.get().expect("the item's id was never recorded");
1121                            l.borrow_mut().push((id, reason));
1122                        }),
1123                )
1124            };
1125            item_slot.set(Some(item));
1126            tree.layout(SizeProposal::exact(200.0, 100.0));
1127
1128            let overlay = tree.show_overlay(crate::overlay::OverlayRequest {
1129                content_id: item,
1130                anchor,
1131                placement: crate::overlay::OverlayPlacement::Below,
1132                dismiss: crate::overlay::DismissBehavior::Manual,
1133                layer: crate::overlay::OverlayLayer::InTree,
1134                parent_overlay: None,
1135                on_dismiss: None,
1136                fade_duration: None,
1137            });
1138            overlay_slot.set(Some(overlay));
1139            tree.layout(SizeProposal::exact(200.0, 100.0));
1140            let at = tree.bounds(item).center();
1141
1142            if kind == "mouse" {
1143                press(&mut tree, at);
1144                release(&mut tree, at);
1145            } else {
1146                let contact = new_contact();
1147                tree.dispatch_pointer(touch(contact, PointerPhase::Down, at));
1148                tree.dispatch_pointer(touch(contact, PointerPhase::Up, at));
1149            }
1150
1151            assert_eq!(tapped.get(), 1, "the {kind} tap completed");
1152            assert!(
1153                log.borrow().is_empty(),
1154                "the {kind} tap must not be cancelled by the teardown it asked \
1155                 for: {:?}",
1156                log.borrow()
1157            );
1158            tree.assert_no_leaked_pointer_state();
1159        }
1160    }
1161
1162    // ---------------------------------------------------------------
1163    // Producer: a peer claiming the sequence
1164    // ---------------------------------------------------------------
1165
1166    /// The member granularity, and the one that is emphatically *not* a
1167    /// pointer cancel: two nested drag-capable ancestors compete for a press a
1168    /// tapping descendant is holding, the inner one wins at the mouse latch,
1169    /// and the outer one is told it lost — **once**, on that sample, and never
1170    /// again however many more moves arrive.
1171    #[test]
1172    fn a_peer_claim_revokes_each_loser_exactly_once() {
1173        let log = log();
1174        let outer_slot = Rc::new(std::cell::Cell::new(None));
1175
1176        let mut tree = WidgetTree::new();
1177        // The tap owner: it takes the capture, which is what enrols the
1178        // drag-capable ancestors above it as competitors.
1179        let child = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
1180        let inner = tree.add(StackWidget::new().child(child).on_drag(|_phase, _c| {}));
1181        let outer = tree.add(
1182            StackWidget::new()
1183                .child(inner)
1184                .on_drag(|_phase, _c| {})
1185                .on_pointer_cancel({
1186                    let log = log.clone();
1187                    let slot = outer_slot.clone();
1188                    move |_p, reason, _ctx| {
1189                        let id = slot.get().expect("the outer id was never recorded");
1190                        log.borrow_mut().push((id, reason));
1191                    }
1192                }),
1193        );
1194        outer_slot.set(Some(outer));
1195        tree.layout(SizeProposal::exact(400.0, 100.0));
1196
1197        press(&mut tree, Point::new(20.0, 50.0));
1198        // Ten samples well past the mouse drag latch: the inner drag wins on
1199        // the first one that clears 5 dp, and the outer must be revoked then
1200        // and only then.
1201        for step in 1..=10 {
1202            moved(&mut tree, Point::new(20.0 + step as f32 * 10.0, 50.0));
1203        }
1204
1205        assert_eq!(
1206            tree.sequence_winner(PointerId::MOUSE),
1207            Some(inner),
1208            "the innermost drag won the press"
1209        );
1210        assert_eq!(
1211            *log.borrow(),
1212            vec![(outer, CancelReason::PeerClaimed)],
1213            "the loser is revoked once, on the sample the peer won, and never again"
1214        );
1215        assert!(
1216            tree.pointers.get(PointerId::MOUSE).is_some(),
1217            "revoking a member is not cancelling the pointer: it is still live"
1218        );
1219
1220        release(&mut tree, Point::new(120.0, 50.0));
1221        assert_eq!(
1222            log.borrow().len(),
1223            1,
1224            "and the release adds nothing: the loser was already told"
1225        );
1226        tree.assert_no_leaked_pointer_state();
1227    }
1228
1229    // ---------------------------------------------------------------
1230    // Producer: rebuild / member death
1231    // ---------------------------------------------------------------
1232
1233    /// A competitor destroyed mid-press is revoked **individually**: the
1234    /// pointer and its winner are untouched.
1235    #[test]
1236    fn a_destroyed_member_is_revoked_alone() {
1237        let mut tree = WidgetTree::new();
1238        let child = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
1239        let ancestor = tree.add(StackWidget::new().child(child).on_drag(|_phase, _c| {}));
1240        tree.layout(SizeProposal::exact(400.0, 100.0));
1241
1242        press(&mut tree, Point::new(20.0, 50.0));
1243        assert!(
1244            tree.sequence_members(PointerId::MOUSE)
1245                .iter()
1246                .any(|(id, _, _)| *id == ancestor),
1247            "the ancestor drag is enrolled through the child's capture"
1248        );
1249
1250        tree.destroy_subtree_for_testing(ancestor);
1251        moved(&mut tree, Point::new(21.0, 50.0));
1252
1253        assert!(
1254            tree.sequence_members(PointerId::MOUSE)
1255                .iter()
1256                .all(|(id, _, _)| *id != ancestor),
1257            "the dead member is dropped from the arbitration"
1258        );
1259    }
1260
1261    /// The captor dying is a different act: the press belonged to it, so the
1262    /// whole pointer is cancelled and the capture it left behind is given back.
1263    #[test]
1264    fn a_destroyed_captor_cancels_the_whole_pointer() {
1265        let log = log();
1266        let slot = Rc::new(std::cell::Cell::new(None));
1267        let mut tree = WidgetTree::new();
1268        let leaf = tree.add(grip(&log, slot.clone()));
1269        slot.set(Some(leaf));
1270        let host = tree.add(StackWidget::new().child(leaf));
1271        tree.layout(SizeProposal::exact(200.0, 100.0));
1272
1273        press(&mut tree, Point::new(20.0, 50.0));
1274        assert_eq!(tree.captured_by(PointerId::MOUSE), Some(leaf));
1275
1276        // Destroy the captor without a layout in between, so the sequence's own
1277        // revalidation is what notices — the mandated hook.
1278        tree.destroy_subtree_for_testing(host);
1279        moved(&mut tree, Point::new(21.0, 50.0));
1280
1281        assert_eq!(
1282            tree.captured_by(PointerId::MOUSE),
1283            None,
1284            "the orphaned capture is given back"
1285        );
1286        assert!(
1287            tree.sequence_members(PointerId::MOUSE).is_empty(),
1288            "and the arbitration is over"
1289        );
1290        tree.assert_no_leaked_pointer_state();
1291    }
1292
1293    // ---------------------------------------------------------------
1294    // Producer: an OS drag taking the pointer
1295    // ---------------------------------------------------------------
1296
1297    /// Escalating an in-app drag to the OS hands the pointer to the platform:
1298    /// this window sees no further move and no `Up`, so whatever the press
1299    /// still had going has to be revoked here.
1300    #[test]
1301    fn escalating_to_an_os_drag_cancels_the_source_pointer() {
1302        /// Stands in for the platform backend: accepts the hand-off and
1303        /// records that it did.
1304        struct AcceptingOps {
1305            began: std::cell::Cell<bool>,
1306        }
1307        impl crate::window::WindowOps for AcceptingOps {
1308            fn open_window(
1309                &mut self,
1310                _config: crate::window::WindowConfig,
1311            ) -> crate::window::TeksiloWindowId {
1312                panic!("not used in this test")
1313            }
1314            fn find_window(&self, _id: &str) -> Option<crate::window::TeksiloWindowId> {
1315                None
1316            }
1317            fn window_state(
1318                &self,
1319                _id: crate::window::TeksiloWindowId,
1320            ) -> Option<crate::window::WindowState> {
1321                None
1322            }
1323            fn windows(&self) -> Vec<crate::window::WindowState> {
1324                Vec::new()
1325            }
1326            fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
1327            fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
1328            fn begin_os_drag(
1329                &mut self,
1330                _data: crate::drag_payload::OutboundDragData,
1331                _image: Option<crate::drag_payload::DragImageData>,
1332                _pointer: teksilo_tokens::PointerKind,
1333            ) -> bool {
1334                self.began.set(true);
1335                true
1336            }
1337        }
1338
1339        let log = log();
1340        let slot = Rc::new(std::cell::Cell::new(None));
1341        let mut tree = WidgetTree::new();
1342        let source = tree.add(recorder(&log, slot.clone()));
1343        slot.set(Some(source));
1344        tree.layout(SizeProposal::exact(200.0, 100.0));
1345
1346        let mut ops = AcceptingOps {
1347            began: std::cell::Cell::new(false),
1348        };
1349        press(&mut tree, Point::new(20.0, 50.0));
1350
1351        let mut ctx = crate::widget::EventContext::new();
1352        ctx.start_drag(
1353            source,
1354            crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"7".to_vec()),
1355        );
1356        tree.collect_from_ctx(ctx, source);
1357        assert!(tree.active_drag.is_some());
1358
1359        // Out of the window: the drag escalates.
1360        tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(-40.0, 50.0)), &mut ops);
1361
1362        assert!(ops.began.get(), "the platform took the drag");
1363        assert_eq!(
1364            *log.borrow(),
1365            vec![(source, CancelReason::OsDragStarted)],
1366            "and the source is told the in-app half of the interaction is over"
1367        );
1368        tree.assert_no_leaked_pointer_state();
1369    }
1370
1371    // ---------------------------------------------------------------
1372    // The queue, and terminality
1373    // ---------------------------------------------------------------
1374
1375    /// A cancel raised from inside a handler is **queued**: the handler that
1376    /// raised it finishes on the state it started with, and the teardown runs
1377    /// afterwards — before the top-level dispatch returns.
1378    #[test]
1379    fn a_cancel_raised_from_a_handler_is_queued_not_reentrant() {
1380        let observed_capture = Rc::new(std::cell::Cell::new(None));
1381        let cancelled = Rc::new(std::cell::Cell::new(false));
1382
1383        let mut tree = WidgetTree::new();
1384        let leaf = {
1385            let seen = observed_capture.clone();
1386            let done = cancelled.clone();
1387            tree.add(
1388                FillWidget::new()
1389                    .on_pointer_event(move |event, ctx| {
1390                        if matches!(event, WidgetEvent::PointerDown { .. }) {
1391                            ctx.capture_pointer();
1392                        }
1393                        if matches!(event, WidgetEvent::PointerMove { .. }) {
1394                            ctx.cancel_pointer_sequence(CancelReason::Deactivated);
1395                            // Still owned, right here: the cancel has not run.
1396                            seen.set(Some(ctx.owns_pointer()));
1397                        }
1398                        EventResponse::Ignored
1399                    })
1400                    .on_pointer_cancel(move |_p, _reason, _ctx| done.set(true)),
1401            )
1402        };
1403        tree.layout(SizeProposal::exact(200.0, 100.0));
1404
1405        press(&mut tree, Point::new(20.0, 50.0));
1406        moved(&mut tree, Point::new(30.0, 50.0));
1407
1408        assert_eq!(
1409            observed_capture.get(),
1410            Some(true),
1411            "the handler that raised the cancel still owned the pointer when it returned"
1412        );
1413        assert!(
1414            cancelled.get(),
1415            "and the teardown ran once the sample finished"
1416        );
1417        assert!(
1418            !tree.has_pending_dispatch(),
1419            "the queue is empty again before the dispatch returns"
1420        );
1421        assert_eq!(tree.captured_by(PointerId::MOUSE), None);
1422        assert_eq!(tree.arena.get(leaf).map(|_| ()), Some(()));
1423        tree.assert_no_leaked_pointer_state();
1424    }
1425
1426    /// A cancel that a handler raises against a pointer which lifts in the very
1427    /// same sample must not fire: by the time the queue drains there is no
1428    /// interaction left to revoke.
1429    #[test]
1430    fn a_cancel_does_not_fire_if_the_pointer_lifted_in_the_same_sample() {
1431        let log = log();
1432        let slot = Rc::new(std::cell::Cell::new(None));
1433
1434        let mut tree = WidgetTree::new();
1435        let leaf = {
1436            let l = log.clone();
1437            let s = slot.clone();
1438            tree.add(
1439                FillWidget::new()
1440                    .on_pointer_event(|event, ctx| {
1441                        if matches!(event, WidgetEvent::PointerDown { .. }) {
1442                            ctx.capture_pointer();
1443                        }
1444                        if matches!(event, WidgetEvent::PointerUp { .. }) {
1445                            // The release itself asks for a cancel — the shape
1446                            // a menu item's "close my menu" handler has.
1447                            ctx.cancel_pointer_sequence(CancelReason::OverlayDismissed);
1448                        }
1449                        EventResponse::Ignored
1450                    })
1451                    .on_pointer_cancel(move |_p, reason, _ctx| {
1452                        let id = s.get().expect("the leaf's id was never recorded");
1453                        l.borrow_mut().push((id, reason));
1454                    }),
1455            )
1456        };
1457        slot.set(Some(leaf));
1458        tree.layout(SizeProposal::exact(200.0, 100.0));
1459
1460        let contact = new_contact();
1461        tree.dispatch_pointer(touch(contact, PointerPhase::Down, Point::new(20.0, 50.0)));
1462        tree.dispatch_pointer(touch(contact, PointerPhase::Up, Point::new(20.0, 50.0)));
1463
1464        assert!(
1465            log.borrow().is_empty(),
1466            "the press was already over when the queued cancel drained: {:?}",
1467            log.borrow()
1468        );
1469        tree.assert_no_leaked_pointer_state();
1470    }
1471
1472    /// `PointerCancel` is terminal. An `Up` that arrives for a press the system
1473    /// already took away completes nothing — the widget has been told to let
1474    /// go, and handing it back the release would resurrect an interaction that
1475    /// no longer exists.
1476    #[test]
1477    fn no_pointer_up_follows_a_cancel() {
1478        let ups = Rc::new(std::cell::Cell::new(0_u32));
1479        let cancels = Rc::new(std::cell::Cell::new(0_u32));
1480
1481        let mut tree = WidgetTree::new();
1482        {
1483            let u = ups.clone();
1484            let c = cancels.clone();
1485            tree.add(
1486                FillWidget::new()
1487                    .on_tap(move |_e, _ctx| u.set(u.get() + 1))
1488                    .on_pointer_cancel(move |_p, _reason, _ctx| c.set(c.get() + 1)),
1489            );
1490        }
1491        tree.layout(SizeProposal::exact(200.0, 100.0));
1492
1493        press(&mut tree, Point::new(20.0, 50.0));
1494        tree.set_window_active(false);
1495        assert_eq!(cancels.get(), 1);
1496
1497        // The platform (or a confused caller) sends the release anyway.
1498        release(&mut tree, Point::new(20.0, 50.0));
1499        assert_eq!(ups.get(), 0, "the tap must not complete after its cancel");
1500
1501        // …and the next press is a fresh interaction, unaffected.
1502        tree.set_window_active(true);
1503        press(&mut tree, Point::new(20.0, 50.0));
1504        release(&mut tree, Point::new(20.0, 50.0));
1505        assert_eq!(ups.get(), 1, "the next press works normally");
1506        tree.assert_no_leaked_pointer_state();
1507    }
1508
1509    /// The whole-pointer and member-level granularities are different acts,
1510    /// and the difference has to be visible from outside: a member revocation
1511    /// leaves the pointer, its capture and its winner exactly where they were.
1512    #[test]
1513    fn revoking_a_member_leaves_the_pointer_alive() {
1514        let mut tree = WidgetTree::new();
1515        let child = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
1516        let ancestor = tree.add(StackWidget::new().child(child).on_drag(|_phase, _c| {}));
1517        tree.layout(SizeProposal::exact(400.0, 100.0));
1518
1519        press(&mut tree, Point::new(20.0, 50.0));
1520        let mut noop = crate::window::NoopWindowOps;
1521        tree.revoke_sequence_member(
1522            PointerId::MOUSE,
1523            ancestor,
1524            CancelReason::PeerClaimed,
1525            &mut noop,
1526        );
1527
1528        assert_eq!(
1529            tree.captured_by(PointerId::MOUSE),
1530            Some(child),
1531            "the capture survives a member revocation"
1532        );
1533        assert!(
1534            tree.pointers.get(PointerId::MOUSE).is_some(),
1535            "and so does the pointer"
1536        );
1537
1538        release(&mut tree, Point::new(20.0, 50.0));
1539        tree.assert_no_leaked_pointer_state();
1540    }
1541
1542    /// A drag the cancelled pointer was driving ends as `Cancelled`, not as a
1543    /// drop: releasing the payload wherever the pointer happened to be is how
1544    /// a lost window focus turns into a data-loss bug.
1545    #[test]
1546    fn cancelling_the_pointer_driving_a_drag_cancels_the_drag() {
1547        let outcome = Rc::new(RefCell::new(None));
1548
1549        let mut tree = WidgetTree::new();
1550        let source = {
1551            let o = outcome.clone();
1552            tree.add(FillWidget::new().on_drag_ended(move |result, _ctx| {
1553                *o.borrow_mut() = Some(result);
1554            }))
1555        };
1556        tree.layout(SizeProposal::exact(200.0, 100.0));
1557
1558        press(&mut tree, Point::new(20.0, 50.0));
1559        let mut ctx = crate::widget::EventContext::new();
1560        ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
1561        tree.collect_from_ctx(ctx, source);
1562        assert!(tree.active_drag.is_some());
1563
1564        tree.set_window_active(false);
1565
1566        assert!(tree.active_drag.is_none(), "the drag session is torn down");
1567        assert_eq!(
1568            *outcome.borrow(),
1569            Some(crate::drag_payload::DropOutcome::Cancelled),
1570            "and its source is told it was cancelled, not dropped"
1571        );
1572        tree.assert_no_leaked_pointer_state();
1573    }
1574
1575    /// A drag mid-flight is exactly the case the funnel exists for. Its phases
1576    /// must not report an `Ended` — that is the `Up` story — and the widget
1577    /// must hear about the revocation instead.
1578    #[test]
1579    fn a_cancelled_drag_reports_no_ended_phase() {
1580        let phases = Rc::new(RefCell::new(Vec::new()));
1581        let cancels = Rc::new(std::cell::Cell::new(0_u32));
1582
1583        let mut tree = WidgetTree::new();
1584        {
1585            let p = phases.clone();
1586            let c = cancels.clone();
1587            tree.add(
1588                FillWidget::new()
1589                    .on_drag(move |phase, _ctx| {
1590                        p.borrow_mut().push(std::mem::discriminant(&phase));
1591                    })
1592                    .on_pointer_cancel(move |_p, _reason, _ctx| c.set(c.get() + 1)),
1593            );
1594        }
1595        tree.layout(SizeProposal::exact(400.0, 100.0));
1596
1597        press(&mut tree, Point::new(20.0, 50.0));
1598        for step in 1..=3 {
1599            moved(&mut tree, Point::new(20.0 + step as f32 * 10.0, 50.0));
1600        }
1601        let before = phases.borrow().len();
1602        assert!(before >= 2, "the drag started and moved");
1603
1604        tree.set_window_active(false);
1605
1606        assert_eq!(cancels.get(), 1, "the dragging widget is told");
1607        assert_eq!(
1608            phases.borrow().len(),
1609            before,
1610            "and no further drag phase — least of all an Ended — is reported"
1611        );
1612        tree.assert_no_leaked_pointer_state();
1613    }
1614
1615    /// A cancelled hovering pointer stops reading as held.
1616    ///
1617    /// A contact that is taken away leaves the table; a mouse or a pen does
1618    /// not, and the entry that stays behind is what every singular accessor
1619    /// reads. Its button mask is what `PointerEntry::is_contacting` answers
1620    /// from, and a `Cancel` sample never reaches `PointerTable::admit` — so
1621    /// before this was cleared, a stylus whose press was revoked by a modal, a
1622    /// window deactivation or an OS drag went on reporting a held tip until the
1623    /// next real sample, and `assert_no_leaked_pointer_state` said so.
1624    ///
1625    /// A **pen**, not a mouse, because the legacy `WidgetEvent::PointerDown`
1626    /// path admits `PointerInfo::mouse`, whose mask is empty — so a mouse press
1627    /// has never read as contacting in the first place and could not show this.
1628    /// The pen helpers build the sample the platform translator builds, mask
1629    /// included.
1630    #[test]
1631    fn a_cancelled_hovering_pointer_stops_reading_as_held() {
1632        let mut tree = WidgetTree::new();
1633        let id_slot = Rc::new(std::cell::Cell::new(None::<WidgetId>));
1634        let cancels = log();
1635        // Built in one chain rather than through `recorder`: a second
1636        // `WidgetBuilder` call on an already-wrapped `impl Widget` re-wraps
1637        // instead of merging, and the inner handler set would be dropped.
1638        let recorded = cancels.clone();
1639        let slot = id_slot.clone();
1640        let node = tree.add(
1641            FillWidget::new()
1642                .on_pointer_cancel(move |_pointer, reason, _ctx| {
1643                    let id = slot.get().expect("the recorder's id was never recorded");
1644                    recorded.borrow_mut().push((id, reason));
1645                })
1646                .on_tap(|_e, _c| {}),
1647        );
1648        id_slot.set(Some(node));
1649        tree.layout(SizeProposal::exact(100.0, 100.0));
1650
1651        tree.pen_down(Point::new(50.0, 50.0), 0.5, (0.0, 0.0));
1652        let pen = tree
1653            .live_pointers()
1654            .find(|p| matches!(p.kind, teksilo_tokens::PointerKind::Pen(_)))
1655            .map(|p| p.id)
1656            .expect("the pen was admitted");
1657        assert!(
1658            tree.pointers.get(pen).is_some_and(|e| e.is_contacting()),
1659            "the tip is on the surface"
1660        );
1661
1662        let mut noop = crate::window::NoopWindowOps;
1663        tree.cancel_pointer(pen, CancelReason::ModalOpened, &mut noop);
1664
1665        assert_eq!(
1666            cancels.borrow().as_slice(),
1667            &[(node, CancelReason::ModalOpened)],
1668            "the node was told"
1669        );
1670        assert!(
1671            tree.pointers.get(pen).is_some(),
1672            "a pen keeps its entry: it is still in proximity, still hovering"
1673        );
1674        assert!(
1675            !tree.pointers.get(pen).is_some_and(|e| e.is_contacting()),
1676            "…but it is no longer contacting anything"
1677        );
1678        tree.assert_no_leaked_pointer_state();
1679    }
1680}