Skip to main content

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