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