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