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