teksilo_core/widget_tree/test_api.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use super::*;
5
6/// Linear interpolation between two points, `t` in `0.0..=1.0`.
7///
8/// Every multi-sample helper in this module walks its path with this, so the
9/// intermediate positions of a drag, a fling and a pinch are produced by one
10/// rule and a test that counts samples can reason about where each one landed.
11fn lerp_point(from: Point, to: Point, t: f32) -> Point {
12 Point::new(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t)
13}
14
15impl WidgetTree {
16 /// The content id of the tooltip anchored at `widget` or anywhere inside
17 /// it.
18 ///
19 /// The attach helpers keep the content id to themselves, so a test that
20 /// needs to drive a tooltip's own surface (promote it, focus into it) has
21 /// no other way to name it. Matching the whole subtree, not just the id,
22 /// is what makes this work for composing controls: `Button` keeps focus on
23 /// its outer node but attaches its tooltip to an inner body root.
24 pub fn tooltip_content_within(&self, widget: WidgetId) -> Option<WidgetId> {
25 self.tooltips
26 .iter()
27 .find(|e| self.is_descendant_of(e.anchor_id, widget))
28 .map(|e| e.content_id)
29 }
30
31 /// Whether that tooltip has been promoted.
32 ///
33 /// Promotion is the line between an informational tip and a panel the user
34 /// asked for: it decides the AT role, the dismiss behaviour, and whether
35 /// the surface takes a Tab stop.
36 pub fn tooltip_is_sticky_within(&self, widget: WidgetId) -> bool {
37 self.tooltips
38 .iter()
39 .any(|e| self.is_descendant_of(e.anchor_id, widget) && e.is_sticky)
40 }
41
42 /// Simulate a click at the center of a widget.
43 pub fn click(&mut self, id: WidgetId) {
44 self.synthesise_tap(id);
45 }
46
47 /// Synthesise a primary-button tap at the center of `id`'s
48 /// resolved bounds. The OS hands the click off to the widget tree
49 /// even though the click never went through the normal hit-test
50 /// path. Used by the Windows custom-title-bar backend when
51 /// `WM_NCHITTEST` reported `HTMINBUTTON`/`HTMAXBUTTON`/`HTCLOSE`
52 /// for an area covering a `ControlButton` — the OS treated the
53 /// area as non-client and `WM_LBUTTONDOWN`/`UP` never fired in
54 /// widget land, so we re-issue a synthetic primary-button down
55 /// + up on the right widget.
56 ///
57 /// Equivalent semantics to [`Self::click`]; named differently so
58 /// production call sites read clearly.
59 ///
60 /// The tap runs on a standalone dispatch, so a handler it reaches
61 /// cannot use the multi-window API. Call
62 /// [`synthesise_tap_with_ops`](Self::synthesise_tap_with_ops) from
63 /// anywhere that already holds a real
64 /// [`WindowOps`](crate::window::WindowOps) sink.
65 pub fn synthesise_tap(&mut self, id: WidgetId) {
66 let mut noop = crate::window::NoopWindowOps;
67 self.synthesise_tap_with_ops(id, &mut noop);
68 }
69
70 /// [`synthesise_tap`](Self::synthesise_tap), dispatched over the
71 /// caller's app-level [`WindowOps`](crate::window::WindowOps) sink.
72 ///
73 /// A synthetic tap is a *nested* dispatch, and everything the tapped
74 /// widget does happens inside it — including the intent it sends and
75 /// the action that intent resolves to. Dispatching it standalone
76 /// therefore hands that action a context with no window sink:
77 /// `ctx.open_window` panics, and `find_window` / `focus_window` /
78 /// `close_window_by_id` silently do nothing. That is how keyboard
79 /// activation in a menu (Enter, Space, a mnemonic, type-ahead — all
80 /// four route through `EventContext::synthetic_click`) lost the
81 /// multi-window API that the same row reached fine by mouse.
82 pub fn synthesise_tap_with_ops(
83 &mut self,
84 id: WidgetId,
85 ops: &mut dyn crate::window::WindowOps,
86 ) {
87 let center = self.arena.bounds(id).center();
88 self.dispatch_event_with_ops(
89 WidgetEvent::pointer_down(center, PointerButton::Primary, Modifiers::NONE),
90 &mut *ops,
91 );
92 self.dispatch_event_with_ops(
93 WidgetEvent::pointer_up(center, PointerButton::Primary, Modifiers::NONE),
94 &mut *ops,
95 );
96 }
97
98 /// Simulate pointer movement to a position.
99 pub fn pointer_move(&mut self, position: Point) {
100 self.dispatch_event(WidgetEvent::pointer_move(position));
101 }
102
103 /// Simulate a key press (down + up), carrying the text the platform
104 /// attaches to the key ([`Key::to_text`]).
105 ///
106 /// That text is not decoration: Escape arrives as U+001B, and a widget
107 /// that inspects `text` behaves differently with it than without. This
108 /// helper used to send `text: None` for every key, so a whole class of
109 /// bug was invisible to every test in the workspace — a field that
110 /// swallowed Escape passed the suite while failing in the user's hands.
111 pub fn press_key(&mut self, key: Key, modifiers: Modifiers) {
112 self.dispatch_event(WidgetEvent::KeyDown {
113 key,
114 modifiers,
115 text: key.to_text().map(str::to_string),
116 });
117 self.dispatch_event(WidgetEvent::KeyUp { key, modifiers });
118 }
119
120 /// Simulate typing text into the focused widget.
121 pub fn type_text(&mut self, _widget: WidgetId, text: &str) {
122 for ch in text.chars() {
123 self.dispatch_event(WidgetEvent::KeyDown {
124 key: Key::Character(ch),
125 modifiers: Modifiers::NONE,
126 text: Some(ch.to_string()),
127 });
128 }
129 }
130
131 /// Simulate a pointer down at a specific position with a specific button.
132 pub fn pointer_down_button(&mut self, position: Point, button: PointerButton) {
133 self.dispatch_event(WidgetEvent::pointer_down(position, button, Modifiers::NONE));
134 }
135
136 /// Simulate a pointer up at a specific position with a specific button.
137 pub fn pointer_up_button(&mut self, position: Point, button: PointerButton) {
138 self.dispatch_event(WidgetEvent::pointer_up(position, button, Modifiers::NONE));
139 }
140
141 /// Simulate a drag from one position to another.
142 pub fn drag(&mut self, from: Point, to: Point) {
143 self.dispatch_event(WidgetEvent::pointer_down(
144 from,
145 PointerButton::Primary,
146 Modifiers::NONE,
147 ));
148 self.dispatch_event(WidgetEvent::pointer_move(to));
149 self.dispatch_event(WidgetEvent::pointer_up(
150 to,
151 PointerButton::Primary,
152 Modifiers::NONE,
153 ));
154 }
155
156 /// Get bounds of a child by index.
157 pub fn child_bounds(&self, parent: WidgetId, index: usize) -> Rect {
158 let children = self.children(parent);
159 self.bounds(children[index])
160 }
161
162 /// Get a child widget ID by index.
163 pub fn child_widget(&self, parent: WidgetId, index: usize) -> WidgetId {
164 self.children(parent)[index]
165 }
166
167 /// Advance this tree's clock by `duration`, and run everything that clock
168 /// drives.
169 ///
170 /// **The one door.** One call moves, to one virtual now: the simulated
171 /// clock, the input timeline, the gesture arenas (today: the long-press
172 /// hold), the press-feedback delays, every live fling, the animation
173 /// scheduler, the frame tick, the overlay manager's clock, tooltip dwell,
174 /// delayed overlays, the pointer-leave grace and overlay auto-dismissal —
175 /// then drains the signal, rebuild and visibility changes any of that
176 /// produced. A caller never has to advance a second thing to keep one of
177 /// those in step with another.
178 ///
179 /// It is not, however, the door to *everything* that is timed; the list
180 /// below is the current boundary, and it is the list that has to grow when
181 /// a subsystem is brought onto this clock.
182 ///
183 /// While this runs, time is **taken over**: the input timeline and the
184 /// animation clock both read the simulated clock and nothing else. A long
185 /// press fires because the caller advanced the hold and never because the
186 /// caller itself took that long; two samples dispatched without an
187 /// intervening advance are stamped the same instant rather than however far
188 /// apart the machine happened to run them; and an animation ages by exactly
189 /// what was advanced. A headless test wants that to persist, and it does. A
190 /// host sharing the tree with a real event loop — the debug automation
191 /// bridge — must give time back when the operation ends, or the window it
192 /// is attached to never measures another gesture and never advances another
193 /// animation frame: see [`resume_real_time`](Self::resume_real_time).
194 ///
195 /// What it does **not** move:
196 ///
197 /// - The shader-driven
198 /// [`AnimatedQuadRegistry`](crate::animated_quad::AnimatedQuadRegistry).
199 /// It is ticked from `render()` and has no simulated door at all.
200 /// - A deferred member's `eligible_at` on a
201 /// [`PointerSequence`](crate::gesture::PointerSequence). Not an
202 /// oversight: eligibility is never stored, it is re-derived against the
203 /// timestamp of whatever sample is being arbitrated, so there is no
204 /// transition to perform at that instant and a press that sat still past
205 /// its `long_press` is already eligible on its very next move. See
206 /// [`PointerSequence::next_hold_deadline`](crate::gesture::PointerSequence::next_hold_deadline).
207 /// A hold's `max_hold`, by contrast, *is* a stored transition and is
208 /// moved — by the gesture pass in (3).
209 /// - Any clock a widget owns itself. A widget that reads the wall clock
210 /// directly rather than taking its deadline from the tree is outside this
211 /// door by construction, and there are several in `teksilo-widgets`.
212 ///
213 /// Dispatched over a no-op window sink; call
214 /// [`advance_time_with_ops`](Self::advance_time_with_ops) from anywhere
215 /// that holds a real one.
216 pub fn advance_time(&mut self, duration: std::time::Duration) {
217 let mut noop = crate::window::NoopWindowOps;
218 self.advance_time_with_ops(duration, &mut noop);
219 }
220
221 /// [`advance_time`](Self::advance_time), over the caller's
222 /// [`WindowOps`](crate::window::WindowOps) sink.
223 ///
224 /// A tick is a dispatch: a long press recognized here runs its handler,
225 /// and that handler may open a window. Standalone,
226 /// [`NoopWindowOps`](crate::window::NoopWindowOps) panics on
227 /// `open_window` — the same trap `synthesise_tap_with_ops` exists for.
228 pub fn advance_time_with_ops(
229 &mut self,
230 duration: std::time::Duration,
231 ops: &mut dyn crate::window::WindowOps,
232 ) {
233 // (0) Take the tree off the wall clock *before* anything reads a
234 // deadline, so this whole call is measured on one axis.
235 self.enter_simulated_mode();
236
237 // (1) Promote before the clock moves. An `animate_to` armed while the
238 // clock read T must start at T; stamping it after the clock reached
239 // T + d starts it d late and the caller's very next assertion is off
240 // by exactly the duration they just advanced.
241 self.process_pending_animations_at(self.sim_clock);
242
243 // (2) The clock itself. A clock that has to be told (a `ManualClock`)
244 // is moved here; an anchored one is read off `sim_clock` by
245 // `input_now`. The overlay manager's mirror must be updated before any
246 // pass below can dismiss, because `OverlayManager::dismiss` stamps the
247 // fade's simulated start from it.
248 self.sim_clock += duration;
249 self.input_clock().advance(duration);
250 self.overlay_manager.set_sim_clock(self.sim_clock);
251
252 // (3) The input layer, in the order the real event loop uses: flings,
253 // then press-feedback delays, then the gesture arenas. `tick_gestures`
254 // owns all three — giving the fling pump its own call site here would
255 // pump every live coast twice per advance.
256 self.tick_gestures_with_ops(self.sim_clock, &mut *ops);
257
258 // (4) The frame tick, and only if one was asked for: an unrequested
259 // advance must not fire the per-frame observers. The delta is the
260 // duration advanced, not a reading of `last_frame_time` — nothing was
261 // rendered, and `last_frame_time` is the *render* pacing reference.
262 if self.take_frame_tick_request() {
263 let delta = duration.as_secs_f32().clamp(0.0, 0.1);
264 self.frame_tick.set(delta);
265 }
266
267 // (5) Animations, at the new now and after the promotion in (1), so an
268 // animation armed before this call has aged by exactly `duration`.
269 self.animation_scheduler
270 .tick(self.sim_clock, &self.arena, self.paint_epoch);
271
272 // (6) The overlay and tooltip passes, in the order they depend on:
273 // a dwell that ripens can show a tooltip, a delayed overlay that
274 // matures can show a surface, and the dismissal passes below must see
275 // both within this same virtual frame.
276 self.process_tooltips();
277 self.process_delayed_overlays();
278 self.process_pointer_leave_overlays();
279 self.process_auto_dismiss_overlays();
280 self.process_overlay_fade_dismissals_sim();
281
282 // (7) Last, so a signal written by a long-press handler, a coasting
283 // fling's chained scroll or an overlay dismissal is flushed inside the
284 // virtual frame that produced it rather than a frame later.
285 self.process_state_changes(&mut *ops);
286 }
287
288 /// [`advance_time`](Self::advance_time), under the name the input side
289 /// reads better by.
290 ///
291 /// An alias, not a second timeline: there is one clock, and moving the
292 /// input axis is moving it.
293 pub fn advance_input_time(&mut self, duration: std::time::Duration) {
294 self.advance_time(duration);
295 }
296
297 /// Get the current simulated clock value.
298 pub fn simulated_now(&self) -> std::time::Instant {
299 self.sim_clock
300 }
301
302 /// Total number of live tooltip attachments, dead ones included.
303 ///
304 /// Distinct from `pending_tooltip_count`, which only counts entries with a
305 /// running dwell. This is the raw table size — the number that must stay
306 /// flat across rebuilds, since `attach_tooltip*` is called from `build()`
307 /// and the table is scanned on every pointer move, every layout pass and
308 /// once per widget in the accessibility walk.
309 pub fn tooltip_entry_count(&self) -> usize {
310 self.tooltips.len()
311 }
312
313 /// Every node inside `root` (inclusive) that Tab traversal would stop on:
314 /// focusable, and not suppressed by a `tab_stop` flag on itself or any
315 /// ancestor.
316 /// Every widget the arena still holds — active, dormant and orphaned alike.
317 ///
318 /// The number a leak test must assert on. `active_widget_count` walks the
319 /// tree from its roots and so cannot see the failure mode that matters
320 /// here: a node kept alive in the arena with nothing pointing at it. A
321 /// parentless orphan (tooltip content is `ctx.add`ed, hence parentless by
322 /// construction) is invisible to every other count in this file, and to the
323 /// accessibility tree, while still paying for itself in the arena's slotmap
324 /// forever.
325 /// Every node inside `root` (inclusive) that Tab traversal would stop on:
326 /// focusable, and not suppressed by a `tab_stop` flag on itself or any
327 /// ancestor.
328 ///
329 /// Pressing Tab and watching focus cannot answer this for a view that
330 /// claims the key for its own navigation — `TableView` moves a cell cursor
331 /// on Tab, so focus never moves and the traversal graph underneath stays
332 /// invisible. A data view should expose exactly one stop however many rows
333 /// are realized; more than one means a control inside a row has leaked
334 /// into the Tab order, where its presence would track the scroll position.
335 ///
336 /// Membership matches the real collector
337 /// ([`collect_scope_entries`](crate::widget_tree::WidgetTree)) exactly: a
338 /// dormant node and a disabled subtree are both skipped, because Tab
339 /// traversal returns at each. The two differ only in *shape* — the real
340 /// collector groups a `traversal_scope` subtree so it can order it
341 /// independently, and this returns one flat list in tree order — which is
342 /// what a membership assertion wants.
343 ///
344 /// The guards are load-bearing rather than cosmetic. Without them this
345 /// reports stops the traversal never visits, and a test asserting that a
346 /// culled or collapsed subtree left the Tab ring passes or fails for a
347 /// reason unrelated to the mechanism it is pinning.
348 pub fn tab_stops_within(&self, root: WidgetId) -> Vec<WidgetId> {
349 let mut out = Vec::new();
350 self.collect_tab_stops_within(root, &mut out);
351 out
352 }
353
354 fn collect_tab_stops_within(&self, id: WidgetId, out: &mut Vec<WidgetId>) {
355 // Dormant: `collect_scope_entries` returns here, so the whole subtree
356 // is off the traversal graph — a `Switcher`'s hidden branch, a closed
357 // popover, a `visible_when` gate that went false.
358 if !self.arena.is_active(id) {
359 return;
360 }
361 let Some(node) = self.arena.get(id) else {
362 return;
363 };
364 // Disabled: likewise a whole-subtree stop in the real collector.
365 if node
366 .enabled_state
367 .as_ref()
368 .map(|s| !s.get())
369 .unwrap_or(false)
370 {
371 return;
372 }
373 if self.is_node_focusable(node) && self.tab_stop_effective(id) {
374 out.push(id);
375 }
376 for &child in self.arena.children(id) {
377 self.collect_tab_stops_within(child, out);
378 }
379 }
380
381 pub fn widget_count(&self) -> usize {
382 self.arena.len()
383 }
384
385 /// Tear down a widget and everything it owns — its subtree, its tooltip,
386 /// and the parentless content it built with
387 /// [`add_detached`](crate::build_context::BuildContext::add_detached).
388 ///
389 /// The application-facing door is `BuildContext::destroy_subtree`; this is
390 /// the same call for tests that hold the tree directly.
391 pub fn destroy_subtree_for_testing(&mut self, id: WidgetId) {
392 self.destroy_subtree(id);
393 }
394
395 /// Panic unless every trace of a pointer interaction is gone.
396 ///
397 /// The one assertion a touch test ends with. A leak here is not a cosmetic
398 /// untidiness: a surviving capture redelivers every later move to a widget
399 /// nobody is pointing at, a surviving sequence lets a stale competitor win
400 /// the *next* press, and a live recognizer entry starts the next contact
401 /// mid-gesture. All three are silent until something much later
402 /// misbehaves, which is why this is checked rather than reasoned about.
403 ///
404 /// A **hovering** pointer resting in the table is not a leak: a mouse that
405 /// has been seen once keeps its entry for the life of the tree, and that
406 /// entry is what every singular accessor reads. What must not survive is a
407 /// pointer still *contacting* the surface, a capture, a sequence, or a
408 /// gesture arena still following a contact.
409 ///
410 /// One thing the design lists is still absent: the touch-motion layer's own
411 /// state — live pans, coasts, the window's pinch and the palm watches. The
412 /// framework press *is* checked, at the bottom of this function.
413 pub fn assert_no_leaked_pointer_state(&self) {
414 let mut leaks: Vec<String> = Vec::new();
415 for entry in self.pointers.iter() {
416 let id = entry.info.id;
417 if entry.is_contacting() {
418 leaks.push(format!(
419 "{id:?} ({:?}) is still contacting the surface",
420 entry.info.kind
421 ));
422 }
423 if let Some(captor) = entry.captured_by {
424 leaks.push(format!("{id:?} still captures {captor:?}"));
425 }
426 if let Some(sequence) = entry.sequence.as_ref() {
427 leaks.push(format!(
428 "{id:?} still has a sequence ({} member(s), winner {:?})",
429 sequence.members().len(),
430 sequence.winner()
431 ));
432 }
433 }
434 for &owner in &self.gesture_owners {
435 if self
436 .arena
437 .get(owner)
438 .and_then(|node| node.handlers.gesture_arena.as_ref())
439 .is_some_and(|set| set.is_live())
440 {
441 leaks.push(format!(
442 "{owner:?} has a gesture arena still following a contact"
443 ));
444 }
445 }
446 // The framework press. Every exit — a release, a cancel, a peer claim —
447 // goes through `end_press`, so a surviving record means one of them was
448 // missed and some node is painted as held by a pointer that is gone.
449 for id in self.arena.active_ids_iter() {
450 if let Some(pointer) = self.pressed_by(id) {
451 leaks.push(format!("{id:?} is still pressed by {pointer:?}"));
452 }
453 }
454 assert!(
455 leaks.is_empty(),
456 "pointer state leaked after the interaction:\n - {}",
457 leaks.join("\n - ")
458 );
459 }
460
461 // ---------------------------------------------------------------
462 // A21 — driving touch and pen from a test
463 // ---------------------------------------------------------------
464 //
465 // Every helper below builds a `PointerSample` in exactly the shape
466 // `teksilo-platform`'s translator builds one (`event_translation.rs`:
467 // a contact holds `ButtonMask::PRIMARY` while it is down and reports
468 // `Some(PointerButton::Primary)` on the two phases that change a
469 // button; a stylus adds its axes) and pushes it through
470 // `dispatch_pointer`, the one ingress door. Nothing here fabricates a
471 // `WidgetEvent`: a helper that stepped around the router would test
472 // the helper rather than the framework, and the hit-test-by-kind, the
473 // sequence, the pan session, the palm watch and the pinch feed all
474 // hang off that door.
475 //
476 // Every one of them puts the tree on the **simulated clock** first, and
477 // then stamps its sample from [`input_now`](Self::input_now). Both halves
478 // are load-bearing: a tree still on the wall clock stamps two consecutive
479 // samples microseconds apart, so a `touch_drag` that means "travel 200 dp,
480 // no time passes" would instead describe a flick at some thousands of dp
481 // per second and hand off to a coast — differently on every machine. Once
482 // simulated, the interval between two samples is exactly what
483 // [`advance_input_time`](Self::advance_input_time) put there and nothing
484 // else, which is what the rest of P14 is for.
485
486 /// Mint a fresh contact identity, the way the platform layer does.
487 ///
488 /// A backend reuses its own contact ids the moment a finger lifts, so
489 /// the allocator mints a `PointerId` per press; this is that call with
490 /// a per-process os id, and it `end`s the mapping immediately so the
491 /// allocator's live table does not grow across a test run.
492 pub fn new_contact(&self) -> crate::pointer::PointerId {
493 use std::sync::atomic::{AtomicU64, Ordering};
494 static NEXT_OS_ID: AtomicU64 = AtomicU64::new(1);
495 let device = crate::pointer::BackendDeviceKey::new(0x7E57);
496 let os_id = NEXT_OS_ID.fetch_add(1, Ordering::Relaxed);
497 let alloc = crate::pointer::PointerIdAllocator::global();
498 let id = alloc.begin(device, os_id);
499 alloc.end(device, os_id);
500 id
501 }
502
503 /// One direct-pointer sample, stamped on this tree's input timeline.
504 ///
505 /// `pub(super)` so a sibling module's tests can dispatch a contact of a kind
506 /// the A21 helpers do not name — `touch_down` and `pen_down` cover the two
507 /// kinds an application sees, and a gate that must refuse
508 /// [`PointerKind::Unknown`](teksilo_tokens::PointerKind::Unknown) can only be
509 /// tested by asking for one.
510 pub(super) fn direct_sample(
511 &self,
512 id: crate::pointer::PointerId,
513 kind: teksilo_tokens::PointerKind,
514 phase: crate::pointer::PointerPhase,
515 at: Point,
516 down: bool,
517 ) -> crate::pointer::PointerSample {
518 use crate::pointer::PointerPhase;
519
520 let mut pointer = crate::pointer::PointerInfo::touch(id, self.input_now());
521 pointer.kind = kind;
522 pointer.buttons = if down {
523 crate::event::ButtonMask::PRIMARY
524 } else {
525 crate::event::ButtonMask::NONE
526 };
527 crate::pointer::PointerSample {
528 pointer,
529 phase,
530 position: at,
531 // The translator reports a button only where one changed.
532 button: match phase {
533 PointerPhase::Down | PointerPhase::Up => Some(PointerButton::Primary),
534 PointerPhase::Move | PointerPhase::Cancel => None,
535 },
536 modifiers: Modifiers::NONE,
537 coalesced: Vec::new(),
538 }
539 }
540
541 /// A finger lands at `at`.
542 pub fn touch_down(&mut self, pointer: crate::pointer::PointerId, at: Point) {
543 self.enter_simulated_mode();
544 let sample = self.direct_sample(
545 pointer,
546 teksilo_tokens::PointerKind::Touch,
547 crate::pointer::PointerPhase::Down,
548 at,
549 true,
550 );
551 self.dispatch_pointer(sample);
552 }
553
554 /// That finger moves to `at`, still down.
555 pub fn touch_move(&mut self, pointer: crate::pointer::PointerId, at: Point) {
556 self.enter_simulated_mode();
557 let sample = self.direct_sample(
558 pointer,
559 teksilo_tokens::PointerKind::Touch,
560 crate::pointer::PointerPhase::Move,
561 at,
562 true,
563 );
564 self.dispatch_pointer(sample);
565 }
566
567 /// That finger lifts at `at`.
568 pub fn touch_up(&mut self, pointer: crate::pointer::PointerId, at: Point) {
569 self.enter_simulated_mode();
570 let sample = self.direct_sample(
571 pointer,
572 teksilo_tokens::PointerKind::Touch,
573 crate::pointer::PointerPhase::Up,
574 at,
575 false,
576 );
577 self.dispatch_pointer(sample);
578 }
579
580 /// The system revokes that finger (a `wl_touch.cancel`, a compositor
581 /// grab). Not an [`touch_up`](Self::touch_up): the end position carries
582 /// no meaning and no tap is completed.
583 pub fn touch_cancel(&mut self, pointer: crate::pointer::PointerId, at: Point) {
584 self.enter_simulated_mode();
585 let sample = self.direct_sample(
586 pointer,
587 teksilo_tokens::PointerKind::Touch,
588 crate::pointer::PointerPhase::Cancel,
589 at,
590 false,
591 );
592 self.dispatch_pointer(sample);
593 }
594
595 /// The live stylus's identity, minting one if the pen has not been seen.
596 ///
597 /// A stylus is singular and it *hovers*, so its table entry outlives a
598 /// lift the way a mouse's does — which is exactly what lets the pen
599 /// helpers take no id and still address one continuous session.
600 fn pen_id(&mut self) -> crate::pointer::PointerId {
601 self.pointers
602 .iter()
603 .find(|e| matches!(e.info.kind, teksilo_tokens::PointerKind::Pen(_)))
604 .map(|e| e.info.id)
605 .unwrap_or_else(|| self.new_contact())
606 }
607
608 /// One stylus sample: the direct-pointer shape plus the axes a digitizer
609 /// reports.
610 fn pen_sample(
611 &mut self,
612 phase: crate::pointer::PointerPhase,
613 at: Point,
614 pressure: Option<f32>,
615 tilt: Option<(f32, f32)>,
616 down: bool,
617 ) -> crate::pointer::PointerSample {
618 self.enter_simulated_mode();
619 let id = self.pen_id();
620 let mut sample = self.direct_sample(
621 id,
622 teksilo_tokens::PointerKind::Pen(teksilo_tokens::PenKind::default()),
623 phase,
624 at,
625 down,
626 );
627 sample.pointer.axes.pressure = pressure;
628 sample.pointer.axes.tilt = tilt;
629 sample
630 }
631
632 /// The stylus tip touches down at `at`.
633 ///
634 /// `pressure` is normalised `0.0..=1.0`; `tilt` is `(tilt_x, tilt_y)` in
635 /// degrees. Both are the axes a real digitizer reports, so a surface that
636 /// reads [`PointerInfo::effective_pressure`](crate::pointer::PointerInfo::effective_pressure)
637 /// sees what it would see from hardware.
638 pub fn pen_down(&mut self, at: Point, pressure: f32, tilt: (f32, f32)) {
639 let sample = self.pen_sample(
640 crate::pointer::PointerPhase::Down,
641 at,
642 Some(pressure),
643 Some(tilt),
644 true,
645 );
646 self.dispatch_pointer(sample);
647 }
648
649 /// The stylus draws to `at`, still on the surface.
650 pub fn pen_move(&mut self, at: Point, pressure: f32, tilt: (f32, f32)) {
651 let sample = self.pen_sample(
652 crate::pointer::PointerPhase::Move,
653 at,
654 Some(pressure),
655 Some(tilt),
656 true,
657 );
658 self.dispatch_pointer(sample);
659 }
660
661 /// The stylus lifts off at `at`. It stays in proximity — a pen hovers,
662 /// so its entry survives the lift and the next `pen_move` continues the
663 /// same session.
664 pub fn pen_up(&mut self, at: Point, pressure: f32, tilt: (f32, f32)) {
665 let sample = self.pen_sample(
666 crate::pointer::PointerPhase::Up,
667 at,
668 Some(pressure),
669 Some(tilt),
670 false,
671 );
672 self.dispatch_pointer(sample);
673 }
674
675 /// The stylus moves in proximity without touching: no tip pressure, no
676 /// button. The one direct-pointer hover in the framework.
677 pub fn pen_hover(&mut self, at: Point) {
678 let sample = self.pen_sample(
679 crate::pointer::PointerPhase::Move,
680 at,
681 Some(0.0),
682 None,
683 false,
684 );
685 self.dispatch_pointer(sample);
686 }
687
688 /// A complete press-and-release at `at` by the named device, and the
689 /// identity it used.
690 ///
691 /// The mouse arm is [`PointerId::MOUSE`](crate::pointer::PointerId::MOUSE)
692 /// and the legacy `PointerDown`/`PointerUp` pair, so
693 /// `tap_with(PointerKind::Mouse, ..)` is the pre-touch-programme click
694 /// with a position rather than a widget id.
695 pub fn tap_with(
696 &mut self,
697 kind: teksilo_tokens::PointerKind,
698 at: Point,
699 ) -> crate::pointer::PointerId {
700 match kind {
701 teksilo_tokens::PointerKind::Touch => {
702 let id = self.new_contact();
703 self.touch_down(id, at);
704 self.touch_up(id, at);
705 id
706 }
707 teksilo_tokens::PointerKind::Pen(_) => {
708 self.pen_down(at, 0.5, (0.0, 0.0));
709 let id = self.pen_id();
710 self.pen_up(at, 0.0, (0.0, 0.0));
711 id
712 }
713 _ => {
714 self.enter_simulated_mode();
715 self.pointer_down_button(at, PointerButton::Primary);
716 self.pointer_up_button(at, PointerButton::Primary);
717 crate::pointer::PointerId::MOUSE
718 }
719 }
720 }
721
722 /// Press at `at`, hold for exactly the kind's `long_press`, release.
723 ///
724 /// The hold comes from the active profile rather than a constant written
725 /// here, and it is advanced *exactly* — the recognizer fires at
726 /// `>= hold`, so a helper that added a safety margin would stop the
727 /// threshold itself from ever being asserted.
728 pub fn long_press_at(
729 &mut self,
730 kind: teksilo_tokens::PointerKind,
731 at: Point,
732 ) -> crate::pointer::PointerId {
733 let hold = self.effective_theme.input.profile(kind).long_press;
734 let id = match kind {
735 teksilo_tokens::PointerKind::Touch => {
736 let id = self.new_contact();
737 self.touch_down(id, at);
738 id
739 }
740 teksilo_tokens::PointerKind::Pen(_) => {
741 self.pen_down(at, 0.5, (0.0, 0.0));
742 self.pen_id()
743 }
744 _ => {
745 self.enter_simulated_mode();
746 self.pointer_down_button(at, PointerButton::Primary);
747 crate::pointer::PointerId::MOUSE
748 }
749 };
750 self.advance_input_time(hold);
751 match kind {
752 teksilo_tokens::PointerKind::Touch => self.touch_up(id, at),
753 teksilo_tokens::PointerKind::Pen(_) => self.pen_up(at, 0.0, (0.0, 0.0)),
754 _ => self.pointer_up_button(at, PointerButton::Primary),
755 }
756 id
757 }
758
759 /// One finger from `from` to `to` in `steps` evenly spaced moves, then a
760 /// lift. Returns the contact's identity, so the caller can ask
761 /// [`sequence_winner`](Self::sequence_winner) about it.
762 ///
763 /// The clock does **not** move: this is a drag, and a drag is decided by
764 /// distance. Use [`fling`](Self::fling) when the speed is the point.
765 pub fn touch_drag(
766 &mut self,
767 from: Point,
768 to: Point,
769 steps: usize,
770 ) -> crate::pointer::PointerId {
771 let id = self.new_contact();
772 self.touch_down(id, from);
773 let steps = steps.max(1);
774 for step in 1..=steps {
775 let t = step as f32 / steps as f32;
776 self.touch_move(id, lerp_point(from, to, t));
777 }
778 self.touch_up(id, to);
779 id
780 }
781
782 /// One finger from `from` to `to` over `over` of simulated time, released
783 /// while still moving — the shape a coast is handed off from.
784 ///
785 /// Sampled at [`FLING_SAMPLE_INTERVAL`](Self::FLING_SAMPLE_INTERVAL) so
786 /// the velocity tracker sees gaps under its `STOP_GAP` and at least its
787 /// `MIN_SAMPLE_SIZE` of them; a flick described by two far-apart samples
788 /// yields no velocity at all and would silently never fling.
789 pub fn fling(
790 &mut self,
791 from: Point,
792 to: Point,
793 over: std::time::Duration,
794 ) -> crate::pointer::PointerId {
795 let interval = Self::FLING_SAMPLE_INTERVAL;
796 let steps = (over.as_secs_f64() / interval.as_secs_f64()).ceil() as usize;
797 let steps = steps.max(crate::kinetic::MIN_SAMPLE_SIZE);
798 let per_step = over / steps as u32;
799
800 let id = self.new_contact();
801 self.touch_down(id, from);
802 for step in 1..=steps {
803 self.advance_input_time(per_step);
804 let t = step as f32 / steps as f32;
805 self.touch_move(id, lerp_point(from, to, t));
806 }
807 self.touch_up(id, to);
808 id
809 }
810
811 /// The cadence [`fling`](Self::fling) samples at: one 60 Hz frame, which
812 /// is under the velocity tracker's `STOP_GAP` and therefore never splits
813 /// a flick into two unrelated runs.
814 pub const FLING_SAMPLE_INTERVAL: std::time::Duration = std::time::Duration::from_micros(16_667);
815
816 /// Two fingers, from `a0`/`b0` to `a1`/`b1` in `steps` moves, then both
817 /// lift. Returns their identities in the order they landed.
818 ///
819 /// Both contacts are down before either moves, which is what a pinch
820 /// needs: the recognizer's reference span is the distance between the two
821 /// landings.
822 pub fn pinch(
823 &mut self,
824 a0: Point,
825 b0: Point,
826 a1: Point,
827 b1: Point,
828 steps: usize,
829 ) -> (crate::pointer::PointerId, crate::pointer::PointerId) {
830 let a = self.new_contact();
831 let b = self.new_contact();
832 self.touch_down(a, a0);
833 self.touch_down(b, b0);
834 let steps = steps.max(1);
835 for step in 1..=steps {
836 let t = step as f32 / steps as f32;
837 self.touch_move(a, lerp_point(a0, a1, t));
838 self.touch_move(b, lerp_point(b0, b1, t));
839 }
840 self.touch_up(a, a1);
841 self.touch_up(b, b1);
842 (a, b)
843 }
844
845 /// Switch the active [`TargetDensity`](teksilo_tokens::TargetDensity).
846 ///
847 /// The name A21 gives [`set_input_density`](Self::set_input_density); an
848 /// alias, because density is one setting and there is one door to it.
849 pub fn set_density(&mut self, density: teksilo_tokens::TargetDensity) {
850 self.set_input_density(density);
851 }
852
853 /// The [`TouchAction`](crate::pointer::touch_action::TouchAction) in force
854 /// at `id`: the intersection of every declaration from the root down to
855 /// it.
856 ///
857 /// This is what a press landing on `id` would *freeze*. Distinct from
858 /// [`sequence_touch_action`](Self::sequence_touch_action), which reports
859 /// what a press already in flight froze — the two differ the moment a
860 /// widget changes its declaration mid-press, which is the whole reason
861 /// the value is frozen.
862 pub fn touch_action_for(&self, id: WidgetId) -> crate::pointer::touch_action::TouchAction {
863 self.effective_touch_action(id)
864 }
865
866 /// Mark a widget as needing repaint.
867 pub fn mark_needs_paint(&mut self, id: WidgetId) {
868 self.arena.mark_needs_paint(id);
869 }
870
871 /// Set a widget subtree as dormant.
872 ///
873 /// Goes through the tree's cancel-aware parking door, so a pointer working
874 /// inside the subtree is cancelled rather than stranded on a widget the
875 /// dispatcher will no longer reach.
876 pub fn set_dormant(&mut self, id: WidgetId) {
877 self.park_subtree(id);
878 self.arena.mark_ancestors_need_layout(id);
879 self.cached_frame = None;
880 self.a11y_dirty = true;
881 }
882
883 /// Activate a dormant widget subtree.
884 pub fn activate(&mut self, id: WidgetId) {
885 self.arena.activate(id);
886 self.arena.mark_ancestors_need_layout(id);
887 self.cached_frame = None;
888 self.a11y_dirty = true;
889 }
890
891 /// Invalidate all per-widget paint caches (paint AND post-paint) and
892 /// the assembled frame cache. Forces every widget to repaint on the
893 /// next `render()` call. Used by the glyph-atlas eviction recovery:
894 /// after an eviction, any retained frame may hold quads whose atlas
895 /// UVs now point at recycled slots.
896 pub fn invalidate_all_paints(&mut self) {
897 for id in self.arena.active_ids() {
898 if let Some(node) = self.arena.get_mut(id) {
899 node.dirty.needs_paint = true;
900 node.cached_paint = None;
901 node.cached_post_paint = None;
902 }
903 }
904 self.cached_frame = None;
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911 use crate::signal::Signal;
912 use crate::test_widgets::{FillWidget, InsetWidget, StackWidget};
913 use crate::widget_builder::WidgetBuilder;
914
915 #[test]
916 fn child_bounds_helper() {
917 let mut tree = WidgetTree::new();
918 let child = tree.add(FillWidget::new());
919 let parent = tree.add(InsetWidget::new(5.0).set_child(child));
920 tree.layout(SizeProposal::exact(100.0, 50.0));
921 let child_bounds = tree.child_bounds(parent, 0);
922 assert_eq!(child_bounds.x, 5.0);
923 }
924
925 #[test]
926 fn signal_get_set_and_derived() {
927 let text = Signal::new(String::new());
928 let is_empty = text.map(|value| value.is_empty());
929 assert!(is_empty.get());
930 text.set("hello".to_string());
931 assert!(!is_empty.get());
932 }
933
934 #[test]
935 fn advance_time_updates_simulated_clock() {
936 let mut tree = WidgetTree::new();
937 let start = tree.simulated_now();
938
939 tree.advance_time(std::time::Duration::from_millis(500));
940 let end = tree.simulated_now();
941
942 assert_eq!(
943 end.duration_since(start),
944 std::time::Duration::from_millis(500)
945 );
946 }
947
948 #[test]
949 fn animate_to_interpolates_over_time() {
950 let mut tree = WidgetTree::new();
951 let owner = tree.add(FillWidget::new());
952 let signal = Signal::<f32>::new_animated(0.0);
953 tree.register_animated_signal(&signal, owner);
954
955 signal.animate_to(
956 100.0,
957 std::time::Duration::from_millis(200),
958 teksilo_tokens::Easing::Linear,
959 );
960
961 tree.tick_animations(std::time::Duration::from_millis(100));
962 assert!(
963 (signal.get() - 50.0).abs() < 2.0,
964 "at 50%: {}",
965 signal.get()
966 );
967
968 tree.tick_animations(std::time::Duration::from_millis(100));
969 assert!(
970 (signal.get() - 100.0).abs() < 0.1,
971 "at 100%: {}",
972 signal.get()
973 );
974
975 assert!(!tree.has_active_animations());
976 }
977
978 #[test]
979 fn animate_to_with_easing() {
980 let mut tree = WidgetTree::new();
981 let owner = tree.add(FillWidget::new());
982 let signal = Signal::<f32>::new_animated(0.0);
983 tree.register_animated_signal(&signal, owner);
984
985 signal.animate_to(
986 100.0,
987 std::time::Duration::from_millis(200),
988 teksilo_tokens::Easing::EaseIn,
989 );
990
991 tree.tick_animations(std::time::Duration::from_millis(100));
992 assert!(
993 (signal.get() - 25.0).abs() < 2.0,
994 "ease-in at 50%: {}",
995 signal.get()
996 );
997 }
998
999 #[test]
1000 fn animate_to_replaces_in_flight() {
1001 let mut tree = WidgetTree::new();
1002 let owner = tree.add(FillWidget::new());
1003 let signal = Signal::<f32>::new_animated(0.0);
1004 tree.register_animated_signal(&signal, owner);
1005
1006 signal.animate_to(
1007 100.0,
1008 std::time::Duration::from_millis(200),
1009 teksilo_tokens::Easing::Linear,
1010 );
1011 tree.tick_animations(std::time::Duration::from_millis(100));
1012 assert!((signal.get() - 50.0).abs() < 2.0);
1013
1014 signal.animate_to(
1015 0.0,
1016 std::time::Duration::from_millis(100),
1017 teksilo_tokens::Easing::Linear,
1018 );
1019 tree.tick_animations(std::time::Duration::from_millis(50));
1020 assert!(
1021 (signal.get() - 25.0).abs() < 3.0,
1022 "mid-replace: {}",
1023 signal.get()
1024 );
1025
1026 tree.tick_animations(std::time::Duration::from_millis(50));
1027 assert!(
1028 (signal.get() - 0.0).abs() < 0.5,
1029 "end-replace: {}",
1030 signal.get()
1031 );
1032 }
1033
1034 #[test]
1035 fn animation_marks_widgets_dirty() {
1036 let mut tree = WidgetTree::new();
1037 let widget = tree.add(FillWidget::new());
1038 let signal = Signal::<f32>::new_animated(100.0);
1039 tree.register_animated_signal(&signal, widget);
1040
1041 signal.bind_to(
1042 widget,
1043 tree.binding_registry(),
1044 crate::binding::BindingLevel::Relayout,
1045 );
1046
1047 tree.layout(SizeProposal::exact(200.0, 100.0));
1048
1049 signal.animate_to(
1050 0.0,
1051 std::time::Duration::from_millis(100),
1052 teksilo_tokens::Easing::Linear,
1053 );
1054
1055 tree.tick_animations(std::time::Duration::from_millis(50));
1056 assert!(tree.needs_redraw());
1057 }
1058
1059 // -----------------------------------------------------------------
1060 // A21 — the touch / pen helpers
1061 // -----------------------------------------------------------------
1062
1063 /// A finger holds `ButtonMask::PRIMARY` for as long as it is down.
1064 ///
1065 /// Normative, not cosmetic: every `accept_buttons` recognizer in the
1066 /// framework gates on `PRIMARY`, so a helper that reported an empty mask
1067 /// would make tap, drag, long-press and multi-tap invisible to a contact —
1068 /// and every touch test in the workspace would then be testing a device the
1069 /// platform layer does not produce (`event_translation.rs` sets the same
1070 /// mask).
1071 #[test]
1072 fn a_touch_helper_reports_the_primary_button_while_it_is_down() {
1073 use std::cell::RefCell;
1074 use std::rc::Rc;
1075
1076 let seen: Rc<RefCell<Vec<(crate::event::ButtonMask, bool)>>> =
1077 Rc::new(RefCell::new(Vec::new()));
1078 let log = seen.clone();
1079 let mut tree = WidgetTree::new();
1080 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
1081 let p = ctx.pointer();
1082 log.borrow_mut().push((p.buttons, p.kind.is_coarse()));
1083 crate::event::EventResponse::Ignored
1084 }));
1085 tree.layout(SizeProposal::exact(100.0, 100.0));
1086
1087 let finger = tree.new_contact();
1088 let at = Point::new(50.0, 50.0);
1089 tree.touch_down(finger, at);
1090 tree.touch_move(finger, Point::new(60.0, 50.0));
1091 tree.touch_up(finger, Point::new(60.0, 50.0));
1092
1093 let seen = seen.borrow();
1094 assert!(
1095 seen.iter().all(|(_, coarse)| *coarse),
1096 "all three are a finger"
1097 );
1098 assert_eq!(
1099 seen.iter().map(|(b, _)| *b).collect::<Vec<_>>(),
1100 vec![
1101 crate::event::ButtonMask::PRIMARY,
1102 crate::event::ButtonMask::PRIMARY,
1103 crate::event::ButtonMask::NONE,
1104 ],
1105 "down and move hold PRIMARY; the lift reports none"
1106 );
1107 tree.assert_no_leaked_pointer_state();
1108 }
1109
1110 /// The stylus helpers carry the axes a digitizer reports, and a hover
1111 /// carries neither a button nor tip pressure.
1112 #[test]
1113 fn the_pen_helpers_carry_pressure_and_tilt_and_hover_carries_neither() {
1114 use std::cell::RefCell;
1115 use std::rc::Rc;
1116
1117 type Sample = (
1118 Option<f32>,
1119 Option<(f32, f32)>,
1120 crate::event::ButtonMask,
1121 f32,
1122 );
1123 let seen: Rc<RefCell<Vec<Sample>>> = Rc::new(RefCell::new(Vec::new()));
1124 let log = seen.clone();
1125 let mut tree = WidgetTree::new();
1126 tree.add(FillWidget::new().on_pointer_event(move |_event, ctx| {
1127 let p = ctx.pointer();
1128 log.borrow_mut().push((
1129 p.axes.pressure,
1130 p.axes.tilt,
1131 p.buttons,
1132 p.effective_pressure(),
1133 ));
1134 crate::event::EventResponse::Ignored
1135 }));
1136 tree.layout(SizeProposal::exact(100.0, 100.0));
1137
1138 tree.pen_hover(Point::new(40.0, 40.0));
1139 tree.pen_down(Point::new(50.0, 50.0), 0.75, (12.0, -30.0));
1140 tree.pen_up(Point::new(50.0, 50.0), 0.0, (12.0, -30.0));
1141
1142 let seen = seen.borrow();
1143 assert_eq!(
1144 seen[0],
1145 (Some(0.0), None, crate::event::ButtonMask::NONE, 0.0),
1146 "a hover reports no tilt, no button and no tip pressure"
1147 );
1148 assert_eq!(
1149 seen[1],
1150 (
1151 Some(0.75),
1152 Some((12.0, -30.0)),
1153 crate::event::ButtonMask::PRIMARY,
1154 0.75
1155 ),
1156 "the tip's pressure and tilt reach the handler"
1157 );
1158 assert_eq!(
1159 seen[2].2,
1160 crate::event::ButtonMask::NONE,
1161 "the lift holds nothing"
1162 );
1163 tree.assert_no_leaked_pointer_state();
1164 }
1165
1166 /// A pen keeps one identity across a lift: it hovers, so its entry outlives
1167 /// the tip leaving the surface and the helpers address one session.
1168 #[test]
1169 fn the_pen_helpers_address_one_session_across_a_lift() {
1170 let mut tree = WidgetTree::new();
1171 tree.add(FillWidget::new().on_tap(|_e, _c| {}));
1172 tree.layout(SizeProposal::exact(100.0, 100.0));
1173
1174 tree.pen_down(Point::new(50.0, 50.0), 0.5, (0.0, 0.0));
1175 let first = tree
1176 .live_pointers()
1177 .find(|p| matches!(p.kind, teksilo_tokens::PointerKind::Pen(_)))
1178 .map(|p| p.id)
1179 .expect("the pen was admitted");
1180 tree.pen_up(Point::new(50.0, 50.0), 0.0, (0.0, 0.0));
1181 tree.pen_hover(Point::new(60.0, 50.0));
1182 let second = tree
1183 .live_pointers()
1184 .find(|p| matches!(p.kind, teksilo_tokens::PointerKind::Pen(_)))
1185 .map(|p| p.id)
1186 .expect("the pen is still in proximity");
1187 assert_eq!(first, second, "one stylus, one identity");
1188 }
1189
1190 /// A test scrollable: the vertical `scroll_container` claim — kinetic, as
1191 /// `ScrollArea`'s is, since a claim that is not kinetic never hands off to
1192 /// a coast — plus the `on_scroll` contract `teksilo-widgets` implements:
1193 /// absorb and answer `Handled`.
1194 fn flingable(offset: crate::signal::Signal<f32>) -> impl Widget + 'static {
1195 FillWidget::new()
1196 .scroll_container(crate::pointer::touch_action::PanAxes::Y)
1197 .on_scroll(move |event, _ctx| {
1198 let crate::event::WidgetEvent::Scroll { delta, .. } = event else {
1199 return crate::event::EventResponse::Ignored;
1200 };
1201 let dy = match *delta {
1202 crate::event::ScrollDelta::Pixels { y, .. } => y,
1203 crate::event::ScrollDelta::Lines { y, .. } => y * 20.0,
1204 };
1205 offset.set((offset.get() + dy).clamp(0.0, 10_000.0));
1206 crate::event::EventResponse::Handled
1207 })
1208 }
1209
1210 /// `fling` hands off to a coast and `touch_drag` over the same path does
1211 /// not.
1212 ///
1213 /// The pair is the assertion: both travel the same distance, and only the
1214 /// one that spends simulated time between its samples produces a velocity.
1215 /// A `fling` helper that forgot to advance the clock would still pan the
1216 /// scroller, so asserting the scroll alone would not notice.
1217 #[test]
1218 fn fling_coasts_where_the_same_drag_does_not() {
1219 let offset = crate::signal::Signal::new(0.0_f32);
1220 let mut tree = WidgetTree::new();
1221 let scroller = tree.add(flingable(offset.clone()));
1222 tree.layout(SizeProposal::exact(200.0, 400.0));
1223
1224 tree.touch_drag(Point::new(100.0, 300.0), Point::new(100.0, 100.0), 8);
1225 assert!(offset.get() > 0.0, "the drag scrolled: {}", offset.get());
1226 assert!(
1227 !tree.is_flinging(scroller),
1228 "…but a drag with no time between its samples has no velocity"
1229 );
1230 tree.assert_no_leaked_pointer_state();
1231
1232 let offset = crate::signal::Signal::new(0.0_f32);
1233 let mut tree = WidgetTree::new();
1234 let scroller = tree.add(flingable(offset.clone()));
1235 tree.layout(SizeProposal::exact(200.0, 400.0));
1236
1237 tree.fling(
1238 Point::new(100.0, 300.0),
1239 Point::new(100.0, 100.0),
1240 std::time::Duration::from_millis(50),
1241 );
1242 assert!(
1243 tree.is_flinging(scroller),
1244 "200 dp in 50 ms is a flick and hands off to a coast"
1245 );
1246 let at_release = offset.get();
1247 tree.advance_time(std::time::Duration::from_millis(100));
1248 assert!(
1249 offset.get() > at_release,
1250 "and the one clock moves it: {at_release} -> {}",
1251 offset.get()
1252 );
1253 }
1254
1255 /// `pinch` produces a real two-contact pinch stream through the single
1256 /// ingress.
1257 #[test]
1258 fn pinch_drives_a_two_contact_pinch() {
1259 use std::cell::RefCell;
1260 use std::rc::Rc;
1261
1262 let phases: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
1263 let log = phases.clone();
1264 let mut tree = WidgetTree::new();
1265 tree.add(FillWidget::new().on_pinch(move |phase, _ctx| {
1266 log.borrow_mut().push(match phase {
1267 crate::gesture::PinchPhase::Started { .. } => "started",
1268 crate::gesture::PinchPhase::Changed { .. } => "changed",
1269 crate::gesture::PinchPhase::Ended { .. } => "ended",
1270 crate::gesture::PinchPhase::Cancelled { .. } => "cancelled",
1271 });
1272 }));
1273 tree.layout(SizeProposal::exact(400.0, 400.0));
1274
1275 tree.pinch(
1276 Point::new(180.0, 200.0),
1277 Point::new(220.0, 200.0),
1278 Point::new(100.0, 200.0),
1279 Point::new(300.0, 200.0),
1280 6,
1281 );
1282
1283 let phases = phases.borrow();
1284 assert!(
1285 phases.contains(&"started"),
1286 "the spread started a pinch: {phases:?}"
1287 );
1288 assert!(
1289 phases.contains(&"changed"),
1290 "…and reported its changes: {phases:?}"
1291 );
1292 tree.assert_no_leaked_pointer_state();
1293 }
1294
1295 /// `long_press_at` holds for exactly the profile's `long_press` — not a
1296 /// millisecond more.
1297 ///
1298 /// The recognizer fires at `>= hold`, so holding for exactly it is what
1299 /// makes the threshold itself observable: a helper that padded the wait
1300 /// would pass with the hold set to anything shorter.
1301 #[test]
1302 fn long_press_at_holds_for_exactly_the_profiles_hold() {
1303 use std::cell::Cell;
1304 use std::rc::Rc;
1305
1306 for kind in [
1307 teksilo_tokens::PointerKind::Mouse,
1308 teksilo_tokens::PointerKind::Touch,
1309 teksilo_tokens::PointerKind::Pen(teksilo_tokens::PenKind::Pen),
1310 ] {
1311 let fired = Rc::new(Cell::new(0));
1312 let f = fired.clone();
1313 let mut tree = WidgetTree::new();
1314 tree.add(FillWidget::new().on_long_press(move |_e, _c| f.set(f.get() + 1)));
1315 tree.layout(SizeProposal::exact(100.0, 100.0));
1316
1317 let before = tree.simulated_now();
1318 tree.long_press_at(kind, Point::new(50.0, 50.0));
1319 assert_eq!(fired.get(), 1, "{kind:?} held long enough, once");
1320 assert_eq!(
1321 tree.simulated_now().duration_since(before),
1322 tree.effective_theme.input.profile(kind).long_press,
1323 "{kind:?}: the helper advanced exactly the profile's hold"
1324 );
1325 tree.assert_no_leaked_pointer_state();
1326 }
1327 }
1328
1329 /// `tap_with` completes a tap for every device.
1330 #[test]
1331 fn tap_with_taps_for_every_device() {
1332 use std::cell::Cell;
1333 use std::rc::Rc;
1334
1335 for kind in [
1336 teksilo_tokens::PointerKind::Mouse,
1337 teksilo_tokens::PointerKind::Touch,
1338 teksilo_tokens::PointerKind::Pen(teksilo_tokens::PenKind::Pen),
1339 ] {
1340 let taps = Rc::new(Cell::new(0));
1341 let t = taps.clone();
1342 let mut tree = WidgetTree::new();
1343 tree.add(FillWidget::new().on_tap(move |_e, _c| t.set(t.get() + 1)));
1344 tree.layout(SizeProposal::exact(100.0, 100.0));
1345 tree.tap_with(kind, Point::new(50.0, 50.0));
1346 assert_eq!(taps.get(), 1, "{kind:?} tapped once");
1347 tree.assert_no_leaked_pointer_state();
1348 }
1349 }
1350
1351 /// `touch_action_for` reports the **declaration** in force at a node — the
1352 /// root-to-target intersection — which is a different question from
1353 /// `sequence_touch_action`'s "what did this press freeze".
1354 ///
1355 /// The two differ the moment a widget changes its declaration mid-press,
1356 /// which is the whole reason the value is frozen at all.
1357 #[test]
1358 fn touch_action_for_reads_the_declaration_and_the_sequence_reads_the_freeze() {
1359 use crate::pointer::touch_action::TouchAction;
1360
1361 let mut tree = WidgetTree::new();
1362 let leaf = tree.add(FillWidget::new().on_tap(|_e, _c| {}));
1363 let outer = tree.add(
1364 StackWidget::new()
1365 .child(leaf)
1366 .touch_action(TouchAction::PAN_Y),
1367 );
1368 tree.layout(SizeProposal::exact(100.0, 100.0));
1369
1370 assert_eq!(tree.touch_action_for(outer), TouchAction::PAN_Y);
1371 assert_eq!(
1372 tree.touch_action_for(leaf),
1373 TouchAction::PAN_Y,
1374 "the fold runs root to target"
1375 );
1376
1377 let finger = tree.new_contact();
1378 tree.touch_down(finger, Point::new(50.0, 50.0));
1379 assert_eq!(tree.sequence_touch_action(finger), TouchAction::PAN_Y);
1380
1381 // The declaration changes under the live press.
1382 tree.arena
1383 .get_mut(outer)
1384 .expect("the node is live")
1385 .touch_action = TouchAction::NONE;
1386 assert_eq!(
1387 tree.touch_action_for(leaf),
1388 TouchAction::NONE,
1389 "the declaration moved"
1390 );
1391 assert_eq!(
1392 tree.sequence_touch_action(finger),
1393 TouchAction::PAN_Y,
1394 "…and the press keeps what it froze"
1395 );
1396 tree.touch_up(finger, Point::new(50.0, 50.0));
1397 tree.assert_no_leaked_pointer_state();
1398 }
1399
1400 /// `set_density` is the one density door under A21's name for it.
1401 #[test]
1402 fn set_density_is_set_input_density() {
1403 let mut a = WidgetTree::new();
1404 let mut b = WidgetTree::new();
1405 a.set_density(teksilo_tokens::TargetDensity::Touch);
1406 b.set_input_density(teksilo_tokens::TargetDensity::Touch);
1407 assert_eq!(a.theme().input, b.theme().input);
1408 assert_eq!(
1409 a.theme().input.density,
1410 teksilo_tokens::TargetDensity::Touch
1411 );
1412 }
1413}