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}