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 clears the framework press visual. A coarse pointer uses `Bounds` 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 reaches only the first, and only by an **explicit** declaration:
826    /// `Auto` resolves to `AfterLongPress` for a direct pointer with an eligible
827    /// pan competitor and a mouse enrols none, but an explicitly declared
828    /// `AfterLongPress` is passed through untouched for every pointer kind — see
829    /// `an_explicitly_deferred_grab_takes_the_hold_from_every_pointer_kind`. The
830    /// `defer_own_drag` half stays out of a mouse's reach either way: it needs a
831    /// live [`MemberRole::Pan`] member.
832    ///
833    /// Read by the framework through `WidgetTree::long_press_is_a_grab`.
834    pub fn has_deferred_grab_for(&self, id: WidgetId) -> bool {
835        self.members.iter().any(|m| {
836            m.id == id
837                && m.is_live()
838                && (m.eligible_at.is_some()
839                    || (m.has_own_drag
840                        && !m.own_drag_withdrawn
841                        && m.own_drag_eligible_at.is_some()))
842        })
843    }
844
845    /// Whether any live member is a pan claimant. A mouse never has one:
846    /// [`GestureProfile::pan_slop`] is `None` for it and [`PanClaim::devices`]
847    /// admits only direct pointers.
848    pub fn has_eligible_pan(&self) -> bool {
849        self.members
850            .iter()
851            .any(|m| m.is_live() && matches!(m.role, MemberRole::Pan(_)))
852    }
853
854    /// Whether `claim` is eligible for this sequence's pointer at all: the
855    /// claim must admit the device, the pointer's profile must have a pan slop,
856    /// and the frozen [`TouchAction`] must permit at least one claimed axis.
857    pub fn pan_is_eligible(&self, claim: &PanClaim, profile: &GestureProfile) -> bool {
858        if profile.pan_slop.is_none() {
859            return false;
860        }
861        if !claim.devices.contains(self.pointer.kind) {
862            return false;
863        }
864        [Axis::X, Axis::Y]
865            .into_iter()
866            .any(|axis| claim.axes.contains(axis) && self.touch_action.allows_pan(axis))
867    }
868
869    /// The axis a pan member of this sequence would win on, if its travel has
870    /// passed `pan_slop` on one the claim and the frozen action both permit.
871    ///
872    /// A diagonal tie resolves by **dominant axis** — the one that has moved
873    /// further — so a pan that is mostly vertical scrolls vertically even when
874    /// both axes are claimed.
875    pub fn pan_axis_past_slop(&self, claim: &PanClaim, profile: &GestureProfile) -> Option<Axis> {
876        let slop = profile.pan_slop?;
877        let mut candidates: Vec<(Axis, f32)> = [Axis::X, Axis::Y]
878            .into_iter()
879            .filter(|axis| claim.axes.contains(*axis) && self.touch_action.allows_pan(*axis))
880            .map(|axis| (axis, self.travel_on(axis)))
881            .filter(|(_, travel)| *travel >= slop)
882            .collect();
883        // Dominant axis first; ties keep X, which is the declaration order.
884        candidates.sort_by(|a, b| b.1.total_cmp(&a.1));
885        candidates.first().map(|(axis, _)| *axis)
886    }
887
888    /// Declare `id` the winner and reject every other live member.
889    ///
890    /// Returns the members that were knocked out, so the caller can cancel each
891    /// exactly once.
892    pub fn decide(&mut self, id: WidgetId) -> Vec<WidgetId> {
893        self.winner = Some(id);
894        let mut losers = Vec::new();
895        for member in &mut self.members {
896            if member.id == id {
897                member.state = MemberState::Won;
898            } else if member.is_live() {
899                member.state = MemberState::Rejected;
900                losers.push(member.id);
901            }
902        }
903        losers
904    }
905
906    /// Withdraw `id` from the running.
907    pub fn reject(&mut self, id: WidgetId) {
908        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
909            && member.is_live()
910        {
911            member.state = MemberState::Rejected;
912        }
913    }
914
915    /// Defer `id`'s decision until it releases or `profile.max_hold` elapses.
916    pub fn hold(&mut self, id: WidgetId, now: EventTime) {
917        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
918            && member.state == MemberState::Possible
919        {
920            member.state = MemberState::Held;
921            member.held_since = Some(now);
922        }
923    }
924
925    /// End `id`'s hold, putting it back in the running.
926    pub fn release_hold(&mut self, id: WidgetId) {
927        if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
928            && member.state == MemberState::Held
929        {
930            member.state = MemberState::Possible;
931            member.held_since = None;
932        }
933    }
934
935    /// Release every hold older than `profile.max_hold`.
936    ///
937    /// A hold exists so an **application** recognizer can await an
938    /// asynchronous decision; leaving one standing would strand the press, so
939    /// the framework times it out rather than trusting the holder.
940    pub fn expire_holds(&mut self, now: EventTime, profile: &GestureProfile) {
941        for member in &mut self.members {
942            if member.state == MemberState::Held
943                && let Some(since) = member.held_since
944                && now.saturating_since(since) >= profile.max_hold
945            {
946                member.state = MemberState::Possible;
947                member.held_since = None;
948            }
949        }
950    }
951
952    /// Whether any member is holding.
953    pub fn is_held(&self) -> bool {
954        self.members.iter().any(|m| m.state == MemberState::Held)
955    }
956
957    /// When [`expire_holds`](Self::expire_holds) next has work: the earliest
958    /// instant at which a standing hold reaches `profile.max_hold`.
959    ///
960    /// **A deferred member's `eligible_at` is deliberately not a term here.**
961    /// It looks like a sibling deadline and is not one. Nothing happens at that
962    /// instant: eligibility is never *stored*, it is re-derived by
963    /// [`SequenceMember::is_eligible_at`] against whatever instant its caller
964    /// names, and no reader *transitions* anything on reaching it. Two call
965    /// sites read it — the arbitration walk, and the arena gate the ordinary
966    /// bubble and the timer tick share, the one naming the sample being
967    /// dispatched and the other the tick's own instant — and each of them only
968    /// answers a question its caller already had. A press that has sat
969    /// still past its `long_press` is already eligible the moment it moves,
970    /// with no intervening tick, so waking the event loop at `eligible_at`
971    /// would buy an idle frame with nothing to do in it. The expiry of a hold
972    /// is the opposite: it is a stored state transition, and if nobody performs
973    /// it the hold stands past the duration the framework promises to trust it
974    /// for.
975    pub fn next_hold_deadline(&self, profile: &GestureProfile) -> Option<EventTime> {
976        self.members
977            .iter()
978            .filter(|m| m.state == MemberState::Held)
979            .filter_map(|m| m.held_since.map(|since| since + profile.max_hold))
980            .min()
981    }
982
983    /// Drop every member whose node is no longer active, reporting them so the
984    /// caller can cancel each individually.
985    ///
986    /// Run every sample: a rebuild mints fresh ids, and a member left pointing
987    /// at a destroyed node would either be fed events forever or silently win.
988    /// The *sequence* dies only when the winner or the captor dies — see
989    /// [`lost_owner`](Self::lost_owner).
990    pub fn revalidate(&mut self, arena: &crate::arena::WidgetArena) -> Vec<WidgetId> {
991        let mut dead = Vec::new();
992        self.members.retain(|member| {
993            if arena.is_active(member.id) {
994                true
995            } else {
996                dead.push(member.id);
997                false
998            }
999        });
1000        dead
1001    }
1002
1003    /// Whether the node that owns this sequence — its winner, or failing that
1004    /// its captor — has gone away. The sequence itself must then be cancelled.
1005    pub fn lost_owner(&self, arena: &crate::arena::WidgetArena) -> bool {
1006        let owner = self.winner.or(self.capture);
1007        owner.is_some_and(|id| !arena.is_active(id))
1008    }
1009
1010    /// The role and state of every member, for
1011    /// [`WidgetTree::sequence_members`](crate::WidgetTree::sequence_members).
1012    pub fn member_report(&self) -> Vec<(WidgetId, MemberRole, MemberState)> {
1013        self.members
1014            .iter()
1015            .map(|m| (m.id, m.role, m.state))
1016            .collect()
1017    }
1018
1019    /// Every live member of one role, innermost first.
1020    pub(crate) fn live_ids_with<F: Fn(&MemberRole) -> bool>(&self, filter: F) -> Vec<WidgetId> {
1021        self.members
1022            .iter()
1023            .filter(|m| m.is_live() && filter(&m.role))
1024            .map(|m| m.id)
1025            .collect()
1026    }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032    use crate::pointer::{BackendDeviceKey, PointerIdAllocator};
1033    use crate::widget_id::WidgetId;
1034    use slotmap::KeyData;
1035    use teksilo_tokens::{PointerKind, TargetDensity};
1036
1037    fn tokens() -> teksilo_tokens::InputTokens {
1038        teksilo_tokens::InputTokens::for_density(TargetDensity::Compact)
1039    }
1040
1041    fn mouse() -> PointerInfo {
1042        PointerInfo::mouse(EventTime::ZERO)
1043    }
1044
1045    fn finger() -> PointerInfo {
1046        let id = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 7);
1047        PointerInfo::touch(id, EventTime::ZERO)
1048    }
1049
1050    fn seq(pointer: PointerInfo, action: TouchAction, path: Vec<WidgetId>) -> PointerSequence {
1051        PointerSequence::new(pointer, path, action, None, Point::ZERO, EventTime::ZERO)
1052    }
1053
1054    /// Synthetic ids for the pure-logic tests: the sequence only ever compares
1055    /// and orders them, so no arena is needed to make them meaningful.
1056    fn ids(n: u64) -> Vec<WidgetId> {
1057        (0..n)
1058            .map(|i| KeyData::from_ffi((1u64 << 32) | (i + 1)).into())
1059            .collect()
1060    }
1061
1062    #[test]
1063    fn a_mouse_latches_at_five_in_every_configuration() {
1064        // The single most important invariant in the package: no frozen
1065        // TouchAction, and no density, may retune the mouse drag latch.
1066        let tokens = tokens();
1067        let profile = tokens.profile(PointerKind::Mouse);
1068        for action in [
1069            TouchAction::AUTO,
1070            TouchAction::NONE,
1071            TouchAction::PAN,
1072            TouchAction::PAN_X,
1073            TouchAction::PAN_Y,
1074            TouchAction::PINCH_ZOOM,
1075            TouchAction::MANIPULATION,
1076        ] {
1077            let s = seq(mouse(), action, ids(1));
1078            assert_eq!(
1079                s.latch_slop(profile),
1080                5.0,
1081                "a mouse under {action:?} must latch at 5.0"
1082            );
1083        }
1084    }
1085
1086    #[test]
1087    fn slop_precise_reaches_only_a_direct_pointer_under_a_frozen_none() {
1088        let tokens = tokens();
1089        let touch_profile = tokens.profile(PointerKind::Touch);
1090        let none = seq(finger(), TouchAction::NONE, ids(1));
1091        assert_eq!(none.latch_slop(touch_profile), touch_profile.slop_precise);
1092        let auto = seq(finger(), TouchAction::AUTO, ids(1));
1093        assert_eq!(auto.latch_slop(touch_profile), touch_profile.drag_slop);
1094    }
1095
1096    #[test]
1097    fn a_mouse_never_has_an_eligible_pan_member() {
1098        let tokens = tokens();
1099        let profile = tokens.profile(PointerKind::Mouse);
1100        let s = seq(mouse(), TouchAction::AUTO, ids(1));
1101        assert!(!s.pan_is_eligible(&PanClaim::both(), profile));
1102    }
1103
1104    #[test]
1105    fn members_stay_innermost_first_whatever_order_they_enrol_in() {
1106        let path = ids(4);
1107        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1108        assert!(s.enrol(path[3], MemberRole::Gesture));
1109        assert!(s.enrol(path[1], MemberRole::RawDrag));
1110        assert!(s.enrol(path[2], MemberRole::Gesture));
1111        let order: Vec<_> = s.members().iter().map(|m| m.id).collect();
1112        assert_eq!(order, vec![path[1], path[2], path[3]]);
1113    }
1114
1115    #[test]
1116    fn the_dead_zone_boundary_refuses_everything_at_or_above_it() {
1117        let path = ids(4);
1118        let mut s = PointerSequence::new(
1119            mouse(),
1120            path.clone(),
1121            TouchAction::AUTO,
1122            Some(path[2]),
1123            Point::ZERO,
1124            EventTime::ZERO,
1125        );
1126        assert!(s.enrol(path[1], MemberRole::Gesture), "below the boundary");
1127        assert!(
1128            !s.enrol(path[2], MemberRole::Gesture),
1129            "the boundary itself"
1130        );
1131        assert!(!s.enrol(path[3], MemberRole::Gesture), "above the boundary");
1132    }
1133
1134    #[test]
1135    fn deciding_rejects_every_other_live_member_exactly_once() {
1136        let path = ids(3);
1137        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1138        s.enrol(path[0], MemberRole::Gesture);
1139        s.enrol(path[1], MemberRole::Gesture);
1140        s.enrol(path[2], MemberRole::Gesture);
1141        let losers = s.decide(path[1]);
1142        assert_eq!(losers, vec![path[0], path[2]]);
1143        assert_eq!(s.winner(), Some(path[1]));
1144        // A second decide reports nothing new: the losers are no longer live.
1145        assert!(s.decide(path[1]).is_empty());
1146    }
1147
1148    #[test]
1149    fn after_long_press_defers_eligibility_and_arms_self_rejection() {
1150        let tokens = tokens();
1151        let profile = tokens.profile(PointerKind::Touch);
1152        let path = ids(2);
1153        let mut s = seq(finger(), TouchAction::PAN_Y, path.clone());
1154        s.enrol_drag(
1155            path[0],
1156            MemberRole::Gesture,
1157            DragActivation::AfterLongPress,
1158            profile,
1159        );
1160        let member = s.members()[0];
1161        assert_eq!(
1162            member.eligible_at,
1163            Some(EventTime::ZERO + profile.long_press)
1164        );
1165        assert!(member.rejects_on_tap_slop);
1166        assert!(!member.is_eligible_at(EventTime::ZERO));
1167        assert!(member.is_eligible_at(EventTime::ZERO + profile.long_press));
1168    }
1169
1170    #[test]
1171    fn auto_activation_defers_only_a_coarse_pointer_facing_a_pan() {
1172        let path = ids(2);
1173
1174        // A mouse is always immediate.
1175        let mut m = seq(mouse(), TouchAction::AUTO, path.clone());
1176        m.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
1177        assert_eq!(
1178            m.resolve_activation(DragActivation::Auto),
1179            DragActivation::Immediate
1180        );
1181
1182        // A finger with no pan competitor is immediate too.
1183        let bare = seq(finger(), TouchAction::AUTO, path.clone());
1184        assert_eq!(
1185            bare.resolve_activation(DragActivation::Auto),
1186            DragActivation::Immediate
1187        );
1188
1189        // A finger facing a pan claimant defers.
1190        let mut contested = seq(finger(), TouchAction::AUTO, path.clone());
1191        contested.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
1192        assert_eq!(
1193            contested.resolve_activation(DragActivation::Auto),
1194            DragActivation::AfterLongPress
1195        );
1196    }
1197
1198    #[test]
1199    fn a_pan_wins_on_the_dominant_axis_and_only_where_permitted() {
1200        let tokens = tokens();
1201        let profile = tokens.profile(PointerKind::Touch);
1202        let slop = profile.pan_slop.expect("touch pans");
1203        let path = ids(1);
1204
1205        let mut s = seq(finger(), TouchAction::PAN, path);
1206        s.set_last_position(Point::new(slop + 10.0, slop + 1.0));
1207        assert_eq!(
1208            s.pan_axis_past_slop(&PanClaim::both(), profile),
1209            Some(Axis::X),
1210            "the axis that travelled further wins the diagonal"
1211        );
1212
1213        // The frozen action forbids X, so the same travel resolves to Y.
1214        let mut only_y = seq(finger(), TouchAction::PAN_Y, ids(1));
1215        only_y.set_last_position(Point::new(slop + 10.0, slop + 1.0));
1216        assert_eq!(
1217            only_y.pan_axis_past_slop(&PanClaim::both(), profile),
1218            Some(Axis::Y)
1219        );
1220    }
1221
1222    #[test]
1223    fn a_hold_expires_at_max_hold_and_not_before() {
1224        let tokens = tokens();
1225        let profile = tokens.profile(PointerKind::Mouse);
1226        let path = ids(1);
1227        let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
1228        s.enrol(path[0], MemberRole::Gesture);
1229        s.hold(path[0], EventTime::ZERO);
1230        assert!(s.is_held());
1231
1232        s.expire_holds(EventTime::from_duration(profile.max_hold / 2), profile);
1233        assert!(s.is_held(), "a hold survives until max_hold");
1234
1235        s.expire_holds(EventTime::from_duration(profile.max_hold), profile);
1236        assert!(!s.is_held(), "and is released at it");
1237        assert_eq!(s.members()[0].state, MemberState::Possible);
1238    }
1239
1240    #[test]
1241    fn revalidate_drops_dead_members_one_at_a_time() {
1242        // The tree-level half — losing the captor cancels the whole sequence —
1243        // is pinned in `gesture_dispatch_impl`; in a real tree a member is
1244        // always an ancestor of the captor and so cannot die on its own, which
1245        // is why the per-member rule is asserted here.
1246        let mut arena = crate::arena::WidgetArena::new();
1247        let live = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
1248        let doomed = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
1249        let mut s = seq(mouse(), TouchAction::AUTO, vec![doomed, live]);
1250        s.enrol(doomed, MemberRole::Gesture);
1251        s.enrol(live, MemberRole::Gesture);
1252        s.set_capture(Some(live));
1253
1254        assert!(s.revalidate(&arena).is_empty(), "nothing has died yet");
1255        arena.destroy(doomed);
1256
1257        assert_eq!(s.revalidate(&arena), vec![doomed]);
1258        assert_eq!(
1259            s.members().iter().map(|m| m.id).collect::<Vec<_>>(),
1260            vec![live],
1261            "only the dead member is dropped"
1262        );
1263        assert!(!s.lost_owner(&arena), "the captor is still alive");
1264
1265        arena.destroy(live);
1266        assert!(
1267            s.lost_owner(&arena),
1268            "losing the captor is what cancels the sequence"
1269        );
1270    }
1271
1272    #[test]
1273    fn the_tap_boundary_is_a_radius_for_a_mouse_and_bounds_for_a_finger() {
1274        let tokens = tokens();
1275        let mouse_profile = tokens.profile(PointerKind::Mouse);
1276        let touch_profile = tokens.profile(PointerKind::Touch);
1277        assert_eq!(
1278            TapBoundary::for_pointer(&mouse(), mouse_profile),
1279            TapBoundary::Radius(mouse_profile.tap_slop)
1280        );
1281        assert_eq!(
1282            TapBoundary::for_pointer(&finger(), touch_profile),
1283            TapBoundary::Bounds
1284        );
1285
1286        // A coarse press well past tap_slop but still inside the control has
1287        // NOT left the boundary — that is the whole point of `Bounds`.
1288        let bounds = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 100.0);
1289        assert!(!TapBoundary::Bounds.left(
1290            Point::new(50.0, 50.0),
1291            Point::new(50.0, 80.0),
1292            Some(bounds),
1293            touch_profile,
1294        ));
1295        assert!(TapBoundary::Bounds.left(
1296            Point::new(50.0, 50.0),
1297            Point::new(50.0, 120.0),
1298            Some(bounds),
1299            touch_profile,
1300        ));
1301        // With no bounds to test, it falls back to the radius rather than
1302        // letting the press travel anywhere.
1303        assert!(TapBoundary::Bounds.left(
1304            Point::new(50.0, 50.0),
1305            Point::new(50.0, 80.0),
1306            None,
1307            touch_profile,
1308        ));
1309    }
1310
1311    /// A press accepted through a `Widget::hit_outset` begins outside the node
1312    /// it was accepted for, so the node's rectangle cannot be its boundary:
1313    /// with `Bounds` taken literally the press is "already gone" on arrival and
1314    /// the tap can never complete. It falls back to the pointer's own radius
1315    /// around where it landed, and sliding onto the control keeps it alive.
1316    #[test]
1317    fn a_press_that_began_outside_the_node_is_bounded_by_its_own_radius() {
1318        let tokens = tokens();
1319        let touch_profile = tokens.profile(PointerKind::Touch);
1320        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 12.0, 12.0);
1321        // Landed 4 dp past the trailing edge — inside the outset ring the
1322        // arena offered it, outside the rectangle.
1323        let origin = Point::new(16.0, 6.0);
1324        assert!(
1325            !TapBoundary::Bounds.left(origin, origin, Some(rect), touch_profile),
1326            "a press cannot have left the boundary on the sample that opened it",
1327        );
1328        assert!(
1329            !TapBoundary::Bounds.left(origin, Point::new(6.0, 6.0), Some(rect), touch_profile),
1330            "sliding onto the control keeps the press",
1331        );
1332        assert!(
1333            TapBoundary::Bounds.left(
1334                origin,
1335                Point::new(16.0 + touch_profile.tap_slop + 1.0, 6.0),
1336                Some(rect),
1337                touch_profile,
1338            ),
1339            "and past the radius it is gone, so the abort gesture still works",
1340        );
1341    }
1342
1343    /// The union term, on its own.
1344    ///
1345    /// The radius half of the outside-origin rule is a *travel* allowance, and
1346    /// on a small control it runs out before the finger has finished arriving:
1347    /// a contact that lands in the outset ring of a wide control and then
1348    /// slides well past `tap_slop` **onto** the control is further from its
1349    /// origin than the radius permits and squarely inside the rectangle. Only
1350    /// the union with the node's bounds keeps that press alive; with the
1351    /// `!rect.contains(position)` term gone, the radius alone kills a press
1352    /// that is sitting on the middle of the thing it is pressing.
1353    #[test]
1354    fn sliding_onto_the_control_keeps_a_press_the_radius_alone_would_lose() {
1355        let tokens = tokens();
1356        let touch_profile = tokens.profile(PointerKind::Touch);
1357        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
1358        // 4 dp past the trailing edge — inside the outset ring, outside the rect.
1359        let origin = Point::new(104.0, 10.0);
1360        // 24 dp of travel, against a Touch `tap_slop` of 18: past the radius,
1361        // and 20 dp inside the control.
1362        let onto = Point::new(80.0, 10.0);
1363        assert!(
1364            super::super::distance(origin, onto) > touch_profile.tap_slop,
1365            "the probe is only discriminating while the travel exceeds tap_slop",
1366        );
1367        assert!(rect.contains(onto), "…and lands inside the control");
1368        assert!(
1369            !TapBoundary::Bounds.left(origin, onto, Some(rect), touch_profile),
1370            "a finger resting on the control it pressed has not left it",
1371        );
1372    }
1373
1374    /// Which rule applies is decided by the **origin**, not by where the
1375    /// pointer is now.
1376    ///
1377    /// The two questions agree on most samples, which is why the distinction
1378    /// has to be pinned on the one geometry where they cannot: a press that
1379    /// began *inside* the node and has moved a short way outside it. The rule
1380    /// for that press is the rectangle — it left the moment it crossed the
1381    /// edge, however little it travelled — while a press that began outside is
1382    /// allowed the pointer's radius around where it landed, so with the same
1383    /// `position` it has not left at all. Reading `position` instead of
1384    /// `origin` collapses both onto the second answer and silently hands every
1385    /// coarse press that starts inside a control a `tap_slop` grace band
1386    /// outside it, which is exactly the slop `Bounds` exists to replace.
1387    #[test]
1388    fn the_boundary_rule_is_chosen_by_where_the_press_began() {
1389        let tokens = tokens();
1390        let touch_profile = tokens.profile(PointerKind::Touch);
1391        let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
1392        // One sample, 4 dp past the trailing edge, reached from two origins —
1393        // both within `tap_slop` of it, so the radius rule cannot fail either.
1394        let position = Point::new(104.0, 10.0);
1395        let from_inside = Point::new(96.0, 10.0);
1396        let from_outside = Point::new(108.0, 10.0);
1397        assert!(rect.contains(from_inside), "the first press began inside");
1398        assert!(
1399            !rect.contains(from_outside) && !rect.contains(position),
1400            "the second began outside, and neither sample is in the rect",
1401        );
1402        for origin in [from_inside, from_outside] {
1403            assert!(
1404                super::super::distance(origin, position) < touch_profile.tap_slop,
1405                "the probe only discriminates while the travel is inside tap_slop",
1406            );
1407        }
1408
1409        assert!(
1410            TapBoundary::Bounds.left(from_inside, position, Some(rect), touch_profile),
1411            "a press that began inside the node is bounded by the node: crossing \
1412             the edge ends it, with no radius grace outside",
1413        );
1414        assert!(
1415            !TapBoundary::Bounds.left(from_outside, position, Some(rect), touch_profile),
1416            "a press that began outside is bounded by its own radius, and this \
1417             one has barely moved",
1418        );
1419    }
1420
1421    // -----------------------------------------------------------------
1422    // The self-drag half of a dual-role member
1423    // -----------------------------------------------------------------
1424
1425    /// `defer_own_drag` refuses anything that is not a live `Pan` member — the
1426    /// structural reason a mouse can never reach it, since a mouse enrols no
1427    /// pan member at all.
1428    #[test]
1429    fn defer_own_drag_refuses_anything_but_a_live_pan_member() {
1430        let tokens = tokens();
1431        let profile = tokens.profile(PointerKind::Touch);
1432        let ids = ids(3);
1433
1434        // A `Gesture` member: the node's drag already has the slot, so there is
1435        // no second half to defer.
1436        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1437        assert!(s.enrol(ids[0], MemberRole::Gesture));
1438        assert!(
1439            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1440            "a Gesture member is not dual-role"
1441        );
1442
1443        // A node that is not a member at all.
1444        assert!(
1445            !s.defer_own_drag(ids[1], DragActivation::Auto, profile),
1446            "a non-member has nothing to attach a deferral to"
1447        );
1448
1449        // A rejected pan member.
1450        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1451        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1452        s.reject(ids[0]);
1453        assert!(
1454            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1455            "a member that is out of the running gets no second half"
1456        );
1457
1458        // A decided sequence.
1459        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1460        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1461        s.decide(ids[0]);
1462        assert!(
1463            !s.defer_own_drag(ids[0], DragActivation::Auto, profile),
1464            "arbitration is over"
1465        );
1466
1467        // …and the one shape that is accepted.
1468        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1469        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1470        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1471    }
1472
1473    /// A mouse sequence never has a pan member, so the shape `defer_own_drag`
1474    /// exists for cannot arise. Stated on the object rather than through a
1475    /// tree, so it is a property of the type and not of one fixture.
1476    #[test]
1477    fn a_mouse_can_never_defer_its_own_drag() {
1478        let tokens = tokens();
1479        let profile = tokens.profile(PointerKind::Mouse);
1480        let ids = ids(1);
1481        let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
1482
1483        // `pan_is_eligible` is what `begin_sequence` gates the enrolment on, and
1484        // for a mouse it is false whatever the claim asks for — so no `Pan`
1485        // member is ever created and the arm has nothing to attach to.
1486        assert!(
1487            !s.pan_is_eligible(&PanClaim::both(), profile),
1488            "the mouse profile has no pan_slop, so no claim is eligible"
1489        );
1490        assert!(!s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1491        assert!(!s.has_deferred_grab_for(ids[0]));
1492    }
1493
1494    /// `Auto` on a direct pointer with an eligible pan defers the self-drag to
1495    /// the long-press deadline; `Immediate` arms it at the press.
1496    #[test]
1497    fn defer_own_drag_resolves_auto_the_way_every_other_drag_resolves_it() {
1498        let tokens = tokens();
1499        let profile = tokens.profile(PointerKind::Touch);
1500        let ids = ids(1);
1501
1502        let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
1503        assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1504        assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
1505        assert!(
1506            deferred.own_drag_blocked(ids[0], EventTime::ZERO),
1507            "Auto + an eligible pan means a hold"
1508        );
1509        assert!(
1510            !deferred.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press),
1511            "…and the hold ends at long_press"
1512        );
1513        assert!(
1514            deferred.has_deferred_grab_for(ids[0]),
1515            "so the hold is spent on the grab and cannot also be a long press"
1516        );
1517
1518        let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
1519        assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1520        assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1521        assert!(
1522            !immediate.own_drag_blocked(ids[0], EventTime::ZERO),
1523            "Immediate arms at the press"
1524        );
1525        assert!(
1526            !immediate.has_deferred_grab_for(ids[0]),
1527            "and spends no hold, so a long press on the same node still fires"
1528        );
1529    }
1530
1531    /// A withdrawal is permanent for the press. That is what keeps a deferral
1532    /// ripening mid-pan from starting a grab under a scrolling finger.
1533    #[test]
1534    fn a_withdrawn_self_drag_stays_blocked_past_its_own_deadline() {
1535        let tokens = tokens();
1536        let profile = tokens.profile(PointerKind::Touch);
1537        let ids = ids(1);
1538        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1539        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1540        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1541
1542        s.withdraw_own_drag(ids[0]);
1543        assert!(
1544            s.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press * 10),
1545            "a withdrawn self-drag does not come back when its timer ripens"
1546        );
1547        assert!(
1548            !s.has_deferred_grab_for(ids[0]),
1549            "and stops spending the hold, so the node's long press is free again"
1550        );
1551    }
1552
1553    /// `promote_own_drag` is what makes the member report name the half that
1554    /// actually won, and it is a no-op on anything else.
1555    #[test]
1556    fn promote_own_drag_renames_the_half_that_won() {
1557        let tokens = tokens();
1558        let profile = tokens.profile(PointerKind::Touch);
1559        let ids = ids(1);
1560
1561        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1562        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1563        assert!(s.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1564        assert!(matches!(s.members()[0].role, MemberRole::Pan(_)));
1565        assert!(s.promote_own_drag(ids[0]));
1566        assert_eq!(s.members()[0].role, MemberRole::Gesture);
1567
1568        // A plain claimant is untouched.
1569        let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
1570        assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1571        assert!(!plain.promote_own_drag(ids[0]));
1572        assert!(matches!(plain.members()[0].role, MemberRole::Pan(_)));
1573
1574        // …and so is one whose self-drag has been withdrawn: the pan won, and
1575        // renaming the member would make the report say otherwise.
1576        let mut withdrawn = seq(finger(), TouchAction::AUTO, ids.clone());
1577        assert!(withdrawn.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1578        assert!(withdrawn.defer_own_drag(ids[0], DragActivation::Auto, profile));
1579        withdrawn.withdraw_own_drag(ids[0]);
1580        assert!(!withdrawn.promote_own_drag(ids[0]));
1581        assert!(matches!(withdrawn.members()[0].role, MemberRole::Pan(_)));
1582    }
1583
1584    /// Only a **deferred** self-drag answers the positional sweep, mirroring
1585    /// `rejects_on_tap_slop`: an `Immediate` one is governed by its recognizer.
1586    #[test]
1587    fn only_a_deferred_self_drag_is_swept_positionally() {
1588        let tokens = tokens();
1589        let profile = tokens.profile(PointerKind::Touch);
1590        let ids = ids(1);
1591
1592        let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
1593        assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1594        assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
1595        assert_eq!(
1596            deferred.unripe_own_drag_members(EventTime::ZERO),
1597            vec![ids[0]]
1598        );
1599
1600        let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
1601        assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1602        assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
1603        assert!(
1604            immediate
1605                .unripe_own_drag_members(EventTime::ZERO)
1606                .is_empty()
1607        );
1608
1609        // A plain claimant never appears.
1610        let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
1611        assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1612        assert!(plain.unripe_own_drag_members(EventTime::ZERO).is_empty());
1613    }
1614
1615    /// …and only while it is still **unripe**. Once the hold has been served the
1616    /// grab is live, and a live grab travelling is the grab doing its job: a
1617    /// sweep that still fired then would make hold-then-drag impossible on any
1618    /// node small enough for the drag to leave its bounds.
1619    #[test]
1620    fn a_ripe_self_drag_is_no_longer_swept() {
1621        let tokens = tokens();
1622        let profile = tokens.profile(PointerKind::Touch);
1623        let ids = ids(1);
1624
1625        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1626        assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
1627        assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
1628
1629        let deadline = EventTime::ZERO + profile.long_press;
1630        assert_eq!(
1631            s.unripe_own_drag_members(
1632                EventTime::ZERO + (profile.long_press - std::time::Duration::from_millis(1)),
1633            ),
1634            vec![ids[0]],
1635            "still inside the hold"
1636        );
1637        assert!(
1638            s.unripe_own_drag_members(deadline).is_empty(),
1639            "the hold has been served; the grab is live and answers to its own \
1640             recognizer from here"
1641        );
1642    }
1643
1644    /// `own_drag_armed_at` is inert for every member carrying no self-drag,
1645    /// which is what makes the gate it feeds free for every other sequence.
1646    #[test]
1647    fn own_drag_armed_is_true_for_a_member_with_no_self_drag() {
1648        let ids = ids(1);
1649        let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
1650        assert!(s.enrol(ids[0], MemberRole::Gesture));
1651        for at in [
1652            EventTime::ZERO,
1653            EventTime::ZERO + std::time::Duration::from_secs(10),
1654        ] {
1655            assert!(s.members()[0].own_drag_armed_at(at));
1656            assert!(!s.own_drag_blocked(ids[0], at));
1657        }
1658    }
1659
1660    /// The per-press activation override is recorded per node,
1661    /// last-writer-wins, and answers `None` for a node that never spoke.
1662    #[test]
1663    fn a_drag_activation_override_is_recorded_per_node() {
1664        let ids = ids(2);
1665        let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
1666        assert_eq!(s.drag_activation_override(ids[0]), None);
1667
1668        s.set_drag_activation_override(ids[0], DragActivation::Immediate);
1669        s.set_drag_activation_override(ids[1], DragActivation::AfterLongPress);
1670        assert_eq!(
1671            s.drag_activation_override(ids[0]),
1672            Some(DragActivation::Immediate)
1673        );
1674        assert_eq!(
1675            s.drag_activation_override(ids[1]),
1676            Some(DragActivation::AfterLongPress)
1677        );
1678
1679        s.set_drag_activation_override(ids[0], DragActivation::Auto);
1680        assert_eq!(
1681            s.drag_activation_override(ids[0]),
1682            Some(DragActivation::Auto),
1683            "answering twice on one press means the second answer"
1684        );
1685    }
1686}