Skip to main content

teksilo_core/widget_tree/
test_api.rs

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