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}