Skip to main content

teksilo_core/gesture/
sequence.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! One arbitration object per live pointer: who is competing for this press,
5//! and which of them owns it.
6//!
7//! # What this replaces
8//!
9//! Before this module the framework had exactly one piece of cross-widget
10//! arbitration: `drag_observers`, a `Vec<WidgetId>` on the tree holding the
11//! draggable ancestors armed by the current press. It was single-pointer (one
12//! `Vec` for the whole tree), drag-only (a scrollable, a previewer or an
13//! explicit captor could not be a competitor at all), and its decision
14//! procedure was implicit in the order of three helper functions.
15//!
16//! A [`PointerSequence`] is the same idea made explicit and made plural: one
17//! per live [`PointerId`](crate::pointer::PointerId), stored on that pointer's
18//! [`PointerEntry`](crate::pointer::table::PointerEntry), carrying the frozen hit
19//! path, the frozen [`TouchAction`], every enrolled [`SequenceMember`], and the
20//! winner once one is decided.
21//!
22//! # The ordered decision procedure
23//!
24//! Stated once here, implemented in `widget_tree/pointer_router.rs`:
25//!
26//! 1. **At press**: hit-test to a target, freeze the hit path (target → root),
27//!    intersect and freeze [`TouchAction`] root-to-target, and record the
28//!    innermost `gesture_dead_zone` node on that path as the enrolment
29//!    boundary.
30//! 2. **The raw-preview pass runs first, root-first.** The first ancestor whose
31//!    `on_pointer_event` answers `Handled` claims the sequence outright as a
32//!    [`MemberRole::RawPreview`] winner. This order is load-bearing —
33//!    `rich_text/mouse.rs` documents relying on an outer wrapper seeing a press
34//!    before an inner one — so previewers are deliberately **not** folded into
35//!    the innermost-first member order below.
36//! 3. **An explicit [`capture_pointer`](crate::widget::EventContext::capture_pointer)
37//!    from an undecided sequence is an arbitration act**, not plumbing: the
38//!    caller is enrolled as [`MemberRole::RawDrag`], and for a precise pointer
39//!    with no eligible pan competitor the sequence is decided there and then.
40//!    Three shipped widgets drive their whole interaction this way — the
41//!    splitter handle, the dock resize handle and the table column grip all
42//!    return `Ignored` from `on_pointer_event`, capture, and work from
43//!    `PointerMove` with no recognizer at all.
44//! 4. **On move while undecided**: timers before positional thresholds, then
45//!    members innermost-first. A `RawDrag` wins past `drag_slop`; a `Gesture`
46//!    wins when its own recognizer recognizes; a [`MemberRole::Pan`] wins only
47//!    on an axis the frozen `TouchAction` permits and only past `pan_slop`.
48//! 5. **On up**: the release sweep — the innermost still-`Possible` member with
49//!    a completable gesture wins, which is the pre-existing
50//!    `arena.process(Up) -> Tap`.
51//!
52//! # A node with two roles
53//!
54//! One node holds exactly **one** member — [`PointerSequence::decide`],
55//! [`PointerSequence::reject`] and [`PointerSequence::hold`] are all keyed on
56//! that, and [`PointerSequence::enrol`] refuses a second. A node can still want
57//! two roles: a scene viewport declares a [`PanClaim`] *and* carries `on_drag`
58//! on the same `HandlerSet`, because its surface is both the camera and the
59//! marquee. Step 1 enrols it as the pan claimant before any handler runs, so
60//! the drag half arrives at an already-taken slot.
61//!
62//! [`PointerSequence::defer_own_drag`] is that half's door: it attaches the
63//! drag's [`DragActivation`] to the member the node already has. The member goes
64//! on competing as a pan at `pan_slop`, while the node's own recognizers are
65//! silenced until the activation allows them — `Auto` on a direct pointer with
66//! an eligible pan resolving, as everywhere else, to a hold. Whichever half
67//! ripens first takes the other out: a won pan withdraws the self-drag
68//! ([`PointerSequence::withdraw_own_drag`]), a recognized self-drag flips the
69//! member's reported role ([`PointerSequence::promote_own_drag`]) so
70//! [`WidgetTree::sequence_members`](crate::WidgetTree::sequence_members) names
71//! the half that actually won.
72//!
73//! The deferral is a **hold**, not a timer. A press that has already travelled
74//! past `long_press_slop` when the deadline arrives was never a hold, so its
75//! self-drag is withdrawn rather than armed — the rule
76//! [`LongPressRecognizer`](super::LongPressRecognizer) applies to itself.
77//! Without it a slow, deliberate pan would arm the grab simply by outlasting
78//! the clock, and a surface that pans only for a *fast* finger is not a surface
79//! that pans.
80//!
81//! And the hold it spends is spent **on that node**.
82//! [`PointerSequence::has_deferred_grab_for`] is keyed on a node, so a
83//! heavyweight widget inside a dual-role container keeps its own touch long
84//! press and its own touch context menu — a deferred grab on the container is
85//! not a declaration that its descendants have given theirs up. The
86//! ancestor-wide door is the explicit
87//! [`LongPressRole::DragHandle`](crate::LongPressRole::DragHandle).
88//!
89//! **The mouse cannot enter that arm at all.** `defer_own_drag` refuses anything
90//! but a live [`MemberRole::Pan`] member, and a mouse enrols none — so on a
91//! mouse a dual-role node is enrolled by step 4's ordinary `Gesture` path and
92//! latches at the 5.0 it always did.
93//!
94//! # Why the mouse is unchanged
95//!
96//! [`GestureProfile::pan_slop`] is `None` for a mouse and
97//! [`PanClaim::devices`] defaults to direct pointers, so **no pan member is
98//! ever eligible for a mouse**. Every mouse sequence is therefore either
99//! decided at press (an explicit capture) or arbitrated exactly as
100//! `drag_observers` arbitrated it: ancestors innermost-first, each latching at
101//! its own `drag_slop`, which for the mouse profile is the 5.0 it has always
102//! been. On touch the same widget defers by `drag_slop` (18) and still beats a
103//! scroller, because `pan_slop` (36) is larger.
104//!
105//! Reference: `docs/events-and-gestures.md`.
106
107use teksilo_canvas::Point;
108use teksilo_tokens::{DragActivation, GestureProfile};
109
110use crate::pointer::touch_action::{Axis, PanClaim, TouchAction};
111use crate::pointer::{EventTime, PointerInfo};
112use crate::widget_id::WidgetId;
113
114/// What a member is competing *as*.
115///
116/// The role decides which threshold the member wins on and, for `Pan`, which
117/// axes the frozen [`TouchAction`] has to permit.
118#[non_exhaustive]
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub enum MemberRole {
121    /// A node whose own gesture recognizers (`on_drag` / `on_swipe`) are
122    /// competing. This is what `drag_observers` used to hold, and it is what an
123    /// ancestor of the pressed control is enrolled as.
124    Gesture,
125    /// A scroll container that declared a [`PanClaim`]. Only ever enrolled for
126    /// a pointer kind the claim's `devices` mask admits and only when the
127    /// pointer's profile has a `pan_slop` — so never for a mouse.
128    Pan(PanClaim),
129    /// A node that took the pointer by an explicit
130    /// [`capture_pointer`](crate::widget::EventContext::capture_pointer) while
131    /// the sequence was undecided, and drives its interaction from
132    /// `PointerMove` rather than from a recognizer.
133    RawDrag,
134    /// A node that answered `Handled` from the root-first preview pass. It has
135    /// already won by the time it is enrolled; the role exists so
136    /// [`WidgetTree::sequence_members`](crate::WidgetTree::sequence_members)
137    /// can report *why*.
138    RawPreview,
139}
140
141/// Where one member stands in the arbitration.
142#[non_exhaustive]
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum MemberState {
145    /// Still in the running.
146    Possible,
147    /// Deferring its own decision — see
148    /// [`hold_gesture`](crate::widget::EventContext::hold_gesture). Released
149    /// automatically at `profile.max_hold`; the framework itself never holds.
150    Held,
151    /// Out of the running, either by its own choice
152    /// ([`reject_gesture`](crate::widget::EventContext::reject_gesture)), by a
153    /// threshold it can no longer meet, or because a peer won.
154    Rejected,
155    /// The winner.
156    Won,
157}
158
159/// One competitor for a pointer sequence.
160#[derive(Debug, Clone, Copy, PartialEq)]
161pub struct SequenceMember {
162    /// The competing node.
163    pub id: WidgetId,
164    /// What it is competing as.
165    pub role: MemberRole,
166    /// The earliest time this member may win, when its activation defers it.
167    /// `None` means "as soon as its threshold is met".
168    pub eligible_at: Option<EventTime>,
169    /// Where it stands.
170    pub state: MemberState,
171    /// When `true`, this member self-rejects the moment the pointer leaves the
172    /// tap boundary — the [`DragActivation::AfterLongPress`] rule.
173    pub(crate) rejects_on_tap_slop: bool,
174    /// When [`state`](Self::state) became [`MemberState::Held`].
175    pub(crate) held_since: Option<EventTime>,
176    /// Whether this member's node **also** owns drag/swipe recognizers of its
177    /// own — the dual-role shape. See
178    /// [`PointerSequence::defer_own_drag`](PointerSequence::defer_own_drag).
179    ///
180    /// `false` for every member a mouse ever enrols, because the arm that sets
181    /// it is reachable only through a [`MemberRole::Pan`] membership.
182    pub(crate) has_own_drag: bool,
183    /// When those self-drag recognizers may start running. `None` means "now".
184    pub(crate) own_drag_eligible_at: Option<EventTime>,
185    /// Whether that self-drag is out of the running for the rest of the press —
186    /// the pan half won, or the press travelled past the tap boundary.
187    pub(crate) own_drag_withdrawn: bool,
188}
189
190impl SequenceMember {
191    /// A fresh member in the running.
192    pub(crate) fn new(id: WidgetId, role: MemberRole) -> Self {
193        Self {
194            id,
195            role,
196            eligible_at: None,
197            state: MemberState::Possible,
198            rejects_on_tap_slop: false,
199            held_since: None,
200            has_own_drag: false,
201            own_drag_eligible_at: None,
202            own_drag_withdrawn: false,
203        }
204    }
205
206    /// Whether this member could still win.
207    pub fn is_live(&self) -> bool {
208        matches!(self.state, MemberState::Possible | MemberState::Held)
209    }
210
211    /// Whether this member may win at `now`. A member deferred by
212    /// [`DragActivation::AfterLongPress`] cannot win before its timer, and a
213    /// holding member cannot win at all until it releases.
214    pub fn is_eligible_at(&self, now: EventTime) -> bool {
215        self.state == MemberState::Possible
216            && self.eligible_at.is_none_or(|deadline| now >= deadline)
217    }
218
219    /// Whether this member's node may run its **own** drag/swipe recognizers at
220    /// `now`.
221    ///
222    /// Always `true` for a member carrying no self-drag deferral — which is
223    /// every member a mouse ever enrols, and every member of every sequence on
224    /// a node that is not dual-role — so this predicate is inert everywhere the
225    /// dual-role shape does not occur.
226    ///
227    /// Three answers, and the third is the load-bearing one:
228    ///
229    /// * no self-drag → `true`, unconditionally;
230    /// * a self-drag still inside its deferral → `false` until `now` reaches
231    ///   `own_drag_eligible_at`;
232    /// * a **withdrawn** self-drag → `false` for the rest of the press, and
233    ///   deliberately so. A withdrawal means the press is not the hold the
234    ///   deferral was waiting for — the pan half took it, or the travel
235    ///   disproved the hold — and the node's recognizers must stay silenced
236    ///   afterwards, or a deferral ripening later would start the node's drag
237    ///   under a finger that had already committed to something else. Because
238    ///   the gate this feeds (`WidgetTree::sequence_blocks_arena`'s rule) is per
239    ///   *node*, that silence covers the node's tap family too, which is the
240    ///   same thing `WidgetTree::cancel_member_taps` does to the pan winner
241    ///   explicitly. For a dual-role node mid-pan that is the wanted answer: a
242    ///   finger that has committed to a pan is not also tapping, double-tapping
243    ///   or long-pressing the surface it is panning. A **completed** tap is not
244    ///   lost to it either way: `end_sequence` nulls the sequence before the
245    ///   release is dispatched, and `TapRecognizer` decides the release against
246    ///   its own [`TapBoundary`], so a press that wandered and lifted inside the
247    ///   node still taps.
248    pub fn own_drag_armed_at(&self, now: EventTime) -> bool {
249        if !self.has_own_drag {
250            return true;
251        }
252        !self.own_drag_withdrawn && self.own_drag_eligible_at.is_none_or(|at| now >= at)
253    }
254}
255
256/// Where a press stops being a tap.
257///
258/// One predicate, three consumers: it fails a tap, it triggers
259/// [`GestureArenaSet::cancel_taps`](super::GestureArenaSet::cancel_taps), and
260/// it will clear the framework press visual. A coarse pointer uses `Bounds`
261/// because a finger's reported centre wanders several device pixels while
262/// resting inside the control it is pressing; a precise pointer keeps the
263/// radius it always had.
264#[derive(Debug, Clone, Copy, PartialEq)]
265pub enum TapBoundary {
266    /// The press fails once it travels further than this from its origin.
267    Radius(f32),
268    /// The press fails once it leaves the pressed node's bounds.
269    Bounds,
270}
271
272impl TapBoundary {
273    /// The boundary a pointer of this kind uses.
274    ///
275    /// `Radius(profile.tap_slop)` for a precise pointer — the pre-existing
276    /// rule, unchanged — and `Bounds` for a coarse one.
277    pub fn for_pointer(pointer: &PointerInfo, profile: &GestureProfile) -> Self {
278        if pointer.kind.is_coarse() {
279            Self::Bounds
280        } else {
281            Self::Radius(profile.tap_slop)
282        }
283    }
284
285    /// Whether `position` has left the boundary, given the press origin and the
286    /// pressed node's bounds in the same (window-logical) space.
287    ///
288    /// A `Bounds` boundary with no bounds to test against — the pressed node
289    /// went away — falls back to the radius, so the answer is never "the press
290    /// can travel anywhere".
291    ///
292    /// So does a `Bounds` boundary whose press **began outside the node**. A
293    /// press can be accepted for a node it did not land in: a control that
294    /// declares a [`Widget::hit_outset`] is offered the ring around it (a 12 dp
295    /// twist arrow lifted to a 24 dp target, a 16 dp clear affordance inside a
296    /// text field), and the miss-only slop pass re-attributes a near miss the
297    /// same way. For such a press the node's rectangle never contained the
298    /// origin, so testing `position` against it alone would report the press as
299    /// already-left at the instant it arrived — and the tap could never
300    /// complete, silently making the whole outset mechanism useless to every
301    /// coarse-pointer tap. The rule for that case is the pointer's own radius
302    /// around where it landed, unioned with the node's bounds so sliding *onto*
303    /// the control keeps the press alive. Android's `ViewGroup` takes the same
304    /// shape from the other direction (`pointInView(x, y, mTouchSlop)` — the
305    /// view's rect inflated by touch slop).
306    ///
307    /// A press that began inside the node is untouched: the rectangle is the
308    /// boundary, exactly as before.
309    ///
310    /// [`Widget::hit_outset`]: crate::widget::Widget::hit_outset
311    pub fn left(
312        &self,
313        origin: Point,
314        position: Point,
315        bounds: Option<teksilo_canvas::Rect>,
316        profile: &GestureProfile,
317    ) -> bool {
318        match self {
319            Self::Radius(radius) => super::distance(origin, position) > *radius,
320            Self::Bounds => match bounds {
321                Some(rect) if rect.contains(origin) => !rect.contains(position),
322                Some(rect) => {
323                    !rect.contains(position) && super::distance(origin, position) > profile.tap_slop
324                }
325                None => super::distance(origin, position) > profile.tap_slop,
326            },
327        }
328    }
329}
330
331/// Everything the tree knows about one press: who is competing for it, and who
332/// won.
333///
334/// Lives in [`PointerEntry::sequence`](crate::pointer::table::PointerEntry::sequence)
335/// for as long as the pointer is down. The ordered decision procedure this
336/// type is the state of is written out in `docs/events-and-gestures.md` §4.2 and
337/// in this module's own header.
338#[derive(Debug, Clone, PartialEq)]
339pub struct PointerSequence {
340    pointer: PointerInfo,
341    path: Vec<WidgetId>,
342    touch_action: TouchAction,
343    dead_zone_boundary: Option<WidgetId>,
344    members: Vec<SequenceMember>,
345    winner: Option<WidgetId>,
346    capture: Option<WidgetId>,
347    press_origin: Point,
348    last_position: Point,
349    started_at: EventTime,
350    pressed_owner: Option<WidgetId>,
351    terminating: bool,
352    taps_cancelled: bool,
353    /// Per-press [`DragActivation`] overrides, queued by
354    /// [`EventContext::set_drag_activation`](crate::widget::EventContext::set_drag_activation)
355    /// from a press handler and read by the enrolment walk that runs
356    /// immediately afterwards.
357    ///
358    /// On the **sequence**, not written back onto the node, because
359    /// `on_pointer_event` previews root-first over every strict ancestor of the
360    /// press target: a node whose press handler answers here fires for presses
361    /// it does not own, and a node write would leave its build-time activation
362    /// changed for the *next* press. A `Vec` rather than a map — a press has at
363    /// most a handful of answering nodes, and the order it is written in is the
364    /// order it is read back in.
365    drag_activation_overrides: Vec<(WidgetId, DragActivation)>,
366}
367
368impl PointerSequence {
369    /// Open a sequence for `pointer`'s press at `origin`.
370    ///
371    /// `path` runs target → root and is **frozen**: a rebuild mid-gesture
372    /// cannot change who was competing for a press that has already started.
373    pub fn new(
374        pointer: PointerInfo,
375        path: Vec<WidgetId>,
376        touch_action: TouchAction,
377        dead_zone_boundary: Option<WidgetId>,
378        origin: Point,
379        started_at: EventTime,
380    ) -> Self {
381        Self {
382            pointer,
383            path,
384            touch_action,
385            dead_zone_boundary,
386            members: Vec::new(),
387            winner: None,
388            capture: None,
389            press_origin: origin,
390            last_position: origin,
391            started_at,
392            pressed_owner: None,
393            terminating: false,
394            taps_cancelled: false,
395            drag_activation_overrides: Vec::new(),
396        }
397    }
398
399    /// Which pointer this sequence follows.
400    pub fn pointer(&self) -> PointerInfo {
401        self.pointer
402    }
403
404    /// The frozen hit path, target → root.
405    pub fn path(&self) -> &[WidgetId] {
406        &self.path
407    }
408
409    /// The [`TouchAction`] frozen at press — the intersection of every
410    /// declaration from the root down to the pressed target.
411    pub fn touch_action(&self) -> TouchAction {
412        self.touch_action
413    }
414
415    /// The innermost `gesture_dead_zone` node on the frozen path, if any.
416    /// Nothing at or above it may be enrolled.
417    pub fn dead_zone_boundary(&self) -> Option<WidgetId> {
418        self.dead_zone_boundary
419    }
420
421    /// Every enrolled member, innermost first.
422    pub fn members(&self) -> &[SequenceMember] {
423        &self.members
424    }
425
426    /// The node that owns this press, once one has been decided.
427    pub fn winner(&self) -> Option<WidgetId> {
428        self.winner
429    }
430
431    /// Whether arbitration is over.
432    pub fn is_decided(&self) -> bool {
433        self.winner.is_some()
434    }
435
436    /// The node holding this pointer's capture, as the sequence recorded it.
437    pub fn capture(&self) -> Option<WidgetId> {
438        self.capture
439    }
440
441    /// Record who holds the capture.
442    pub fn set_capture(&mut self, captor: Option<WidgetId>) {
443        self.capture = captor;
444    }
445
446    /// The node whose gesture arena took the press — the tap owner, when the
447    /// press was not claimed by anything else.
448    pub fn pressed_owner(&self) -> Option<WidgetId> {
449        self.pressed_owner
450    }
451
452    /// Record the node whose gesture arena took the press.
453    pub fn set_pressed_owner(&mut self, owner: Option<WidgetId>) {
454        self.pressed_owner = owner;
455    }
456
457    /// Where the press landed.
458    pub fn press_origin(&self) -> Point {
459        self.press_origin
460    }
461
462    /// Where the pointer was at its most recent sample.
463    pub fn last_position(&self) -> Point {
464        self.last_position
465    }
466
467    /// Record the pointer's current position.
468    pub fn set_last_position(&mut self, position: Point) {
469        self.last_position = position;
470    }
471
472    /// When the press landed, on the tree's input timeline.
473    pub fn started_at(&self) -> EventTime {
474        self.started_at
475    }
476
477    /// Whether the press has already been told it is no longer a tap.
478    ///
479    /// The `cancel_taps` revocation fires **once** per press: it resets the
480    /// node's [`TapStreak`](super::TapStreak), and repeating it on every
481    /// subsequent move would keep clearing state a live drag may still want.
482    pub fn taps_cancelled(&self) -> bool {
483        self.taps_cancelled
484    }
485
486    /// Record that the tap family has been revoked for this press.
487    pub fn set_taps_cancelled(&mut self) {
488        self.taps_cancelled = true;
489    }
490
491    /// Whether the sequence is inside its own terminal dispatch — set while the
492    /// `Up` that ends it is being delivered, so a teardown triggered from a
493    /// handler cannot cancel a press that has already completed.
494    pub fn is_terminating(&self) -> bool {
495        self.terminating
496    }
497
498    /// Mark the sequence as inside its terminal dispatch.
499    pub fn set_terminating(&mut self, terminating: bool) {
500        self.terminating = terminating;
501    }
502
503    /// How far the pointer has travelled from the press point.
504    pub fn travel(&self) -> f32 {
505        super::distance(self.press_origin, self.last_position)
506    }
507
508    /// How far the pointer has travelled along one axis.
509    pub fn travel_on(&self, axis: Axis) -> f32 {
510        match axis {
511            Axis::X => (self.last_position.x - self.press_origin.x).abs(),
512            Axis::Y => (self.last_position.y - self.press_origin.y).abs(),
513        }
514    }
515
516    /// The slop a positional member of this sequence latches at.
517    ///
518    /// `profile.drag_slop` in every configuration **except** a direct pointer
519    /// under a frozen [`TouchAction::NONE`], where the subtree has declared
520    /// that a contact does nothing but manipulate it and the jitter floor is
521    /// the right threshold. A precise pointer always uses `drag_slop`: reading
522    /// `slop_precise` for it would silently retune every mouse drag latch from
523    /// 5 dp to 2.
524    pub fn latch_slop(&self, profile: &GestureProfile) -> f32 {
525        if self.pointer.kind.is_direct() && self.touch_action.is_none() {
526            profile.slop_precise
527        } else {
528            profile.drag_slop
529        }
530    }
531
532    /// Whether `id` may be enrolled at all: it must be on the frozen path and
533    /// strictly below the dead-zone boundary.
534    pub fn may_enrol(&self, id: WidgetId) -> bool {
535        let Some(index) = self.path.iter().position(|p| *p == id) else {
536            return false;
537        };
538        match self.dead_zone_boundary {
539            Some(boundary) => match self.path.iter().position(|p| *p == boundary) {
540                Some(boundary_index) => index < boundary_index,
541                None => true,
542            },
543            None => true,
544        }
545    }
546
547    /// Depth of `id` on the frozen path, innermost first. Used to keep
548    /// [`members`](Self::members) sorted no matter what order enrolment
549    /// happened in.
550    fn depth_of(&self, id: WidgetId) -> usize {
551        self.path
552            .iter()
553            .position(|p| *p == id)
554            .unwrap_or(usize::MAX)
555    }
556
557    /// Whether `id` is already enrolled.
558    pub fn has_member(&self, id: WidgetId) -> bool {
559        self.members.iter().any(|m| m.id == id)
560    }
561
562    /// Enrol `id` as a competitor, keeping the member list innermost-first.
563    ///
564    /// Refused — and reported as `false` — when `id` is at or above the
565    /// dead-zone boundary, when it is not on the frozen path, or when it is
566    /// already enrolled.
567    pub fn enrol(&mut self, id: WidgetId, role: MemberRole) -> bool {
568        if self.has_member(id) || !self.may_enrol(id) {
569            return false;
570        }
571        let member = SequenceMember::new(id, role);
572        let depth = self.depth_of(id);
573        let at = self
574            .members
575            .iter()
576            .position(|m| self.depth_of(m.id) > depth)
577            .unwrap_or(self.members.len());
578        self.members.insert(at, member);
579        true
580    }
581
582    /// Enrol a drag member whose [`DragActivation`] defers it.
583    ///
584    /// `AfterLongPress` (and `Auto` resolving to it) sets `eligible_at` to the
585    /// long-press deadline and arms the self-rejection rule: the member is out
586    /// the moment the press travels past the tap boundary, because that travel
587    /// is a pan, not a considered grab.
588    pub fn enrol_drag(
589        &mut self,
590        id: WidgetId,
591        role: MemberRole,
592        activation: DragActivation,
593        profile: &GestureProfile,
594    ) -> bool {
595        if !self.enrol(id, role) {
596            return false;
597        }
598        if self.resolve_activation(activation) == DragActivation::AfterLongPress
599            && let Some(member) = self.members.iter_mut().find(|m| m.id == id)
600        {
601            member.eligible_at = Some(self.started_at + profile.long_press);
602            member.rejects_on_tap_slop = true;
603        }
604        true
605    }
606
607    /// Give the press owner's **own** drag a say when the node is already
608    /// enrolled in another role.
609    ///
610    /// One node holds exactly one [`SequenceMember`] — [`decide`](Self::decide),
611    /// [`reject`](Self::reject) and [`hold`](Self::hold) are all keyed on that —
612    /// and [`enrol`](Self::enrol) refuses a second. But a node can genuinely
613    /// want two roles: a `SceneView` with selection or magnetism on declares a
614    /// [`PanClaim`] *and* carries `on_drag` on the same `HandlerSet`.
615    /// `begin_sequence` enrols it as the pan claimant before any handler runs,
616    /// so its drag was refused and its [`DragActivation`] was never consulted —
617    /// and its `DragRecognizer`, driven by the ordinary capture dispatch that
618    /// *precedes* the arbitration walk, then latched at `drag_slop` and decided
619    /// the sequence at half the travel the pan needed. A surface shaped like
620    /// that could not pan under a finger at all.
621    ///
622    /// Rather than enrol the node twice, the deferral is attached to the member
623    /// it already has. The member goes on competing as a pan on `pan_slop`; its
624    /// node's own recognizers are held off until `activation` allows them, by
625    /// the same [`resolve_activation`](Self::resolve_activation) every other
626    /// drag member is resolved through — so `Auto` on a direct pointer with an
627    /// eligible pan means `AfterLongPress`, and `Immediate` means "today's
628    /// behaviour, on request".
629    ///
630    /// Refused, and reported as `false`, unless the member exists, is live and
631    /// holds a [`MemberRole::Pan`] — so a mouse, which enrols no pan member at
632    /// all ([`GestureProfile::pan_slop`] is `None` for it and
633    /// [`PanClaim::devices`] admits only direct pointers), can never reach it.
634    /// A decided sequence refuses too: arbitration is over.
635    pub fn defer_own_drag(
636        &mut self,
637        id: WidgetId,
638        activation: DragActivation,
639        profile: &GestureProfile,
640    ) -> bool {
641        if self.is_decided() {
642            return false;
643        }
644        let resolved = self.resolve_activation(activation);
645        let started_at = self.started_at;
646        let Some(member) = self
647            .members
648            .iter_mut()
649            .find(|m| m.id == id && m.is_live() && matches!(m.role, MemberRole::Pan(_)))
650        else {
651            return false;
652        };
653        member.has_own_drag = true;
654        if resolved == DragActivation::AfterLongPress {
655            member.own_drag_eligible_at = Some(started_at + profile.long_press);
656        }
657        true
658    }
659
660    /// Take a member's deferred self-drag out of the running for good.
661    ///
662    /// Three callers, one rule each:
663    ///
664    /// * the **pan half won**, so the press *is* a pan and the node's
665    ///   recognizers must stay silent for the rest of it;
666    /// * the press **travelled past `long_press_slop` before the deadline**, so
667    ///   it was never a hold. That is the rule
668    ///   [`LongPressRecognizer`](super::LongPressRecognizer) applies to itself —
669    ///   it fails on the first move past that slop rather than waiting for its
670    ///   timer — and applying it here is what stops a deliberate, slow pan from
671    ///   arming a grab merely by outlasting the clock;
672    /// * the press **left the tap boundary**, the same reading
673    ///   [`enrol_drag`](Self::enrol_drag)'s `rejects_on_tap_slop` gives that
674    ///   travel.
675    ///
676    /// The last two are both positional and both apply only while the self-drag
677    /// is still unripe — see `Self::unripe_own_drag_members`. They are not
678    /// redundant: `long_press_slop` is a radius around the press and bites on a
679    /// surface the finger never leaves, while [`TapBoundary`] is the node's own
680    /// rect for a coarse pointer and bites on a small node the finger slides off
681    /// without travelling far.
682    pub fn withdraw_own_drag(&mut self, id: WidgetId) {
683        if let Some(member) = self.members.iter_mut().find(|m| m.id == id) {
684            member.own_drag_withdrawn = true;
685        }
686    }
687
688    /// Whether `id`'s **own** drag/swipe recognizers must be kept out of this
689    /// press at `now`. `false` for a node that is not a member, and for every
690    /// member carrying no self-drag.
691    pub fn own_drag_blocked(&self, id: WidgetId, now: EventTime) -> bool {
692        self.members
693            .iter()
694            .find(|m| m.id == id)
695            .is_some_and(|m| !m.own_drag_armed_at(now))
696    }
697
698    /// The self-drag half of a dual-role member ripened and took the press:
699    /// flip the member's role to [`MemberRole::Gesture`] so
700    /// [`member_report`](Self::member_report) names the half that actually won
701    /// rather than the pan claim the node was also holding.
702    ///
703    /// A no-op — and reported as `false` — for a member with no self-drag, or
704    /// one whose self-drag has been withdrawn.
705    pub(crate) fn promote_own_drag(&mut self, id: WidgetId) -> bool {
706        let Some(member) = self
707            .members
708            .iter_mut()
709            .find(|m| m.id == id && m.has_own_drag && !m.own_drag_withdrawn)
710        else {
711            return false;
712        };
713        member.role = MemberRole::Gesture;
714        true
715    }
716
717    /// Every live member whose self-drag is **still waiting out its hold** at
718    /// `now` — deferred, not withdrawn, and not yet ripe — innermost first.
719    ///
720    /// The self-drag counterpart of `rejects_on_tap_slop`, and scoped three
721    /// ways:
722    ///
723    /// * only a **deferred** self-drag is swept. An `Immediate` one is armed
724    ///   from the press and is governed by its recognizer, exactly as an
725    ///   `Immediate` drag member is.
726    /// * only an **unripe** one. Once the hold has been served the grab is live,
727    ///   and a live grab travelling is the grab doing its job — withdrawing it
728    ///   then would make hold-then-drag impossible on any node small enough for
729    ///   a drag to leave its bounds.
730    /// * only a **live** member, because a rejected one competes for nothing.
731    ///
732    /// The sweep this feeds is what makes the deferral a *hold* rather than a
733    /// timer: see `WidgetTree::tick_sequence_timers`.
734    pub(crate) fn unripe_own_drag_members(&self, now: EventTime) -> Vec<WidgetId> {
735        self.members
736            .iter()
737            .filter(|m| {
738                m.is_live()
739                    && m.has_own_drag
740                    && !m.own_drag_withdrawn
741                    && m.own_drag_eligible_at.is_some_and(|at| now < at)
742            })
743            .map(|m| m.id)
744            .collect()
745    }
746
747    /// Record a per-press [`DragActivation`] for `id`, overriding the node's
748    /// build-time declaration for this press alone.
749    ///
750    /// Last writer wins: a handler that answers twice on one press means the
751    /// second answer.
752    pub fn set_drag_activation_override(&mut self, id: WidgetId, activation: DragActivation) {
753        if let Some(slot) = self
754            .drag_activation_overrides
755            .iter_mut()
756            .find(|(other, _)| *other == id)
757        {
758            slot.1 = activation;
759        } else {
760            self.drag_activation_overrides.push((id, activation));
761        }
762    }
763
764    /// The per-press [`DragActivation`] a handler chose for `id`, if one did.
765    pub fn drag_activation_override(&self, id: WidgetId) -> Option<DragActivation> {
766        self.drag_activation_overrides
767            .iter()
768            .find(|(other, _)| *other == id)
769            .map(|(_, activation)| *activation)
770    }
771
772    /// What [`DragActivation::Auto`] means for this sequence.
773    ///
774    /// `Immediate` for a precise pointer or a subtree that has declared
775    /// [`TouchAction::NONE`] (nothing else can want the press); `AfterLongPress`
776    /// for a coarse pointer with an eligible pan competitor, because the axis
777    /// is already spoken for.
778    pub fn resolve_activation(&self, activation: DragActivation) -> DragActivation {
779        match activation {
780            DragActivation::Auto => {
781                if !self.pointer.kind.is_direct() || self.touch_action.is_none() {
782                    DragActivation::Immediate
783                } else if self.has_eligible_pan() {
784                    DragActivation::AfterLongPress
785                } else {
786                    DragActivation::Immediate
787                }
788            }
789            other => other,
790        }
791    }
792
793    /// Whether **`id`'s own** grab on this press is waiting out the long-press
794    /// deadline — i.e. the hold is what arms *that node's* grab.
795    ///
796    /// Two deferrals answer to this, and both are set only when
797    /// [`resolve_activation`](Self::resolve_activation) answered
798    /// [`DragActivation::AfterLongPress`]:
799    ///
800    /// * a member deferred whole, by [`enrol_drag`](Self::enrol_drag) —
801    ///   `eligible_at`;
802    /// * the **self-drag** half of a dual-role member, by
803    ///   [`defer_own_drag`](Self::defer_own_drag) — `own_drag_eligible_at`. It
804    ///   has to count: the node's grab is armed by the same hold, so without
805    ///   this a `SceneView` with selection on would spend one hold on both its
806    ///   marquee and its own long press.
807    ///
808    /// # Why it is keyed on a node and not on the sequence
809    ///
810    /// "One hold cannot mean two things" is a statement about **one node**, not
811    /// about a press. A press reaches an ancestor chain, and a deferred grab
812    /// somewhere on it says nothing about what a hold means further in: a
813    /// `SceneView` that marquees after a hold is not thereby declaring that
814    /// every heavyweight widget inside it has given up its touch long press and
815    /// its touch context menu. A finger has no secondary button — the hold *is*
816    /// the context-menu route — so answering this sequence-wide silently
817    /// removed the only touch route to a context menu from every descendant of
818    /// any dual-role container, which is an accessibility loss and not a rule.
819    ///
820    /// The ancestor-wide door exists and is **explicit**:
821    /// `LongPressRole::DragHandle`, which `WidgetTree::long_press_is_a_grab`
822    /// walks from the queried node to the root. A container that really does
823    /// own every hold in its subtree says so there.
824    ///
825    /// A mouse never has one of these at all: both resolutions need an eligible
826    /// pan competitor and a mouse enrols none.
827    ///
828    /// Read by the framework through `WidgetTree::long_press_is_a_grab`.
829    pub fn has_deferred_grab_for(&self, id: WidgetId) -> bool {
830        self.members.iter().any(|m| {
831            m.id == id
832                && m.is_live()
833                && (m.eligible_at.is_some()
834                    || (m.has_own_drag
835                        && !m.own_drag_withdrawn
836                        && m.own_drag_eligible_at.is_some()))
837        })
838    }
839
840    /// Whether any live member is a pan claimant. A mouse never has one:
841    /// [`GestureProfile::pan_slop`] is `None` for it and [`PanClaim::devices`]
842    /// admits only direct pointers.
843    pub fn has_eligible_pan(&self) -> bool {
844        self.members
845            .iter()
846            .any(|m| m.is_live() && matches!(m.role, MemberRole::Pan(_)))
847    }
848
849    /// Whether `claim` is eligible for this sequence's pointer at all: the
850    /// claim must admit the device, the pointer's profile must have a pan slop,
851    /// and the frozen [`TouchAction`] must permit at least one claimed axis.
852    pub fn pan_is_eligible(&self, claim: &PanClaim, profile: &GestureProfile) -> bool {
853        if profile.pan_slop.is_none() {
854            return false;
855        }
856        if !claim.devices.contains(self.pointer.kind) {
857            return false;
858        }
859        [Axis::X, Axis::Y]
860            .into_iter()
861            .any(|axis| claim.axes.contains(axis) && self.touch_action.allows_pan(axis))
862    }
863
864    /// The axis a pan member of this sequence would win on, if its travel has
865    /// passed `pan_slop` on one the claim and the frozen action both permit.
866    ///
867    /// A diagonal tie resolves by **dominant axis** — the one that has moved
868    /// further — so a pan that is mostly vertical scrolls vertically even when
869    /// both axes are claimed.
870    pub fn pan_axis_past_slop(&self, claim: &PanClaim, profile: &GestureProfile) -> Option<Axis> {
871        let slop = profile.pan_slop?;
872        let mut candidates: Vec<(Axis, f32)> = [Axis::X, Axis::Y]
873            .into_iter()
874            .filter(|axis| claim.axes.contains(*axis) && self.touch_action.allows_pan(*axis))
875            .map(|axis| (axis, self.travel_on(axis)))
876            .filter(|(_, travel)| *travel >= slop)
877            .collect();
878        // Dominant axis first; ties keep X, which is the declaration order.
879        candidates.sort_by(|a, b| b.1.total_cmp(&a.1));
880        candidates.first().map(|(axis, _)| *axis)
881    }
882
883    /// Declare `id` the winner and reject every other live member.
884    ///
885    /// Returns the members that were knocked out, so the caller can cancel each
886    /// exactly once.
887    pub fn decide(&mut self, id: WidgetId) -> Vec<WidgetId> {
888        self.winner = Some(id);
889        let mut losers = Vec::new();
890        for member in &mut self.members {
891            if member.id == id {
892                member.state = MemberState::Won;
893            } else if member.is_live() {
894                member.state = MemberState::Rejected;
895                losers.push(member.id);
896            }
897        }
898        losers
899    }
900
901    /// Withdraw `id` from the running.
902    pub fn reject(&mut self, id: WidgetId) {
903        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
904            && member.is_live()
905        {
906            member.state = MemberState::Rejected;
907        }
908    }
909
910    /// Defer `id`'s decision until it releases or `profile.max_hold` elapses.
911    pub fn hold(&mut self, id: WidgetId, now: EventTime) {
912        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
913            && member.state == MemberState::Possible
914        {
915            member.state = MemberState::Held;
916            member.held_since = Some(now);
917        }
918    }
919
920    /// End `id`'s hold, putting it back in the running.
921    pub fn release_hold(&mut self, id: WidgetId) {
922        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
923            && member.state == MemberState::Held
924        {
925            member.state = MemberState::Possible;
926            member.held_since = None;
927        }
928    }
929
930    /// Release every hold older than `profile.max_hold`.
931    ///
932    /// A hold exists so an **application** recognizer can await an
933    /// asynchronous decision; leaving one standing would strand the press, so
934    /// the framework times it out rather than trusting the holder.
935    pub fn expire_holds(&mut self, now: EventTime, profile: &GestureProfile) {
936        for member in &mut self.members {
937            if member.state == MemberState::Held
938                && let Some(since) = member.held_since
939                && now.saturating_since(since) >= profile.max_hold
940            {
941                member.state = MemberState::Possible;
942                member.held_since = None;
943            }
944        }
945    }
946
947    /// Whether any member is holding.
948    pub fn is_held(&self) -> bool {
949        self.members.iter().any(|m| m.state == MemberState::Held)
950    }
951
952    /// When [`expire_holds`](Self::expire_holds) next has work: the earliest
953    /// instant at which a standing hold reaches `profile.max_hold`.
954    ///
955    /// **A deferred member's `eligible_at` is deliberately not a term here.**
956    /// It looks like a sibling deadline and is not one. Nothing happens at that
957    /// instant: eligibility is never *stored*, it is re-derived by
958    /// [`SequenceMember::is_eligible_at`] against whatever instant its caller
959    /// names, and no reader *transitions* anything on reaching it. Two call
960    /// sites read it — the arbitration walk, and the arena gate the ordinary
961    /// bubble and the timer tick share, the one naming the sample being
962    /// dispatched and the other the tick's own instant — and each of them only
963    /// answers a question its caller already had. A press that has sat
964    /// still past its `long_press` is already eligible the moment it moves,
965    /// with no intervening tick, so waking the event loop at `eligible_at`
966    /// would buy an idle frame with nothing to do in it. The expiry of a hold
967    /// is the opposite: it is a stored state transition, and if nobody performs
968    /// it the hold stands past the duration the framework promises to trust it
969    /// for.
970    pub fn next_hold_deadline(&self, profile: &GestureProfile) -> Option<EventTime> {
971        self.members
972            .iter()
973            .filter(|m| m.state == MemberState::Held)
974            .filter_map(|m| m.held_since.map(|since| since + profile.max_hold))
975            .min()
976    }
977
978    /// Drop every member whose node is no longer active, reporting them so the
979    /// caller can cancel each individually.
980    ///
981    /// Run every sample: a rebuild mints fresh ids, and a member left pointing
982    /// at a destroyed node would either be fed events forever or silently win.
983    /// The *sequence* dies only when the winner or the captor dies — see
984    /// [`lost_owner`](Self::lost_owner).
985    pub fn revalidate(&mut self, arena: &crate::arena::WidgetArena) -> Vec<WidgetId> {
986        let mut dead = Vec::new();
987        self.members.retain(|member| {
988            if arena.is_active(member.id) {
989                true
990            } else {
991                dead.push(member.id);
992                false
993            }
994        });
995        dead
996    }
997
998    /// Whether the node that owns this sequence — its winner, or failing that
999    /// its captor — has gone away. The sequence itself must then be cancelled.
1000    pub fn lost_owner(&self, arena: &crate::arena::WidgetArena) -> bool {
1001        let owner = self.winner.or(self.capture);
1002        owner.is_some_and(|id| !arena.is_active(id))
1003    }
1004
1005    /// The role and state of every member, for
1006    /// [`WidgetTree::sequence_members`](crate::WidgetTree::sequence_members).
1007    pub fn member_report(&self) -> Vec<(WidgetId, MemberRole, MemberState)> {
1008        self.members
1009            .iter()
1010            .map(|m| (m.id, m.role, m.state))
1011            .collect()
1012    }
1013
1014    /// Every live member of one role, innermost first.
1015    pub(crate) fn live_ids_with<F: Fn(&MemberRole) -> bool>(&self, filter: F) -> Vec<WidgetId> {
1016        self.members
1017            .iter()
1018            .filter(|m| m.is_live() && filter(&m.role))
1019            .map(|m| m.id)
1020            .collect()
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027    use crate::pointer::{BackendDeviceKey, PointerIdAllocator};
1028    use crate::widget_id::WidgetId;
1029    use slotmap::KeyData;
1030    use teksilo_tokens::{PointerKind, TargetDensity};
1031
1032    fn tokens() -> teksilo_tokens::InputTokens {
1033        teksilo_tokens::InputTokens::for_density(TargetDensity::Compact)
1034    }
1035
1036    fn mouse() -> PointerInfo {
1037        PointerInfo::mouse(EventTime::ZERO)
1038    }
1039
1040    fn finger() -> PointerInfo {
1041        let id = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 7);
1042        PointerInfo::touch(id, EventTime::ZERO)
1043    }
1044
1045    fn seq(pointer: PointerInfo, action: TouchAction, path: Vec<WidgetId>) -> PointerSequence {
1046        PointerSequence::new(pointer, path, action, None, Point::ZERO, EventTime::ZERO)
1047    }
1048
1049    /// Synthetic ids for the pure-logic tests: the sequence only ever compares
1050    /// and orders them, so no arena is needed to make them meaningful.
1051    fn ids(n: u64) -> Vec<WidgetId> {
1052        (0..n)
1053            .map(|i| KeyData::from_ffi((1u64 << 32) | (i + 1)).into())
1054            .collect()
1055    }
1056
1057    #[test]
1058    fn a_mouse_latches_at_five_in_every_configuration() {
1059        // The single most important invariant in the package: no frozen
1060        // TouchAction, and no density, may retune the mouse drag latch.
1061        let tokens = tokens();
1062        let profile = tokens.profile(PointerKind::Mouse);
1063        for action in [
1064            TouchAction::AUTO,
1065            TouchAction::NONE,
1066            TouchAction::PAN,
1067            TouchAction::PAN_X,
1068            TouchAction::PAN_Y,
1069            TouchAction::PINCH_ZOOM,
1070            TouchAction::MANIPULATION,
1071        ] {
1072            let s = seq(mouse(), action, ids(1));
1073            assert_eq!(
1074                s.latch_slop(profile),
1075                5.0,
1076                "a mouse under {action:?} must latch at 5.0"
1077            );
1078        }
1079    }
1080
1081    #[test]
1082    fn slop_precise_reaches_only_a_direct_pointer_under_a_frozen_none() {
1083        let tokens = tokens();
1084        let touch_profile = tokens.profile(PointerKind::Touch);
1085        let none = seq(finger(), TouchAction::NONE, ids(1));
1086        assert_eq!(none.latch_slop(touch_profile), touch_profile.slop_precise);
1087        let auto = seq(finger(), TouchAction::AUTO, ids(1));
1088        assert_eq!(auto.latch_slop(touch_profile), touch_profile.drag_slop);
1089    }
1090
1091    #[test]
1092    fn a_mouse_never_has_an_eligible_pan_member() {
1093        let tokens = tokens();
1094        let profile = tokens.profile(PointerKind::Mouse);
1095        let s = seq(mouse(), TouchAction::AUTO, ids(1));
1096        assert!(!s.pan_is_eligible(&PanClaim::both(), profile));
1097    }
1098
1099    #[test]
1100    fn members_stay_innermost_first_whatever_order_they_enrol_in() {
1101        let path = ids(4);
1102        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1103        assert!(s.enrol(path[3], MemberRole::Gesture));
1104        assert!(s.enrol(path[1], MemberRole::RawDrag));
1105        assert!(s.enrol(path[2], MemberRole::Gesture));
1106        let order: Vec<_> = s.members().iter().map(|m| m.id).collect();
1107        assert_eq!(order, vec![path[1], path[2], path[3]]);
1108    }
1109
1110    #[test]
1111    fn the_dead_zone_boundary_refuses_everything_at_or_above_it() {
1112        let path = ids(4);
1113        let mut s = PointerSequence::new(
1114            mouse(),
1115            path.clone(),
1116            TouchAction::AUTO,
1117            Some(path[2]),
1118            Point::ZERO,
1119            EventTime::ZERO,
1120        );
1121        assert!(s.enrol(path[1], MemberRole::Gesture), "below the boundary");
1122        assert!(
1123            !s.enrol(path[2], MemberRole::Gesture),
1124            "the boundary itself"
1125        );
1126        assert!(!s.enrol(path[3], MemberRole::Gesture), "above the boundary");
1127    }
1128
1129    #[test]
1130    fn deciding_rejects_every_other_live_member_exactly_once() {
1131        let path = ids(3);
1132        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1133        s.enrol(path[0], MemberRole::Gesture);
1134        s.enrol(path[1], MemberRole::Gesture);
1135        s.enrol(path[2], MemberRole::Gesture);
1136        let losers = s.decide(path[1]);
1137        assert_eq!(losers, vec![path[0], path[2]]);
1138        assert_eq!(s.winner(), Some(path[1]));
1139        // A second decide reports nothing new: the losers are no longer live.
1140        assert!(s.decide(path[1]).is_empty());
1141    }
1142
1143    #[test]
1144    fn after_long_press_defers_eligibility_and_arms_self_rejection() {
1145        let tokens = tokens();
1146        let profile = tokens.profile(PointerKind::Touch);
1147        let path = ids(2);
1148        let mut s = seq(finger(), TouchAction::PAN_Y, path.clone());
1149        s.enrol_drag(
1150            path[0],
1151            MemberRole::Gesture,
1152            DragActivation::AfterLongPress,
1153            profile,
1154        );
1155        let member = s.members()[0];
1156        assert_eq!(
1157            member.eligible_at,
1158            Some(EventTime::ZERO + profile.long_press)
1159        );
1160        assert!(member.rejects_on_tap_slop);
1161        assert!(!member.is_eligible_at(EventTime::ZERO));
1162        assert!(member.is_eligible_at(EventTime::ZERO + profile.long_press));
1163    }
1164
1165    #[test]
1166    fn auto_activation_defers_only_a_coarse_pointer_facing_a_pan() {
1167        let path = ids(2);
1168
1169        // A mouse is always immediate.
1170        let mut m = seq(mouse(), TouchAction::AUTO, path.clone());
1171        m.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
1172        assert_eq!(
1173            m.resolve_activation(DragActivation::Auto),
1174            DragActivation::Immediate
1175        );
1176
1177        // A finger with no pan competitor is immediate too.
1178        let bare = seq(finger(), TouchAction::AUTO, path.clone());
1179        assert_eq!(
1180            bare.resolve_activation(DragActivation::Auto),
1181            DragActivation::Immediate
1182        );
1183
1184        // A finger facing a pan claimant defers.
1185        let mut contested = seq(finger(), TouchAction::AUTO, path.clone());
1186        contested.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
1187        assert_eq!(
1188            contested.resolve_activation(DragActivation::Auto),
1189            DragActivation::AfterLongPress
1190        );
1191    }
1192
1193    #[test]
1194    fn a_pan_wins_on_the_dominant_axis_and_only_where_permitted() {
1195        let tokens = tokens();
1196        let profile = tokens.profile(PointerKind::Touch);
1197        let slop = profile.pan_slop.expect("touch pans");
1198        let path = ids(1);
1199
1200        let mut s = seq(finger(), TouchAction::PAN, path);
1201        s.set_last_position(Point::new(slop + 10.0, slop + 1.0));
1202        assert_eq!(
1203            s.pan_axis_past_slop(&PanClaim::both(), profile),
1204            Some(Axis::X),
1205            "the axis that travelled further wins the diagonal"
1206        );
1207
1208        // The frozen action forbids X, so the same travel resolves to Y.
1209        let mut only_y = seq(finger(), TouchAction::PAN_Y, ids(1));
1210        only_y.set_last_position(Point::new(slop + 10.0, slop + 1.0));
1211        assert_eq!(
1212            only_y.pan_axis_past_slop(&PanClaim::both(), profile),
1213            Some(Axis::Y)
1214        );
1215    }
1216
1217    #[test]
1218    fn a_hold_expires_at_max_hold_and_not_before() {
1219        let tokens = tokens();
1220        let profile = tokens.profile(PointerKind::Mouse);
1221        let path = ids(1);
1222        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1223        s.enrol(path[0], MemberRole::Gesture);
1224        s.hold(path[0], EventTime::ZERO);
1225        assert!(s.is_held());
1226
1227        s.expire_holds(EventTime::from_duration(profile.max_hold / 2), profile);
1228        assert!(s.is_held(), "a hold survives until max_hold");
1229
1230        s.expire_holds(EventTime::from_duration(profile.max_hold), profile);
1231        assert!(!s.is_held(), "and is released at it");
1232        assert_eq!(s.members()[0].state, MemberState::Possible);
1233    }
1234
1235    #[test]
1236    fn revalidate_drops_dead_members_one_at_a_time() {
1237        // The tree-level half — losing the captor cancels the whole sequence —
1238        // is pinned in `gesture_dispatch_impl`; in a real tree a member is
1239        // always an ancestor of the captor and so cannot die on its own, which
1240        // is why the per-member rule is asserted here.
1241        let mut arena = crate::arena::WidgetArena::new();
1242        let live = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
1243        let doomed = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
1244        let mut s = seq(mouse(), TouchAction::AUTO, vec![doomed, live]);
1245        s.enrol(doomed, MemberRole::Gesture);
1246        s.enrol(live, MemberRole::Gesture);
1247        s.set_capture(Some(live));
1248
1249        assert!(s.revalidate(&arena).is_empty(), "nothing has died yet");
1250        arena.destroy(doomed);
1251
1252        assert_eq!(s.revalidate(&arena), vec![doomed]);
1253        assert_eq!(
1254            s.members().iter().map(|m| m.id).collect::<Vec<_>>(),
1255            vec![live],
1256            "only the dead member is dropped"
1257        );
1258        assert!(!s.lost_owner(&arena), "the captor is still alive");
1259
1260        arena.destroy(live);
1261        assert!(
1262            s.lost_owner(&arena),
1263            "losing the captor is what cancels the sequence"
1264        );
1265    }
1266
1267    #[test]
1268    fn the_tap_boundary_is_a_radius_for_a_mouse_and_bounds_for_a_finger() {
1269        let tokens = tokens();
1270        let mouse_profile = tokens.profile(PointerKind::Mouse);
1271        let touch_profile = tokens.profile(PointerKind::Touch);
1272        assert_eq!(
1273            TapBoundary::for_pointer(&mouse(), mouse_profile),
1274            TapBoundary::Radius(mouse_profile.tap_slop)
1275        );
1276        assert_eq!(
1277            TapBoundary::for_pointer(&finger(), touch_profile),
1278            TapBoundary::Bounds
1279        );
1280
1281        // A coarse press well past tap_slop but still inside the control has
1282        // NOT left the boundary — that is the whole point of `Bounds`.
1283        let bounds = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 100.0);
1284        assert!(!TapBoundary::Bounds.left(
1285            Point::new(50.0, 50.0),
1286            Point::new(50.0, 80.0),
1287            Some(bounds),
1288            touch_profile,
1289        ));
1290        assert!(TapBoundary::Bounds.left(
1291            Point::new(50.0, 50.0),
1292            Point::new(50.0, 120.0),
1293            Some(bounds),
1294            touch_profile,
1295        ));
1296        // With no bounds to test, it falls back to the radius rather than
1297        // letting the press travel anywhere.
1298        assert!(TapBoundary::Bounds.left(
1299            Point::new(50.0, 50.0),
1300            Point::new(50.0, 80.0),
1301            None,
1302            touch_profile,
1303        ));
1304    }
1305
1306    /// A press accepted through a `Widget::hit_outset` begins outside the node
1307    /// it was accepted for, so the node's rectangle cannot be its boundary:
1308    /// with `Bounds` taken literally the press is "already gone" on arrival and
1309    /// the tap can never complete. It falls back to the pointer's own radius
1310    /// around where it landed, and sliding onto the control keeps it alive.
1311    #[test]
1312    fn a_press_that_began_outside_the_node_is_bounded_by_its_own_radius() {
1313        let tokens = tokens();
1314        let touch_profile = tokens.profile(PointerKind::Touch);
1315        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 12.0, 12.0);
1316        // Landed 4 dp past the trailing edge — inside the outset ring the
1317        // arena offered it, outside the rectangle.
1318        let origin = Point::new(16.0, 6.0);
1319        assert!(
1320            !TapBoundary::Bounds.left(origin, origin, Some(rect), touch_profile),
1321            "a press cannot have left the boundary on the sample that opened it",
1322        );
1323        assert!(
1324            !TapBoundary::Bounds.left(origin, Point::new(6.0, 6.0), Some(rect), touch_profile),
1325            "sliding onto the control keeps the press",
1326        );
1327        assert!(
1328            TapBoundary::Bounds.left(
1329                origin,
1330                Point::new(16.0 + touch_profile.tap_slop + 1.0, 6.0),
1331                Some(rect),
1332                touch_profile,
1333            ),
1334            "and past the radius it is gone, so the abort gesture still works",
1335        );
1336    }
1337
1338    /// The union term, on its own.
1339    ///
1340    /// The radius half of the outside-origin rule is a *travel* allowance, and
1341    /// on a small control it runs out before the finger has finished arriving:
1342    /// a contact that lands in the outset ring of a wide control and then
1343    /// slides well past `tap_slop` **onto** the control is further from its
1344    /// origin than the radius permits and squarely inside the rectangle. Only
1345    /// the union with the node's bounds keeps that press alive; with the
1346    /// `!rect.contains(position)` term gone, the radius alone kills a press
1347    /// that is sitting on the middle of the thing it is pressing.
1348    #[test]
1349    fn sliding_onto_the_control_keeps_a_press_the_radius_alone_would_lose() {
1350        let tokens = tokens();
1351        let touch_profile = tokens.profile(PointerKind::Touch);
1352        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
1353        // 4 dp past the trailing edge — inside the outset ring, outside the rect.
1354        let origin = Point::new(104.0, 10.0);
1355        // 24 dp of travel, against a Touch `tap_slop` of 18: past the radius,
1356        // and 20 dp inside the control.
1357        let onto = Point::new(80.0, 10.0);
1358        assert!(
1359            super::super::distance(origin, onto) > touch_profile.tap_slop,
1360            "the probe is only discriminating while the travel exceeds tap_slop",
1361        );
1362        assert!(rect.contains(onto), "…and lands inside the control");
1363        assert!(
1364            !TapBoundary::Bounds.left(origin, onto, Some(rect), touch_profile),
1365            "a finger resting on the control it pressed has not left it",
1366        );
1367    }
1368
1369    /// Which rule applies is decided by the **origin**, not by where the
1370    /// pointer is now.
1371    ///
1372    /// The two questions agree on most samples, which is why the distinction
1373    /// has to be pinned on the one geometry where they cannot: a press that
1374    /// began *inside* the node and has moved a short way outside it. The rule
1375    /// for that press is the rectangle — it left the moment it crossed the
1376    /// edge, however little it travelled — while a press that began outside is
1377    /// allowed the pointer's radius around where it landed, so with the same
1378    /// `position` it has not left at all. Reading `position` instead of
1379    /// `origin` collapses both onto the second answer and silently hands every
1380    /// coarse press that starts inside a control a `tap_slop` grace band
1381    /// outside it, which is exactly the slop `Bounds` exists to replace.
1382    #[test]
1383    fn the_boundary_rule_is_chosen_by_where_the_press_began() {
1384        let tokens = tokens();
1385        let touch_profile = tokens.profile(PointerKind::Touch);
1386        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
1387        // One sample, 4 dp past the trailing edge, reached from two origins —
1388        // both within `tap_slop` of it, so the radius rule cannot fail either.
1389        let position = Point::new(104.0, 10.0);
1390        let from_inside = Point::new(96.0, 10.0);
1391        let from_outside = Point::new(108.0, 10.0);
1392        assert!(rect.contains(from_inside), "the first press began inside");
1393        assert!(
1394            !rect.contains(from_outside) && !rect.contains(position),
1395            "the second began outside, and neither sample is in the rect",
1396        );
1397        for origin in [from_inside, from_outside] {
1398            assert!(
1399                super::super::distance(origin, position) < touch_profile.tap_slop,
1400                "the probe only discriminates while the travel is inside tap_slop",
1401            );
1402        }
1403
1404        assert!(
1405            TapBoundary::Bounds.left(from_inside, position, Some(rect), touch_profile),
1406            "a press that began inside the node is bounded by the node: crossing \
1407             the edge ends it, with no radius grace outside",
1408        );
1409        assert!(
1410            !TapBoundary::Bounds.left(from_outside, position, Some(rect), touch_profile),
1411            "a press that began outside is bounded by its own radius, and this \
1412             one has barely moved",
1413        );
1414    }
1415
1416    // -----------------------------------------------------------------
1417    // The self-drag half of a dual-role member
1418    // -----------------------------------------------------------------
1419
1420    /// `defer_own_drag` refuses anything that is not a live `Pan` member — the
1421    /// structural reason a mouse can never reach it, since a mouse enrols no
1422    /// pan member at all.
1423    #[test]
1424    fn defer_own_drag_refuses_anything_but_a_live_pan_member() {
1425        let tokens = tokens();
1426        let profile = tokens.profile(PointerKind::Touch);
1427        let ids = ids(3);
1428
1429        // A `Gesture` member: the node's drag already has the slot, so there is
1430        // no second half to defer.
1431        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1432        assert!(s.enrol(ids[0], MemberRole::Gesture));
1433        assert!(
1434            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1435            "a Gesture member is not dual-role"
1436        );
1437
1438        // A node that is not a member at all.
1439        assert!(
1440            !s.defer_own_drag(ids[1], DragActivation::Auto, profile),
1441            "a non-member has nothing to attach a deferral to"
1442        );
1443
1444        // A rejected pan member.
1445        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1446        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1447        s.reject(ids[0]);
1448        assert!(
1449            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1450            "a member that is out of the running gets no second half"
1451        );
1452
1453        // A decided sequence.
1454        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1455        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1456        s.decide(ids[0]);
1457        assert!(
1458            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1459            "arbitration is over"
1460        );
1461
1462        // …and the one shape that is accepted.
1463        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1464        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1465        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1466    }
1467
1468    /// A mouse sequence never has a pan member, so the shape `defer_own_drag`
1469    /// exists for cannot arise. Stated on the object rather than through a
1470    /// tree, so it is a property of the type and not of one fixture.
1471    #[test]
1472    fn a_mouse_can_never_defer_its_own_drag() {
1473        let tokens = tokens();
1474        let profile = tokens.profile(PointerKind::Mouse);
1475        let ids = ids(1);
1476        let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
1477
1478        // `pan_is_eligible` is what `begin_sequence` gates the enrolment on, and
1479        // for a mouse it is false whatever the claim asks for — so no `Pan`
1480        // member is ever created and the arm has nothing to attach to.
1481        assert!(
1482            !s.pan_is_eligible(&PanClaim::both(), profile),
1483            "the mouse profile has no pan_slop, so no claim is eligible"
1484        );
1485        assert!(!s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1486        assert!(!s.has_deferred_grab_for(ids[0]));
1487    }
1488
1489    /// `Auto` on a direct pointer with an eligible pan defers the self-drag to
1490    /// the long-press deadline; `Immediate` arms it at the press.
1491    #[test]
1492    fn defer_own_drag_resolves_auto_the_way_every_other_drag_resolves_it() {
1493        let tokens = tokens();
1494        let profile = tokens.profile(PointerKind::Touch);
1495        let ids = ids(1);
1496
1497        let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
1498        assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1499        assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
1500        assert!(
1501            deferred.own_drag_blocked(ids[0], EventTime::ZERO),
1502            "Auto + an eligible pan means a hold"
1503        );
1504        assert!(
1505            !deferred.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press),
1506            "…and the hold ends at long_press"
1507        );
1508        assert!(
1509            deferred.has_deferred_grab_for(ids[0]),
1510            "so the hold is spent on the grab and cannot also be a long press"
1511        );
1512
1513        let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
1514        assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1515        assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1516        assert!(
1517            !immediate.own_drag_blocked(ids[0], EventTime::ZERO),
1518            "Immediate arms at the press"
1519        );
1520        assert!(
1521            !immediate.has_deferred_grab_for(ids[0]),
1522            "and spends no hold, so a long press on the same node still fires"
1523        );
1524    }
1525
1526    /// A withdrawal is permanent for the press. That is what keeps a deferral
1527    /// ripening mid-pan from starting a grab under a scrolling finger.
1528    #[test]
1529    fn a_withdrawn_self_drag_stays_blocked_past_its_own_deadline() {
1530        let tokens = tokens();
1531        let profile = tokens.profile(PointerKind::Touch);
1532        let ids = ids(1);
1533        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1534        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1535        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1536
1537        s.withdraw_own_drag(ids[0]);
1538        assert!(
1539            s.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press * 10),
1540            "a withdrawn self-drag does not come back when its timer ripens"
1541        );
1542        assert!(
1543            !s.has_deferred_grab_for(ids[0]),
1544            "and stops spending the hold, so the node's long press is free again"
1545        );
1546    }
1547
1548    /// `promote_own_drag` is what makes the member report name the half that
1549    /// actually won, and it is a no-op on anything else.
1550    #[test]
1551    fn promote_own_drag_renames_the_half_that_won() {
1552        let tokens = tokens();
1553        let profile = tokens.profile(PointerKind::Touch);
1554        let ids = ids(1);
1555
1556        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1557        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1558        assert!(s.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1559        assert!(matches!(s.members()[0].role, MemberRole::Pan(_)));
1560        assert!(s.promote_own_drag(ids[0]));
1561        assert_eq!(s.members()[0].role, MemberRole::Gesture);
1562
1563        // A plain claimant is untouched.
1564        let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
1565        assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1566        assert!(!plain.promote_own_drag(ids[0]));
1567        assert!(matches!(plain.members()[0].role, MemberRole::Pan(_)));
1568
1569        // …and so is one whose self-drag has been withdrawn: the pan won, and
1570        // renaming the member would make the report say otherwise.
1571        let mut withdrawn = seq(finger(), TouchAction::AUTO, ids.clone());
1572        assert!(withdrawn.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1573        assert!(withdrawn.defer_own_drag(ids[0], DragActivation::Auto, profile));
1574        withdrawn.withdraw_own_drag(ids[0]);
1575        assert!(!withdrawn.promote_own_drag(ids[0]));
1576        assert!(matches!(withdrawn.members()[0].role, MemberRole::Pan(_)));
1577    }
1578
1579    /// Only a **deferred** self-drag answers the positional sweep, mirroring
1580    /// `rejects_on_tap_slop`: an `Immediate` one is governed by its recognizer.
1581    #[test]
1582    fn only_a_deferred_self_drag_is_swept_positionally() {
1583        let tokens = tokens();
1584        let profile = tokens.profile(PointerKind::Touch);
1585        let ids = ids(1);
1586
1587        let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
1588        assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1589        assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
1590        assert_eq!(
1591            deferred.unripe_own_drag_members(EventTime::ZERO),
1592            vec![ids[0]]
1593        );
1594
1595        let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
1596        assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1597        assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1598        assert!(
1599            immediate
1600                .unripe_own_drag_members(EventTime::ZERO)
1601                .is_empty()
1602        );
1603
1604        // A plain claimant never appears.
1605        let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
1606        assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1607        assert!(plain.unripe_own_drag_members(EventTime::ZERO).is_empty());
1608    }
1609
1610    /// …and only while it is still **unripe**. Once the hold has been served the
1611    /// grab is live, and a live grab travelling is the grab doing its job: a
1612    /// sweep that still fired then would make hold-then-drag impossible on any
1613    /// node small enough for the drag to leave its bounds.
1614    #[test]
1615    fn a_ripe_self_drag_is_no_longer_swept() {
1616        let tokens = tokens();
1617        let profile = tokens.profile(PointerKind::Touch);
1618        let ids = ids(1);
1619
1620        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1621        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1622        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1623
1624        let deadline = EventTime::ZERO + profile.long_press;
1625        assert_eq!(
1626            s.unripe_own_drag_members(
1627                EventTime::ZERO + (profile.long_press - std::time::Duration::from_millis(1)),
1628            ),
1629            vec![ids[0]],
1630            "still inside the hold"
1631        );
1632        assert!(
1633            s.unripe_own_drag_members(deadline).is_empty(),
1634            "the hold has been served; the grab is live and answers to its own \
1635             recognizer from here"
1636        );
1637    }
1638
1639    /// `own_drag_armed_at` is inert for every member carrying no self-drag,
1640    /// which is what makes the gate it feeds free for every other sequence.
1641    #[test]
1642    fn own_drag_armed_is_true_for_a_member_with_no_self_drag() {
1643        let ids = ids(1);
1644        let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
1645        assert!(s.enrol(ids[0], MemberRole::Gesture));
1646        for at in [
1647            EventTime::ZERO,
1648            EventTime::ZERO + std::time::Duration::from_secs(10),
1649        ] {
1650            assert!(s.members()[0].own_drag_armed_at(at));
1651            assert!(!s.own_drag_blocked(ids[0], at));
1652        }
1653    }
1654
1655    /// The per-press activation override is recorded per node,
1656    /// last-writer-wins, and answers `None` for a node that never spoke.
1657    #[test]
1658    fn a_drag_activation_override_is_recorded_per_node() {
1659        let ids = ids(2);
1660        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1661        assert_eq!(s.drag_activation_override(ids[0]), None);
1662
1663        s.set_drag_activation_override(ids[0], DragActivation::Immediate);
1664        s.set_drag_activation_override(ids[1], DragActivation::AfterLongPress);
1665        assert_eq!(
1666            s.drag_activation_override(ids[0]),
1667            Some(DragActivation::Immediate)
1668        );
1669        assert_eq!(
1670            s.drag_activation_override(ids[1]),
1671            Some(DragActivation::AfterLongPress)
1672        );
1673
1674        s.set_drag_activation_override(ids[0], DragActivation::Auto);
1675        assert_eq!(
1676            s.drag_activation_override(ids[0]),
1677            Some(DragActivation::Auto),
1678            "answering twice on one press means the second answer"
1679        );
1680    }
1681}