teksilo_core/widget_tree/pointer_state.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Pointer / hover / capture / gesture-owner state: the per-node probes
5//! the router consults, the per-pointer `PointerSequence` arbitration that
6//! enrols the competing ancestors, and the gesture-recognizer tick that
7//! drives them.
8
9use super::*;
10use crate::pointer::touch_action::{Axis, PanClaim, TouchAction};
11
12impl WidgetTree {
13 /// This tree's input clock — the one source of
14 /// [`EventTime`](crate::pointer::EventTime)s for everything the pointer
15 /// path does.
16 ///
17 /// A [`MonotonicClock`](crate::pointer::clock::MonotonicClock) anchored at
18 /// the tree epoch by default. The epoch is the same `Instant`
19 /// [`simulated_now`](Self::simulated_now) starts at, so the input timeline
20 /// and the simulated animation timeline are one axis rather than two.
21 pub fn input_clock(&self) -> std::rc::Rc<dyn crate::pointer::clock::InputClock> {
22 self.input_clock.clone()
23 }
24
25 /// Replace the input clock.
26 ///
27 /// A headless test installs a
28 /// [`ManualClock`](crate::pointer::clock::ManualClock) here so gesture
29 /// deadlines fire exactly when it says, with no sleeping and no dependence
30 /// on how long the test itself took.
31 pub fn set_input_clock(&mut self, clock: std::rc::Rc<dyn crate::pointer::clock::InputClock>) {
32 self.input_clock = clock;
33 // The axis just changed underneath. Any offset carried forward from a
34 // hand-back belongs to the *old* clock's readings and means nothing
35 // against the new one, so it goes with it; then re-anchor the simulated
36 // origin against the new clock (or drop it, if the new clock is itself
37 // the virtual axis).
38 self.sim_input_offset = std::time::Duration::ZERO;
39 self.sim_input_origin = None;
40 self.rearm_sim_input_origin();
41 }
42
43 /// Put this tree on the simulated clock, if it is not already.
44 ///
45 /// Called from the one door that moves simulated time, and it moves **both**
46 /// axes together:
47 ///
48 /// * the animation scheduler's stored instants are rebased from the wall
49 /// clock onto [`simulated_now`](Self::simulated_now), so an animation
50 /// in flight when the freeze happens keeps the phase it had. Without the
51 /// rebase every such animation would be measured from a start lying in
52 /// the simulated clock's future — the simulated clock reads the tree's
53 /// epoch plus whatever has been advanced, which on a live window is far
54 /// behind the wall clock — and its elapsed time would clamp to zero for
55 /// good;
56 /// * the input axis is anchored where it stood — see the
57 /// `sim_input_origin` field — so no stamp taken before the switch lands
58 /// in the virtual future.
59 ///
60 /// Re-entrant on purpose: [`resume_real_time`](Self::resume_real_time) may
61 /// have handed time back since the last call, and the next advance has to
62 /// take it again.
63 pub(super) fn enter_simulated_mode(&mut self) {
64 if !self.sim_time_frozen {
65 self.sim_time_frozen = true;
66 let now = std::time::Instant::now();
67 self.animation_scheduler.rebase(now, self.sim_clock);
68 self.announcer_polite.rebase(now, self.sim_clock);
69 self.announcer_assertive.rebase(now, self.sim_clock);
70 }
71 if self.sim_input_origin.is_none() {
72 self.rearm_sim_input_origin();
73 }
74 }
75
76 /// Hand time back to the wall clock, carrying forward everything that was
77 /// advanced while it was simulated.
78 ///
79 /// **Who calls this.** A host that shares a live tree with a real event
80 /// loop — the debug automation bridge — after each operation that may have
81 /// advanced the clock. A headless test does not: a test wants the freeze,
82 /// and wants it to survive between calls, so that two samples it dispatches
83 /// without advancing are stamped the *same* instant rather than however
84 /// many microseconds apart the machine happened to run them.
85 ///
86 /// **The animation axis** is handed back by rebasing the scheduler's stored
87 /// instants the other way, the exact inverse of what
88 /// `enter_simulated_mode` did. An animation half-way through when the
89 /// operation ends is half-way through on the wall clock too, and the next
90 /// real layout pass advances it from there — neither snapped to its end
91 /// (which is what ticking at the wall clock against a start stamped on the
92 /// simulated one gives) nor stuck (which is what ticking a live tree at a
93 /// simulated clock nothing is advancing any more gives).
94 ///
95 /// **The input axis cannot simply drop its origin.** A frozen axis that has
96 /// been advanced reads *ahead* of the raw clock; dropping the origin would
97 /// send [`input_now`](Self::input_now) backwards, and a monotone
98 /// [`EventTime`](crate::pointer::EventTime) is a platform conformance
99 /// invariant every velocity tracker, tap streak and hold relies on. So the
100 /// gap is measured — afresh, against this hand-back's own readings, never
101 /// added to what a previous one measured, and floored at zero for the case
102 /// where the raw clock is already the later of the two — and kept in
103 /// `sim_input_offset`: the reading at this instant is the later of the
104 /// frozen reading and the raw one, and it moves with the wall clock from
105 /// here.
106 ///
107 /// A no-op on a tree that is not simulating time, so calling it after every
108 /// operation costs nothing.
109 pub fn resume_real_time(&mut self) {
110 if !self.sim_time_frozen {
111 return;
112 }
113 self.sim_time_frozen = false;
114 let now = std::time::Instant::now();
115 self.animation_scheduler.rebase(self.sim_clock, now);
116 self.announcer_polite.rebase(self.sim_clock, now);
117 self.announcer_assertive.rebase(self.sim_clock, now);
118 let Some((base, base_at)) = self.sim_input_origin.take() else {
119 // An unanchored clock — a `ManualClock` — *is* the virtual axis and
120 // was never frozen against the wall clock, so there is nothing to
121 // carry forward.
122 return;
123 };
124 let frozen = base + self.sim_clock.saturating_duration_since(base_at);
125 // `saturating_since` rather than `-`: on a tree advanced by less than
126 // it spent on the wall clock the raw reading is already ahead, and the
127 // right offset is then none at all.
128 self.sim_input_offset = frozen.saturating_since(self.input_clock.now());
129 }
130
131 /// Anchor (or drop) the simulated input origin against the current clock.
132 fn rearm_sim_input_origin(&mut self) {
133 self.sim_input_origin = if self.sim_time_frozen && self.input_clock.epoch().is_some() {
134 // `input_now`, not the raw clock: after a hand-back the axis runs
135 // an offset ahead of the clock, and re-freezing at the raw reading
136 // would step it backwards by exactly that offset.
137 Some((self.input_now(), self.sim_clock))
138 } else {
139 // Either the tree still runs on real time, or its clock has no
140 // wall-clock anchor and is moved directly by `advance_time`.
141 None
142 };
143 }
144
145 /// The current time on this tree's input timeline.
146 ///
147 /// While the axis is frozen this is a reading of
148 /// [`sim_clock`](Self::simulated_now), not of the wall clock, so a deadline
149 /// can only be reached by advancing the clock. Once
150 /// [`resume_real_time`](Self::resume_real_time) has handed it back it is
151 /// the clock again, plus everything that was advanced.
152 pub fn input_now(&self) -> crate::pointer::EventTime {
153 match self.sim_input_origin {
154 Some((base, base_at)) => base + self.sim_clock.saturating_duration_since(base_at),
155 None => self.input_clock.now() + self.sim_input_offset,
156 }
157 }
158
159 /// Everything a recognizer on `id` is allowed to know beyond the event in
160 /// front of it: now, the profile for the pointer being dispatched, the
161 /// node's own bounds, and the pointer itself.
162 ///
163 /// Rebuilt per dispatch rather than cached, so a theme change, a density
164 /// change or a different pointer kind reaches the recognizers without any
165 /// of them holding a copy of a threshold.
166 pub(crate) fn recognizer_context(
167 &self,
168 id: WidgetId,
169 ) -> crate::gesture::RecognizerContext<'static> {
170 let pointer = self.current_input.pointer;
171 let profile = *self.effective_theme.input.profile(pointer.kind);
172 let size = self.bounds(id).size();
173 // A dispatch that carries no timestamp of its own (a hand-built
174 // `WidgetEvent` from a test) reads the tree clock instead.
175 let now = if pointer.time == crate::pointer::EventTime::ZERO {
176 self.input_now()
177 } else {
178 pointer.time
179 };
180 crate::gesture::RecognizerContext::new(
181 now,
182 profile,
183 Rect::new(0.0, 0.0, size.width, size.height),
184 pointer,
185 )
186 }
187
188 // -----------------------------------------------------------------
189 // The pointer table
190 // -----------------------------------------------------------------
191
192 /// The pointer this dispatch is serving.
193 ///
194 /// Outside a pointer dispatch the input snapshot holds its default — the
195 /// mouse — which is exactly what every legacy `WidgetEvent` has always
196 /// meant, so a caller that names no pointer keeps naming the mouse.
197 pub(crate) fn current_pointer_id(&self) -> crate::pointer::PointerId {
198 self.current_input.pointer.id
199 }
200
201 /// The widget holding the capture of the pointer this dispatch is serving.
202 ///
203 /// Capture is **per pointer**: two contacts hold independent captures and
204 /// each is released only by its own Up or Cancel. For the mouse — the only
205 /// pointer that existed before the touch programme — this is the old
206 /// singular `pointer_captured_by`, unchanged.
207 pub(crate) fn current_pointer_capture(&self) -> Option<WidgetId> {
208 self.pointers
209 .get(self.current_pointer_id())
210 .and_then(|e| e.captured_by)
211 }
212
213 /// Set (or clear) the capture of the pointer this dispatch is serving.
214 pub(crate) fn set_current_pointer_capture(&mut self, captor: Option<WidgetId>) {
215 let id = self.current_pointer_id();
216 if let Some(entry) = self.pointers.get_mut(id) {
217 entry.captured_by = captor;
218 }
219 }
220
221 /// Set (or clear) the capture of a *named* pointer — the door
222 /// [`EventContext::capture_pointer_id`](crate::widget::EventContext::capture_pointer_id)
223 /// opens for a handler driving a pointer other than the one it is serving.
224 ///
225 /// A capture asked for on a pointer that is not live is dropped, not
226 /// invented: no sample will ever be delivered to it, so an entry conjured
227 /// to hold it would be a capture nothing can release. Reaching this means
228 /// the handler ran outside a pointer dispatch entirely (an assistive
229 /// technology action, a timer), where there is no pointer to capture.
230 pub(crate) fn set_pointer_capture(
231 &mut self,
232 pointer: crate::pointer::PointerId,
233 captor: Option<WidgetId>,
234 ) {
235 if let Some(entry) = self.pointers.get_mut(pointer) {
236 entry.captured_by = captor;
237 }
238 }
239
240 /// Release the capture a drag session was holding.
241 ///
242 /// A drag owns one pointer, but which one is not recorded on the session
243 /// yet (arbitration lands with the gesture package), so this releases both
244 /// the pointer being dispatched and anything the drag's source widget
245 /// still holds. For the mouse those are the same capture, which is why
246 /// this is a faithful stand-in for the old blanket clear.
247 pub(super) fn release_drag_capture(&mut self, source: Option<WidgetId>) {
248 self.set_current_pointer_capture(None);
249 if let Some(src) = source {
250 self.pointers.release_captures_of(src);
251 }
252 }
253
254 /// The widget the **hover owner** is over.
255 ///
256 /// Hover belongs to the hover owner and to nobody else: a contact never
257 /// produces hover, so a finger arriving beside a hovering mouse leaves
258 /// this — and every `on_hover` handler, tooltip dwell and cursor shape —
259 /// exactly where it was.
260 pub(crate) fn hovered_id(&self) -> Option<WidgetId> {
261 self.pointers.hover_owner().and_then(|e| e.hovered)
262 }
263
264 /// Where the hover owner is, if one is live.
265 ///
266 /// The position hover recovery must re-hit-test at. Distinct from
267 /// [`last_pointer_position`](Self::last_pointer_position), which reports
268 /// the *primary* pointer and so answers for a touch-only device too —
269 /// where re-deriving hover from it would invent a hover no finger ever
270 /// produced.
271 pub(crate) fn hover_owner_position(&self) -> Option<teksilo_canvas::Point> {
272 self.pointers.hover_owner().map(|e| e.position)
273 }
274
275 /// Admit the sample being dispatched into the pointer table, refreshing
276 /// its position and its [`PointerInfo`](crate::pointer::PointerInfo), and
277 /// publish the modality.
278 ///
279 /// Returns `false` when the table refused the pointer (a palm, or the
280 /// contact cap), in which case the sample must not be dispatched at all.
281 pub(super) fn admit_current_pointer(
282 &mut self,
283 position: teksilo_canvas::Point,
284 is_down: bool,
285 is_move: bool,
286 ) -> bool {
287 let info = self.current_input.pointer;
288 // Where this pointer was *before* this sample. Only a move of the
289 // hover owner updates `previous_pointer_position`, because the one
290 // reader — the overlay safe triangle — wants the last sample that was
291 // still over the anchor the pointer is leaving, and a press or a
292 // second contact is not that.
293 let was_hover_owner = self.pointers.hover_owner_id() == Some(info.id);
294 let before = self.pointers.get(info.id).map(|e| e.position);
295 if self.pointers.admit(info, position, is_down).is_none() {
296 return false;
297 }
298 if is_move && was_hover_owner {
299 self.previous_pointer_position = before;
300 }
301 if info.kind.hovers() {
302 self.hover_owner_outside_window = false;
303 }
304 if self.last_pointer_kind_signal.get() != info.kind {
305 self.last_pointer_kind_signal.set(info.kind);
306 }
307 true
308 }
309
310 /// Hand the hover-owner role to the pointer being dispatched, and take
311 /// hover away from whoever held it.
312 ///
313 /// The later sample wins: on a machine with both a mouse and a pen, the
314 /// device the user just moved owns hover, and the one that lost it is sent
315 /// a [`PointerLeave`](crate::event::WidgetEvent::PointerLeave) for the
316 /// widget it was over — otherwise that widget stays lit for a pointer that
317 /// is no longer pointing at it. A contact is refused outright.
318 pub(super) fn claim_hover_owner_for_current(&mut self, ops: &mut dyn crate::window::WindowOps) {
319 let id = self.current_pointer_id();
320 let Some(displaced) = self.pointers.claim_hover_owner(id) else {
321 return;
322 };
323 let stale = self
324 .pointers
325 .get_mut(displaced)
326 .and_then(|entry| entry.hovered.take());
327 if let Some(old) = stale {
328 // Credited to the pointer that just *lost* the role — it is the one
329 // no longer pointing at `old` — not to the claimant.
330 let leave = WidgetEvent::PointerLeave {
331 pointer: self
332 .pointers
333 .get(displaced)
334 .map(|entry| entry.info)
335 .unwrap_or_else(|| crate::pointer::PointerInfo::mouse(self.input_now())),
336 };
337 self.dispatch_to_widget(old, &leave, &mut *ops);
338 self.tooltip_pointer_leave(old, &mut *ops);
339 }
340 // The new owner starts with no hover of its own; the move that gave it
341 // the role establishes one immediately afterwards.
342 self.update_hover_within_signals(stale, None);
343 self.set_hovered(None);
344 }
345
346 /// Whether `id` carries a drag or swipe handler (hence gets a drag/swipe
347 /// recognizer once its arena is built).
348 fn widget_has_drag(&self, id: WidgetId) -> bool {
349 self.arena
350 .get(id)
351 .map(|n| n.any_handler(|h| h.on_drag.is_some() || h.on_swipe.is_some()))
352 .unwrap_or(false)
353 }
354
355 /// Whether `id` is a gesture dead-zone boundary — a press inside its
356 /// subtree must not arm a drag/swipe on any ancestor above it. See
357 /// [`WidgetNode::gesture_dead_zone`](crate::arena::WidgetNode::gesture_dead_zone).
358 fn is_gesture_dead_zone(&self, id: WidgetId) -> bool {
359 self.arena
360 .get(id)
361 .map(|n| n.gesture_dead_zone)
362 .unwrap_or(false)
363 }
364
365 /// Whether `id` is a keyboard-capture surface — while focused it
366 /// receives every `KeyDown` raw, bypassing shortcut resolution. See
367 /// [`WidgetNode::keyboard_capture`](crate::arena::WidgetNode::keyboard_capture).
368 pub(super) fn is_keyboard_capture(&self, id: WidgetId) -> bool {
369 self.arena
370 .get(id)
371 .map(|n| n.keyboard_capture)
372 .unwrap_or(false)
373 }
374
375 // -----------------------------------------------------------------
376 // The arbitration spine: one `PointerSequence` per live pointer
377 // -----------------------------------------------------------------
378
379 /// The frozen hit path for `target`: target → root.
380 fn hit_path(&self, target: WidgetId) -> Vec<WidgetId> {
381 let mut path = Vec::new();
382 let mut current = Some(target);
383 while let Some(id) = current {
384 path.push(id);
385 current = self.arena.parent(id);
386 }
387 path
388 }
389
390 /// The **innermost** gesture dead zone on `path`.
391 ///
392 /// Nothing at or above it may be enrolled — for a mouse exactly as for a
393 /// finger. A dead zone is deliberately *not* sugar for
394 /// [`TouchAction::NONE`]: a mouse ignores touch actions entirely, so the
395 /// substitution would delete the mouse behaviour the flag exists for, and
396 /// on a direct pointer it would drop the latch to `slop_precise` and turn
397 /// the `DeadZone` widget's own regression into a 2 px hair trigger.
398 fn dead_zone_on(&self, path: &[WidgetId]) -> Option<WidgetId> {
399 path.iter()
400 .copied()
401 .find(|id| self.is_gesture_dead_zone(*id))
402 }
403
404 /// The gesture profile for the pointer this dispatch is serving.
405 pub(super) fn current_profile(&self) -> teksilo_tokens::GestureProfile {
406 *self
407 .effective_theme
408 .input
409 .profile(self.current_input.pointer.kind)
410 }
411
412 /// The sequence for the pointer this dispatch is serving.
413 pub(crate) fn current_sequence(&self) -> Option<&crate::gesture::PointerSequence> {
414 self.pointers
415 .get(self.current_pointer_id())
416 .and_then(|e| e.sequence.as_ref())
417 }
418
419 /// Run `f` against the current pointer's sequence, taking it out of the
420 /// table for the duration so `self` stays fully borrowable.
421 ///
422 /// The sequence is put back only if the pointer is still live afterwards —
423 /// a handler that ended the pointer must not have its sequence resurrected.
424 fn with_sequence<R>(
425 &mut self,
426 f: impl FnOnce(&mut Self, &mut crate::gesture::PointerSequence) -> R,
427 ) -> Option<R> {
428 let pointer = self.current_pointer_id();
429 let mut sequence = self.pointers.get_mut(pointer)?.sequence.take()?;
430 let result = f(self, &mut sequence);
431 if let Some(entry) = self.pointers.get_mut(pointer) {
432 entry.sequence = Some(sequence);
433 }
434 Some(result)
435 }
436
437 /// Open the arbitration for the press being dispatched, **before** any
438 /// handler runs.
439 ///
440 /// It has to be before: `ctx.touch_action()` reports the frozen value from
441 /// inside the press handler, and an explicit `capture_pointer()` made there
442 /// needs a sequence to enrol into. Pan claimants are enrolled here too, so
443 /// that the `DragActivation::Auto` question — "is anything else already
444 /// claiming this axis?" — has an answer during the press.
445 ///
446 /// A direct pointer forms **no sequence at all** when
447 /// [`InputTokens::touch_enabled`](teksilo_tokens::InputTokens::touch_enabled)
448 /// is off: the kill switch means the framework arbitrates nothing for a
449 /// contact, and the sample takes the legacy route unchanged.
450 pub(super) fn begin_sequence(&mut self, target: WidgetId, position: teksilo_canvas::Point) {
451 use crate::gesture::{MemberRole, PointerSequence};
452
453 let pointer = self.current_input.pointer;
454 if pointer.kind.is_direct() && !self.effective_theme.input.touch_enabled {
455 crate::trace_input!(
456 Gestures,
457 "no sequence for {:?}: touch_enabled=false",
458 pointer.id
459 );
460 return;
461 }
462 let path = self.hit_path(target);
463 let touch_action = self.effective_touch_action(target);
464 let boundary = self.dead_zone_on(&path);
465 let now = self.recognizer_context(target).now;
466 let mut sequence =
467 PointerSequence::new(pointer, path, touch_action, boundary, position, now);
468
469 // Pan claimants: direct pointers only. `PanClaim::devices` defaults to
470 // DIRECT and the mouse profile has no `pan_slop` at all, so this loop
471 // adds nothing for a mouse — which is what keeps every mouse sequence
472 // arbitrating exactly as `drag_observers` did.
473 let profile = self.current_profile();
474 if pointer.kind.is_direct() {
475 for (id, claim) in self.pan_candidates(target, touch_action) {
476 if sequence.pan_is_eligible(&claim, &profile) {
477 sequence.enrol(id, MemberRole::Pan(claim));
478 }
479 }
480 }
481
482 crate::trace_input!(
483 Gestures,
484 "sequence opened for {:?}: action={:?} dead_zone={:?} pan_members={}",
485 pointer.id,
486 touch_action,
487 boundary,
488 sequence.members().len()
489 );
490 if let Some(entry) = self.pointers.get_mut(pointer.id) {
491 entry.sequence = Some(sequence);
492 }
493 }
494
495 /// Enrol the competitors that only become knowable once the press has been
496 /// dispatched, and feed each of them the `Down`.
497 ///
498 /// The gesture members are the pre-existing drag observers, expressed on
499 /// the sequence and with the same three rules:
500 ///
501 /// * the captured widget's own drag owns the gesture — it is enrolled as
502 /// the innermost member and no ancestor is;
503 /// * a dead-zone boundary stops the walk;
504 /// * only nodes carrying `on_drag` / `on_swipe` compete.
505 ///
506 /// The captured widget's arena has already seen this `Down` through the
507 /// normal bubble, so only the ancestors are fed here.
508 ///
509 /// **Both enrolment doors have a dual-role fallback.** A node that declares
510 /// a [`PanClaim`] is already a member by the time this runs —
511 /// `begin_sequence` enrols pan claimants before any handler does anything —
512 /// so if that same node also carries `on_drag`, `enrol` (captured) and
513 /// `enrol_drag` (ancestor) both refuse it. Neither refusal is a decision: it
514 /// is a slot collision. Each falls through to
515 /// [`PointerSequence::defer_own_drag`](crate::gesture::PointerSequence::defer_own_drag),
516 /// which attaches the drag's activation to the member the node already has.
517 ///
518 /// The ancestor door additionally **feeds** the deferred node its `Down`,
519 /// exactly as an ordinarily-enrolled drag ancestor is fed: the press bubble
520 /// stops at the captor (`try_handler_bubble`'s `Down` arm answers `Handled`
521 /// the moment a node has an arena), so without that feed the ancestor's
522 /// `DragRecognizer` has no origin and could never latch however the
523 /// arbitration ruled. The *move* bubble has no such stop, so nothing
524 /// afterwards needs feeding.
525 pub(super) fn enrol_sequence_members(
526 &mut self,
527 down_event: &WidgetEvent,
528 ops: &mut dyn crate::window::WindowOps,
529 ) {
530 use crate::gesture::MemberRole;
531
532 let captured = self.current_pointer_capture();
533 let profile = self.current_profile();
534
535 // Which ancestors compete, decided against the frozen path.
536 let Some(to_enrol) = self.with_sequence(|tree, sequence| {
537 sequence.set_capture(captured);
538 sequence.set_pressed_owner(captured);
539 // A decided sequence still enrols its competitors — as **rejected**
540 // ones. Enrolling them is what lets the router stop their
541 // recognizers from being fed at all: an ancestor that never becomes
542 // a member is invisible to the arbitration, and an explicit captor
543 // would lose the press to it on the very next move.
544 let decided = sequence.is_decided();
545 let Some(captured) = captured else {
546 // Nothing took the press, so there is nothing for an ancestor
547 // to observe *through* — the pre-existing gate, kept verbatim.
548 return Vec::new();
549 };
550 if tree.widget_has_drag(captured) {
551 // The innermost drag owns the gesture: it is the member, and no
552 // ancestor is. Its own arena drives it through the capture
553 // route, so it is never fed here.
554 if sequence.enrol(captured, MemberRole::Gesture) {
555 if decided {
556 sequence.reject(captured);
557 }
558 } else {
559 // Already enrolled — today only as the pan claimant
560 // `begin_sequence` put there before any handler ran. One
561 // node, one member, so its drag does not get a slot of its
562 // own; `defer_own_drag` gives it a say on the member it
563 // has, resolving its `DragActivation` against the pan it is
564 // competing with. A mouse enrols no pan member, so it never
565 // reaches this arm.
566 let activation = Self::sequence_drag_activation(tree, sequence, captured);
567 sequence.defer_own_drag(captured, activation, &profile);
568 }
569 return Vec::new();
570 }
571 if !sequence.may_enrol(captured) {
572 // The press landed inside a dead zone (the captured control
573 // *is* the dead zone) — arm no ancestor at all.
574 return Vec::new();
575 }
576 let mut out = Vec::new();
577 let mut current = tree.arena.parent(captured);
578 while let Some(id) = current {
579 if !sequence.may_enrol(id) {
580 break;
581 }
582 if tree.widget_has_drag(id) {
583 let activation = Self::sequence_drag_activation(tree, sequence, id);
584 if sequence.enrol_drag(id, MemberRole::Gesture, activation, &profile) {
585 if decided {
586 sequence.reject(id);
587 } else {
588 out.push((id, true));
589 }
590 } else if sequence.defer_own_drag(id, activation, &profile) {
591 // The second door to the same refusal, and the one a
592 // *heavyweight* child's press goes through: `enrol_drag`
593 // opens with `enrol`, which declines an ancestor already
594 // enrolled as this sequence's pan claimant. Without this
595 // arm that ancestor's drag is never fed the press at all
596 // — so a dual-role container could not be dragged from
597 // anywhere its own content took the capture.
598 //
599 // No `decided` guard, unlike the branch above: a decided
600 // sequence is `defer_own_drag`'s own first refusal, so
601 // this arm is unreachable once one exists.
602 out.push((id, true));
603 }
604 }
605 current = tree.arena.parent(id);
606 }
607 out
608 }) else {
609 return;
610 };
611
612 for (id, feed) in to_enrol {
613 if !feed {
614 continue;
615 }
616 // Build the arena (the bubble never reached this ancestor) and feed
617 // it the press so its DragRecognizer records the origin.
618 {
619 let WidgetTree {
620 arena,
621 gesture_owners,
622 ..
623 } = self;
624 if let Some(node) = arena.get_mut(id) {
625 Self::ensure_gesture_arena(node, id, gesture_owners);
626 }
627 }
628 self.feed_member_arena(id, down_event, &mut *ops);
629 }
630 }
631
632 /// The [`DragActivation`](teksilo_tokens::DragActivation) that governs
633 /// `id`'s own drag for **this** press: the per-press override a handler
634 /// queued with
635 /// [`set_drag_activation`](crate::widget::EventContext::set_drag_activation),
636 /// or failing that the node's build-time declaration.
637 ///
638 /// The override is read off the sequence rather than the node because a
639 /// press handler answers per *press*, not per node — see
640 /// `PointerSequence::drag_activation_overrides`.
641 fn sequence_drag_activation(
642 tree: &WidgetTree,
643 sequence: &crate::gesture::PointerSequence,
644 id: WidgetId,
645 ) -> teksilo_tokens::DragActivation {
646 sequence.drag_activation_override(id).unwrap_or_else(|| {
647 tree.arena
648 .get(id)
649 .map(|n| n.drag_activation)
650 .unwrap_or(teksilo_tokens::DragActivation::Auto)
651 })
652 }
653
654 /// A handler chose a [`DragActivation`](teksilo_tokens::DragActivation) for
655 /// **this press** with
656 /// [`set_drag_activation`](crate::widget::EventContext::set_drag_activation).
657 ///
658 /// Stashed on the sequence, so it dies with the press. Writing it back onto
659 /// the node would outlive the press it was chosen for — and
660 /// `on_pointer_event` previews root-first over every strict ancestor of the
661 /// target, so a node that answers here also answers for presses it does not
662 /// own.
663 pub(super) fn note_drag_activation_override(
664 &mut self,
665 source: WidgetId,
666 activation: teksilo_tokens::DragActivation,
667 ) {
668 self.with_sequence(|_, sequence| {
669 sequence.set_drag_activation_override(source, activation);
670 });
671 }
672
673 /// Record where the pointer is, so every positional threshold reads one
674 /// number rather than each member tracking its own.
675 pub(super) fn note_sequence_position(&mut self, position: teksilo_canvas::Point) {
676 let pointer = self.current_pointer_id();
677 if let Some(entry) = self.pointers.get_mut(pointer)
678 && let Some(sequence) = entry.sequence.as_mut()
679 {
680 sequence.set_last_position(position);
681 }
682 }
683
684 /// Now, on the input timeline, for the sample being dispatched.
685 ///
686 /// A hand-built `WidgetEvent` carries no timestamp, so it reads the tree
687 /// clock — which is what lets a test drive a deadline with a
688 /// [`ManualClock`](crate::pointer::clock::ManualClock).
689 pub(super) fn sequence_now(&self) -> crate::pointer::EventTime {
690 let stamped = self.current_input.pointer.time;
691 if stamped == crate::pointer::EventTime::ZERO {
692 self.input_now()
693 } else {
694 stamped
695 }
696 }
697
698 /// **Timers before positional thresholds** — step 4 of the decision
699 /// procedure, run before the move is dispatched anywhere.
700 ///
701 /// It has to be before: a member reached through the ordinary capture
702 /// bubble would otherwise recognize on this very sample, and a deferred
703 /// drag that should have withdrawn — or a peer that should have been frozen
704 /// by a hold — would already have won by the time the arbitration was
705 /// consulted.
706 ///
707 /// Three rules, all no-ops for a press that stayed where it landed:
708 ///
709 /// * a hold older than `profile.max_hold` is released, because the
710 /// framework never trusts a holder to answer — the one rule here that is
711 /// *also* driven by the clock, through
712 /// [`expire_sequence_holds`](Self::expire_sequence_holds), so a contact
713 /// that never moves is released on time too;
714 /// * a member armed by [`DragActivation::AfterLongPress`](teksilo_tokens::DragActivation::AfterLongPress) withdraws once
715 /// the press leaves the tap boundary — that travel is a pan, not a
716 /// considered grab — and so does the *self-drag* half of a dual-role
717 /// member, which is deferred by the same resolution but cannot withdraw
718 /// the member itself (the member is a pan claimant and goes on competing).
719 /// The self-drag has a **second** positional rule the whole-member case
720 /// does not: travel past `long_press_slop` before the deadline, the rule
721 /// [`LongPressRecognizer`](crate::gesture::LongPressRecognizer) applies to
722 /// itself, which is what makes its deferral a hold rather than a timer;
723 /// * the pressed node's **tap family** is revoked, once, when the press
724 /// leaves the tap boundary — WCAG 2.2 SC 2.5.2's "slide off to abort":
725 /// the activation is abandoned, a drag the same press started is not.
726 ///
727 /// All of them but that second self-drag rule read the one
728 /// [`TapBoundary`](crate::gesture::TapBoundary) predicate, which is also
729 /// what `TapRecognizer` fails on, so the router and the recognizer cannot
730 /// disagree about whether a press has slid off.
731 pub(super) fn tick_sequence_timers(&mut self) {
732 use crate::gesture::MemberState;
733
734 let profile = self.current_profile();
735 let now = self.sequence_now();
736 let Some(revoke) = self.with_sequence(|tree, sequence| {
737 sequence.expire_holds(now, &profile);
738 if sequence.is_decided() {
739 return None;
740 }
741 let origin = sequence.press_origin();
742 let position = sequence.last_position();
743 let boundary = crate::gesture::TapBoundary::for_pointer(&sequence.pointer(), &profile);
744 let left = |id: WidgetId| {
745 let bounds = tree.arena.is_active(id).then(|| tree.arena.bounds(id));
746 boundary.left(origin, position, bounds, &profile)
747 };
748 let rejects: Vec<WidgetId> = sequence
749 .members()
750 .iter()
751 .filter(|m| m.state == MemberState::Possible && m.rejects_on_tap_slop)
752 .filter(|m| left(m.id))
753 .map(|m| m.id)
754 .collect();
755 for id in rejects {
756 sequence.reject(id);
757 }
758 // The *self*-drag half of a dual-role member withdraws on either
759 // positional rule, because the member itself is a pan claimant and
760 // goes on competing — only its drag half can be taken out.
761 //
762 // `long_press_slop` is the load-bearing one, and it is the rule
763 // `LongPressRecognizer` applies to itself: it fails on the first
764 // move past that slop rather than waiting for its timer. Arming the
765 // self-drag on the clock alone made the deferral a *timer*, so a
766 // deliberate, slow pan — a finger positioning precisely, which is
767 // exactly when a scene is panned slowly — crossed the deadline
768 // mid-travel, armed the grab and lost the pan for the rest of the
769 // press. A hold that has already wandered 18 dp is not a hold.
770 //
771 // `TapBoundary` stays beside it and is not redundant: it is the
772 // node's own rect for a coarse pointer, so it bites on a small node
773 // the finger slides off without travelling far, where the radius
774 // does not. Conversely a viewport-filling claimant is never left, so
775 // on that shape the radius is the only positional rule there is.
776 //
777 // Both apply only while the self-drag is still unripe. Once the hold
778 // has been served the grab is live, and travel is the grab doing its
779 // job.
780 let travelled_past_hold = sequence.travel() > profile.long_press_slop;
781 let withdraw: Vec<WidgetId> = sequence
782 .unripe_own_drag_members(now)
783 .into_iter()
784 .filter(|id| travelled_past_hold || left(*id))
785 .collect();
786 for id in withdraw {
787 sequence.withdraw_own_drag(id);
788 }
789 let owner = sequence.pressed_owner()?;
790 if sequence.taps_cancelled() || !left(owner) {
791 return None;
792 }
793 sequence.set_taps_cancelled();
794 Some(owner)
795 }) else {
796 return;
797 };
798 let Some(owner) = revoke else {
799 return;
800 };
801 let pointer = self.current_pointer_id();
802 if let Some(node) = self.arena.get_mut(owner)
803 && let Some(set) = node.handlers.gesture_arena.as_mut()
804 {
805 set.cancel_taps(pointer);
806 }
807 }
808
809 /// Advance an undecided sequence with a move: **timers before positional
810 /// thresholds**, then members innermost-first.
811 ///
812 /// * a `RawDrag` member wins past the sequence's latch slop;
813 /// * a `Gesture` member wins when its own recognizer recognizes — which for
814 /// a mouse is at `drag_slop`, the 5.0 it has always been;
815 /// * a `Pan` member wins only on an axis the frozen `TouchAction` permits
816 /// and only past `pan_slop`, which a mouse profile does not have;
817 /// * a member deferred by [`DragActivation::AfterLongPress`](teksilo_tokens::DragActivation::AfterLongPress) cannot win
818 /// before its timer and self-rejects once the press leaves the tap
819 /// boundary.
820 ///
821 /// A `Gesture` member whose id is the press owner is skipped **and stops
822 /// the walk**: its recognizer is already being driven by the capture
823 /// dispatch, and letting an ancestor past it would break the "innermost
824 /// drag owns the gesture" rule. A `RawDrag` and a `Pan` are not, because
825 /// this walk is the *only* place either is ever evaluated — the press owner
826 /// is very often a pan claimant, since an implicit arena capture makes any
827 /// node with a tap handler the owner, and an editing surface has both.
828 /// `RawPreview` rides along in the same match and is dead there: a preview
829 /// claim decides the sequence as it is enrolled, and a decided sequence
830 /// yields no candidates at all.
831 ///
832 /// A **dual-role** member — one whose node also owns `on_drag`, so
833 /// [`PointerSequence::defer_own_drag`](crate::gesture::PointerSequence::defer_own_drag)
834 /// attached its self-drag to the same slot — is evaluated here **only** as
835 /// the pan it is enrolled as. Its own recognizers are driven by the ordinary
836 /// move bubble and gated there by `sequence_blocks_arena`, which reads the
837 /// same deferral, so its drag half needs nothing from this walk.
838 ///
839 /// That asymmetry is the bubble's, not this walk's, and it is worth stating
840 /// because it decides where a dual-role node's press and its moves each come
841 /// from. `try_handler_bubble`'s **`Down`** arm returns `Handled` the moment a
842 /// node has an arena, so the press bubble stops at the captor and an
843 /// *ancestor*'s recognizers never see the origin — which is why
844 /// `enrol_sequence_members` feeds the `Down` explicitly, for a dual-role
845 /// ancestor exactly as for an ordinary drag ancestor. Its **`Move`** arm
846 /// returns `Ignored` when nothing recognized, so the move bubble carries on
847 /// past the captor and reaches every ancestor by itself.
848 pub(super) fn advance_sequence(
849 &mut self,
850 move_event: &WidgetEvent,
851 ops: &mut dyn crate::window::WindowOps,
852 ) {
853 use crate::gesture::MemberRole;
854
855 let profile = self.current_profile();
856 let now = self.sequence_now();
857
858 let Some(candidates) = self.with_sequence(|_, sequence| {
859 if sequence.is_decided() || sequence.is_held() {
860 // No peer may win while a member is deferring its own answer.
861 return Vec::new();
862 }
863 sequence
864 .members()
865 .iter()
866 .filter(|m| m.is_eligible_at(now))
867 .map(|m| (m.id, m.role))
868 .collect()
869 }) else {
870 return;
871 };
872
873 // The stop rule reads the node whose arena took the **press**, not the
874 // live captor: a member that wins mid-dispatch takes the capture, and
875 // keying on the live value would make the winner look like the thing
876 // that stops the walk.
877 let owner = self.current_sequence().and_then(|s| s.pressed_owner());
878 for (id, role) in candidates {
879 // The stop rule is about the roles the **capture dispatch** drives,
880 // and only those. A `Gesture` member's recognizer is fed through the
881 // ordinary capture bubble, so evaluating it here would double-drive
882 // it, and letting an ancestor past it would break "the innermost
883 // drag owns the gesture" — it stops the walk.
884 //
885 // A `RawDrag` and a `Pan` are both decided *here* and nowhere else:
886 // a raw drag on the sequence's own travel, a pan on
887 // `pan_axis_past_slop`, whose product is a synthesised `Scroll`
888 // rather than a `GestureEvent` fed to an arena. Breaking at either
889 // would mean a member that also owns the press arena could never
890 // win — and for a pan claimant that is every editing surface, which
891 // takes the press for its caret (so the implicit arena capture makes
892 // it the `pressed_owner`) and scrolls itself under a finger.
893 if Some(id) == owner && matches!(role, MemberRole::Gesture | MemberRole::RawPreview) {
894 break;
895 }
896 let won = match role {
897 MemberRole::Gesture => self.feed_member_arena(id, move_event, &mut *ops),
898 MemberRole::RawDrag => self
899 .current_sequence()
900 .is_some_and(|s| s.travel() >= s.latch_slop(&profile)),
901 // A **dual-role** member is evaluated here only as the pan it is
902 // enrolled as; its own recognizers ride the ordinary move
903 // bubble, which reaches them either way. See this function's
904 // doc comment for why that asymmetry is the bubble's.
905 MemberRole::Pan(claim) => self
906 .current_sequence()
907 .and_then(|s| s.pan_axis_past_slop(&claim, &profile))
908 .is_some(),
909 MemberRole::RawPreview => false,
910 };
911 // The `active_drag` half is a **guard**, not a second way in. The
912 // router enters this walk only while no drag is in flight (both
913 // call sites in `pointer_router.rs` test `active_drag.is_none()`),
914 // and the only thing here that can raise one is
915 // `feed_member_arena` on this very candidate — which reports it as
916 // `won` in the same breath. So the `else` winner below is not
917 // reachable on today's call paths, and the winner recorded is
918 // always the member that won. What DOES happen when a surface
919 // raises its own drag from the capture dispatch is that this walk
920 // is not entered at all, leaving the sequence undecided:
921 // `a_drag_raised_by_the_press_owner_takes_the_press_out_of_the_arbitration`
922 // (`pan_arbiter/tests.rs`) pins that.
923 if won || self.active_drag.is_some() {
924 let winner = if won { id } else { owner.unwrap_or(id) };
925 self.decide_sequence(winner);
926 // A pan claimant that won owns the rest of the press as a
927 // *scroll*: from here every sample for this contact is
928 // synthesised onto the claimant chain rather than delivered as
929 // a pointer move. Recorded only for a genuine pan win.
930 if won && matches!(role, MemberRole::Pan(_)) {
931 let pointer = self.current_pointer_id();
932 self.note_pan_claimed(pointer, winner);
933 // The pan half of a dual-role member won, so its self-drag
934 // is out for the rest of the press. Without this the
935 // member's `Won` state would unblock its arena and a
936 // deferral ripening mid-pan would start its drag under a
937 // scrolling finger. Inert for a claimant that carries no
938 // drag of its own.
939 self.with_sequence(|_, sequence| sequence.withdraw_own_drag(winner));
940 // …and the claimant's own **tap family** goes with it. A pan
941 // that won IS the press; leaving the winner's
942 // `TapRecognizer` armed fires its `on_tap` on the release as
943 // well, and on a viewport-filling claimant the slide-off
944 // sweep never revokes it — a coarse pointer's tap boundary
945 // is the node's own rect, and a finger panning a full-window
946 // surface never leaves it. Tap-family only: the same
947 // `cancel_taps` grain WCAG 2.2 SC 2.5.2's slide-off rule
948 // uses, so a drag the same press started is untouched and
949 // the winner receives no `PointerCancel`.
950 self.cancel_member_taps(winner, pointer);
951 }
952 return;
953 }
954 }
955 }
956
957 /// Declare `winner` the owner of the current sequence and cancel every
958 /// competitor it knocked out — each exactly once.
959 ///
960 /// "Exactly once" is structural rather than bookkept:
961 /// [`PointerSequence::decide`](crate::gesture::PointerSequence::decide)
962 /// reports only the members that were still live and flips them to
963 /// `Rejected` as it goes, and a decided sequence returns early above — so a
964 /// loser knocked out in an earlier sample cannot be knocked out again.
965 pub(super) fn decide_sequence(&mut self, winner: WidgetId) {
966 let mut noop = crate::window::NoopWindowOps;
967 self.decide_sequence_with_ops(winner, &mut noop);
968 }
969
970 /// [`decide_sequence`](Self::decide_sequence) with the caller's
971 /// [`WindowOps`](crate::window::WindowOps), so a loser's
972 /// `on_pointer_cancel` can reach the multi-window API like any other
973 /// handler.
974 pub(super) fn decide_sequence_with_ops(
975 &mut self,
976 winner: WidgetId,
977 ops: &mut dyn crate::window::WindowOps,
978 ) {
979 let Some(losers) = self.with_sequence(|_, sequence| {
980 if sequence.is_decided() {
981 return Vec::new();
982 }
983 crate::trace_input!(
984 Gestures,
985 "sequence for {:?} decided: {:?}",
986 sequence.pointer().id,
987 winner
988 );
989 sequence.decide(winner)
990 }) else {
991 return;
992 };
993 let pointer = self.current_pointer_id();
994 for id in losers {
995 // Member-level, not pointer-level: the pointer is very much alive
996 // and its winner is about to go on using it.
997 self.revoke_sequence_member(
998 pointer,
999 id,
1000 crate::pointer::CancelReason::PeerClaimed,
1001 &mut *ops,
1002 );
1003 }
1004 }
1005
1006 /// Revoke only `id`'s **tap family** for `pointer` — tap, double tap,
1007 /// triple tap, long press — and leave everything else on the node running.
1008 ///
1009 /// The [`cancel_taps`](crate::gesture::GestureArenaSet::cancel_taps) grain,
1010 /// not [`cancel`](crate::gesture::GestureArenaSet::cancel): the node has not
1011 /// had an interaction taken away, so it is sent no `PointerCancel`, and a
1012 /// drag the same press is driving survives.
1013 pub(super) fn cancel_member_taps(&mut self, id: WidgetId, pointer: crate::pointer::PointerId) {
1014 if let Some(node) = self.arena.get_mut(id)
1015 && let Some(set) = node.handlers.gesture_arena.as_mut()
1016 {
1017 set.cancel_taps(pointer);
1018 }
1019 }
1020
1021 /// Take a member out of the running and revoke whatever its recognizers had
1022 /// accumulated for this contact.
1023 pub(super) fn cancel_member_arena(&mut self, id: WidgetId, pointer: crate::pointer::PointerId) {
1024 if let Some(node) = self.arena.get_mut(id)
1025 && let Some(set) = node.handlers.gesture_arena.as_mut()
1026 {
1027 set.cancel(pointer);
1028 }
1029 }
1030
1031 /// Re-check every member against the arena, once per sample.
1032 ///
1033 /// A member whose node was destroyed is cancelled **individually** and
1034 /// dropped; the sequence itself dies only when its winner or its captor
1035 /// goes away, because those are the two nodes the press actually belongs
1036 /// to.
1037 pub(super) fn revalidate_sequence(&mut self, ops: &mut dyn crate::window::WindowOps) {
1038 let pointer = self.current_pointer_id();
1039 let capture = self.current_pointer_capture();
1040 let Some((dead, lost_owner)) = self.with_sequence(|tree, sequence| {
1041 sequence.set_capture(capture);
1042 let dead = sequence.revalidate(&tree.arena);
1043 // Which owner died decides how the cancel reads: a winner that went
1044 // away had won the press outright, a captor that went away leaves
1045 // the capture with nobody holding it.
1046 let lost_owner = sequence
1047 .lost_owner(&tree.arena)
1048 .then(|| match sequence.winner() {
1049 Some(winner) if !tree.arena.is_active(winner) => {
1050 crate::pointer::CancelReason::WidgetDestroyed
1051 }
1052 _ => crate::pointer::CancelReason::CaptureOrphaned,
1053 });
1054 if lost_owner.is_some() {
1055 // The cancel that finishes the teardown is queued behind this
1056 // sample, so the sequence outlives this line by one dispatch.
1057 // Nothing may win a press whose owner is already gone in the
1058 // meantime — withdraw every competitor now, which also silences
1059 // their recognizers for the rest of the sample
1060 // (`sequence_blocks_arena`).
1061 let live: Vec<WidgetId> = sequence.members().iter().map(|m| m.id).collect();
1062 for id in live {
1063 sequence.reject(id);
1064 }
1065 }
1066 (dead, lost_owner)
1067 }) else {
1068 return;
1069 };
1070 for id in dead {
1071 self.revoke_sequence_member(
1072 pointer,
1073 id,
1074 crate::pointer::CancelReason::WidgetDestroyed,
1075 &mut *ops,
1076 );
1077 }
1078 if let Some(reason) = lost_owner {
1079 crate::trace_input!(
1080 Gestures,
1081 "sequence for {pointer:?} cancelled: its owner is gone ({reason:?})"
1082 );
1083 self.cancel_pointer(pointer, reason, &mut *ops);
1084 }
1085 }
1086
1087 /// The release sweep. The pointer sequence ended without a positional
1088 /// competitor latching, so feed the terminating `Up` to every member that
1089 /// is still following the press.
1090 ///
1091 /// This is what stops an ancestor `DragRecognizer` — armed on the press
1092 /// while an interactive descendant held the capture — from staying armed
1093 /// indefinitely and starting a phantom drag on the next *hover* move.
1094 pub(super) fn end_sequence(
1095 &mut self,
1096 up_event: &WidgetEvent,
1097 ops: &mut dyn crate::window::WindowOps,
1098 ) {
1099 use crate::gesture::MemberRole;
1100
1101 let pointer = self.current_pointer_id();
1102 let capture = self.current_pointer_capture();
1103 let Some(members) = self.with_sequence(|_, sequence| {
1104 sequence.set_terminating(true);
1105 sequence.live_ids_with(|role| matches!(role, MemberRole::Gesture))
1106 }) else {
1107 return;
1108 };
1109 for id in members {
1110 if Some(id) == capture {
1111 // Its own arena is about to see this `Up` through the capture
1112 // dispatch; feeding it twice would count the release twice.
1113 continue;
1114 }
1115 // The press belongs to the captor, so a member's tap family is
1116 // revoked first: its TapRecognizer saw the whole press, and fed the
1117 // `Up` it would fire the member's tap beside the captor's (a button
1118 // in a dock collapsing the accordion whose header drags the dock).
1119 // What is left, an `Up` while the recognizer is not mid-drag,
1120 // resolves it to `Failed` and clears `down_position`, so this only
1121 // tidies recognizer state.
1122 self.cancel_member_taps(id, pointer);
1123 self.feed_member_arena(id, up_event, &mut *ops);
1124 }
1125 if let Some(entry) = self.pointers.get_mut(pointer) {
1126 entry.sequence = None;
1127 }
1128 }
1129
1130 /// Stop every gesture arena that is still following `pointer`.
1131 ///
1132 /// A press ends at exactly one node — the captor — and the release is
1133 /// delivered only there, so any *other* arena that saw the `Down` is left
1134 /// following a contact that no longer exists. It happens on the ordinary
1135 /// path: an ancestor drag that wins a press a tapping descendant was
1136 /// holding takes the capture with it, and the descendant's arena never
1137 /// sees the `Up`.
1138 ///
1139 /// A stale entry is not inert. The next press reuses it instead of
1140 /// instantiating fresh recognizers, so a contact starts mid-gesture on
1141 /// state left over from the previous one. Ended rather than cancelled: the
1142 /// press was completed, not revoked, and the node's
1143 /// [`TapStreak`](crate::gesture::TapStreak) — which lives outside the live
1144 /// set precisely so it can outlive a contact — must survive, or touch
1145 /// double-tap would be impossible.
1146 pub(super) fn release_arenas_following(&mut self, pointer: crate::pointer::PointerId) {
1147 let owners: Vec<WidgetId> = self.gesture_owners.iter().copied().collect();
1148 for id in owners {
1149 if let Some(node) = self.arena.get_mut(id)
1150 && let Some(set) = node.handlers.gesture_arena.as_mut()
1151 {
1152 set.end(pointer);
1153 }
1154 }
1155 }
1156
1157 /// Apply the arbitration acts a handler queued on its context.
1158 pub(super) fn apply_gesture_acts(
1159 &mut self,
1160 acts: &[crate::widget::GestureAct],
1161 source: WidgetId,
1162 ) {
1163 use crate::widget::GestureAct;
1164
1165 let now = self.sequence_now();
1166 let mut claim = false;
1167 self.with_sequence(|_, sequence| {
1168 for act in acts {
1169 match act {
1170 GestureAct::Claim => claim = true,
1171 GestureAct::Reject => {
1172 claim = false;
1173 sequence.reject(source);
1174 }
1175 GestureAct::Hold => {
1176 claim = false;
1177 if !sequence.has_member(source) {
1178 sequence.enrol(source, crate::gesture::MemberRole::Gesture);
1179 }
1180 sequence.hold(source, now);
1181 }
1182 GestureAct::Release => {
1183 sequence.release_hold(source);
1184 }
1185 }
1186 }
1187 });
1188 if claim {
1189 self.with_sequence(|_, sequence| {
1190 if !sequence.has_member(source) {
1191 sequence.enrol(source, crate::gesture::MemberRole::Gesture);
1192 }
1193 });
1194 self.decide_sequence(source);
1195 }
1196 }
1197
1198 /// A handler took the pointer with an explicit
1199 /// [`capture_pointer`](crate::widget::EventContext::capture_pointer).
1200 ///
1201 /// That is an arbitration act, not plumbing: the caller is enrolled as a
1202 /// [`MemberRole::RawDrag`](crate::gesture::MemberRole::RawDrag) competitor,
1203 /// and for a precise pointer with no eligible pan competitor the sequence
1204 /// is decided there and then — which is what makes the splitter handle, the
1205 /// dock resize handle and the table column grip win their own presses
1206 /// instead of losing them to an ancestor that happens to carry `on_drag`.
1207 pub(super) fn note_explicit_capture(&mut self, source: WidgetId) {
1208 use crate::gesture::MemberRole;
1209
1210 let Some(decide) = self.with_sequence(|_, sequence| {
1211 if sequence.is_decided() {
1212 return false;
1213 }
1214 if !sequence.enrol(source, MemberRole::RawDrag) && !sequence.has_member(source) {
1215 return false;
1216 }
1217 // A precise pointer has no pan competitor by construction
1218 // (`GestureProfile::pan_slop` is `None` for a mouse), so this is a
1219 // decision at press. A contact defers to `drag_slop` instead, which
1220 // is what lets a scroller still beat it at `pan_slop`.
1221 !sequence.pointer().kind.is_direct() && !sequence.has_eligible_pan()
1222 }) else {
1223 return;
1224 };
1225 if decide {
1226 self.decide_sequence(source);
1227 }
1228 }
1229
1230 /// A recognizer on `source` produced a gesture that owns the rest of the
1231 /// press (a drag or a swipe). That is the observable act of winning, so it
1232 /// decides the sequence — whether the recognizer was reached through the
1233 /// ordinary capture bubble or fed by the arbitration itself.
1234 pub(super) fn note_gesture_recognized(&mut self, source: WidgetId) {
1235 use crate::gesture::MemberRole;
1236
1237 let now = self.sequence_now();
1238 let claimed = self
1239 .with_sequence(|_, sequence| {
1240 if sequence.is_decided() {
1241 return false;
1242 }
1243 // A self-drag still inside its deferral cannot claim. The arena
1244 // gate normally makes this unreachable — a blocked recognizer
1245 // produces nothing to report — but the guard is what makes "the
1246 // deferral binds every route" true by *reading* it rather than
1247 // by enumerating the routes.
1248 if sequence.own_drag_blocked(source, now) {
1249 return false;
1250 }
1251 if !sequence.has_member(source) {
1252 sequence.enrol(source, MemberRole::Gesture);
1253 }
1254 // The self-drag half of a dual-role member won: say so, so
1255 // `sequence_members` names the half that took the press rather
1256 // than the pan claim the node was also holding.
1257 sequence.promote_own_drag(source);
1258 sequence.has_member(source)
1259 })
1260 .unwrap_or(false);
1261 if claimed {
1262 self.decide_sequence(source);
1263 }
1264 }
1265
1266 /// Whether `id`'s gesture recognizers must be kept out of this pointer
1267 /// event.
1268 ///
1269 /// A member that lost — because a peer won, because it withdrew, or because
1270 /// another member is holding — must not go on recognizing through the
1271 /// ordinary bubble. Only its *recognizers* are silenced: its
1272 /// `on_pointer_event`, `on_hover` and everything else still run, because
1273 /// losing an arbitration is not the same as being removed from the tree.
1274 ///
1275 /// Inert for every sequence nothing has decided, held or rejected — which
1276 /// is every plain mouse tap.
1277 ///
1278 /// Asked of the sequence belonging to the pointer being dispatched. The
1279 /// timer-driven path has no pointer being dispatched and asks
1280 /// [`sequence_blocks_arena_for`](Self::sequence_blocks_arena_for) instead,
1281 /// naming the contact whose gesture is in hand.
1282 pub(super) fn sequence_blocks_arena(&self, id: WidgetId) -> bool {
1283 match self.current_sequence() {
1284 Some(sequence) => Self::sequence_blocks_member(sequence, id, self.sequence_now()),
1285 None => false,
1286 }
1287 }
1288
1289 /// [`sequence_blocks_arena`](Self::sequence_blocks_arena) for a named
1290 /// contact and a named instant, rather than for whatever sample is being
1291 /// dispatched.
1292 ///
1293 /// The timer path needs both: nothing is being dispatched during a tick, so
1294 /// `current_sequence` would answer about the wrong contact (or about none),
1295 /// and `sequence_now` would answer with the timestamp of the last sample
1296 /// dispatched — which for a contact resting on a control is its own press.
1297 pub(super) fn sequence_blocks_arena_for(
1298 &self,
1299 pointer: crate::pointer::PointerId,
1300 id: WidgetId,
1301 now: crate::pointer::EventTime,
1302 ) -> bool {
1303 match self.pointers.get(pointer).and_then(|e| e.sequence.as_ref()) {
1304 Some(sequence) => Self::sequence_blocks_member(sequence, id, now),
1305 None => false,
1306 }
1307 }
1308
1309 /// The rule itself, shared by both doors above.
1310 fn sequence_blocks_member(
1311 sequence: &crate::gesture::PointerSequence,
1312 id: WidgetId,
1313 now: crate::pointer::EventTime,
1314 ) -> bool {
1315 use crate::gesture::MemberState;
1316
1317 let Some(member) = sequence.members().iter().find(|m| m.id == id) else {
1318 return false;
1319 };
1320 // A node that also claims a pan has its *own* drag recognizers gated
1321 // separately from its membership: the member goes on competing as a pan
1322 // while its self-drag waits out the deferral its `DragActivation` asked
1323 // for, and stays silenced for good once that self-drag is withdrawn —
1324 // which is why the clause is read in the `Won` arm too. A withdrawal
1325 // means the pan took the press, and a pan that owns the press owns the
1326 // node's recognizers with it. Inert for every member carrying no
1327 // self-drag, which is every member a mouse ever enrols.
1328 let own_drag_armed = member.own_drag_armed_at(now);
1329 match member.state {
1330 MemberState::Rejected => true,
1331 MemberState::Won => !own_drag_armed,
1332 _ => {
1333 if !own_drag_armed {
1334 return true;
1335 }
1336 if let Some(winner) = sequence.winner() {
1337 return winner != id;
1338 }
1339 // A hold freezes every peer: no one may win while a member is
1340 // still deciding.
1341 if sequence.is_held() && member.state != MemberState::Held {
1342 return true;
1343 }
1344 // A member deferred by `DragActivation::AfterLongPress` cannot
1345 // win before its timer, and that has to hold on the ordinary
1346 // bubble too — otherwise the deferral would only bind the
1347 // arbitration's own walk.
1348 !member.is_eligible_at(now)
1349 }
1350 }
1351 }
1352
1353 /// A preview handler answered `Handled` on a press. The **root-first**
1354 /// preview pass is the first step of the decision procedure, so this claims
1355 /// the sequence outright.
1356 pub(super) fn note_preview_claim(&mut self, source: WidgetId) {
1357 use crate::gesture::MemberRole;
1358
1359 let claimed = self
1360 .with_sequence(|_, sequence| {
1361 if sequence.is_decided() {
1362 return false;
1363 }
1364 sequence.enrol(source, MemberRole::RawPreview)
1365 })
1366 .unwrap_or(false);
1367 if claimed {
1368 self.decide_sequence(source);
1369 }
1370 }
1371
1372 /// The winner of `pointer`'s sequence, if one has been decided.
1373 pub fn sequence_winner(&self, pointer: crate::pointer::PointerId) -> Option<WidgetId> {
1374 self.pointers
1375 .get(pointer)
1376 .and_then(|e| e.sequence.as_ref())
1377 .and_then(|s| s.winner())
1378 }
1379
1380 /// Every competitor for `pointer`'s press, innermost first.
1381 ///
1382 /// The observable form of the cross-widget arbitration, and the successor
1383 /// to the old `armed_drag_observers()`: an app can assert that a press on a
1384 /// control inside a draggable container enrols no ancestor at all.
1385 pub fn sequence_members(
1386 &self,
1387 pointer: crate::pointer::PointerId,
1388 ) -> Vec<(
1389 WidgetId,
1390 crate::gesture::MemberRole,
1391 crate::gesture::MemberState,
1392 )> {
1393 self.pointers
1394 .get(pointer)
1395 .and_then(|e| e.sequence.as_ref())
1396 .map(|s| s.member_report())
1397 .unwrap_or_default()
1398 }
1399
1400 /// The [`TouchAction`] frozen for the pointer this dispatch is serving —
1401 /// what [`EventContext::touch_action`](crate::widget::EventContext::touch_action)
1402 /// reports.
1403 pub(crate) fn current_frozen_touch_action(&self) -> TouchAction {
1404 self.current_sequence()
1405 .map(|s| s.touch_action())
1406 .unwrap_or(TouchAction::AUTO)
1407 }
1408
1409 /// The [`TouchAction`] frozen for `pointer`'s press.
1410 pub fn sequence_touch_action(&self, pointer: crate::pointer::PointerId) -> TouchAction {
1411 self.pointers
1412 .get(pointer)
1413 .and_then(|e| e.sequence.as_ref())
1414 .map(|s| s.touch_action())
1415 .unwrap_or(TouchAction::AUTO)
1416 }
1417
1418 /// Feed one raw pointer event to `id`'s gesture arena set WITHOUT firing
1419 /// its `on_pointer_event` or taking the implicit capture (another node
1420 /// already holds it). Returns `true` if a gesture was recognized, in which
1421 /// case it is dispatched so the `on_drag` handler's `start_drag` runs and
1422 /// `active_drag` takes over.
1423 fn feed_member_arena(
1424 &mut self,
1425 id: WidgetId,
1426 event: &WidgetEvent,
1427 ops: &mut dyn crate::window::WindowOps,
1428 ) -> bool {
1429 let localized = Self::localize_event(&self.arena.local_frame(id), event);
1430 let event = localized.as_ref().unwrap_or(event);
1431 let cx = self.recognizer_context(id);
1432 let raw = match event {
1433 WidgetEvent::PointerDown {
1434 position,
1435 button,
1436 modifiers,
1437 ..
1438 } => crate::gesture::RawPointerEvent::Down {
1439 position: *position,
1440 button: *button,
1441 modifiers: *modifiers,
1442 pointer: cx.pointer,
1443 time: cx.now,
1444 },
1445 WidgetEvent::PointerMove { position, .. } => crate::gesture::RawPointerEvent::Move {
1446 position: *position,
1447 pointer: cx.pointer,
1448 time: cx.now,
1449 },
1450 WidgetEvent::PointerUp {
1451 position,
1452 button,
1453 modifiers,
1454 ..
1455 } => crate::gesture::RawPointerEvent::Up {
1456 position: *position,
1457 button: *button,
1458 modifiers: *modifiers,
1459 pointer: cx.pointer,
1460 time: cx.now,
1461 },
1462 _ => return false,
1463 };
1464 let mut ctx = self.make_event_context(&mut *ops);
1465 let WidgetTree { arena, .. } = self;
1466 let recognized = if let Some(node) = arena.get_mut(id) {
1467 if let Some(arena_ref) = node.handlers.gesture_arena.as_mut() {
1468 if let Some(gesture) = arena_ref.process(&raw, &cx) {
1469 Self::dispatch_recognized_gesture(node, gesture, &mut ctx);
1470 true
1471 } else {
1472 false
1473 }
1474 } else {
1475 false
1476 }
1477 } else {
1478 false
1479 };
1480 self.collect_from_ctx(ctx, id);
1481 recognized
1482 }
1483
1484 /// The clock a newly promoted animation must be stamped with: the same one
1485 /// the scheduler will later be ticked against.
1486 ///
1487 /// The wall clock, unless [`advance_time`](Self::advance_time) has taken
1488 /// this tree onto its simulated one and not yet handed it back — see
1489 /// [`resume_real_time`](Self::resume_real_time), which is what returns the
1490 /// answer to the wall clock, rebasing the scheduler as it goes.
1491 ///
1492 /// Everything that reads a time on the animation axis has to read it here,
1493 /// promotion and tick alike, or the two drift apart and the drift *is* the
1494 /// elapsed time the animation is measured by. Promoting at
1495 /// `Instant::now()` while ticking at [`Self::sim_clock`] gave every
1496 /// animation a start in the scheduler's future and froze its progress
1497 /// completely, with no number of further ticks recovering it — a failure
1498 /// that reproduced as a function of machine load rather than of behaviour,
1499 /// green when the suite ran alone and red once the runner filled the cores
1500 /// and each test's wall-clock time stretched past the simulated time it was
1501 /// asking for. The overlay manager keeps its real and simulated timestamps
1502 /// apart for the same reason.
1503 pub(super) fn animation_clock(&self) -> std::time::Instant {
1504 if self.sim_time_frozen {
1505 self.sim_clock
1506 } else {
1507 std::time::Instant::now()
1508 }
1509 }
1510
1511 /// Advance time-driven gesture recognizers (currently only
1512 /// [`crate::gesture::LongPressRecognizer`]) across every widget that
1513 /// has a gesture arena. Must be called by the event loop on each
1514 /// wake-up; otherwise long-press will never fire during an idle hold.
1515 ///
1516 /// When a recognizer transitions to `Recognized`, the corresponding
1517 /// handler on the owning widget is invoked with a fresh
1518 /// [`EventContext`], and any commands / overlay requests it emits are
1519 /// collected through the normal post-event path.
1520 /// Release every hold that has stood for `profile.max_hold`, across every
1521 /// contact — the time-driven half of
1522 /// [`tick_sequence_timers`](Self::tick_sequence_timers), lifted out so the
1523 /// gesture tick can run it too.
1524 ///
1525 /// Two things separate it from its move-driven sibling and are why it is a
1526 /// distinct function rather than a call to that one.
1527 ///
1528 /// * **It reads the caller's `now`, not the sample's.** `sequence_now`
1529 /// answers with the timestamp of the last event *dispatched*, which
1530 /// during a tick is the press — so calling `tick_sequence_timers` from
1531 /// here would expire holds against the instant they were taken and never
1532 /// expire anything at all.
1533 /// * **It is not scoped to the current pointer.** The rest of the sequence
1534 /// machinery serves the contact being dispatched; a tick serves the whole
1535 /// tree, and two fingers each holding on their own node must both be
1536 /// released.
1537 ///
1538 /// The other two rules in `tick_sequence_timers` — the deferred member's
1539 /// withdrawal and the tap family's revocation — stay behind, because both
1540 /// are decided by the [`TapBoundary`](crate::gesture::TapBoundary)
1541 /// against where the pointer now is. A contact that has not moved cannot
1542 /// have left the boundary, so running them here could only ever repeat the
1543 /// answer the last move already gave.
1544 pub(super) fn expire_sequence_holds(&mut self, now: crate::pointer::EventTime) {
1545 let Self {
1546 pointers,
1547 effective_theme,
1548 ..
1549 } = self;
1550 for entry in pointers.iter_mut() {
1551 let Some(sequence) = entry.sequence.as_mut() else {
1552 continue;
1553 };
1554 let profile = effective_theme.input.profile(sequence.pointer().kind);
1555 sequence.expire_holds(now, profile);
1556 }
1557 }
1558
1559 pub fn tick_gestures(&mut self, now: std::time::Instant) {
1560 let mut noop = crate::window::NoopWindowOps;
1561 self.tick_gestures_with_ops(now, &mut noop);
1562 }
1563
1564 /// App-facing variant of [`tick_gestures`](Self::tick_gestures)
1565 /// that accepts a real [`WindowOps`](crate::window::WindowOps)
1566 /// sink so gesture-recognized handlers can call the multi-window
1567 /// API synchronously.
1568 pub fn tick_gestures_with_ops(
1569 &mut self,
1570 now: std::time::Instant,
1571 ops: &mut dyn crate::window::WindowOps,
1572 ) {
1573 // Snapshot the gesture-owners set into the reusable scratch.
1574 // Previously this iterated every active widget; in practice
1575 // only a tiny fraction carry a gesture arena, so visiting the
1576 // rest was pure overhead.
1577 // `mem::take` lets the loop borrow `&mut self` for
1578 // `make_event_context` etc. without conflicting with the
1579 // scratch buffer; we put the storage back at the end.
1580 // The fling pump rides the same pass. It is an input deadline like a
1581 // long press, it is folded into the same `WaitUntil`
1582 // (`next_input_deadline`), and giving it its own call site would mean
1583 // every host had to learn a second one.
1584 self.tick_flings_with_ops(now, &mut *ops);
1585 // …and so does the press-feedback delay, for the same reason: it is an
1586 // input deadline folded into the same `WaitUntil`, and a finger resting
1587 // on a control produces no further samples to resolve it from.
1588 self.resolve_press_delays(self.event_time_for(now));
1589 // …and so does a standing hold's expiry, for the third time for the same
1590 // reason: a contact resting on a control produces no further samples,
1591 // and the framework's promise is that it stops trusting a holder after
1592 // `max_hold` — not that it stops trusting one after `max_hold` *and* a
1593 // move. See `expire_sequence_holds`.
1594 self.expire_sequence_holds(self.event_time_for(now));
1595 // …and so does the tree-owned long press, for the fourth time for the
1596 // same reason. It is not a recognizer: the affordances it reaches (a
1597 // context-menu factory the router walks up to, a tooltip on a node that
1598 // may be **disabled** and so has no arena at all) are the tree's, not a
1599 // widget's. See `super::touch_route`.
1600 self.resolve_touch_routes(self.event_time_for(now), &mut *ops);
1601
1602 let mut ids = std::mem::take(&mut self.active_ids_scratch);
1603 ids.clear();
1604 ids.extend(
1605 self.gesture_owners
1606 .iter()
1607 .copied()
1608 .filter(|id| self.arena.is_active(*id)),
1609 );
1610 let now = self.event_time_for(now);
1611 for &id in &ids {
1612 let cx = self.recognizer_context(id);
1613 let cx = crate::gesture::RecognizerContext { now, ..cx };
1614 let gestures = match self.arena.get_mut(id) {
1615 Some(node) => node
1616 .handlers
1617 .gesture_arena
1618 .as_mut()
1619 .map(|arena| arena.tick(&cx))
1620 .unwrap_or_default(),
1621 None => Vec::new(),
1622 };
1623 if gestures.is_empty() {
1624 continue;
1625 }
1626
1627 // One entry per contact: two fingers holding on the same node both
1628 // long-press, and neither may be dropped.
1629 for (pointer, gesture) in gestures {
1630 // The arbitration binds the timer path exactly as it binds the
1631 // sample path: a member that has been rejected, or that a peer's
1632 // hold has frozen, does not get to deliver a gesture just
1633 // because its own timer came due. Asked per contact, since two
1634 // fingers on one node are two independent sequences.
1635 //
1636 // It comes out one step later than on the sample path, which
1637 // withholds the *feed* — a tick is not addressed to a member,
1638 // so the whole node's recognizers advance and the gesture is
1639 // then dropped rather than deferred. That is what `Rejected`
1640 // wants anyway; for the transient `Held` case it means a peer
1641 // silenced at the instant its timer ripened loses that gesture
1642 // rather than firing it late, and the hold that silenced it is
1643 // released in this same pass (`expire_sequence_holds`, above)
1644 // once it reaches `max_hold`.
1645 if self.sequence_blocks_arena_for(pointer, id, now) {
1646 continue;
1647 }
1648 // One hold cannot mean two things. Where the hold is what arms
1649 // a grab — a reorderable row under a finger, whose drag member
1650 // was deferred to this very deadline, or one inside a node that
1651 // declared `LongPressRole::DragHandle` — the row's own long
1652 // press does not also fire.
1653 //
1654 // A mouse keeps its hold wherever the claim was *inferred*: it
1655 // enrols no pan competitor, so `DragActivation::Auto` is never
1656 // resolved to `AfterLongPress` on its sequence and nothing is
1657 // deferred by that route, and the `DragHandle` walk is gated on
1658 // a direct pointer because a mouse spends no hold arming a drag
1659 // it never asked for. A node that *declares*
1660 // `DragActivation::AfterLongPress` has asked: the declaration
1661 // passes through `resolve_activation` untouched, so the grab is
1662 // deferred to the hold for every pointer kind and that node's
1663 // own long press is spent under a mouse too. See
1664 // `long_press_is_a_grab` for the three doors.
1665 if matches!(gesture, crate::gesture::GestureEvent::LongPress(_))
1666 && self.long_press_is_a_grab(pointer, id)
1667 {
1668 continue;
1669 }
1670 // Install the contact this gesture belongs to for the length
1671 // of the dispatch. A hold is recognised here, by a deadline,
1672 // not by a sample — and `current_input` is saved-and-restored
1673 // around every dispatch (`run_one_dispatch`), so without this
1674 // it holds `InputSnapshot::default()` and every handler reached
1675 // from a hold is told it is serving **the mouse**, whatever the
1676 // device was.
1677 //
1678 // The snapshot is more than the device: it is the key
1679 // `make_event_context` builds the rest of the context from, so
1680 // every answer looked up by `current_pointer_id()` comes out
1681 // for the wrong pointer without it. What the context carries
1682 // in, and all of it: the device and the id the snapshot holds
1683 // outright (`EventContext::pointer_kind`, `pointer`), the
1684 // captor (`current_pointer_capture`), the frozen `TouchAction`
1685 // (`current_frozen_touch_action`, keyed through the contact's
1686 // sequence) and the press snapshot (`current_press_snapshot`).
1687 // What the context carries back out is a separate list, below.
1688 // The fling pump resolves its pointer from the table the same
1689 // way (`dispatch_chained_scroll`).
1690 let installed = self
1691 .pointers
1692 .get(pointer)
1693 .map(|entry| entry.info)
1694 .unwrap_or(self.current_input.pointer);
1695 let previous_input = std::mem::replace(
1696 &mut self.current_input,
1697 crate::pointer::InputSnapshot::for_recognized_gesture(installed),
1698 );
1699 let mut ctx = self.make_event_context(&mut *ops);
1700 if let Some(node) = self.arena.get_mut(id) {
1701 Self::dispatch_recognized_gesture(node, gesture, &mut ctx);
1702 }
1703 // `collect_from_ctx` **after** the restore would be wrong:
1704 // two of the requests a handler can queue name no pointer and
1705 // are applied to whichever one `current_pointer_id()` answers
1706 // with at collection time — an unnamed
1707 // `capture_pointer()`/`release_pointer()`, and
1708 // `cancel_pointer_sequence()`. Collected after the restore,
1709 // a hold that captured would have captured the mouse and a
1710 // hold that cancelled would have cancelled it. The sample path
1711 // has the same order — `run_one_dispatch` restores only once
1712 // `dispatch_event_impl`, collection included, has returned.
1713 self.collect_from_ctx(ctx, id);
1714 self.current_input = previous_input;
1715 }
1716 self.arena.mark_needs_paint(id);
1717 }
1718 self.active_ids_scratch = ids;
1719 }
1720
1721 /// Read an `Instant` handed in by the event loop on this tree's input
1722 /// timeline.
1723 ///
1724 /// Three answers, and not one of them is unconditionally the plain
1725 /// subtraction from the shared epoch (see
1726 /// [`input_clock`](Self::input_clock)) that a single axis would suggest:
1727 /// two ignore the caller's instant outright, and the third subtracts and
1728 /// then adds back whatever was advanced before the axis was handed back.
1729 ///
1730 /// * **Time is simulated.** The argument is discarded: the tree has one
1731 /// now, and it is not the caller's — an `Instant` handed in by a real
1732 /// event loop is on an axis this tree has stopped following.
1733 /// * **Time is real, under an anchored clock.** The caller's instant is
1734 /// honoured, as the distance from the shared epoch, *plus* whatever was
1735 /// advanced before the axis was handed back — a subtraction and then an
1736 /// addition, because the axis runs `sim_input_offset` ahead of the clock
1737 /// the caller read.
1738 /// * **A clock with no wall-clock anchor** — a
1739 /// [`ManualClock`](crate::pointer::clock::ManualClock) in a test — also
1740 /// ignores the argument and answers with its own reading, which is the
1741 /// whole point of installing one.
1742 pub(super) fn event_time_for(&self, now: std::time::Instant) -> crate::pointer::EventTime {
1743 match self.input_clock.epoch() {
1744 // While the axis is frozen the tree has one now, and it is not the
1745 // caller's: an `Instant` handed in by a real event loop is on an
1746 // axis this tree has stopped following for the length of the
1747 // advance.
1748 Some(_) if self.sim_input_origin.is_some() => self.input_now(),
1749 // Otherwise the caller's instant is honoured — two real events
1750 // milliseconds apart must not be stamped the same moment — shifted
1751 // by whatever was advanced before the axis was handed back.
1752 Some(epoch) => {
1753 crate::pointer::EventTime::from_duration(now.saturating_duration_since(epoch))
1754 + self.sim_input_offset
1755 }
1756 None => self.input_now(),
1757 }
1758 }
1759
1760 /// Turn an input-timeline deadline back into an `Instant` for the event
1761 /// loop, which schedules in wall-clock terms.
1762 pub(super) fn instant_for(&self, time: crate::pointer::EventTime) -> std::time::Instant {
1763 match self.input_clock.epoch() {
1764 // Inverse of the frozen branch of `event_time_for`: a deadline on
1765 // the virtual axis is reported against the virtual clock, so what
1766 // comes back is comparable with `simulated_now()` and not with a
1767 // wall clock this tree is not following for the length of the
1768 // advance.
1769 Some(_) if self.sim_input_origin.is_some() => {
1770 self.sim_clock + time.saturating_since(self.input_now())
1771 }
1772 // Inverse of the offset branch. Subtracting what was advanced is
1773 // what keeps this in the future: the deadline was stamped on an
1774 // axis running `sim_input_offset` ahead of the clock the event loop
1775 // schedules against, and reporting it unshifted would hand back an
1776 // instant already past — a `WaitUntil` that can never ripen and a
1777 // loop that spins on it.
1778 Some(epoch) => epoch + time.as_duration().saturating_sub(self.sim_input_offset),
1779 // An unanchored clock has no wall-clock answer; the best available
1780 // one is "as far from now as it is from the clock's reading".
1781 None => std::time::Instant::now() + time.saturating_since(self.input_now()),
1782 }
1783 }
1784
1785 /// Earliest wall-clock deadline at which any active gesture arena
1786 /// needs [`WidgetTree::tick_gestures`] called — typically a pending
1787 /// long-press timeout. Returns `None` when no recognizer is waiting.
1788 pub fn next_gesture_deadline(&self) -> Option<std::time::Instant> {
1789 // Iterate just the widgets that actually carry a gesture arena.
1790 // `filter` for `is_active` skips dormant entries that may still
1791 // be in the set after a hide-without-detach.
1792 self.gesture_owners
1793 .iter()
1794 .copied()
1795 .filter(|id| self.arena.is_active(*id))
1796 .filter_map(|id| self.arena.get(id))
1797 .filter_map(|node| node.handlers.gesture_arena.as_ref())
1798 .filter_map(|arena| arena.next_deadline())
1799 .min()
1800 .map(|deadline| self.instant_for(deadline))
1801 }
1802
1803 /// The earliest instant at which a standing hold reaches its
1804 /// `max_hold` and [`expire_sequence_holds`](Self::expire_sequence_holds)
1805 /// has work.
1806 ///
1807 /// Folded into [`next_input_deadline`](Self::next_input_deadline) beside
1808 /// the gesture, fling and press-feedback terms. A deadline the tick can
1809 /// serve but nothing reports is a wake the event loop never takes, which
1810 /// leaves the hold standing exactly as long as it did before the tick knew
1811 /// how to release it.
1812 pub(super) fn next_sequence_hold_deadline(&self) -> Option<crate::pointer::EventTime> {
1813 self.pointers
1814 .iter()
1815 .filter_map(|entry| entry.sequence.as_ref())
1816 .filter_map(|sequence| {
1817 let profile = self.effective_theme.input.profile(sequence.pointer().kind);
1818 sequence.next_hold_deadline(profile)
1819 })
1820 .min()
1821 }
1822
1823 /// The [`TouchAction`] permitted for `target`: every node's own
1824 /// declaration from the root down to `target` (inclusive), intersected.
1825 /// An ancestor's `NONE` wins no matter what a descendant declares —
1826 /// intersection is absorbing at `NONE` (see
1827 /// `crate::pointer::touch_action`), so this needs no early exit to get
1828 /// that right; it just folds the whole chain.
1829 ///
1830 /// One of the two path folds the arbitration reads: `begin_sequence` asks
1831 /// it what a press may do, and `feed_pinch` asks it whether a subtree
1832 /// admits a two-contact pinch.
1833 pub(crate) fn effective_touch_action(&self, target: WidgetId) -> TouchAction {
1834 let mut chain = Vec::new();
1835 let mut current = Some(target);
1836 while let Some(id) = current {
1837 chain.push(id);
1838 current = self.arena.parent(id);
1839 }
1840 // `chain` is target..=root (innermost first); fold root-to-target so
1841 // the read matches the CSS `touch-action` model this mirrors — an
1842 // ancestor's declaration is applied before a descendant's narrows it
1843 // further. `intersect` is commutative and associative, so the fold
1844 // order can never change the *answer*, only which step "loses" reads
1845 // as the natural one.
1846 chain
1847 .iter()
1848 .rev()
1849 .map(|&id| {
1850 self.arena
1851 .get(id)
1852 .map(|n| n.touch_action)
1853 .unwrap_or(TouchAction::AUTO)
1854 })
1855 .fold(TouchAction::AUTO, TouchAction::intersect)
1856 }
1857
1858 /// Every [`PanClaim`] from `target` up to the root, **innermost first**
1859 /// — the order a boundary pan chains along (a nested scrollable hits its
1860 /// edge and hands off to its container), so it is normative. A claimant
1861 /// is excluded entirely — never narrowed — when `allowed` forbids any
1862 /// axis it declares.
1863 ///
1864 /// The second of the two path folds the arbitration reads, and the chain
1865 /// a synthesised pan is delivered along — see `widget_tree::pan_arbiter`.
1866 pub(crate) fn pan_candidates(
1867 &self,
1868 target: WidgetId,
1869 allowed: TouchAction,
1870 ) -> Vec<(WidgetId, PanClaim)> {
1871 let mut result = Vec::new();
1872 let mut current = Some(target);
1873 while let Some(id) = current {
1874 if let Some(claim) = self.arena.get(id).and_then(|n| n.pan_claim) {
1875 let x_ok = !claim.axes.contains(Axis::X) || allowed.allows_pan_x();
1876 let y_ok = !claim.axes.contains(Axis::Y) || allowed.allows_pan_y();
1877 if x_ok && y_ok {
1878 result.push((id, claim));
1879 }
1880 }
1881 current = self.arena.parent(id);
1882 }
1883 result
1884 }
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889 use super::*;
1890 use crate::test_widgets::FillWidget;
1891 use crate::widget_builder::WidgetBuilder;
1892
1893 #[test]
1894 fn destroy_subtree_clears_dangling_pointer_capture() {
1895 use crate::event::{EventResponse, Modifiers, PointerButton};
1896 use crate::test_widgets::StackWidget;
1897
1898 let mut tree = WidgetTree::new();
1899 let child = tree.add(FillWidget::new().on_pointer_event(|event, ctx| {
1900 if matches!(event, WidgetEvent::PointerDown { .. }) {
1901 ctx.capture_pointer();
1902 }
1903 EventResponse::Ignored
1904 }));
1905 let parent = tree.add(StackWidget::new().child(child));
1906 tree.layout(SizeProposal::exact(100.0, 50.0));
1907
1908 // A press inside the child captures the pointer to it.
1909 tree.dispatch_event(WidgetEvent::pointer_down(
1910 Point::new(50.0, 25.0),
1911 PointerButton::Primary,
1912 Modifiers::NONE,
1913 ));
1914 assert_eq!(
1915 tree.pointer_captured_by(),
1916 Some(child),
1917 "PointerDown handler should have captured the pointer"
1918 );
1919
1920 // Tearing down the capturing subtree (e.g. mid-gesture rebuild) must
1921 // release the capture eagerly rather than leaving a dangling id that
1922 // swallows every later Move/Up until the next layout pass heals it.
1923 tree.destroy_subtree(parent);
1924 assert_eq!(
1925 tree.pointer_captured_by(),
1926 None,
1927 "destroy_subtree must clear a capture anchored at a destroyed widget"
1928 );
1929 }
1930}
1931
1932/// The two path folds `effective_touch_action` / `pan_candidates` declare
1933/// for the arbitration package (P08) — exercised here in isolation, apart
1934/// from the dispatch paths that now read them (`begin_sequence`,
1935/// `feed_pinch`, and the pan arbiter).
1936#[cfg(test)]
1937mod touch_action_tests {
1938 use super::*;
1939 use crate::pointer::touch_action::PanAxes;
1940 use crate::test_widgets::FillWidget;
1941 use crate::widget_builder::WidgetBuilder;
1942
1943 /// A 20-deep chain, root to target, where one mid-level node declares
1944 /// `PAN_Y` and another declares `PINCH_ZOOM`. Neither permission is
1945 /// shared by the other, so the intersection collapses to `NONE` — the
1946 /// clearest possible demonstration that the fold really intersects the
1947 /// *whole* chain rather than reading only the nearest declaration.
1948 #[test]
1949 fn effective_touch_action_intersects_the_whole_root_to_target_chain() {
1950 let mut tree = WidgetTree::new();
1951 let mut chain = vec![tree.add(FillWidget::new())]; // depth 0: the root
1952 for depth in 1..20usize {
1953 let parent = *chain.last().expect("root was pushed");
1954 let id = if depth == 5 {
1955 tree.add_child(parent, FillWidget::new().touch_action(TouchAction::PAN_Y))
1956 } else if depth == 12 {
1957 tree.add_child(
1958 parent,
1959 FillWidget::new().touch_action(TouchAction::PINCH_ZOOM),
1960 )
1961 } else {
1962 tree.add_child(parent, FillWidget::new())
1963 };
1964 chain.push(id);
1965 }
1966 assert_eq!(chain.len(), 20, "the path must be 20 nodes deep");
1967 let target = *chain.last().expect("chain is non-empty");
1968 tree.layout(SizeProposal::exact(50.0, 50.0));
1969
1970 assert_eq!(
1971 tree.effective_touch_action(target),
1972 TouchAction::NONE,
1973 "PAN_Y at depth 5 and PINCH_ZOOM at depth 12 share no permission"
1974 );
1975
1976 // Every node above depth 5 (inclusive) is untouched: the plain
1977 // `AUTO` prefix intersects down to exactly `PAN_Y`.
1978 assert_eq!(tree.effective_touch_action(chain[5]), TouchAction::PAN_Y);
1979 }
1980
1981 /// `pan_candidates` walks target-to-root (innermost first) and drops a
1982 /// claimant whose declared axes the allowed action forbids, rather than
1983 /// narrowing it.
1984 #[test]
1985 fn pan_candidates_orders_innermost_first_and_filters_by_allowed_axes() {
1986 let mut tree = WidgetTree::new();
1987 // root claims X; an unclaimed node in between; target (innermost)
1988 // claims Y.
1989 let root = tree.add(FillWidget::new().scroll_container(PanAxes::X));
1990 let mid = tree.add_child(root, FillWidget::new());
1991 let target = tree.add_child(mid, FillWidget::new().scroll_container(PanAxes::Y));
1992 tree.layout(SizeProposal::exact(50.0, 50.0));
1993
1994 let x_claim = PanClaim {
1995 axes: PanAxes::X,
1996 devices: teksilo_tokens::PointerKindMask::DIRECT,
1997 kinetic: true,
1998 };
1999 let y_claim = PanClaim {
2000 axes: PanAxes::Y,
2001 devices: teksilo_tokens::PointerKindMask::DIRECT,
2002 kinetic: true,
2003 };
2004
2005 // Both axes allowed: both claims survive, innermost (target) first.
2006 assert_eq!(
2007 tree.pan_candidates(target, TouchAction::PAN),
2008 vec![(target, y_claim), (root, x_claim)]
2009 );
2010
2011 // Only PAN_X allowed: target's Y-axis claim is forbidden and
2012 // excluded outright; root's X-axis claim still survives.
2013 assert_eq!(
2014 tree.pan_candidates(target, TouchAction::PAN_X),
2015 vec![(root, x_claim)]
2016 );
2017
2018 // Only PAN_Y allowed: the reverse — root's claim is excluded,
2019 // target's survives.
2020 assert_eq!(
2021 tree.pan_candidates(target, TouchAction::PAN_Y),
2022 vec![(target, y_claim)]
2023 );
2024 }
2025}
2026
2027/// The one-clock rule: the input timeline and the tree's simulated clock are
2028/// one axis, seeded from one epoch.
2029#[cfg(test)]
2030mod clock_tests {
2031 use super::*;
2032 use crate::pointer::EventTime;
2033 use crate::pointer::clock::ManualClock;
2034
2035 /// The whole point of taking the epoch as a parameter rather than
2036 /// capturing it: `EventTime::ZERO` and the tree's simulated clock name the
2037 /// *same* instant, so one `advance_time` moves gesture deadlines and
2038 /// animations against the same origin.
2039 ///
2040 /// If this ever fails, the two timelines have drifted apart and a test that
2041 /// advances one has silently stopped advancing the other.
2042 #[test]
2043 fn the_input_clock_shares_the_trees_epoch() {
2044 let tree = WidgetTree::new();
2045 assert_eq!(
2046 tree.input_clock().epoch(),
2047 Some(tree.simulated_now()),
2048 "the input clock must be anchored at the instant sim_clock starts from"
2049 );
2050 }
2051
2052 /// …and stays one axis as simulated time moves: the offset between the
2053 /// simulated clock and the epoch is exactly what was advanced.
2054 #[test]
2055 fn advancing_simulated_time_moves_along_the_input_axis() {
2056 let mut tree = WidgetTree::new();
2057 let epoch = tree
2058 .input_clock()
2059 .epoch()
2060 .expect("the default input clock is monotonic");
2061
2062 tree.advance_time(std::time::Duration::from_millis(400));
2063 assert_eq!(
2064 tree.simulated_now().duration_since(epoch),
2065 std::time::Duration::from_millis(400)
2066 );
2067
2068 tree.advance_time(std::time::Duration::from_millis(100));
2069 assert_eq!(
2070 tree.simulated_now().duration_since(epoch),
2071 std::time::Duration::from_millis(500)
2072 );
2073 }
2074
2075 /// The simulated input axis continues from where the wall clock left it,
2076 /// rather than restarting at the simulated clock's own offset.
2077 ///
2078 /// A stamp taken before the switch would otherwise land in the virtual
2079 /// *future*: `sim_clock` only moves when it is advanced, so on a tree that
2080 /// has been alive for 50 ms it still reads the epoch while every sample
2081 /// dispatched so far is stamped 50 ms. Every interval measured from one of
2082 /// those is then clamped to zero — a hold that can never elapse, a coast
2083 /// that never starts.
2084 #[test]
2085 fn the_simulated_input_axis_continues_from_the_wall_clock() {
2086 let mut tree = WidgetTree::new();
2087 // Time the tree spent on the wall clock before anything simulated it —
2088 // in a real suite this is however long the test took to get here.
2089 std::thread::sleep(std::time::Duration::from_millis(50));
2090 let stamped_before_the_switch = tree.input_now();
2091 assert!(
2092 stamped_before_the_switch >= EventTime::from_millis(50),
2093 "the default clock is the wall clock until told otherwise: {stamped_before_the_switch:?}"
2094 );
2095
2096 tree.advance_time(std::time::Duration::from_millis(20));
2097 let after_one = tree.input_now();
2098 assert!(
2099 after_one >= stamped_before_the_switch + std::time::Duration::from_millis(20),
2100 "the axis carries on from the reading it had, not from the epoch: \
2101 {stamped_before_the_switch:?} -> {after_one:?}"
2102 );
2103
2104 // …and from then on it moves by exactly what is advanced, and by
2105 // nothing else — however long this test itself takes.
2106 std::thread::sleep(std::time::Duration::from_millis(20));
2107 tree.advance_time(std::time::Duration::from_millis(30));
2108 assert_eq!(
2109 tree.input_now(),
2110 after_one + std::time::Duration::from_millis(30),
2111 "a simulated tree's input timeline answers to the clock alone"
2112 );
2113 }
2114
2115 /// A test can take the input timeline over entirely.
2116 #[test]
2117 fn a_manual_clock_replaces_the_default() {
2118 let mut tree = WidgetTree::new();
2119 let manual = std::rc::Rc::new(ManualClock::new(EventTime::from_millis(30)));
2120 tree.set_input_clock(manual.clone());
2121
2122 assert_eq!(tree.input_now(), EventTime::from_millis(30));
2123 manual.advance(std::time::Duration::from_millis(70));
2124 assert_eq!(tree.input_now(), EventTime::from_millis(100));
2125 // Reading does not move it — a manual clock is only moved by its owner.
2126 assert_eq!(tree.input_now(), EventTime::from_millis(100));
2127 }
2128
2129 /// The default clock actually reads the wall clock, so a real window's
2130 /// gestures advance without anyone ticking anything.
2131 #[test]
2132 fn the_default_clock_is_monotonic() {
2133 let tree = WidgetTree::new();
2134 let a = tree.input_now();
2135 let b = tree.input_now();
2136 assert!(b >= a);
2137 }
2138
2139 /// Handing the axis back never steps it backwards.
2140 ///
2141 /// The whole reason the hand-back is not just "drop the origin": a frozen
2142 /// axis that has been advanced reads *ahead* of the raw clock, and every
2143 /// stamp already issued sits at that reading. Going back to the raw clock
2144 /// would re-issue times that have already been handed out — a velocity
2145 /// tracker fitting a negative interval, a tap streak whose second tap is
2146 /// older than its first, a hold that un-elapses.
2147 #[test]
2148 fn handing_the_axis_back_never_steps_it_backwards() {
2149 let mut tree = WidgetTree::new();
2150 tree.advance_time(std::time::Duration::from_millis(500));
2151 let frozen = tree.input_now();
2152
2153 tree.resume_real_time();
2154 let resumed = tree.input_now();
2155
2156 assert!(
2157 resumed >= frozen,
2158 "the axis must carry the advance forward, not discard it: \
2159 {frozen:?} -> {resumed:?}"
2160 );
2161 // …and it keeps every bit of what was advanced, rather than trading it
2162 // for however little wall time the test itself took.
2163 assert!(
2164 resumed >= EventTime::from_millis(500),
2165 "500 ms was advanced and must still be on the axis: {resumed:?}"
2166 );
2167 }
2168
2169 /// After the hand-back, real events are stamped from the real clock again:
2170 /// two of them separated by real time are two distinct moments.
2171 ///
2172 /// This is the whole point of the hand-back. While the axis is frozen every
2173 /// dispatch reads one instant, which is exactly right for a test driving
2174 /// the clock itself and exactly wrong for a live window: a bridge that
2175 /// advanced the clock once would leave every subsequent human keystroke,
2176 /// tap and drag stamped the same moment, and no gesture decided by time
2177 /// could ever be recognized again.
2178 #[test]
2179 fn after_the_hand_back_real_events_get_distinct_and_later_times() {
2180 use crate::test_widgets::FillWidget;
2181 use crate::widget_builder::WidgetBuilder;
2182 use std::cell::RefCell;
2183 use std::rc::Rc;
2184
2185 let seen: Rc<RefCell<Vec<EventTime>>> = Rc::new(RefCell::new(Vec::new()));
2186 let log = seen.clone();
2187 let mut tree = WidgetTree::new();
2188 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
2189 log.borrow_mut().push(ctx.pointer().time);
2190 crate::event::EventResponse::Ignored
2191 }));
2192 tree.layout(SizeProposal::exact(100.0, 100.0));
2193
2194 tree.pointer_move(Point::new(10.0, 10.0));
2195 tree.advance_time(std::time::Duration::from_millis(200));
2196 tree.resume_real_time();
2197
2198 tree.pointer_move(Point::new(20.0, 20.0));
2199 std::thread::sleep(std::time::Duration::from_millis(5));
2200 tree.pointer_move(Point::new(30.0, 30.0));
2201
2202 let seen = seen.borrow();
2203 assert_eq!(seen.len(), 3, "three moves reached the widget: {seen:?}");
2204 assert!(
2205 seen[1] >= seen[0] + std::time::Duration::from_millis(200),
2206 "an event after the advance is at least the advance later: {seen:?}"
2207 );
2208 assert!(
2209 seen[2] > seen[1],
2210 "two real events 5 ms apart are two moments, not one: {seen:?}"
2211 );
2212 }
2213
2214 /// …and the next advance freezes it again.
2215 ///
2216 /// The hand-back is not a latch in the other direction: a bridge running a
2217 /// second operation must get the same determinism the first one did, and a
2218 /// test's helpers (which enter simulated mode on every sample) must keep
2219 /// stamping two un-advanced samples the same instant.
2220 #[test]
2221 fn the_freeze_comes_back_after_a_hand_back() {
2222 let mut tree = WidgetTree::new();
2223 tree.advance_time(std::time::Duration::from_millis(100));
2224 tree.resume_real_time();
2225
2226 tree.advance_time(std::time::Duration::from_millis(100));
2227 let a = tree.input_now();
2228 std::thread::sleep(std::time::Duration::from_millis(5));
2229 let b = tree.input_now();
2230 assert_eq!(a, b, "re-frozen: the wall clock stopped moving the axis");
2231 }
2232
2233 /// Every hand-back re-measures the offset against its own readings; none
2234 /// of them adds to what the last one measured.
2235 ///
2236 /// The live bridge hands the axis back after **every** operation, so this
2237 /// is its ordinary path rather than an edge case — and accumulating
2238 /// instead of assigning compounds: each cycle would carry the previous
2239 /// offset into the frozen reading *and* add the previous offset again, so
2240 /// the axis would run `2ⁿ − 1` advances ahead after `n` of them. Four
2241 /// 50 ms cycles is 200 ms of advance and would be reported as 750 ms, and
2242 /// every duration measured from a stamp taken before the run — a hold, a
2243 /// tap streak, a fling's velocity window — would be wrong by the
2244 /// difference.
2245 ///
2246 /// **This is the only guard on the offset's magnitude.** Its companion
2247 /// `a_deadline_armed_after_the_hand_back_is_in_the_future` is insensitive
2248 /// to it by construction — the offset cancels between `event_time_for` and
2249 /// `instant_for`, so that test holds for a wrong offset as readily as for a
2250 /// right one — and nothing else asserts a number. So the
2251 /// ceiling below is expressed **per cycle**: the allowance scales with the
2252 /// work done, so the error it admits per hand-back stays at
2253 /// `SLACK_PER_CYCLE` whatever `CYCLES` is. A single absolute ceiling
2254 /// instead divides by the cycle count — slack at a handful of cycles, and
2255 /// firing on the loop's own wall-clock noise at a hundred.
2256 #[test]
2257 fn repeated_hand_backs_re_measure_the_offset_rather_than_accumulating_it() {
2258 const CYCLES: u32 = 4;
2259 const PER_CYCLE: std::time::Duration = std::time::Duration::from_millis(50);
2260 // What one hand-back may cost beyond what it advanced: the wall clock
2261 // moves while the loop runs, and the loop's own work is not free.
2262 const SLACK_PER_CYCLE: std::time::Duration = std::time::Duration::from_millis(10);
2263 let advanced = PER_CYCLE * CYCLES;
2264
2265 let mut tree = WidgetTree::new();
2266 let before = tree.input_now();
2267 for _ in 0..CYCLES {
2268 tree.advance_time(PER_CYCLE);
2269 tree.resume_real_time();
2270 }
2271 let after = tree.input_now();
2272 let gained = after.saturating_since(before);
2273
2274 assert!(
2275 gained >= advanced,
2276 "every advance must still be on the axis: {gained:?} < {advanced:?}"
2277 );
2278 // The wall clock also moved while the loop ran, so the axis is allowed
2279 // to have gained a little more than was advanced — but only a little,
2280 // and the allowance is per hand-back rather than for the run.
2281 // Accumulating would put it at 750 ms, three and a half times over.
2282 assert!(
2283 gained <= advanced + SLACK_PER_CYCLE * CYCLES,
2284 "the axis gained {gained:?} for {advanced:?} of advancing over \
2285 {CYCLES} hand-backs — the offset is compounding across them"
2286 );
2287 }
2288
2289 /// An animation in flight when the tree is put on the simulated clock
2290 /// keeps the phase it had, and goes on progressing.
2291 ///
2292 /// The scheduler stores absolute instants, and the simulated clock reads
2293 /// the tree's epoch plus whatever has been advanced — on a live window,
2294 /// far behind the wall clock the animation was stamped against. Measuring
2295 /// it there without rebasing clamps its elapsed time to roughly zero and
2296 /// it never moves again, however many frames the operation advances.
2297 #[test]
2298 fn an_animation_in_flight_keeps_its_phase_when_time_is_taken_over() {
2299 use crate::signal::Signal;
2300 use crate::test_widgets::FillWidget;
2301 use std::time::Duration;
2302
2303 let mut tree = WidgetTree::new();
2304 let owner = tree.add(FillWidget::new());
2305 tree.layout(SizeProposal::exact(100.0, 100.0));
2306
2307 // Long enough that a loaded CI runner overshooting the sleep below by
2308 // a few hundred milliseconds — a macOS runner has been seen to take
2309 // 250 ms over a 100 ms sleep — cannot run it to completion before the
2310 // take-over, which would leave nothing for the advance to move.
2311 const DURATION: Duration = Duration::from_millis(4000);
2312
2313 let value = Signal::<f32>::new_animated(0.0);
2314 tree.register_animated_signal(&value, owner);
2315 value.animate_to(1.0, DURATION, teksilo_tokens::Easing::Linear);
2316
2317 // Promote and age it on the wall clock, exactly as a live window does.
2318 tree.layout(SizeProposal::exact(100.0, 100.0));
2319 std::thread::sleep(Duration::from_millis(100));
2320 tree.layout(SizeProposal::exact(100.0, 100.0));
2321 let on_the_wall_clock = value.get();
2322 // 100 ms of 4000 is 0.025; the sleep never undershoots, and the bound
2323 // above allows the runner nearly two seconds of overshoot.
2324 assert!(
2325 (0.02..0.5).contains(&on_the_wall_clock),
2326 "in flight after 100 ms of {DURATION:?}: {on_the_wall_clock}"
2327 );
2328
2329 // Now an automation operation takes time over and advances a further
2330 // 1000 ms. The animation must have moved on by exactly that share of
2331 // its duration — not stuck where the wall clock left it, and not
2332 // restarted from the simulated clock's own reading.
2333 tree.advance_time(Duration::from_millis(1000));
2334 let simulated = value.get();
2335 let moved = simulated - on_the_wall_clock;
2336 // 1000 ms of 4000 is 0.25. Whatever wall-clock time passed between the
2337 // reading above and the take-over is in there too, so the bound is
2338 // loose above — by 400 ms — and tight below.
2339 assert!(
2340 (0.245..0.35).contains(&moved),
2341 "the advance keeps the phase and adds its own 1000 ms of \
2342 {DURATION:?}: {on_the_wall_clock} -> {simulated}"
2343 );
2344 }
2345
2346 /// …and once time is handed back, the wall clock goes on driving it from
2347 /// where the advance left it — neither frozen nor snapped to its end.
2348 ///
2349 /// The two failures this rules out are the two halves of getting the axis
2350 /// wrong on a *live* attached window. Ticking at the simulated clock a
2351 /// live tree no longer advances freezes every animation outright. Ticking
2352 /// at the wall clock against a start stamped on the simulated one hands
2353 /// the animation an elapsed time of the tree's whole age and completes it
2354 /// on the first real frame.
2355 #[test]
2356 fn an_animation_goes_on_progressing_after_the_hand_back() {
2357 use crate::signal::Signal;
2358 use crate::test_widgets::FillWidget;
2359 use std::time::Duration;
2360
2361 let mut tree = WidgetTree::new();
2362 let owner = tree.add(FillWidget::new());
2363 tree.layout(SizeProposal::exact(100.0, 100.0));
2364
2365 // The tree spends real time alive before anything simulates it, and
2366 // more of it than will be advanced. That is the live condition, and it
2367 // is what makes the *second* assertion below discriminating: with the
2368 // wall clock ahead of the simulated one, an un-rebased hand-back hands
2369 // the animation an elapsed time longer than its whole duration.
2370 std::thread::sleep(Duration::from_millis(300));
2371
2372 let value = Signal::<f32>::new_animated(0.0);
2373 tree.register_animated_signal(&value, owner);
2374 // 2 s rather than something short: the 100 ms slept after the
2375 // hand-back is a floor, not a figure, and a loaded runner has
2376 // overshot it by 150 ms — the "not snapped to the end" bound below
2377 // must hold through that.
2378 value.animate_to(
2379 1.0,
2380 Duration::from_millis(2000),
2381 teksilo_tokens::Easing::Linear,
2382 );
2383
2384 // The operation promotes it and advances it a twentieth of the way.
2385 tree.advance_time(Duration::from_millis(100));
2386 let at_hand_back = value.get();
2387 assert!(
2388 (0.04..0.06).contains(&at_hand_back),
2389 "a twentieth through after 100 ms of 2000: {at_hand_back}"
2390 );
2391
2392 // The operation ends and the window goes back to painting frames.
2393 tree.resume_real_time();
2394 std::thread::sleep(Duration::from_millis(100));
2395 tree.layout(SizeProposal::exact(100.0, 100.0));
2396
2397 let after = value.get();
2398 // At least 100 ms of 2000 (0.05) moved it; the sleep only overshoots.
2399 assert!(
2400 after > at_hand_back + 0.045,
2401 "frozen: 100 ms of real time moved it from {at_hand_back} to {after}"
2402 );
2403 assert!(
2404 after < 0.95,
2405 "snapped to the end: 100 ms of 2000 took it from {at_hand_back} to {after}"
2406 );
2407 }
2408
2409 /// A `max_duration` cap measures the animation's own age across the
2410 /// hand-back, not the tree's.
2411 ///
2412 /// `started_at` is the only stored instant the cap reads, and until this
2413 /// test nothing asserted that the rebase shifts it: no production caller
2414 /// sets `max_duration` at all, so the branch is entered only from the
2415 /// public
2416 /// [`Signal::try_animate_with_options`](crate::signal::Signal::try_animate_with_options)
2417 /// and from the scheduler's own unit tests, which never change axis.
2418 /// Left behind on the abandoned axis, `started_at` makes the cap measure
2419 /// the tree's whole wall-clock age instead of the animation's own elapsed
2420 /// time, and the first real frame after the hand-back retires the
2421 /// animation outright — the same snap the rebase exists to prevent,
2422 /// arriving through a different door.
2423 ///
2424 /// The cap is deliberately smaller than the tree's age at the final layout
2425 /// and larger than the animation's own elapsed time there, so the two ways
2426 /// of measuring it disagree about whether it has been reached.
2427 #[test]
2428 fn a_capped_animation_survives_the_hand_back() {
2429 use crate::animation::AnimationRequest;
2430 use crate::signal::Signal;
2431 use crate::test_widgets::FillWidget;
2432 use std::time::Duration;
2433
2434 // The animation lives 200 ms before the final reading — 100 simulated,
2435 // 100 real — and the tree at least 600 ms. The cap sits between the
2436 // two with room on both sides: a loaded runner overshoots a sleep by
2437 // 100 ms or more, and every overshoot ages the tree further but the
2438 // animation only through the real half.
2439 const CAP: Duration = Duration::from_millis(400);
2440 const DURATION: Duration = Duration::from_millis(2000);
2441
2442 let mut tree = WidgetTree::new();
2443 let owner = tree.add(FillWidget::new());
2444 tree.layout(SizeProposal::exact(100.0, 100.0));
2445
2446 // Age the tree past the cap before the animation is armed at all.
2447 std::thread::sleep(Duration::from_millis(500));
2448
2449 let value = Signal::<f32>::new_animated(0.0);
2450 tree.register_animated_signal(&value, owner);
2451 value
2452 .try_animate_with_options(AnimationRequest {
2453 target: 1.0,
2454 duration: DURATION,
2455 easing: teksilo_tokens::Easing::Linear,
2456 max_duration: Some(CAP),
2457 ..AnimationRequest::default()
2458 })
2459 .expect("an animated signal accepts a request");
2460
2461 // 100 ms simulated, then 100 ms real: 200 ms of the animation's own
2462 // life, against a tree already older than the cap.
2463 tree.advance_time(Duration::from_millis(100));
2464 let at_hand_back = value.get();
2465 tree.resume_real_time();
2466 std::thread::sleep(Duration::from_millis(100));
2467 tree.layout(SizeProposal::exact(100.0, 100.0));
2468
2469 let after = value.get();
2470 assert!(
2471 tree.has_active_animations(),
2472 "a {CAP:?} cap retired a {DURATION:?} animation 200 ms in: \
2473 {at_hand_back} -> {after}"
2474 );
2475 assert!(
2476 after > at_hand_back,
2477 "the animation must go on progressing: {at_hand_back} -> {after}"
2478 );
2479 }
2480
2481 /// An animation paused across a hand-back resumes from where it was
2482 /// paused, rather than being driven backwards by the gap between the axes.
2483 ///
2484 /// A loop, because only loops are paused with their window: a one-shot
2485 /// tween runs on without focus and never reads the pause mark.
2486 ///
2487 /// The pause mark is the scheduler's one instant that is not per-animation,
2488 /// and until this test nothing asserted that the rebase shifts it: on
2489 /// reactivate the scheduler moves each `start_time` forward by
2490 /// `now - paused_at`, so a mark left behind on the abandoned axis measures
2491 /// the whole gap between the axes and puts the start ahead of the reading
2492 /// that follows it — the animation's elapsed time collapses, and it
2493 /// replays from near zero once the wall clock reaches the new start.
2494 ///
2495 /// The wall clock is deliberately left further ahead of the simulated one
2496 /// than the real time that elapses after the hand-back, which is exactly
2497 /// the condition under which the collapse leaves the animation *behind*
2498 /// where it was paused rather than merely slowed.
2499 #[test]
2500 fn an_animation_paused_across_the_hand_back_resumes_forwards() {
2501 use crate::signal::Signal;
2502 use crate::test_widgets::FillWidget;
2503 use std::time::Duration;
2504
2505 let mut tree = WidgetTree::new();
2506 let owner = tree.add(FillWidget::new());
2507 tree.layout(SizeProposal::exact(100.0, 100.0));
2508 std::thread::sleep(Duration::from_millis(300));
2509
2510 let value = Signal::<f32>::new_animated(0.0);
2511 tree.register_animated_signal(&value, owner);
2512 value.animate_looping(
2513 1.0,
2514 Duration::from_millis(1000),
2515 teksilo_tokens::Easing::Linear,
2516 None,
2517 );
2518
2519 tree.advance_time(Duration::from_millis(100));
2520 let at_pause = value.get();
2521 assert!(
2522 (0.05..0.2).contains(&at_pause),
2523 "a tenth through after 100 ms of 1000: {at_pause}"
2524 );
2525
2526 // The window loses focus while the operation still owns the clock, and
2527 // regains it after the hand-back — the ordering that makes the pause
2528 // mark and the reading it is subtracted from land on different axes.
2529 tree.set_window_active(false);
2530 tree.resume_real_time();
2531 tree.set_window_active(true);
2532
2533 std::thread::sleep(Duration::from_millis(150));
2534 tree.layout(SizeProposal::exact(100.0, 100.0));
2535
2536 let after = value.get();
2537 assert!(
2538 after > at_pause,
2539 "frozen or driven backwards across a paused hand-back: \
2540 {at_pause} -> {after}"
2541 );
2542 }
2543
2544 /// …and the same, with the gap between the two axes the other way round:
2545 /// the pause mark is stamped on the axis the scheduler is measured
2546 /// against, not on the wall clock.
2547 ///
2548 /// The twin of the test above, and it cannot be merged with it. Which of
2549 /// the two mistakes is observable depends on the *sign* of the gap at the
2550 /// moment of the pause: a mark the rebase left behind only yields a
2551 /// spurious offset while the wall clock leads, and a mark taken from the
2552 /// wall clock instead of the animation clock only survives the
2553 /// subtraction — rather than flooring at zero — while the simulated clock
2554 /// leads. Each test rules out the sign the other needs, so each covers one
2555 /// mistake.
2556 ///
2557 /// Here the simulated clock is advanced past a tree milliseconds old, so
2558 /// it leads — and a wall-clock mark, shifted by the hand-back's rebase
2559 /// like the animation-axis instant it is not, comes out a whole advance
2560 /// early and is subtracted from the reading on reactivate as if the window
2561 /// had been dark for that long.
2562 #[test]
2563 fn a_pause_mark_is_stamped_on_the_animation_axis() {
2564 use crate::signal::Signal;
2565 use crate::test_widgets::FillWidget;
2566 use std::time::Duration;
2567
2568 let mut tree = WidgetTree::new();
2569 let owner = tree.add(FillWidget::new());
2570 tree.layout(SizeProposal::exact(100.0, 100.0));
2571
2572 let value = Signal::<f32>::new_animated(0.0);
2573 tree.register_animated_signal(&value, owner);
2574 value.animate_looping(
2575 1.0,
2576 Duration::from_millis(5000),
2577 teksilo_tokens::Easing::Linear,
2578 None,
2579 );
2580
2581 // A second of simulated time on a tree milliseconds old: the advance,
2582 // not a sleep, is what separates the axes, and it separates them the
2583 // other way.
2584 tree.advance_time(Duration::from_millis(1000));
2585 let at_pause = value.get();
2586 assert!(
2587 (0.15..0.25).contains(&at_pause),
2588 "a fifth through after 1000 ms of 5000: {at_pause}"
2589 );
2590
2591 tree.set_window_active(false);
2592 tree.resume_real_time();
2593 tree.set_window_active(true);
2594
2595 std::thread::sleep(Duration::from_millis(150));
2596 tree.layout(SizeProposal::exact(100.0, 100.0));
2597
2598 let after = value.get();
2599 assert!(
2600 after > at_pause,
2601 "the deactivation cost the animation its phase: {at_pause} -> {after}"
2602 );
2603 }
2604
2605 /// A gesture the **timer** recognised is dispatched under its own contact.
2606 ///
2607 /// The whole class of defect this pins: a hold is not a sample, so nothing
2608 /// on the sample path installs a snapshot for it, and `current_input` is
2609 /// saved-and-restored around every dispatch — so a handler reached from a
2610 /// hold used to be told, unconditionally, that it was serving the mouse.
2611 /// It was measured that way (a probe on a real `long_press_at(Touch, ..)`
2612 /// printed `Mouse`), and it cost the first host of the touch-text contract
2613 /// a duplicate guard: `TouchSelection::on_long_press` refused every finger.
2614 ///
2615 /// This asserts the whole of what the context carries **in**: the device,
2616 /// the id, the captor, the frozen `TouchAction` and the press snapshot.
2617 /// What a handler asks the tree *for* from inside a hold — a capture, a
2618 /// cancel — travels the other way and is asserted by
2619 /// `a_hold_captures_and_cancels_the_contact_that_held`.
2620 ///
2621 /// The mouse half is not decoration: it is what proves the fix installs the
2622 /// *holding contact* rather than hard-coding a finger.
2623 #[test]
2624 fn a_hold_is_dispatched_under_the_contact_that_held() {
2625 use crate::TouchAction;
2626 use crate::test_widgets::FillWidget;
2627 use crate::widget_builder::WidgetBuilder;
2628
2629 /// Every answer the context is built from the installed snapshot.
2630 #[derive(Debug, Clone, Copy)]
2631 struct Answers {
2632 kind: teksilo_tokens::PointerKind,
2633 id: crate::pointer::PointerId,
2634 captor: Option<WidgetId>,
2635 touch_action: TouchAction,
2636 press_inside: bool,
2637 }
2638
2639 for kind in [
2640 teksilo_tokens::PointerKind::Touch,
2641 teksilo_tokens::PointerKind::Mouse,
2642 ] {
2643 let seen: std::rc::Rc<std::cell::Cell<Option<Answers>>> = Default::default();
2644 let mut tree = WidgetTree::new();
2645 let held_by = {
2646 let seen = seen.clone();
2647 tree.add(
2648 // A declared, non-`AUTO` action so the frozen value is
2649 // distinguishable from the neutral one a pointer with no
2650 // sequence answers with.
2651 FillWidget::new()
2652 .touch_action(TouchAction::PAN_Y)
2653 .on_long_press(move |_e, ctx| {
2654 seen.set(Some(Answers {
2655 kind: ctx.pointer_kind(),
2656 id: ctx.pointer().id,
2657 // Read off the field rather than through
2658 // `owns_pointer()`, which also needs a
2659 // `dispatch_node` — and a timer dispatch,
2660 // addressed to a node rather than walking to
2661 // one, sets none.
2662 captor: ctx.pointer_captor,
2663 touch_action: ctx.touch_action(),
2664 press_inside: ctx.press_is_inside(),
2665 }));
2666 }),
2667 )
2668 };
2669 tree.layout(SizeProposal::exact(100.0, 100.0));
2670
2671 let held = tree.long_press_at(kind, Point::new(50.0, 50.0));
2672 let seen = seen.get().expect("the hold was dispatched");
2673
2674 assert_eq!(
2675 seen.kind, kind,
2676 "a {kind:?} hold was dispatched as {:?}",
2677 seen.kind
2678 );
2679 assert_eq!(
2680 seen.id, held,
2681 "a hold must carry the identity of the contact that held"
2682 );
2683 assert_eq!(
2684 seen.captor,
2685 Some(held_by),
2686 "the captor is looked up by the dispatched pointer, and the \
2687 holding contact's is the node whose arena took its press"
2688 );
2689 assert_eq!(
2690 seen.touch_action,
2691 TouchAction::PAN_Y,
2692 "the frozen action is read off the dispatched pointer's \
2693 sequence, so a hold under the wrong pointer reads the \
2694 neutral {:?} of a pointer that has none",
2695 TouchAction::AUTO
2696 );
2697 assert!(
2698 seen.press_inside,
2699 "the press snapshot is keyed by the dispatched pointer, and \
2700 the contact that held is holding a press"
2701 );
2702 }
2703 }
2704
2705 /// What a hold handler asks the tree **for** is applied to the contact that
2706 /// held.
2707 ///
2708 /// The other half of
2709 /// `a_hold_is_dispatched_under_the_contact_that_held`: that one asserts
2710 /// what the context is built with, this one what the context is collected
2711 /// into. Two requests carry no pointer of their own
2712 /// and are resolved against `current_pointer_id()` at collection —
2713 /// `capture_pointer()` and `cancel_pointer_sequence()` — so collecting
2714 /// after `current_input` is restored, rather than before, silently
2715 /// addresses both to the mouse.
2716 ///
2717 /// The mouse is made live in both arms and is *not* the contact under test,
2718 /// so a misrouted request lands somewhere the assertions can see rather
2719 /// than on a pointer the table does not hold.
2720 #[test]
2721 fn a_hold_captures_and_cancels_the_contact_that_held() {
2722 use crate::pointer::{CancelReason, PointerId};
2723 use crate::test_widgets::FillWidget;
2724 use crate::widget_builder::WidgetBuilder;
2725
2726 // The capture the handler takes rides on the contact, and the mouse —
2727 // live, hovering, holding nothing — is left alone. The contact's own
2728 // entry is already captured by this node (the arena takes it implicitly
2729 // at the press), so the mouse half is what a misroute shows up in.
2730 {
2731 let mut tree = WidgetTree::new();
2732 let node = tree.add(FillWidget::new().on_long_press(|_e, ctx| {
2733 ctx.capture_pointer();
2734 }));
2735 tree.layout(SizeProposal::exact(100.0, 100.0));
2736 tree.pointer_move(Point::new(10.0, 10.0));
2737
2738 let contact = hold_a_finger(&mut tree, Point::new(50.0, 50.0));
2739
2740 assert_eq!(
2741 tree.captured_by(contact),
2742 Some(node),
2743 "a capture taken from a hold belongs to the contact that held"
2744 );
2745 assert_eq!(
2746 tree.captured_by(PointerId::MOUSE),
2747 None,
2748 "the mouse was not the thing holding, and must not have been \
2749 captured on its behalf"
2750 );
2751 }
2752
2753 // The cancel the handler raises revokes the contact, and only it. The
2754 // finger's entry is gone (a contact that is taken away ceases to exist);
2755 // the mouse's press-less entry is untouched.
2756 {
2757 let mut tree = WidgetTree::new();
2758 tree.add(FillWidget::new().on_long_press(|_e, ctx| {
2759 ctx.cancel_pointer_sequence(CancelReason::WidgetDestroyed);
2760 }));
2761 tree.layout(SizeProposal::exact(100.0, 100.0));
2762 tree.pointer_move(Point::new(10.0, 10.0));
2763
2764 let contact = hold_a_finger(&mut tree, Point::new(50.0, 50.0));
2765
2766 assert!(
2767 !tree.live_pointers().any(|p| p.id == contact),
2768 "a cancel raised from a hold revokes the contact that held"
2769 );
2770 assert!(
2771 tree.live_pointers().any(|p| p.id == PointerId::MOUSE),
2772 "and revokes nothing else"
2773 );
2774 }
2775 }
2776
2777 /// Press one finger at `at` and let its hold ripen, without releasing it.
2778 ///
2779 /// [`long_press_at`](WidgetTree::long_press_at) lifts the contact, and a
2780 /// lift takes the capture back and ends the entry — so what the hold's own
2781 /// handler did to the pointer table is only observable before it.
2782 fn hold_a_finger(tree: &mut WidgetTree, at: Point) -> crate::pointer::PointerId {
2783 let hold = tree
2784 .effective_theme
2785 .input
2786 .profile(teksilo_tokens::PointerKind::Touch)
2787 .long_press;
2788 let contact = tree.new_contact();
2789 tree.touch_down(contact, at);
2790 tree.advance_input_time(hold);
2791 contact
2792 }
2793
2794 /// A deadline armed after the hand-back is reported to the event loop as a
2795 /// *future* instant.
2796 ///
2797 /// `instant_for` is what the winit loop turns into
2798 /// `ControlFlow::WaitUntil`. A deadline reported in the past is not a
2799 /// harmless rounding error: the loop wakes immediately, finds nothing
2800 /// ripe, re-derives the same past instant and spins at full CPU on a
2801 /// deadline that can never arrive.
2802 ///
2803 /// The advance is deliberately a large fraction of the hold, so that
2804 /// shifting by it once too often or once too few — the two ways
2805 /// `instant_for` can be wrong — moves the answer by far more than the
2806 /// tolerance below. Reported *early* is the spinning loop above; reported
2807 /// *late* is a long press the user waits an extra advance for.
2808 ///
2809 /// What this test does **not** cover is the offset's magnitude: it is
2810 /// subtracted here by exactly the amount `event_time_for` added, so the two
2811 /// cancel and the assertions below hold for a wrong offset as readily as
2812 /// for a right one. That number is guarded only by
2813 /// `repeated_hand_backs_re_measure_the_offset_rather_than_accumulating_it`.
2814 #[test]
2815 fn a_deadline_armed_after_the_hand_back_is_in_the_future() {
2816 use crate::test_widgets::FillWidget;
2817 use crate::widget_builder::WidgetBuilder;
2818
2819 // The tree spends real time alive before anything simulates it — which
2820 // on a live app is every second since launch, and is what makes a
2821 // deadline reported against the simulated clock land in the past.
2822 let mut tree = WidgetTree::new();
2823 tree.add(FillWidget::new().on_long_press(|_e, _c| {}));
2824 tree.layout(SizeProposal::exact(100.0, 100.0));
2825 std::thread::sleep(std::time::Duration::from_millis(60));
2826
2827 let advanced = std::time::Duration::from_millis(200);
2828 tree.advance_time(advanced);
2829 tree.resume_real_time();
2830
2831 tree.pointer_down_button(Point::new(50.0, 50.0), PointerButton::Primary);
2832 let hold = tree
2833 .theme()
2834 .input
2835 .profile(teksilo_tokens::PointerKind::Mouse)
2836 .long_press;
2837 let slack = std::time::Duration::from_millis(50);
2838 // The two assertions below only mean something while the advance
2839 // dominates the tolerance. Stated here so a later change to either
2840 // constant fails loudly instead of quietly re-opening the gap a 10 ms
2841 // advance and a 30 ms tolerance left.
2842 assert!(
2843 slack * 4 <= advanced && advanced * 4 >= hold,
2844 "the tolerance must be a fraction of the advance, and the advance a \
2845 large fraction of the hold: slack {slack:?}, advanced {advanced:?}, \
2846 hold {hold:?}"
2847 );
2848 let deadline = tree
2849 .next_timer_deadline()
2850 .expect("a held press has a long-press deadline");
2851 let wait = deadline.saturating_duration_since(std::time::Instant::now());
2852
2853 assert!(
2854 wait > std::time::Duration::ZERO,
2855 "the loop must be given something it can wait for"
2856 );
2857 // And it is the hold away — not the hold minus what was advanced
2858 // (subtracted a second time, the spinning loop), and not the hold plus
2859 // it (never subtracted at all). The tolerance is a quarter of the
2860 // advance, so neither can hide inside it.
2861 assert!(
2862 wait <= hold,
2863 "wake in ~{hold:?} after a {advanced:?} advance, got {wait:?} — reported late"
2864 );
2865 assert!(
2866 wait + slack >= hold,
2867 "wake in ~{hold:?} after a {advanced:?} advance, got {wait:?} — reported early"
2868 );
2869 tree.pointer_up_button(Point::new(50.0, 50.0), PointerButton::Primary);
2870 }
2871}
2872
2873/// The per-pointer table replacing the tree's singular pointer state: two
2874/// contacts hold two captures, hover belongs to the hover owner alone, the
2875/// contact cap is enforced at the door, and a nested dispatch waits its turn.
2876#[cfg(test)]
2877mod pointer_table_tests {
2878 use super::*;
2879 use crate::event::{EventResponse, Modifiers, PointerButton};
2880 use crate::pointer::{
2881 BackendDeviceKey, EventTime, PointerId, PointerIdAllocator, PointerInfo, PointerPhase,
2882 PointerSample,
2883 };
2884 use crate::test_widgets::FillWidget;
2885 use crate::widget_builder::WidgetBuilder;
2886 use std::cell::RefCell;
2887 use std::rc::Rc;
2888
2889 /// A fresh contact identity. Minted through the real allocator so it is
2890 /// monotonic and distinct from [`PointerId::MOUSE`].
2891 fn contact_id(raw: u64) -> PointerId {
2892 let alloc = PointerIdAllocator::global();
2893 let device = BackendDeviceKey::new(0xC0FFEE);
2894 let id = alloc.begin(device, raw);
2895 alloc.end(device, raw);
2896 id
2897 }
2898
2899 fn contact(id: PointerId, phase: PointerPhase, at: Point) -> PointerSample {
2900 PointerSample {
2901 pointer: PointerInfo::touch(id, EventTime::ZERO),
2902 phase,
2903 position: at,
2904 button: None,
2905 modifiers: Modifiers::NONE,
2906 coalesced: Vec::new(),
2907 }
2908 }
2909
2910 /// A widget that captures the pointer on press and holds it. The shape of
2911 /// every real drag handle (a slider knob, a splitter divider).
2912 fn capturing() -> impl crate::widget::Widget + 'static {
2913 FillWidget::new().on_pointer_event(|event, ctx| {
2914 if matches!(event, WidgetEvent::PointerDown { .. }) {
2915 ctx.capture_pointer();
2916 }
2917 EventResponse::Ignored
2918 })
2919 }
2920
2921 /// Two contacts on two widgets hold **independent** captures, and one
2922 /// lifting leaves the other's alone. With a single `pointer_captured_by`
2923 /// the second press overwrote the first, and the first finger's stream
2924 /// silently moved to the second widget.
2925 #[test]
2926 fn two_contacts_hold_independent_captures() {
2927 let mut tree = WidgetTree::new();
2928 let a = tree.add(capturing());
2929 let b = tree.add(capturing());
2930 let _root = tree.add(SideBySide { a, b });
2931 tree.layout(SizeProposal::exact(100.0, 100.0));
2932
2933 let first = contact_id(1);
2934 let second = contact_id(2);
2935 tree.dispatch_pointer(contact(first, PointerPhase::Down, Point::new(25.0, 50.0)));
2936 tree.dispatch_pointer(contact(second, PointerPhase::Down, Point::new(75.0, 50.0)));
2937
2938 assert_eq!(tree.captured_by(first), Some(a));
2939 assert_eq!(tree.captured_by(second), Some(b));
2940
2941 tree.dispatch_pointer(contact(first, PointerPhase::Up, Point::new(25.0, 50.0)));
2942 assert_eq!(tree.captured_by(first), None, "the lifted contact is gone");
2943 assert_eq!(
2944 tree.captured_by(second),
2945 Some(b),
2946 "one contact lifting must not release the other's capture"
2947 );
2948 }
2949
2950 /// A finger arriving while the mouse hovers must not touch hover at all —
2951 /// not the id, not the signal, not the `on_hover` handlers behind it.
2952 #[test]
2953 fn a_second_contact_never_churns_the_hover_signal() {
2954 let mut tree = WidgetTree::new();
2955 let a = tree.add(FillWidget::new());
2956 let b = tree.add(FillWidget::new());
2957 let _root = tree.add(SideBySide { a, b });
2958 tree.layout(SizeProposal::exact(100.0, 100.0));
2959
2960 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(25.0, 50.0)));
2961 assert_eq!(tree.hovered(), Some(a));
2962
2963 let churn = Rc::new(std::cell::Cell::new(0usize));
2964 let observer = {
2965 let churn = churn.clone();
2966 tree.hovered_signal()
2967 .observe(move |_| churn.set(churn.get() + 1))
2968 };
2969
2970 let finger = contact_id(3);
2971 tree.dispatch_pointer(contact(finger, PointerPhase::Down, Point::new(75.0, 50.0)));
2972 tree.dispatch_pointer(contact(finger, PointerPhase::Move, Point::new(80.0, 50.0)));
2973 tree.dispatch_pointer(contact(finger, PointerPhase::Up, Point::new(80.0, 50.0)));
2974
2975 assert_eq!(
2976 tree.hovered(),
2977 Some(a),
2978 "the mouse is still hovering where it was"
2979 );
2980 assert_eq!(churn.get(), 0, "a contact must not write the hover signal");
2981 drop(observer);
2982 }
2983
2984 /// …and the contact is never the hover owner, so it has no hover of its
2985 /// own to report either.
2986 #[test]
2987 fn a_contact_is_never_the_hover_owner() {
2988 let mut tree = WidgetTree::new();
2989 let target = tree.add(FillWidget::new());
2990 tree.layout(SizeProposal::exact(100.0, 100.0));
2991
2992 let finger = contact_id(4);
2993 tree.dispatch_pointer(contact(finger, PointerPhase::Down, Point::new(50.0, 50.0)));
2994 tree.dispatch_pointer(contact(finger, PointerPhase::Move, Point::new(52.0, 50.0)));
2995
2996 assert_eq!(tree.hover_owner(), None, "a finger cannot own hover");
2997 assert_eq!(tree.hovered(), None);
2998 assert_eq!(tree.hovered_for(finger), None);
2999 assert_eq!(
3000 tree.primary_pointer().map(|p| p.id),
3001 Some(finger),
3002 "it is still the primary pointer — primary and hover owner are not the same role"
3003 );
3004 assert_eq!(
3005 tree.pointer_position(finger),
3006 Some(Point::new(52.0, 50.0)),
3007 "and its position is tracked all the same"
3008 );
3009 let _ = target;
3010 }
3011
3012 /// A synthetic pen: a hovering-capable pointer *does* take the role, and
3013 /// the mouse it displaces is told its widget is no longer hovered.
3014 #[test]
3015 fn a_pen_takes_the_hover_owner_role_from_the_mouse() {
3016 let mut tree = WidgetTree::new();
3017 let a = tree.add(FillWidget::new());
3018 let b = tree.add(FillWidget::new());
3019 let _root = tree.add(SideBySide { a, b });
3020 tree.layout(SizeProposal::exact(100.0, 100.0));
3021
3022 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(25.0, 50.0)));
3023 assert_eq!(tree.hovered(), Some(a));
3024
3025 let stylus = contact_id(5);
3026 let mut pen = PointerInfo::touch(stylus, EventTime::ZERO);
3027 pen.kind = teksilo_tokens::PointerKind::Pen(teksilo_tokens::PenKind::Pen);
3028 let mut sample = contact(stylus, PointerPhase::Move, Point::new(75.0, 50.0));
3029 sample.pointer = pen;
3030 tree.dispatch_pointer(sample);
3031
3032 assert_eq!(
3033 tree.hover_owner().map(|p| p.id),
3034 Some(stylus),
3035 "the later hovering sample wins the role"
3036 );
3037 assert_eq!(tree.hovered(), Some(b));
3038 assert_eq!(
3039 tree.hovered_for(PointerId::MOUSE),
3040 None,
3041 "the displaced owner was told to let go"
3042 );
3043 }
3044
3045 /// The tenth simultaneous contact is admitted; the eleventh is refused at
3046 /// the door and produces no event at all.
3047 #[test]
3048 fn the_eleventh_contact_is_dropped_at_the_door() {
3049 use crate::pointer::table::PointerTable;
3050
3051 let presses = Rc::new(std::cell::Cell::new(0usize));
3052 let mut tree = WidgetTree::new();
3053 let counted = {
3054 let presses = presses.clone();
3055 tree.add(FillWidget::new().on_pointer_event(move |event, _ctx| {
3056 if matches!(event, WidgetEvent::PointerDown { .. }) {
3057 presses.set(presses.get() + 1);
3058 }
3059 EventResponse::Ignored
3060 }))
3061 };
3062 tree.layout(SizeProposal::exact(100.0, 100.0));
3063
3064 let ids: Vec<_> = (0..PointerTable::DEFAULT_CAP)
3065 .map(|n| contact_id(100 + n as u64))
3066 .collect();
3067 for &id in &ids {
3068 tree.dispatch_pointer(contact(id, PointerPhase::Down, Point::new(50.0, 50.0)));
3069 }
3070 assert_eq!(presses.get(), PointerTable::DEFAULT_CAP);
3071 assert_eq!(tree.live_pointers().count(), PointerTable::DEFAULT_CAP);
3072
3073 let overflow = contact_id(200);
3074 tree.dispatch_pointer(contact(
3075 overflow,
3076 PointerPhase::Down,
3077 Point::new(50.0, 50.0),
3078 ));
3079 assert_eq!(
3080 presses.get(),
3081 PointerTable::DEFAULT_CAP,
3082 "the eleventh contact must not reach a widget"
3083 );
3084 assert_eq!(tree.captured_by(overflow), None);
3085 assert_eq!(tree.live_pointers().count(), PointerTable::DEFAULT_CAP);
3086 let _ = counted;
3087 }
3088
3089 /// A palm the digitizer flagged never reaches a widget either.
3090 #[test]
3091 fn a_palm_never_reaches_a_widget() {
3092 let presses = Rc::new(std::cell::Cell::new(0usize));
3093 let mut tree = WidgetTree::new();
3094 {
3095 let presses = presses.clone();
3096 tree.add(FillWidget::new().on_pointer_event(move |event, _ctx| {
3097 if matches!(event, WidgetEvent::PointerDown { .. }) {
3098 presses.set(presses.get() + 1);
3099 }
3100 EventResponse::Ignored
3101 }));
3102 }
3103 tree.layout(SizeProposal::exact(100.0, 100.0));
3104
3105 let id = contact_id(300);
3106 let mut sample = contact(id, PointerPhase::Down, Point::new(50.0, 50.0));
3107 sample.pointer.palm = true;
3108 tree.dispatch_pointer(sample);
3109 assert_eq!(presses.get(), 0);
3110 assert_eq!(tree.live_pointers().count(), 0);
3111 }
3112
3113 /// A dispatch reached from inside a dispatch is queued, not run inline:
3114 /// the rest of the outer bubble runs on the state it started with, and the
3115 /// nested dispatch replays afterwards — still before the top-level call
3116 /// returns.
3117 #[test]
3118 fn a_nested_dispatch_is_queued_and_drained_after() {
3119 let log: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
3120
3121 let mut tree = WidgetTree::new();
3122 let other = {
3123 let log = log.clone();
3124 tree.add(FillWidget::new().on_tap(move |_e, _ctx| {
3125 log.borrow_mut().push("nested");
3126 }))
3127 };
3128 // Two handlers on the child, in the order the bubble runs them:
3129 // `on_pointer_event` (the pre-gesture intercept) queues the nested
3130 // dispatch, `on_tap` is the outer work still to come after it. That
3131 // pair is what makes the ordering below evidence rather than
3132 // coincidence.
3133 let child = {
3134 let log_pointer = log.clone();
3135 let log_tap = log.clone();
3136 tree.add(
3137 FillWidget::new()
3138 .on_pointer_event(move |event, ctx| {
3139 if matches!(event, WidgetEvent::PointerUp { .. }) {
3140 log_pointer.borrow_mut().push("child-press");
3141 // Re-enters the dispatch door from inside a handler.
3142 ctx.synthetic_click(other);
3143 }
3144 EventResponse::Ignored
3145 })
3146 .on_tap(move |_e, _ctx| {
3147 log_tap.borrow_mut().push("child-tap");
3148 }),
3149 )
3150 };
3151 // Keep the two halves disjoint, so the synthetic click lands on
3152 // `other` and not back on `child` (which would re-enter for ever).
3153 let _root = tree.add(SideBySide { a: child, b: other });
3154 tree.layout(SizeProposal::exact(100.0, 100.0));
3155
3156 let at = tree.bounds(child).center();
3157 tree.dispatch_event(WidgetEvent::pointer_down(
3158 at,
3159 PointerButton::Primary,
3160 Modifiers::NONE,
3161 ));
3162 tree.dispatch_event(WidgetEvent::pointer_up(
3163 at,
3164 PointerButton::Primary,
3165 Modifiers::NONE,
3166 ));
3167
3168 assert_eq!(
3169 *log.borrow(),
3170 vec!["child-press", "child-tap", "nested"],
3171 "the outer dispatch must finish on the state it started with, and the \
3172 nested dispatch replay only once it has — run inline it would read \
3173 child-press, nested, child-tap"
3174 );
3175 assert!(
3176 !tree.has_pending_dispatch(),
3177 "the queue must be empty again before the top-level call returns"
3178 );
3179 }
3180
3181 /// Localisation reads the captor's **current** bounds on every event, so a
3182 /// captured control inside a container that moves keeps reporting sensible
3183 /// widget-local coordinates rather than coordinates relative to where it
3184 /// used to be.
3185 #[test]
3186 fn a_captured_widget_localises_against_its_moving_bounds() {
3187 let seen: Rc<RefCell<Vec<Point>>> = Rc::new(RefCell::new(Vec::new()));
3188 let mut tree = WidgetTree::new();
3189 let knob = {
3190 let seen = seen.clone();
3191 tree.add(FillWidget::new().on_pointer_event(move |event, ctx| {
3192 match event {
3193 WidgetEvent::PointerDown { .. } => ctx.capture_pointer(),
3194 WidgetEvent::PointerMove { position, .. } => seen.borrow_mut().push(*position),
3195 _ => {}
3196 }
3197 EventResponse::Ignored
3198 }))
3199 };
3200 let offset = crate::signal::Signal::new(0.0f32);
3201 let _root = tree.add(ShiftedSlot {
3202 child: knob,
3203 offset: offset.clone(),
3204 });
3205 tree.layout(SizeProposal::exact(100.0, 100.0));
3206
3207 tree.dispatch_event(WidgetEvent::pointer_down(
3208 Point::new(30.0, 40.0),
3209 PointerButton::Primary,
3210 Modifiers::NONE,
3211 ));
3212 assert_eq!(tree.pointer_captured_by(), Some(knob));
3213 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(30.0, 40.0)));
3214
3215 // The container slides its child 20 dp to the trailing side.
3216 offset.set(20.0);
3217 tree.arena.mark_all_dirty();
3218 tree.layout(SizeProposal::exact(100.0, 100.0));
3219
3220 tree.dispatch_event(WidgetEvent::pointer_move(Point::new(30.0, 40.0)));
3221
3222 assert_eq!(
3223 *seen.borrow(),
3224 vec![Point::new(30.0, 40.0), Point::new(10.0, 40.0)],
3225 "the same window position must localise against the captor's new origin"
3226 );
3227 }
3228
3229 /// The localisation contract is **deliberately asymmetric**, and this pins
3230 /// the asymmetry so nobody "fixes" it into a defect.
3231 ///
3232 /// `localize_event` rewrites `PointerDown` / `PointerUp` / `PointerMove` /
3233 /// `Gesture` into the receiver's own space, and has no arm for `Scroll` or
3234 /// `PointerCancel`. That is why those two name their position
3235 /// `window_position`: the router routes by the first (hit-testing is
3236 /// necessarily window-space), and `common/scrollable.rs` feeds it to
3237 /// `KineticScroller::pan`, whose tracker follows the *pointer* — and since
3238 /// localisation resolves against the captor's **current** bounds on every
3239 /// event (the test above), a localised value would fold the measured
3240 /// widget's own motion into the velocity.
3241 ///
3242 /// Two independent things keep it that way, and the two assertions below
3243 /// answer for one each — which is why both are here rather than one
3244 /// standing in for the other:
3245 ///
3246 /// * **`Scroll`** reaches its handler through the localising route
3247 /// (`dispatch_to_widget` → `localize_event`), so the missing arm is the
3248 /// whole of its protection. Adding one reads like a tidy-up; the scroll
3249 /// assertion is what goes red.
3250 /// * **`PointerCancel`** is delivered by the cancel funnel through
3251 /// `dispatch_to_widget_direct`, which does not localise at all, so an arm
3252 /// added to `localize_event` would be inert on that path. What guards it
3253 /// is that `pointer_cancel_event` records the pointer table's own
3254 /// (window-space) position verbatim; localising it at the funnel, which
3255 /// knows the recipient and could, is the single change the cancel
3256 /// assertion catches.
3257 #[test]
3258 fn scroll_and_cancel_stay_in_window_space_while_a_press_is_localised() {
3259 #[derive(Default)]
3260 struct Seen {
3261 press: Vec<Point>,
3262 scroll: Vec<Option<Point>>,
3263 cancel: Vec<Option<Point>>,
3264 }
3265 let seen: Rc<RefCell<Seen>> = Rc::new(RefCell::new(Seen::default()));
3266 let mut tree = WidgetTree::new();
3267 let target = {
3268 let a = seen.clone();
3269 let b = seen.clone();
3270 tree.add(
3271 FillWidget::new()
3272 .on_pointer_event(move |event, ctx| {
3273 match event {
3274 WidgetEvent::PointerDown { position, .. } => {
3275 a.borrow_mut().press.push(*position);
3276 // Hold the pointer so the cancel funnel has
3277 // someone to address.
3278 ctx.capture_pointer();
3279 }
3280 WidgetEvent::PointerCancel {
3281 window_position, ..
3282 } => a.borrow_mut().cancel.push(*window_position),
3283 _ => {}
3284 }
3285 EventResponse::Ignored
3286 })
3287 .on_scroll(move |event, _ctx| {
3288 if let WidgetEvent::Scroll {
3289 window_position, ..
3290 } = event
3291 {
3292 b.borrow_mut().scroll.push(*window_position);
3293 }
3294 EventResponse::Ignored
3295 }),
3296 )
3297 };
3298 // The slot puts its child 20 dp along, so window x and local x differ by
3299 // exactly 20 and a localised value is distinguishable from a raw one.
3300 let _root = tree.add(ShiftedSlot {
3301 child: target,
3302 offset: crate::signal::Signal::new(20.0f32),
3303 });
3304 tree.layout(SizeProposal::exact(100.0, 100.0));
3305
3306 let at = Point::new(30.0, 40.0);
3307 tree.dispatch_event(WidgetEvent::pointer_down(
3308 at,
3309 PointerButton::Primary,
3310 Modifiers::NONE,
3311 ));
3312 tree.dispatch_event(WidgetEvent::Scroll {
3313 delta: crate::event::ScrollDelta::Lines { x: 0.0, y: 1.0 },
3314 modifiers: Modifiers::NONE,
3315 window_position: Some(at),
3316 phase: crate::pointer::ScrollPhase::Discrete,
3317 pointer: crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
3318 });
3319 let captor = tree.pointer_captured_by();
3320 assert_eq!(
3321 captor,
3322 Some(target),
3323 "the press must have taken the capture"
3324 );
3325 let pointer = tree.pointers.primary_id().expect("a live pointer");
3326 tree.cancel_pointer(
3327 pointer,
3328 crate::pointer::CancelReason::Platform,
3329 &mut crate::window::NoopWindowOps,
3330 );
3331
3332 let seen = seen.borrow();
3333 assert_eq!(
3334 seen.press,
3335 vec![Point::new(10.0, 40.0)],
3336 "a press is localised: window x 30 minus the slot's 20 dp offset"
3337 );
3338 assert_eq!(
3339 seen.scroll,
3340 vec![Some(at)],
3341 "`Scroll::window_position` must arrive as produced: localising it would feed \
3342 the kinetic tracker a frame that moves with the widget it measures"
3343 );
3344 assert_eq!(
3345 seen.cancel,
3346 vec![Some(at)],
3347 "`PointerCancel::window_position` must arrive as the revoking path recorded \
3348 it, for the same reason"
3349 );
3350 }
3351
3352 // --- fixtures --------------------------------------------------------
3353
3354 /// Splits its bounds down the middle: `a` on the leading half, `b` on the
3355 /// trailing one, so two pointers can land on two different widgets.
3356 #[derive(Debug)]
3357 struct SideBySide {
3358 a: WidgetId,
3359 b: WidgetId,
3360 }
3361
3362 impl crate::widget::Widget for SideBySide {
3363 fn layout_response(
3364 &self,
3365 proposal: SizeProposal,
3366 _ctx: &crate::widget::LayoutContext,
3367 ) -> crate::widget::LayoutResponse {
3368 proposal.resolve(0.0, 0.0).into()
3369 }
3370 fn place_children(
3371 &self,
3372 bounds: Rect,
3373 _proposal: SizeProposal,
3374 children: &mut [crate::widget::WidgetPlacement],
3375 _ctx: &crate::widget::LayoutContext,
3376 ) {
3377 let half = bounds.width / 2.0;
3378 for (index, c) in children.iter_mut().enumerate() {
3379 c.origin = Point::new(bounds.x + half * index as f32, bounds.y);
3380 c.size = teksilo_canvas::Size::new(half, bounds.height);
3381 }
3382 }
3383 fn children(&self) -> Vec<WidgetId> {
3384 vec![self.a, self.b]
3385 }
3386 }
3387
3388 /// Places its single child at a signal-driven horizontal offset, so a test
3389 /// can move a captured widget between two pointer samples.
3390 #[derive(Debug)]
3391 struct ShiftedSlot {
3392 child: WidgetId,
3393 offset: crate::signal::Signal<f32>,
3394 }
3395
3396 impl crate::widget::Widget for ShiftedSlot {
3397 fn layout_response(
3398 &self,
3399 proposal: SizeProposal,
3400 _ctx: &crate::widget::LayoutContext,
3401 ) -> crate::widget::LayoutResponse {
3402 proposal.resolve(0.0, 0.0).into()
3403 }
3404 fn place_children(
3405 &self,
3406 bounds: Rect,
3407 _proposal: SizeProposal,
3408 children: &mut [crate::widget::WidgetPlacement],
3409 _ctx: &crate::widget::LayoutContext,
3410 ) {
3411 for c in children.iter_mut() {
3412 c.origin = Point::new(bounds.x + self.offset.get(), bounds.y);
3413 c.size = bounds.size();
3414 }
3415 }
3416 fn children(&self) -> Vec<WidgetId> {
3417 vec![self.child]
3418 }
3419 }
3420}
3421
3422/// The arbitration binds the **timer** path, not only the sample path.
3423#[cfg(test)]
3424mod tick_arbitration_tests {
3425 use super::*;
3426 use crate::event::EventResponse;
3427 use crate::test_widgets::{FillWidget, StackWidget};
3428 use crate::widget_builder::WidgetBuilder;
3429 use std::cell::Cell;
3430 use std::rc::Rc;
3431 use std::time::Duration;
3432
3433 /// A tree whose innermost child holds the sequence on its press, under an
3434 /// ancestor that competes for the same press (`on_drag` is what enrols it)
3435 /// and also carries a long-press recognizer.
3436 ///
3437 /// `max_hold` is raised past `long_press` so the hold is still standing
3438 /// when the ancestor's timer comes due; with the shipped 250 ms hold and
3439 /// 500 ms long press the hold always expires first and the two never
3440 /// overlap, so there would be nothing to observe.
3441 fn tree_with_a_holder_under_a_long_pressing_peer(
3442 hold_on_press: bool,
3443 ) -> (WidgetTree, Rc<Cell<bool>>) {
3444 let long_pressed = Rc::new(Cell::new(false));
3445 let flag = long_pressed.clone();
3446
3447 let mut tree = WidgetTree::new();
3448 let mut theme = tree.theme().clone();
3449 theme.input.gestures.mouse.max_hold = Duration::from_millis(2000);
3450 tree.set_theme(theme);
3451
3452 let child = tree.add(FillWidget::new().on_pointer_event(move |event, ctx| {
3453 if hold_on_press && matches!(event, WidgetEvent::PointerDown { .. }) {
3454 ctx.hold_gesture();
3455 }
3456 EventResponse::Ignored
3457 }));
3458 tree.add(
3459 StackWidget::new()
3460 .child(child)
3461 .on_drag(|_phase, _c| {})
3462 .on_long_press(move |_e, _c| flag.set(true)),
3463 );
3464 tree.layout(SizeProposal::exact(300.0, 50.0));
3465 (tree, long_pressed)
3466 }
3467
3468 /// The control: with nothing holding, the peer's long press does fire on
3469 /// the tick. Without this the test below would pass on a fixture that
3470 /// could never long-press at all.
3471 #[test]
3472 fn a_peers_long_press_fires_on_the_tick_when_nothing_holds() {
3473 let (mut tree, long_pressed) = tree_with_a_holder_under_a_long_pressing_peer(false);
3474 tree.pointer_down_button(Point::new(20.0, 25.0), PointerButton::Primary);
3475 tree.advance_time(Duration::from_millis(600));
3476 assert!(
3477 long_pressed.get(),
3478 "the ancestor is a member with a long-press recognizer and its \
3479 timer came due"
3480 );
3481 }
3482
3483 /// …and it does not while a peer is holding.
3484 ///
3485 /// `hold_gesture` freezes the arbitration: no other member may win while a
3486 /// member is still deciding. The sample path has always honoured that
3487 /// (`sequence_blocks_arena`); the timer path dispatched whatever a
3488 /// recognizer produced, so a long press whose deadline happened to fall
3489 /// inside a hold fired anyway — which is the same recognizer winning, one
3490 /// door over.
3491 #[test]
3492 fn a_peers_long_press_does_not_fire_on_the_tick_while_a_member_holds() {
3493 let (mut tree, long_pressed) = tree_with_a_holder_under_a_long_pressing_peer(true);
3494 tree.pointer_down_button(Point::new(20.0, 25.0), PointerButton::Primary);
3495 assert!(
3496 tree.sequence_members(crate::pointer::PointerId::MOUSE)
3497 .iter()
3498 .any(|(_, _, state)| *state == crate::gesture::MemberState::Held),
3499 "the fixture must actually be holding"
3500 );
3501
3502 tree.advance_time(Duration::from_millis(600));
3503 assert!(
3504 !long_pressed.get(),
3505 "no peer may win while a member is holding — the timer path is not \
3506 a way around the arbitration"
3507 );
3508 }
3509}