Skip to main content

retroglyph_ui/interact/
mod.rs

1//! Pointer and keyboard focus tracking for interactive widgets, without a
2//! retained widget tree.
3//!
4//! [`ListState`](crate::ListState) answers "where is this list scrolled to
5//! and what's selected"; this module answers the sibling question, "what
6//! did the user just do to this widget" (hover, click, drag, focus,
7//! scroll) for widgets that don't have a natural selection index of their
8//! own (buttons, tabs, draggable panes, ...). Four independently usable
9//! pieces, composed by [`Interaction`] the way [`ListState`](crate::ListState)
10//! composes with [`crate::widget::Table`]:
11//!
12//! - [`Pointer`]: raw mouse position/button/scroll state from a stream of
13//!   [`Event`]s.
14//! - [`HitTester`]: resolves a pointer position to the topmost registered
15//!   widget id.
16//! - [`FocusRing`]: which id holds keyboard focus, plus Tab/Shift+Tab
17//!   cycling.
18//! - [`Response`]: what [`Interaction::interact`] reports back to a
19//!   widget call site, gated by what it asked for via [`Sense`].
20//!
21//! # Example
22//!
23//! ```
24//! use retroglyph_core::backend::{Backend, Headless};
25//! use retroglyph_core::grid::Rect;
26//! use retroglyph_core::terminal::Terminal;
27//! use retroglyph_ui::{Interaction, Sense};
28//!
29//! #[derive(Clone, Copy, PartialEq, Eq)]
30//! enum WidgetId {
31//!     SaveButton,
32//! }
33//!
34//! fn draw<B: Backend>(
35//!     term: &mut Terminal<B>,
36//!     interaction: &mut Interaction<WidgetId>,
37//! ) -> bool {
38//!     let area = Rect::new(0, 0, 10, 1);
39//!     let response = interaction.interact(area, WidgetId::SaveButton, Sense::click());
40//!     // ... draw the button, using response.hovered()/focused() to pick a style ...
41//!     response.clicked()
42//! }
43//!
44//! let mut term = Terminal::new(Headless::new(20, 10));
45//! let mut interaction = Interaction::<WidgetId>::new();
46//! interaction.begin_frame();
47//! let saved = draw(&mut term, &mut interaction);
48//! interaction.end_frame();
49//! assert!(!saved); // nothing clicked yet: no input was fed in
50//! ```
51
52mod consumed;
53mod density;
54mod focus;
55mod hit;
56mod pointer;
57mod response;
58mod sense;
59mod shortcuts;
60
61pub use consumed::Consumed;
62pub use density::Density;
63pub use focus::FocusRing;
64pub use hit::HitTester;
65pub use pointer::Pointer;
66pub use response::Response;
67pub use sense::Sense;
68pub use shortcuts::Shortcuts;
69
70use alloc::vec::Vec;
71use core::time::Duration;
72
73use retroglyph_core::app::Frame;
74use retroglyph_core::event::{Event, KeyCode, MouseButton};
75use retroglyph_core::grid::{Pos, Rect};
76use retroglyph_core::surface::Surface;
77
78use crate::Ui;
79use crate::animate::Tween;
80
81/// Default [`Interaction::with_drag_threshold`].
82///
83/// The pointer must move strictly farther than this many cells from its press-down position
84/// (see `past_drag_threshold`'s `>` comparison) before a [`Sense::DRAG`] widget reports
85/// [`Response::dragging`] instead of a click-in-progress, so at the default of `1` the pointer
86/// has to reach a cell at least two away from the origin.
87///
88/// Terminal pointer positions are already cell-quantized, so there is no sub-cell jitter to
89/// absorb the way a pixel-based UI needs to: `1` exists only to let a click that wanders to an
90/// immediately adjacent cell still count as a click, while a move to the next cell out is a
91/// deliberate drag. Lower (`0`) turns any single-cell move into a drag and makes shaky clicks
92/// hard to land; higher delays drag recognition by that many extra cells. Picked by feel, not
93/// measured.
94pub const DEFAULT_DRAG_THRESHOLD: u16 = 1;
95
96/// Default [`Interaction::with_double_click_window`].
97///
98/// A second [`Response::clicked`] within this many [`begin_frame`](Interaction::begin_frame)
99/// calls of the first counts as [`Response::double_clicked`].
100///
101/// Measured in frames rather than a `Duration` because this module has no wall clock (see the
102/// field comment on `double_click_window`). `30` frames is about half a second at a 60 fps redraw
103/// rate, matching the double-click timing desktop environments use; because it is counted in
104/// frames, the effective window scales inversely with frame rate (a 30 fps app gets ~1 s, a
105/// 120 fps app ~250 ms), so an app that runs far from 60 fps should override it via
106/// [`Interaction::with_double_click_window`]. Too small drops deliberate but slow double-clicks;
107/// too large pairs clicks a user meant as separate. Chosen to land near half a second at 60 fps,
108/// not otherwise measured.
109pub const DEFAULT_DOUBLE_CLICK_WINDOW: u16 = 30;
110
111/// Ties [`Pointer`], [`HitTester`], and [`FocusRing`] together into the one
112/// piece of state a draw pass needs to make its widgets interactive.
113///
114/// # Frame lifecycle
115///
116/// [`frame`](Self::frame) is the documented way to drive one frame: it wraps a closure with
117/// [`begin_frame`](Self::begin_frame) and [`end_frame`](Self::end_frame), and hands the closure a
118/// [`Ui`] pairing the surface passed in with `self`.
119///
120/// ```
121/// use retroglyph_core::backend::{Backend, Headless};
122/// use retroglyph_core::grid::Rect;
123/// use retroglyph_core::terminal::Terminal;
124/// use retroglyph_ui::{Interaction, Sense};
125///
126/// #[derive(Clone, Copy, PartialEq, Eq)]
127/// enum WidgetId {
128///     SaveButton,
129/// }
130///
131/// let mut term = Terminal::new(Headless::new(20, 10));
132/// let mut interaction = Interaction::<WidgetId>::new();
133/// let clicked = interaction.frame(&mut term.surface(), |ui| {
134///     let area = Rect::new(0, 0, 10, 1);
135///     let response = ui.interaction().interact(area, WidgetId::SaveButton, Sense::click());
136///     // ... draw the button, using response.hovered()/focused() to pick a style ...
137///     response.clicked()
138/// });
139/// assert!(!clicked); // nothing clicked yet: no input was fed in
140/// ```
141///
142/// `begin_frame`/`handle_event`/`end_frame` stay public for callers driving the lifecycle
143/// themselves (e.g. to interleave event handling between frames rather than all at once), but
144/// `frame` is what each step below describes:
145///
146/// ```text
147/// interaction.begin_frame();                 // 1
148/// for event in poll_events() {
149///     interaction.handle_event(&event);       // 2
150/// }
151/// draw(&mut term, &mut interaction, &state);  // 3: calls interaction.interact(...)
152/// interaction.end_frame();                    // 4
153/// ```
154///
155/// 1. [`begin_frame`](Self::begin_frame) snapshots which id (if any) is
156///    under the pointer, and whether it pressed/released/scrolled, using
157///    *last* frame's hit registrations and pointer events: this frame's
158///    registrations aren't complete until step 3 finishes, and this frame's
159///    events haven't arrived yet (they're step 2), so every [`Response`] in
160///    a given frame is one frame stale relative to what's being drawn/fed in
161///    *this* frame: uniformly for hover, press, release, click, and
162///    scroll, all resolved from that one snapshot. At typical redraw rates
163///    this is imperceptible; it's the same kind of trade-off
164///    [`ListState::ensure_visible`](crate::ListState::ensure_visible)
165///    documents for a different reason (only the caller knows the current
166///    viewport height), applied here because only the *previous* frame
167///    knows the full hit list and the pointer's position as of the input
168///    that's about to be processed. `dragging` and [`Response::held`] are exceptions: both
169///    re-check the pointer's *live* position (via [`Pointer::pos`]/[`Pointer::is_down`]) rather
170///    than the frame-stale snapshot, because a drag-in-progress or a press-cancel needs to react
171///    the instant the pointer moves, not one frame later. Keyboard focus is the remaining
172///    exception: [`Response::focused`] and Enter/Space activation read [`FocusRing`]'s `current`
173///    live, since it's plain level state with no hit-testing involved: no staleness to trade
174///    off.
175/// 2. [`handle_event`](Self::handle_event) updates pointer position/buttons and, by default,
176///    cycles focus on Tab/Shift+Tab, then reports whether this interaction [claimed the
177///    event](Consumed), resolved against the same last-frame registrations step 1 just read: an
178///    app juggling more than one [`Interaction`] can stop routing an event the moment one of them
179///    claims it, without waiting for step 3 to run.
180/// 3. Each widget calls [`interact`](Self::interact) with its rect, a
181///    caller-chosen id, and a [`Sense`] describing what it cares about; it
182///    gets back a [`Response`] and, as a side effect, registers itself for
183///    step 1 of the *next* frame.
184/// 4. [`end_frame`](Self::end_frame) releases the active widget if step 1
185///    saw the pointer go up.
186///
187/// One consequence worth knowing: a full press-then-release gesture that
188/// arrives as two events in the *same* [`handle_event`](Self::handle_event)
189/// batch (both fed in during step 2 of one frame, e.g. a synthetic test
190/// firing them back to back) takes an extra frame to resolve versus a
191/// realistic press and release arriving in separate frames, because step
192/// 1's hover snapshot for that frame still reflects the pointer's
193/// position from *before* those events. Real input rarely lands this way
194/// (a physical click's down and up are milliseconds apart, i.e. several
195/// frames at typical redraw rates), so this only tends to show up in tests.
196///
197/// # Why `Id` is a type parameter, not a hash
198///
199/// Immediate-mode toolkits like egui derive a widget's identity from its
200/// call-site source location (optionally salted with data) hashed down to
201/// an opaque integer, flexible, but it means two widgets can collide onto
202/// the same id at runtime with no compile-time signal, and the id carries
203/// no meaning a debugger can show you. `Interaction<Id>` instead asks the
204/// app for whatever id type it already has lying around: typically a
205/// small `Copy` enum like the hand-rolled hit-target enum an app would
206/// otherwise define anyway. Collisions become unrepresentable if the enum
207/// is exhaustive, and `{:?}`-printing an id tells you exactly which widget
208/// it is. The cost is one generic parameter; `Id: Copy + PartialEq` is all
209/// any of this module asks for.
210///
211/// Consistently with that: everything here holds its state in a plain,
212/// explicitly-owned struct threaded through `&mut self`, the same
213/// convention [`ListState`](crate::ListState) uses, rather than the
214/// interior-mutability/global-context pattern egui's `Memory` relies on to
215/// keep its implicit ids from needing to be threaded everywhere.
216// Several of these are independent one-shot snapshots (primary/secondary
217// press/release, keyboard activation), not states of a single state
218// machine: see the field-level comment above `resolved_press` for why
219// they're snapshotted individually rather than read live off `pointer`.
220#[allow(clippy::struct_excessive_bools)]
221#[derive(Debug, Clone)]
222pub struct Interaction<Id> {
223    pointer: Pointer,
224    hits: HitTester<Id>,
225    // A copy of last frame's finalized `hits`, taken in `begin_frame` before `hits` is cleared
226    // for this frame's `interact` calls. `handle_event` and `wants_pointer` read this rather than
227    // `hits`: `hits` itself is empty for most of a frame (from `begin_frame` until the first
228    // `interact` call resolves in step 3), but a pointer event delivered any time between
229    // `begin_frame` and that first `interact` call still needs a hit-test target, and the only
230    // complete one available yet is last frame's.
231    prev_hits: HitTester<Id>,
232    focus: FocusRing<Id>,
233    resolved_hover: Option<Id>,
234    // The pointer position `resolved_hover` was computed from, kept
235    // alongside it so `interact` can independently ask "was *my* rect under
236    // the pointer" (see `scroll_delta` below) without needing `resolved_hover`
237    // to have picked this id as the single topmost winner.
238    resolved_pos: Option<Pos>,
239    // Snapshots of the pointer's one-shot flags, taken once in `begin_frame`
240    // and read by every `interact` call for the rest of this frame. Not read
241    // straight off `pointer` during `interact`: `handle_event` runs *between*
242    // `begin_frame` and `interact` calls (see the frame lifecycle docs), so a
243    // press/release arriving this frame would otherwise be visible to
244    // `interact` immediately while `resolved_hover` (computed before that
245    // event) still reflects last frame's pointer position: `active` would
246    // then latch onto whatever was hovered *last* frame, not the widget the
247    // fresh press actually landed on. Resolving everything from one
248    // consistent snapshot keeps hover/press/release/click/scroll uniformly
249    // one frame behind the input that produced them, matching the docs.
250    resolved_press: bool,
251    resolved_release: bool,
252    resolved_secondary_press: bool,
253    resolved_secondary_release: bool,
254    resolved_scroll: i32,
255    active: Option<Id>,
256    // Tracked separately from `active`: a secondary press can land on one
257    // widget while the primary button is mid-drag on another (or not
258    // pressed at all), so the two buttons need independent "which widget
259    // did this press originate on" state.
260    secondary_active: Option<Id>,
261    drag_origin: Option<Pos>,
262    drag_threshold: u16,
263    activate_focused: bool,
264    // Counts `begin_frame` calls, used as a frame-count clock for double-click windowing: this
265    // module otherwise has no notion of wall-clock time (see the `no_std`-friendly habits
266    // documented on `Pointer`), so `double_click_window` is measured in frames rather than a
267    // `Duration`, matching how `drag_threshold` is measured in cells rather than pixels.
268    frame_count: u64,
269    // The id and `frame_count` of the most recent click not yet paired into a double-click. Only
270    // one slot is needed: like `active`/`secondary_active`, a click can only resolve on one
271    // widget per frame, and a second click within the window immediately consumes this (see
272    // `interact`), so there's never more than one pending single-click to remember.
273    last_click: Option<(Id, u64)>,
274    double_click_window: u16,
275    // `focus.focused()` as of the end of the *previous* frame, snapshotted in `begin_frame`
276    // before this frame's `handle_event` (Tab cycling) or `interact` (click-to-focus) calls can
277    // move it: `gained_focus`/`lost_focus` need last frame's answer to diff against, the same
278    // role `resolved_hover`'s pointer-snapshot fields play for hover/press/release.
279    prev_focused: Option<Id>,
280    // Backs [`animate`](Self::animate): one `(id, Tween)` per `id` that's had `animate` called on
281    // it since it last settled at rest. A plain `Vec` scanned linearly, not a `HashMap`, to keep
282    // `animate` (like every other method here) usable with any `Id: Copy + PartialEq`, no
283    // `Hash`/`Eq` required, the same call [`HitTester`] already makes for its own per-`Id`
284    // registrations. A flip is detected by comparing `target` against the `Tween`'s own
285    // [`Tween::target`], so no parallel last-seen `bool` is needed.
286    tweens: Vec<(Id, Tween)>,
287}
288
289impl<Id> Interaction<Id> {
290    /// A fresh interaction context: nothing hovered, focused, or active.
291    #[must_use]
292    pub const fn new() -> Self {
293        Self {
294            pointer: Pointer::new(),
295            hits: HitTester::new(),
296            prev_hits: HitTester::new(),
297            focus: FocusRing::new(),
298            resolved_hover: None,
299            resolved_pos: None,
300            resolved_press: false,
301            resolved_release: false,
302            resolved_secondary_press: false,
303            resolved_secondary_release: false,
304            resolved_scroll: 0,
305            active: None,
306            secondary_active: None,
307            drag_origin: None,
308            drag_threshold: DEFAULT_DRAG_THRESHOLD,
309            activate_focused: false,
310            frame_count: 0,
311            last_click: None,
312            double_click_window: DEFAULT_DOUBLE_CLICK_WINDOW,
313            prev_focused: None,
314            tweens: Vec::new(),
315        }
316    }
317
318    /// Override how far (in cells) the pointer must move from its press
319    /// origin before a [`Sense::DRAG`] widget reports
320    /// [`Response::dragging`] rather than a click-in-progress. Defaults to
321    /// [`DEFAULT_DRAG_THRESHOLD`].
322    #[must_use]
323    pub const fn with_drag_threshold(mut self, cells: u16) -> Self {
324        self.drag_threshold = cells;
325        self
326    }
327
328    /// Override how many [`begin_frame`](Self::begin_frame) calls may separate two clicks on the
329    /// same widget for the second to report [`Response::double_clicked`]. Defaults to
330    /// [`DEFAULT_DOUBLE_CLICK_WINDOW`].
331    #[must_use]
332    pub const fn with_double_click_window(mut self, frames: u16) -> Self {
333        self.double_click_window = frames;
334        self
335    }
336
337    /// Read access to the pointer's current position/button/scroll state,
338    /// e.g. to draw a custom cursor glyph.
339    #[must_use]
340    pub const fn pointer(&self) -> &Pointer {
341        &self.pointer
342    }
343
344    /// Read access to the focus ring, e.g. to render a "press Tab to
345    /// begin" hint when nothing is focused yet.
346    #[must_use]
347    pub const fn focus(&self) -> &FocusRing<Id> {
348        &self.focus
349    }
350
351    /// Mutable access to the focus ring, e.g. to drive it from a gamepad
352    /// shoulder button instead of (or in addition to) Tab/Shift+Tab.
353    pub const fn focus_mut(&mut self) -> &mut FocusRing<Id> {
354        &mut self.focus
355    }
356}
357
358impl<Id: Copy + PartialEq> Interaction<Id> {
359    /// The id the pointer resolved to this frame, if any: the same value
360    /// [`interact`](Self::interact) compares against to decide each
361    /// widget's [`Response::hovered`], resolved from last frame's hit-test
362    /// (see [`Interaction`](Self) for why there's a frame of latency).
363    ///
364    /// Unlike `Response::hovered`, this isn't filtered by the hovered
365    /// widget's [`Sense`]: it's the topmost id under the pointer
366    /// regardless of what that id is listening for, which is what makes it
367    /// useful for drawing a hover-driven readout (a tooltip, a cost
368    /// preview) *before* the widget it depends on has been registered this
369    /// frame, rather than having to stash the value for next frame by hand.
370    #[must_use]
371    pub const fn hovered(&self) -> Option<Id> {
372        self.resolved_hover
373    }
374
375    /// Run one frame: [`begin_frame`](Self::begin_frame), then `f` (given a [`Ui`] pairing
376    /// `surface` with `self`), then [`end_frame`](Self::end_frame).
377    ///
378    /// This is the documented way to drive the [frame lifecycle](Self#frame-lifecycle): the three
379    /// calls are easy to get right once and easy to forget (particularly `end_frame`) when spread
380    /// across a caller's own draw loop by hand.
381    pub fn frame<R>(
382        &mut self,
383        surface: &mut Surface<'_>,
384        f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R,
385    ) -> R {
386        self.begin_frame();
387        let result = f(&mut Ui::new(surface, self));
388        self.end_frame();
389        result
390    }
391
392    /// Resolve hover/press against last frame's registrations, finalize the
393    /// focus order, and clear the hit registry for this frame's
394    /// [`interact`](Self::interact) calls. Call once per frame, before
395    /// processing input or drawing.
396    pub fn begin_frame(&mut self) {
397        self.frame_count += 1;
398        // Taken before `focus.begin_frame` (a no-op on `current`) and before this frame's own
399        // `handle_event`/`interact` calls can move focus: see the field comment on `prev_focused`.
400        self.prev_focused = self.focus.focused();
401        self.resolved_pos = self.pointer.pos();
402        self.resolved_hover = self.resolved_pos.and_then(|pos| self.hits.topmost_at(pos));
403        self.resolved_press = self.pointer.pressed(MouseButton::Left);
404        self.resolved_release = self.pointer.released(MouseButton::Left);
405        self.resolved_secondary_press = self.pointer.pressed(MouseButton::Right);
406        self.resolved_secondary_release = self.pointer.released(MouseButton::Right);
407        self.resolved_scroll = self.pointer.scroll_delta();
408
409        if self.resolved_press {
410            self.active = self.resolved_hover;
411            self.drag_origin = self.resolved_pos;
412        }
413        if self.resolved_secondary_press {
414            self.secondary_active = self.resolved_hover;
415        }
416
417        // `prev_hits` becomes last frame's finalized registrations (what `hits` held coming into
418        // this call, used above to compute `resolved_hover`), and `hits` is left holding
419        // whatever `prev_hits` held before, immediately cleared below: this reuses that buffer's
420        // capacity for this frame's `interact` calls rather than allocating a fresh one.
421        core::mem::swap(&mut self.hits, &mut self.prev_hits);
422        self.hits.clear();
423        self.focus.begin_frame();
424        // Now that this frame's snapshot is taken, clear the one-shot flags
425        // so next frame's `handle_event` calls start from a clean slate.
426        self.pointer.end_frame();
427    }
428
429    /// Feed a raw input event: updates the pointer, and (by default) Tab cycles focus (see
430    /// [`FocusRing::handle_event`] if you need to override that), then reports whether this
431    /// interaction claimed it.
432    ///
433    /// Resolved against *last* frame's registrations, the same snapshot
434    /// [`wants_pointer`](Self::wants_pointer)/[`wants_keyboard`](Self::wants_keyboard) read: this
435    /// frame's [`interact`](Self::interact) calls haven't run yet (they're step 3 of the [frame
436    /// lifecycle](Self#frame-lifecycle); `handle_event` is step 2), so `self`'s hit/focus
437    /// registrations at the time this runs are still whatever the previous frame left behind.
438    ///
439    /// A pointer event ([`Event::Mouse`]) is claimed if its position lands on a registered rect,
440    /// regardless of that rect's [`Sense`] (even a [`Sense::hover`]-only widget still owns the
441    /// pointer at its own position; a wheel event over it shouldn't fall through to whatever's
442    /// behind it either). `Tab`/`Shift+Tab` are claimed whenever anything is registered as
443    /// focusable, matching [`FocusRing::advance`]/[`retreat`](FocusRing::retreat)'s own condition
444    /// for actually moving focus. `Enter`/`Space` are claimed only when they double as
445    /// [`Sense::CLICK`] activation, i.e. a [`Sense::FOCUSABLE`] widget currently holds focus:
446    /// otherwise the same keys reach an app's own text input or other key handling unclaimed.
447    /// Everything else ([`Event::Resize`], [`Event::Paste`], [`Event::FocusGained`]/
448    /// [`Event::FocusLost`], and any key this interaction doesn't bind) is never claimed, even
449    /// while a widget is focused and active: those need to reach whatever's behind an open
450    /// overlay (a resize still has to reflow the screen under a dropdown), which a coarser "is
451    /// the overlay open" gate cannot express without also swallowing them.
452    #[must_use]
453    pub fn handle_event(&mut self, event: &Event) -> Consumed {
454        let claimed = match event {
455            Event::Mouse(mouse) => self.prev_hits.topmost_at(mouse.position).is_some(),
456            Event::Key(key)
457                if key.is_down() && matches!(key.code, KeyCode::Tab | KeyCode::BackTab) =>
458            {
459                self.focus.has_order()
460            }
461            // A widget's `Sense` (whether Enter/Space double as its activation, per
462            // `Sense::FOCUSABLE`/`Sense::CLICK`) isn't visible here, only per-`interact` call: the
463            // same approximation `activate_focused` below makes, conservative in the same
464            // direction (a focused non-activating widget makes this report claimed even though
465            // no `interact` call will actually consume the key as a click).
466            Event::Key(_) => is_activation_key(event) && self.focus.focused().is_some(),
467            // `Resize`/`Paste`/`FocusGained`/`FocusLost`, plus any future variant: never claimed.
468            Event::Resize(..) | Event::Paste(_) | Event::FocusGained | Event::FocusLost | _ => {
469                false
470            }
471        };
472
473        self.pointer.handle_event(event);
474        self.focus.handle_event(event);
475        self.activate_focused |= is_activation_key(event);
476
477        claimed.into()
478    }
479
480    /// Whether the pointer is over a rect registered last frame, so a pointer event delivered
481    /// right now would land on a widget rather than fall through to whatever's behind this
482    /// interaction.
483    ///
484    /// Answerable before this frame has drawn anything: [`interact`](Self::interact) registers
485    /// each rect for the *next* frame's hit test (see the [frame lifecycle](Self#frame-lifecycle)),
486    /// so last frame's registrations, and therefore this answer, are already complete the moment
487    /// [`begin_frame`](Self::begin_frame) returns. An app with more than one [`Interaction`] (a
488    /// menu bar above a screen stack, say) can call this on the frontmost one before routing a
489    /// pointer event anywhere, the same role `io.WantCaptureMouse` plays in Dear `ImGui`.
490    ///
491    /// Reads the pointer's *live* position, not a frame-stale snapshot: unlike
492    /// [`Response::hovered`], which only updates once a frame via `begin_frame`, this is meant to
493    /// be called right after [`handle_event`](Self::handle_event) to decide where to route the
494    /// event that was just fed in, so it has to reflect that event's effect on the pointer
495    /// immediately, not wait for the next `begin_frame`.
496    #[must_use]
497    pub fn wants_pointer(&self) -> bool {
498        self.pointer
499            .pos()
500            .is_some_and(|pos| self.prev_hits.topmost_at(pos).is_some())
501    }
502
503    /// Whether a widget currently holds keyboard focus, so a key event delivered right now is
504    /// likely to be consumed rather than fall through.
505    ///
506    /// Unlike [`wants_pointer`](Self::wants_pointer), this reads live, not a frame-stale
507    /// snapshot: focus is plain level state that persists across frames until
508    /// [`FocusRing::request`]/[`clear`](FocusRing::clear) moves it, the same reasoning
509    /// [`Response::focused`] documents.
510    #[must_use]
511    pub const fn wants_keyboard(&self) -> bool {
512        self.focus.focused().is_some()
513    }
514
515    /// Eases toward `1.0` when `target` is `true`, `0.0` otherwise, over `duration`, advancing by
516    /// `frame.delta`. Owns one [`Tween`] per `id`, created (at rest, on whichever side `target`
517    /// starts on) the first time this is called for that `id`, and dropped once it settles back
518    /// at rest at its current target, so this doesn't grow unbounded across a long-running app
519    /// with many transient `Id`s (an overlay's per-item ids, say).
520    ///
521    /// The bridge from [`Response`] to [`crate::animate`]: a widget's
522    /// `render` (already handed a `Response`) calls this once per frame with, say,
523    /// `response.hovered()` as `target`, and blends its idle/hover style by the result, without
524    /// declaring a `Tween` field of its own or hand-diffing this frame's `hovered()` against
525    /// last frame's to find the edge that should retarget it, both of which an app would
526    /// otherwise need to do once per animated `Id`.
527    ///
528    /// `duration` only takes effect while `id`'s tween is created, i.e. the first call for that
529    /// `id`, or the first call after a previous one settled and was pruned: changing it on a
530    /// later call while the tween is still in flight has no effect, the same as
531    /// [`Tween::retarget`] leaving `duration` untouched.
532    pub fn animate(&mut self, id: Id, target: bool, duration: Duration, frame: &Frame) -> f32 {
533        let target_value = f32::from(target);
534        let index = if let Some(index) = self
535            .tweens
536            .iter()
537            .position(|(existing, ..)| *existing == id)
538        {
539            let (_, tween) = &mut self.tweens[index];
540            // `target_value` is always exactly `0.0` or `1.0` (`f32::from(bool)`), and
541            // `Tween::target` only ever holds a value this same conversion produced, so exact
542            // equality is correct here, not an epsilon comparison.
543            #[allow(clippy::float_cmp)]
544            if tween.target() != target_value {
545                tween.retarget(target_value);
546            }
547            tween.update(frame.delta);
548            index
549        } else {
550            self.tweens.push((
551                id,
552                Tween::new(target_value, target_value).duration(duration),
553            ));
554            self.tweens.len() - 1
555        };
556
557        let (_, tween) = &self.tweens[index];
558        let value = tween.value();
559        if tween.is_finished() {
560            self.tweens.remove(index);
561        }
562        value
563    }
564
565    /// Register `rect` as a barrier: a pointer inside it never resolves to anything registered by
566    /// an earlier [`interact`](Self::interact) call, no matter how the two overlap.
567    ///
568    /// [`Ui::modal`](crate::Ui::modal) is the documented way to use this; call it directly only
569    /// when driving [`interact`](Self::interact) without a [`Ui`].
570    pub fn push_barrier(&mut self, rect: Rect) {
571        self.hits.push_barrier(rect);
572    }
573
574    /// Register `id`'s `rect` for whatever `sense` asks for, and report
575    /// what happened to it, resolved from *last* frame's input: see the
576    /// [`Interaction`] docs for the frame lifecycle this implies.
577    pub fn interact(&mut self, rect: Rect, id: Id, sense: Sense) -> Response<Id> {
578        // `DISABLED` is a modifier, not a capability: it never changes what
579        // gets hit-tested (so `hovered` keeps working), only what gets
580        // registered with `FocusRing` and what `Response` reports back.
581        let disabled = sense.is_disabled();
582
583        if sense.wants_pointer() {
584            self.hits.push(rect, id);
585        }
586        if sense.contains(Sense::FOCUSABLE) && !disabled {
587            self.focus.register(id);
588        }
589
590        let hovered = sense.wants_pointer() && self.resolved_hover == Some(id);
591        let is_active = self.active == Some(id);
592        let senses_click = sense.contains(Sense::CLICK) && !disabled;
593        let is_focused = !disabled && self.focus.is_focused(id);
594        let key_activated =
595            senses_click && sense.contains(Sense::FOCUSABLE) && is_focused && self.activate_focused;
596        // `is_active` is assigned from `resolved_hover` alone in `begin_frame`,
597        // independent of `sense`, so a disabled (or non-`CLICK`-sensing,
598        // e.g. hover-only or `NONE`) widget can still become "active"
599        // merely by being topmost at press time even though it never asked
600        // for `CLICK`: gated on `senses_click` (which already folds in
601        // `!disabled`) the same way `held`/`clicked` below are, rather than
602        // relying on `is_active` alone.
603        let released_here = senses_click && is_active && self.resolved_release;
604        // Not gated on `self.pointer.is_down()`: the release
605        // frame (where `is_down` just went false) must still see `dragging
606        // == true` so `clicked` below correctly stays suppressed for a
607        // drag's terminating release, not just the frames in between.
608        let dragging =
609            is_active && sense.contains(Sense::DRAG) && !disabled && self.past_drag_threshold();
610
611        // Live re-check, not gated on `hovered`/`resolved_hover` the way `pressed`
612        // is: those are resolved from *last* frame's hit-test snapshot (see the `Interaction`
613        // frame-lifecycle docs), but a slide-off cancellation needs to see the pointer's
614        // *current* position the instant it leaves this rect, not one frame later. Mirrors how
615        // `dragging` above already reads `self.pointer.pos()` live instead of `resolved_pos`, and
616        // how `scroll_delta` below bypasses the single-topmost-winner rule: same "read live
617        // state, scoped to my own rect" shape, applied a third time.
618        let held = senses_click
619            && is_active
620            && self.pointer.is_down(MouseButton::Left)
621            && self.pointer.pos().is_some_and(|pos| rect.contains_pos(pos));
622
623        if senses_click && sense.contains(Sense::FOCUSABLE) && released_here && hovered && !dragging
624        {
625            self.focus.request(id);
626        }
627
628        // Scroll isn't gated on `hovered` (single topmost
629        // winner) the way click/press/release/drag are: a scrollable
630        // container's own rect is usually fully covered by its rows/items
631        // (each independently sensing HOVER | CLICK so they're individually
632        // clickable), which would otherwise shadow the container at every
633        // point inside it and make it un-scrollable. Any rect the resolved
634        // pointer position falls within gets scroll credit, regardless of
635        // what's drawn on top of it, matching how wheel input behaves in
636        // most real UIs (it reaches the nearest scrollable ancestor, not
637        // just whatever's topmost at the exact pixel).
638        let scrollable_here = !disabled
639            && sense.wants_pointer()
640            && sense.contains(Sense::SCROLL)
641            && self.resolved_pos.is_some_and(|pos| rect.contains_pos(pos));
642
643        // The secondary button gets a narrower resolution than the primary
644        // one: no drag-threshold suppression (secondary-button drags aren't
645        // a gesture this module tracks), and it doesn't drive focus the way
646        // a primary click does (see `Response::secondary_clicked`'s doc
647        // comment).
648        let secondary_is_active = !disabled && self.secondary_active == Some(id);
649        let secondary_clicked = sense.contains(Sense::SECONDARY_CLICK)
650            && !disabled
651            && secondary_is_active
652            && self.resolved_secondary_release
653            && hovered;
654
655        let clicked = (senses_click && released_here && hovered && !dragging) || key_activated;
656        // A click pairs with the *previous* click only if it landed on this same `id` within
657        // `double_click_window` frames: checked and consumed here, per `id`, rather than as a
658        // single crate-wide "last click" slot, so two widgets clicked in quick succession don't
659        // spuriously pair with each other. Consuming the pairing (resetting to `None` instead of
660        // leaving it set) means a third click starts counting fresh: each pair of qualifying
661        // clicks reports exactly one `double_clicked` frame, not one on every click after the
662        // second.
663        let double_clicked = clicked
664            && self.last_click.is_some_and(|(last_id, last_frame)| {
665                last_id == id
666                    && self.frame_count.saturating_sub(last_frame)
667                        <= u64::from(self.double_click_window)
668            });
669        if clicked {
670            self.last_click = if double_clicked {
671                None
672            } else {
673                Some((id, self.frame_count))
674            };
675        }
676
677        // `is_active.then_some(self.drag_origin).flatten()`, matching `press_origin` below: not
678        // gated on `resolved_pos`/`hovered`, live, matching `held`/`dragging` above,
679        // so a drag that has moved outside this widget's own rect still keeps reporting. Gated on
680        // `senses_click` for the same reason `released_here`/`held` are above: `is_active` alone
681        // says nothing about whether this widget ever asked for `CLICK`.
682        let press_origin = (senses_click && is_active)
683            .then_some(self.drag_origin)
684            .flatten();
685        let drag_delta = press_origin.zip(self.pointer.pos()).map(|(origin, pos)| {
686            (
687                i32::from(pos.x) - i32::from(origin.x),
688                i32::from(pos.y) - i32::from(origin.y),
689            )
690        });
691
692        // Diffed against `prev_focused`, snapshotted in `begin_frame` before this frame's own
693        // Tab-cycling/click-to-focus could move it: see that field's doc comment.
694        let gained_focus = is_focused && self.prev_focused != Some(id);
695        let lost_focus = !is_focused && self.prev_focused == Some(id);
696
697        Response {
698            id,
699            hovered,
700            pressed: (senses_click && is_active && self.resolved_press) || key_activated,
701            released: released_here || key_activated,
702            clicked,
703            double_clicked,
704            held,
705            dragging,
706            focused: is_focused,
707            gained_focus,
708            lost_focus,
709            secondary_clicked,
710            disabled,
711            scroll_delta: if scrollable_here {
712                self.resolved_scroll
713            } else {
714                0
715            },
716            // Resolved from the same `resolved_pos`/`resolved_hover` snapshot `hovered` comes
717            // from above, so the two stay consistent (one frame stale together) rather than
718            // mixing a stale hover flag with a live position.
719            pointer_pos: hovered.then_some(self.resolved_pos).flatten(),
720            // `drag_origin` is set once, in `begin_frame`, when a press lands on `active`, and
721            // cleared in `end_frame` on release: live state, not part of the per-`interact`
722            // resolved-snapshot fields above, matching how `held` reads `active` directly rather
723            // than a snapshot of it.
724            press_origin,
725            drag_delta,
726            rect,
727        }
728    }
729
730    /// Release the active widget (both primary and secondary), e.g. so a
731    /// later [`focus_mut`](Self::focus_mut)-driven Tab handling starts
732    /// clean. Call once per frame, after drawing.
733    pub const fn end_frame(&mut self) {
734        if self.resolved_release {
735            self.active = None;
736            self.drag_origin = None;
737        }
738        if self.resolved_secondary_release {
739            self.secondary_active = None;
740        }
741        self.activate_focused = false;
742    }
743
744    fn past_drag_threshold(&self) -> bool {
745        let (Some(origin), Some(pos)) = (self.drag_origin, self.pointer.pos()) else {
746            return false;
747        };
748        origin.x.abs_diff(pos.x).max(origin.y.abs_diff(pos.y)) > self.drag_threshold
749    }
750}
751
752impl<Id> Default for Interaction<Id> {
753    fn default() -> Self {
754        Self::new()
755    }
756}
757
758const fn is_activation_key(event: &Event) -> bool {
759    let Event::Key(key) = event else {
760        return false;
761    };
762    key.is_down() && matches!(key.code, KeyCode::Enter | KeyCode::Char(' '))
763}
764
765#[cfg(test)]
766mod tests {
767    use alloc::borrow::ToOwned as _;
768
769    use retroglyph_core::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
770
771    use super::*;
772
773    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
774    enum Id {
775        Save,
776        Cancel,
777    }
778
779    fn click_at(interaction: &mut Interaction<Id>, pos: Pos) {
780        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
781            MouseEventKind::Down(MouseButton::Left),
782            pos,
783            KeyModifiers::NONE,
784        )));
785        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
786            MouseEventKind::Up(MouseButton::Left),
787            pos,
788            KeyModifiers::NONE,
789        )));
790    }
791
792    /// Registers `Save`/`Cancel` at fixed rects and returns their responses,
793    /// modeling one full frame (see the [`Interaction`] docs for the
794    /// lifecycle). `events` are fed in between `begin_frame` and the
795    /// `interact` calls, exactly where the documented lifecycle puts them --
796    /// e.g. a `Tab` press only affects focus registered as of the *start*
797    /// of this call, and a click resolves against hits registered by the
798    /// *previous* `frame`/`frame_with_events` call.
799    fn frame_with_events(
800        interaction: &mut Interaction<Id>,
801        events: &[Event],
802    ) -> (Response<Id>, Response<Id>) {
803        interaction.begin_frame();
804        for event in events {
805            let _ = interaction.handle_event(event);
806        }
807        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
808        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
809        interaction.end_frame();
810        (save, cancel)
811    }
812
813    fn frame(interaction: &mut Interaction<Id>) -> (Response<Id>, Response<Id>) {
814        frame_with_events(interaction, &[])
815    }
816
817    #[test]
818    fn click_is_resolved_one_frame_after_the_pointer_event() {
819        let mut interaction = Interaction::<Id>::new();
820
821        // Frame 1: nothing registered yet, so nothing can resolve.
822        let (save1, _) = frame(&mut interaction);
823        assert!(!save1.clicked());
824
825        // Click lands between frame 1 and frame 2, over "Save"'s rect.
826        click_at(&mut interaction, Pos::new(2, 0));
827
828        // Frame 2: resolves against frame 1's registrations.
829        let (save2, cancel2) = frame(&mut interaction);
830        assert!(save2.clicked());
831        assert!(!cancel2.clicked());
832    }
833
834    /// Regression test for a real bug caught while building the
835    /// `interaction_demo` example: pointer flags used to get cleared in
836    /// `end_frame` (the same frame `handle_event` set them in), so a press
837    /// recorded by `handle_event` was always gone by the time the *next*
838    /// frame's `begin_frame` went looking for it, and `active` could never
839    /// be set at all. Fixed by moving flag consumption into `begin_frame`
840    /// itself. This mirrors the realistic call pattern (`handle_event`
841    /// between `begin_frame` and drawing, once per frame) rather than
842    /// `click_at`'s frame-boundary-agnostic style above.
843    #[test]
844    fn press_and_release_in_separate_frames_still_resolves_a_click() {
845        let mut interaction = Interaction::<Id>::new();
846        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
847
848        let down = Event::Mouse(MouseEvent::new(
849            MouseEventKind::Down(MouseButton::Left),
850            Pos::new(2, 0),
851            KeyModifiers::NONE,
852        ));
853        // frame 2: press delivered via handle_event, same as a real tick.
854        let (save2, _) = frame_with_events(&mut interaction, &[down]);
855        assert!(!save2.pressed()); // this frame's hover snapshot predates the event
856
857        // frame 3: begin_frame now sees frame 2's press against frame 2's
858        // (correctly positioned) hit registrations.
859        let (save3, _) = frame(&mut interaction);
860        assert!(save3.pressed());
861
862        let up = Event::Mouse(MouseEvent::new(
863            MouseEventKind::Up(MouseButton::Left),
864            Pos::new(2, 0),
865            KeyModifiers::NONE,
866        ));
867        // frame 4: release delivered the same way.
868        let _ = frame_with_events(&mut interaction, &[up]);
869
870        // frame 5: resolves the release.
871        let (save5, _) = frame(&mut interaction);
872        assert!(save5.clicked());
873    }
874
875    #[test]
876    fn hover_follows_the_pointer_without_a_click() {
877        let mut interaction = Interaction::<Id>::new();
878        let _ = frame(&mut interaction);
879
880        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
881            MouseEventKind::Moved,
882            Pos::new(7, 0),
883            KeyModifiers::NONE,
884        )));
885
886        let (save, cancel) = frame(&mut interaction);
887        assert!(!save.hovered());
888        assert!(cancel.hovered());
889        assert!(!cancel.clicked());
890    }
891
892    #[test]
893    fn tab_focuses_then_enter_activates_without_any_pointer() {
894        let mut interaction = Interaction::<Id>::new();
895        let _ = frame(&mut interaction); // registers Save/Cancel as focusable for the *next* frame
896
897        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
898        let (save, _) = frame_with_events(&mut interaction, &[tab]);
899        assert!(save.focused());
900        assert!(!save.clicked());
901
902        let enter = Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
903        let (save, cancel) = frame_with_events(&mut interaction, &[enter]);
904        assert!(save.clicked());
905        assert!(!cancel.clicked());
906    }
907
908    #[test]
909    fn drag_past_threshold_suppresses_the_click() {
910        let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
911        let _ = frame(&mut interaction);
912
913        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
914            MouseEventKind::Down(MouseButton::Left),
915            Pos::new(2, 0),
916            KeyModifiers::NONE,
917        )));
918        interaction.begin_frame();
919        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
920        assert!(!save.dragging()); // hasn't moved yet
921        interaction.end_frame();
922
923        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
924            MouseEventKind::Moved,
925            Pos::new(4, 0), // 2 cells from the press origin
926            KeyModifiers::NONE,
927        )));
928        interaction.begin_frame();
929        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
930        assert!(save.dragging());
931        interaction.end_frame();
932
933        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
934            MouseEventKind::Up(MouseButton::Left),
935            Pos::new(4, 0),
936            KeyModifiers::NONE,
937        )));
938        interaction.begin_frame();
939        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
940        assert!(!save.clicked()); // released after dragging, not a click
941        assert!(save.released());
942    }
943
944    #[test]
945    fn held_is_true_while_pressed_and_hovering_and_false_once_the_pointer_slides_off() {
946        let mut interaction = Interaction::<Id>::new();
947        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
948
949        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
950            MouseEventKind::Down(MouseButton::Left),
951            Pos::new(2, 0), // over Save
952            KeyModifiers::NONE,
953        )));
954
955        // frame 2: press resolves against frame 1's registrations, pointer still over Save.
956        let (save, _) = frame(&mut interaction);
957        assert!(save.held());
958
959        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
960            MouseEventKind::Moved,
961            Pos::new(20, 0), // outside Save's rect, still held down
962            KeyModifiers::NONE,
963        )));
964        interaction.begin_frame();
965        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
966        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
967        interaction.end_frame();
968        assert!(!save.held()); // slid off before release: cancels immediately
969
970        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
971            MouseEventKind::Moved,
972            Pos::new(2, 0), // back over Save, still held down, before release
973            KeyModifiers::NONE,
974        )));
975        interaction.begin_frame();
976        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
977        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
978        interaction.end_frame();
979        assert!(save.held()); // back inside: held again
980
981        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
982            MouseEventKind::Up(MouseButton::Left),
983            Pos::new(2, 0),
984            KeyModifiers::NONE,
985        )));
986        interaction.begin_frame();
987        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
988        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
989        interaction.end_frame();
990        assert!(!save.held());
991        assert!(save.released());
992        assert!(save.clicked());
993    }
994
995    #[test]
996    fn held_requires_click_sense() {
997        let mut interaction = Interaction::<Id>::new();
998        interaction.begin_frame();
999        let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
1000        interaction.end_frame();
1001
1002        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1003            MouseEventKind::Down(MouseButton::Left),
1004            Pos::new(2, 0), // over Save
1005            KeyModifiers::NONE,
1006        )));
1007
1008        interaction.begin_frame();
1009        // `active` is assigned from whichever id was topmost at press time, regardless of that
1010        // id's own `Sense` (see `begin_frame`'s `self.active = self.resolved_hover;`), so Save
1011        // is `is_active` here even though it only sensed `HOVER`: `held` must still stay false.
1012        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
1013        interaction.end_frame();
1014        assert!(!save.held());
1015    }
1016
1017    #[test]
1018    fn hover_only_sense_must_not_report_pressed_or_released() {
1019        let mut interaction = Interaction::<Id>::new();
1020        let _ = frame(&mut interaction); // frame 1: Save/Cancel registered with Sense::click()
1021        click_at(&mut interaction, Pos::new(2, 0)); // full press+release cycle over Save's rect
1022
1023        interaction.begin_frame();
1024        // Save is `is_active` here, same as `held_requires_click_sense` above, but this call
1025        // only senses `HOVER`: `pressed`/`released`/`press_origin`/`drag_delta` must all stay at
1026        // their not-asked-for defaults, matching `held`'s existing guard.
1027        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
1028        interaction.end_frame();
1029
1030        assert!(save.hovered());
1031        assert!(!save.pressed());
1032        assert!(!save.released());
1033        assert_eq!(save.press_origin(), None);
1034        assert_eq!(save.drag_delta(), None);
1035    }
1036
1037    #[test]
1038    fn sense_none_returns_response_default() {
1039        let mut interaction = Interaction::<Id>::new();
1040        let _ = frame(&mut interaction); // frame 1: Save/Cancel registered with Sense::click()
1041        click_at(&mut interaction, Pos::new(2, 0)); // full press+release cycle over Save's rect
1042
1043        interaction.begin_frame();
1044        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::NONE);
1045        interaction.end_frame();
1046
1047        // Per `Sense::NONE`'s own doc, `interact` registers the id nowhere and reports nothing it
1048        // never asked for. `rect` is the one exception: it always echoes back this frame's
1049        // `interact` area regardless of `Sense` (see `rect_echoes_back_this_frames_area`), so it's
1050        // deliberately not asserted here.
1051        assert!(!save.hovered());
1052        assert!(!save.pressed());
1053        assert!(!save.released());
1054        assert!(!save.clicked());
1055        assert!(!save.double_clicked());
1056        assert!(!save.held());
1057        assert!(!save.dragging());
1058        assert!(!save.focused());
1059        assert!(!save.gained_focus());
1060        assert!(!save.lost_focus());
1061        assert!(!save.secondary_clicked());
1062        assert!(!save.disabled());
1063        assert_eq!(save.scroll_delta(), 0);
1064        assert_eq!(save.pointer_pos(), None);
1065        assert_eq!(save.press_origin(), None);
1066        assert_eq!(save.drag_delta(), None);
1067    }
1068
1069    #[test]
1070    fn pointer_pos_matches_hover_and_is_none_elsewhere() {
1071        let mut interaction = Interaction::<Id>::new();
1072        let _ = frame(&mut interaction);
1073
1074        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1075            MouseEventKind::Moved,
1076            Pos::new(2, 0),
1077            KeyModifiers::NONE,
1078        )));
1079
1080        let (save, cancel) = frame(&mut interaction);
1081        assert_eq!(save.pointer_pos(), Some(Pos::new(2, 0)));
1082        assert_eq!(cancel.pointer_pos(), None);
1083    }
1084
1085    #[test]
1086    fn press_origin_stays_put_for_the_duration_of_a_drag() {
1087        let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
1088        let _ = frame(&mut interaction);
1089
1090        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1091            MouseEventKind::Down(MouseButton::Left),
1092            Pos::new(2, 0),
1093            KeyModifiers::NONE,
1094        )));
1095        interaction.begin_frame();
1096        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
1097        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::drag());
1098        interaction.end_frame();
1099        assert_eq!(save.press_origin(), Some(Pos::new(2, 0)));
1100        assert_eq!(cancel.press_origin(), None); // press never landed on Cancel
1101
1102        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1103            MouseEventKind::Moved,
1104            Pos::new(4, 0),
1105            KeyModifiers::NONE,
1106        )));
1107        interaction.begin_frame();
1108        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
1109        interaction.end_frame();
1110        // Still the original press position, not the pointer's current one.
1111        assert_eq!(save.press_origin(), Some(Pos::new(2, 0)));
1112    }
1113
1114    #[test]
1115    fn drag_delta_keeps_reporting_once_the_pointer_leaves_the_rect() {
1116        let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
1117        let _ = frame(&mut interaction);
1118
1119        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1120            MouseEventKind::Down(MouseButton::Left),
1121            Pos::new(2, 0),
1122            KeyModifiers::NONE,
1123        )));
1124        interaction.begin_frame();
1125        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
1126        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::drag());
1127        interaction.end_frame();
1128        assert_eq!(save.drag_delta(), Some((0, 0))); // press just landed, hasn't moved yet
1129        assert_eq!(cancel.drag_delta(), None); // press never landed on Cancel
1130
1131        // Move far past Save's own rect (5 cells wide): drag_delta must keep tracking the full
1132        // displacement from the press origin, unlike `pointer_pos`, which would go `None` here.
1133        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1134            MouseEventKind::Moved,
1135            Pos::new(20, 3),
1136            KeyModifiers::NONE,
1137        )));
1138        interaction.begin_frame();
1139        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
1140        interaction.end_frame();
1141        assert_eq!(save.pointer_pos(), None); // outside Save's rect now
1142        assert_eq!(save.drag_delta(), Some((18, 3)));
1143    }
1144
1145    #[test]
1146    fn scroll_reports_only_while_hovered_and_sensed() {
1147        let mut interaction = Interaction::<Id>::new();
1148        let _ = frame(&mut interaction);
1149
1150        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1151            MouseEventKind::Moved,
1152            Pos::new(2, 0),
1153            KeyModifiers::NONE,
1154        )));
1155        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1156            MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
1157            Pos::new(2, 0),
1158            KeyModifiers::NONE,
1159        )));
1160
1161        interaction.begin_frame();
1162        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::scroll());
1163        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::scroll());
1164        interaction.end_frame();
1165
1166        assert_eq!(save.scroll_delta(), 1);
1167        assert_eq!(cancel.scroll_delta(), 0); // outside Cancel's rect
1168    }
1169
1170    /// Regression test for a real bug caught while building the
1171    /// `interaction_demo` example: a scrollable container whose rows are
1172    /// individually `Sense::HOVER | Sense::CLICK`-sensed (so they're each
1173    /// clickable) covers its own rect completely, so under the old
1174    /// "scroll only reports for the single topmost-hovered id" rule the
1175    /// container could never win hover against its own rows and would
1176    /// never see a scroll. Fixed by making `SCROLL` independent of the
1177    /// topmost-hover winner: see [`Sense::SCROLL`]'s doc comment.
1178    #[test]
1179    fn scroll_reaches_a_container_through_an_overlapping_child() {
1180        let mut interaction = Interaction::<Id>::new();
1181        interaction.begin_frame();
1182        // The child (Cancel, standing in for a list row) is registered
1183        // *after* the container (Save), so it's topmost at any point they
1184        // share, exactly like a row drawn on top of its list container.
1185        let _ = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
1186        let _ = interaction.interact(
1187            Rect::new(0, 0, 10, 1),
1188            Id::Cancel,
1189            Sense::HOVER | Sense::CLICK,
1190        );
1191        interaction.end_frame();
1192
1193        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1194            MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
1195            Pos::new(2, 0),
1196            KeyModifiers::NONE,
1197        )));
1198
1199        interaction.begin_frame();
1200        let container = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
1201        let child = interaction.interact(
1202            Rect::new(0, 0, 10, 1),
1203            Id::Cancel,
1204            Sense::HOVER | Sense::CLICK,
1205        );
1206        interaction.end_frame();
1207
1208        assert_eq!(container.scroll_delta(), 1);
1209        assert!(child.hovered()); // the child still wins plain hover/click resolution
1210    }
1211
1212    #[test]
1213    fn hover_only_sense_never_reports_clicked() {
1214        let mut interaction = Interaction::<Id>::new();
1215        let _ = frame(&mut interaction);
1216        click_at(&mut interaction, Pos::new(2, 0));
1217
1218        interaction.begin_frame();
1219        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
1220        interaction.end_frame();
1221
1222        assert!(save.hovered());
1223        assert!(!save.clicked());
1224    }
1225
1226    fn right_click_at(interaction: &mut Interaction<Id>, pos: Pos) {
1227        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1228            MouseEventKind::Down(MouseButton::Right),
1229            pos,
1230            KeyModifiers::NONE,
1231        )));
1232        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1233            MouseEventKind::Up(MouseButton::Right),
1234            pos,
1235            KeyModifiers::NONE,
1236        )));
1237    }
1238
1239    #[test]
1240    fn secondary_click_is_independent_of_the_primary_button() {
1241        fn frame_secondary(interaction: &mut Interaction<Id>) -> (Response<Id>, Response<Id>) {
1242            interaction.begin_frame();
1243            let save = interaction.interact(
1244                Rect::new(0, 0, 5, 1),
1245                Id::Save,
1246                Sense::click() | Sense::SECONDARY_CLICK,
1247            );
1248            let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
1249            interaction.end_frame();
1250            (save, cancel)
1251        }
1252
1253        let mut interaction = Interaction::<Id>::new();
1254        let _ = frame_secondary(&mut interaction); // frame 1: register
1255        right_click_at(&mut interaction, Pos::new(2, 0)); // over Save
1256
1257        let (save, cancel) = frame_secondary(&mut interaction); // frame 2: resolves
1258        assert!(save.secondary_clicked());
1259        assert!(!save.clicked()); // primary button never touched
1260        assert!(!cancel.secondary_clicked());
1261    }
1262
1263    #[test]
1264    fn secondary_click_not_sensed_never_reports_even_when_right_clicked() {
1265        let mut interaction = Interaction::<Id>::new();
1266        let _ = frame(&mut interaction); // Save/Cancel sensed with Sense::click() only
1267        right_click_at(&mut interaction, Pos::new(2, 0));
1268
1269        let (save, _) = frame(&mut interaction);
1270        assert!(!save.secondary_clicked()); // not sensed, so never reported
1271    }
1272
1273    fn frame_disabled(interaction: &mut Interaction<Id>) -> Response<Id> {
1274        interaction.begin_frame();
1275        let save = interaction.interact(
1276            Rect::new(0, 0, 5, 1),
1277            Id::Save,
1278            Sense::click() | Sense::SECONDARY_CLICK | Sense::DISABLED,
1279        );
1280        interaction.end_frame();
1281        save
1282    }
1283
1284    #[test]
1285    fn disabled_widget_never_reports_click_press_or_held() {
1286        let mut interaction = Interaction::<Id>::new();
1287        let _ = frame_disabled(&mut interaction); // frame 1: registers Save
1288
1289        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1290            MouseEventKind::Down(MouseButton::Left),
1291            Pos::new(2, 0), // over Save
1292            KeyModifiers::NONE,
1293        )));
1294        let save = frame_disabled(&mut interaction); // frame 2: press resolves
1295        assert!(!save.pressed());
1296        assert!(!save.held());
1297
1298        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1299            MouseEventKind::Up(MouseButton::Left),
1300            Pos::new(2, 0),
1301            KeyModifiers::NONE,
1302        )));
1303        let save = frame_disabled(&mut interaction); // frame 3: release resolves
1304        assert!(!save.released());
1305        assert!(!save.clicked());
1306    }
1307
1308    #[test]
1309    fn disabled_widget_never_reports_dragging_or_scroll() {
1310        // The `!disabled` guards on `dragging` and `scrollable_here` in `interact` are otherwise
1311        // never exercised, since `frame_disabled` above only senses `CLICK`/`SECONDARY_CLICK`.
1312        let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
1313        let sense = Sense::drag() | Sense::SCROLL | Sense::DISABLED;
1314
1315        interaction.begin_frame();
1316        let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
1317        interaction.end_frame();
1318
1319        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1320            MouseEventKind::Down(MouseButton::Left),
1321            Pos::new(2, 0), // over Save
1322            KeyModifiers::NONE,
1323        )));
1324        interaction.begin_frame();
1325        let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
1326        interaction.end_frame();
1327
1328        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1329            MouseEventKind::Moved,
1330            Pos::new(4, 0), // 2 cells from the press origin, past the threshold
1331            KeyModifiers::NONE,
1332        )));
1333        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1334            MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
1335            Pos::new(4, 0),
1336            KeyModifiers::NONE,
1337        )));
1338
1339        interaction.begin_frame();
1340        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
1341        interaction.end_frame();
1342
1343        assert!(!save.dragging());
1344        assert_eq!(save.scroll_delta(), 0);
1345        assert!(save.hovered()); // hit-testing keeps working while disabled
1346    }
1347
1348    #[test]
1349    fn disabled_widget_still_reports_hover() {
1350        let mut interaction = Interaction::<Id>::new();
1351        let _ = frame_disabled(&mut interaction);
1352
1353        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1354            MouseEventKind::Moved,
1355            Pos::new(2, 0), // over Save
1356            KeyModifiers::NONE,
1357        )));
1358
1359        let save = frame_disabled(&mut interaction);
1360        assert!(save.hovered()); // hit-testing keeps working while disabled
1361    }
1362
1363    #[test]
1364    fn disabled_widget_never_reports_secondary_click() {
1365        let mut interaction = Interaction::<Id>::new();
1366        let _ = frame_disabled(&mut interaction);
1367        right_click_at(&mut interaction, Pos::new(2, 0));
1368
1369        let save = frame_disabled(&mut interaction);
1370        assert!(!save.secondary_clicked());
1371    }
1372
1373    #[test]
1374    fn disabled_widget_is_skipped_by_focus_ring() {
1375        let mut interaction = Interaction::<Id>::new();
1376
1377        // Frame 1: register Save (disabled) and Cancel (enabled) for the
1378        // *next* frame's Tab order, mirroring `tab_focuses_then_enter_activates_without_any_pointer`.
1379        interaction.begin_frame();
1380        let _ = interaction.interact(
1381            Rect::new(0, 0, 5, 1),
1382            Id::Save,
1383            Sense::click() | Sense::DISABLED,
1384        );
1385        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
1386        interaction.end_frame();
1387
1388        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1389        interaction.begin_frame(); // finalizes frame 1's registrations into the Tab order
1390        let _ = interaction.handle_event(&tab);
1391        // Save is registered nowhere in that order while disabled, so the
1392        // first Tab lands directly on Cancel.
1393        assert_eq!(interaction.focus().focused(), Some(Id::Cancel));
1394    }
1395
1396    #[test]
1397    fn disabling_a_focused_widget_clears_its_response_and_self_heals_on_the_next_tab() {
1398        let mut interaction = Interaction::<Id>::new();
1399        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel, both focusable
1400
1401        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1402        let (save, _) = frame_with_events(&mut interaction, &[tab]);
1403        assert!(save.focused()); // Save now holds focus
1404
1405        // frame 3: Save becomes disabled without focus moving anywhere else.
1406        interaction.begin_frame();
1407        let save = interaction.interact(
1408            Rect::new(0, 0, 5, 1),
1409            Id::Save,
1410            Sense::click() | Sense::DISABLED,
1411        );
1412        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
1413        interaction.end_frame();
1414        assert!(!save.focused()); // Response no longer claims focus...
1415
1416        // ...and the next Tab moves cleanly onto Cancel, exactly like any
1417        // other stale-focus-not-in-order recovery.
1418        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1419        let (_, cancel) = frame_with_events(&mut interaction, &[tab]);
1420        assert!(cancel.focused());
1421    }
1422
1423    #[test]
1424    fn disabled_bit_reports_via_response_even_on_a_hover_only_sense() {
1425        // `Sense::hover()` never granted click/drag/focus in the first
1426        // place, so `DISABLED` here changes nothing about what's reported
1427        // except `Response::disabled` itself.
1428        let mut interaction = Interaction::<Id>::new();
1429        interaction.begin_frame();
1430        let disabled = interaction.interact(
1431            Rect::new(0, 0, 5, 1),
1432            Id::Save,
1433            Sense::hover() | Sense::DISABLED,
1434        );
1435        let plain = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::hover());
1436        interaction.end_frame();
1437
1438        assert!(disabled.disabled());
1439        assert!(!plain.disabled());
1440        assert_eq!(disabled.hovered(), plain.hovered()); // both unhovered, same either way
1441    }
1442    #[test]
1443    fn wants_pointer_is_false_before_any_frame_has_registered_anything() {
1444        let interaction = Interaction::<Id>::new();
1445        assert!(!interaction.wants_pointer());
1446    }
1447
1448    #[test]
1449    fn a_pointer_event_on_a_registered_rect_is_consumed() {
1450        let mut interaction = Interaction::<Id>::new();
1451        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
1452        let _ = frame(&mut interaction); // frame 2: frame 1's registrations become `prev_hits`
1453
1454        let consumed = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1455            MouseEventKind::Moved,
1456            Pos::new(2, 0), // over Save
1457            KeyModifiers::NONE,
1458        )));
1459        assert_eq!(consumed, Consumed::Yes);
1460        assert!(interaction.wants_pointer());
1461    }
1462
1463    #[test]
1464    fn a_pointer_event_on_empty_space_is_not_consumed() {
1465        let mut interaction = Interaction::<Id>::new();
1466        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
1467        let _ = frame(&mut interaction); // frame 2: frame 1's registrations become `prev_hits`
1468
1469        let consumed = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1470            MouseEventKind::Moved,
1471            Pos::new(20, 20), // outside both Save and Cancel
1472            KeyModifiers::NONE,
1473        )));
1474        assert_eq!(consumed, Consumed::No);
1475        assert!(!interaction.wants_pointer());
1476    }
1477
1478    /// Regression case for the #598 defect: a coarse "is a widget focused and active" gate would
1479    /// wrongly swallow events a focused/active widget has no business claiming, starving whatever
1480    /// needed them: `Resize`/`Paste`/`FocusLost`/`FocusGained`, and any key that isn't an
1481    /// activation key. `Consumed` must report `No` for all of them even while a widget holds
1482    /// focus and is mid-press, so a caller routing by [`Consumed`] doesn't reproduce that bug.
1483    #[test]
1484    fn unclaimed_events_are_never_consumed_even_while_focused_and_active() {
1485        let mut interaction = Interaction::<Id>::new();
1486        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel as focusable
1487
1488        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1489        let (save, _) = frame_with_events(&mut interaction, &[tab]); // frame 2: Tab focuses Save
1490        assert!(save.focused());
1491
1492        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
1493            MouseEventKind::Down(MouseButton::Left),
1494            Pos::new(2, 0), // over Save, now active and mid-press
1495            KeyModifiers::NONE,
1496        )));
1497
1498        assert_eq!(
1499            interaction.handle_event(&Event::Resize(80, 24)),
1500            Consumed::No
1501        );
1502        assert_eq!(
1503            interaction.handle_event(&Event::Paste("hello".to_owned())),
1504            Consumed::No
1505        );
1506        assert_eq!(interaction.handle_event(&Event::FocusLost), Consumed::No);
1507
1508        // A non-activation key and `FocusGained` also aren't claimed by a focused/active widget:
1509        // only an activation key while focused, or a pointer event while active, is consumed.
1510        assert_eq!(
1511            interaction.handle_event(&Event::Key(KeyEvent::new(
1512                KeyCode::Char('a'),
1513                KeyModifiers::NONE
1514            ))),
1515            Consumed::No
1516        );
1517        assert_eq!(
1518            interaction.handle_event(&Event::Key(KeyEvent::new(
1519                KeyCode::Escape,
1520                KeyModifiers::NONE
1521            ))),
1522            Consumed::No
1523        );
1524        assert_eq!(interaction.handle_event(&Event::FocusGained), Consumed::No);
1525    }
1526
1527    #[test]
1528    fn tab_is_consumed_only_when_something_is_registered_as_focusable() {
1529        let mut interaction = Interaction::<Id>::new();
1530        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1531
1532        // Nothing has ever been registered yet.
1533        assert_eq!(interaction.handle_event(&tab), Consumed::No);
1534
1535        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel as focusable
1536        let _ = frame(&mut interaction); // frame 2: frame 1's registrations finalize the order
1537        assert_eq!(interaction.handle_event(&tab), Consumed::Yes);
1538    }
1539
1540    #[test]
1541    fn rect_echoes_back_this_frames_area() {
1542        let mut interaction = Interaction::<Id>::new();
1543        interaction.begin_frame();
1544        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
1545        interaction.end_frame();
1546        assert_eq!(save.rect(), Rect::new(0, 0, 5, 1));
1547    }
1548
1549    #[test]
1550    fn a_second_click_within_the_window_reports_double_clicked() {
1551        let mut interaction = Interaction::<Id>::new().with_double_click_window(5);
1552        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
1553
1554        click_at(&mut interaction, Pos::new(2, 0));
1555        let (save, _) = frame(&mut interaction); // frame 2: first click resolves
1556        assert!(save.clicked());
1557        assert!(!save.double_clicked());
1558
1559        click_at(&mut interaction, Pos::new(2, 0));
1560        let (save, _) = frame(&mut interaction); // frame 3: second click, well within the window
1561        assert!(save.clicked());
1562        assert!(save.double_clicked());
1563
1564        // A third click starts counting fresh rather than pairing with the second again.
1565        click_at(&mut interaction, Pos::new(2, 0));
1566        let (save, _) = frame(&mut interaction);
1567        assert!(save.clicked());
1568        assert!(!save.double_clicked());
1569    }
1570
1571    #[test]
1572    fn a_second_click_outside_the_window_does_not_pair() {
1573        let mut interaction = Interaction::<Id>::new().with_double_click_window(1);
1574        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
1575
1576        click_at(&mut interaction, Pos::new(2, 0));
1577        let _ = frame(&mut interaction); // frame 2: first click resolves
1578        let _ = frame(&mut interaction); // frame 3: window (1 frame) already elapsed
1579
1580        click_at(&mut interaction, Pos::new(2, 0));
1581        let (save, _) = frame(&mut interaction); // frame 4: too late to pair with frame 2's click
1582        assert!(save.clicked());
1583        assert!(!save.double_clicked());
1584    }
1585
1586    #[test]
1587    fn a_second_click_on_a_different_widget_does_not_pair() {
1588        let mut interaction = Interaction::<Id>::new().with_double_click_window(5);
1589        let _ = frame(&mut interaction);
1590
1591        click_at(&mut interaction, Pos::new(2, 0)); // Save
1592        let _ = frame(&mut interaction);
1593
1594        click_at(&mut interaction, Pos::new(7, 0)); // Cancel, not Save
1595        let (save, cancel) = frame(&mut interaction);
1596        assert!(!save.double_clicked());
1597        assert!(cancel.clicked());
1598        assert!(!cancel.double_clicked());
1599    }
1600
1601    #[test]
1602    fn gained_and_lost_focus_are_one_shot_edges_around_level_focused() {
1603        let mut interaction = Interaction::<Id>::new();
1604        let _ = frame(&mut interaction); // registers Save/Cancel as focusable for the *next* frame
1605
1606        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1607        let (save, cancel) = frame_with_events(&mut interaction, core::slice::from_ref(&tab));
1608        assert!(save.focused());
1609        assert!(save.gained_focus()); // just became focused this frame
1610        assert!(!save.lost_focus());
1611        assert!(!cancel.gained_focus());
1612
1613        // Still focused next frame, with nothing moving focus: level state stays true, but the
1614        // one-shot edge does not re-fire.
1615        let (save, _) = frame(&mut interaction);
1616        assert!(save.focused());
1617        assert!(!save.gained_focus());
1618        assert!(!save.lost_focus());
1619
1620        // Tab moves focus off Save and onto Cancel.
1621        let (save, cancel) = frame_with_events(&mut interaction, core::slice::from_ref(&tab));
1622        assert!(!save.focused());
1623        assert!(save.lost_focus());
1624        assert!(!save.gained_focus());
1625        assert!(cancel.focused());
1626        assert!(cancel.gained_focus());
1627    }
1628
1629    /// Regression test for retroglyph#704: clicking a `Sense::CLICK` widget that never asked for
1630    /// `Sense::FOCUSABLE` (e.g. a hoverable-but-not-Tab-stoppable row) used to call
1631    /// `FocusRing::request` unconditionally, silently stealing focus from whatever *was* actually
1632    /// in the Tab ring, without ever removing that widget from the ring itself.
1633    #[test]
1634    fn clicking_a_non_focusable_widget_must_not_steal_focus() {
1635        let mut interaction = Interaction::<Id>::new();
1636
1637        // frame 1: Save is focusable (Sense::click()), Cancel is clickable but not focusable.
1638        interaction.begin_frame();
1639        let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
1640        let _ = interaction.interact(
1641            Rect::new(6, 0, 5, 1),
1642            Id::Cancel,
1643            Sense::HOVER | Sense::CLICK,
1644        );
1645        interaction.end_frame();
1646
1647        // Tab focuses Save, the only widget registered with FOCUSABLE.
1648        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
1649        interaction.begin_frame();
1650        let _ = interaction.handle_event(&tab);
1651        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
1652        let _ = interaction.interact(
1653            Rect::new(6, 0, 5, 1),
1654            Id::Cancel,
1655            Sense::HOVER | Sense::CLICK,
1656        );
1657        interaction.end_frame();
1658        assert!(save.focused());
1659
1660        // Click Cancel, which never asked for FOCUSABLE.
1661        click_at(&mut interaction, Pos::new(7, 0));
1662        interaction.begin_frame();
1663        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
1664        let cancel = interaction.interact(
1665            Rect::new(6, 0, 5, 1),
1666            Id::Cancel,
1667            Sense::HOVER | Sense::CLICK,
1668        );
1669        interaction.end_frame();
1670        assert!(cancel.clicked());
1671        assert!(!cancel.focused()); // never registered as focusable: must not report focus either
1672        assert!(save.focused()); // focus must stay put, not get silently stolen
1673    }
1674
1675    fn frame_with_delta(delta: Duration) -> Frame {
1676        Frame { delta, frame: 0 }
1677    }
1678
1679    // Exact float equality is intentional in the `animate_*` tests below, mirroring
1680    // `animate::tween`'s own tests: every value under test is produced by
1681    // `Easing::Linear` (`Tween`'s default) at exactly-representable fractions of `duration`, not
1682    // an accumulated or transcendental result where an epsilon comparison would be appropriate.
1683    #[test]
1684    #[allow(clippy::float_cmp)]
1685    fn animate_starts_at_rest_on_whichever_side_target_begins_on() {
1686        let mut interaction = Interaction::<Id>::new();
1687        let frame = frame_with_delta(Duration::ZERO);
1688
1689        assert_eq!(
1690            interaction.animate(Id::Save, false, Duration::from_millis(100), &frame),
1691            0.0
1692        );
1693        assert_eq!(
1694            interaction.animate(Id::Cancel, true, Duration::from_millis(100), &frame),
1695            1.0
1696        );
1697    }
1698
1699    #[test]
1700    #[allow(clippy::float_cmp)]
1701    fn animate_eases_toward_the_target_once_it_flips() {
1702        let mut interaction = Interaction::<Id>::new();
1703        let duration = Duration::from_millis(100);
1704
1705        // At rest at 0.0 until `target` flips true.
1706        let value =
1707            interaction.animate(Id::Save, false, duration, &frame_with_delta(Duration::ZERO));
1708        assert_eq!(value, 0.0);
1709
1710        // Flips: retargets toward 1.0, halfway through `duration` after one more update.
1711        let value = interaction.animate(
1712            Id::Save,
1713            true,
1714            duration,
1715            &frame_with_delta(Duration::from_millis(50)),
1716        );
1717        assert_eq!(value, 0.5); // Easing::Linear (Tween's default), so exactly halfway
1718        assert!(value < 1.0);
1719
1720        // Finishes once `duration` has fully elapsed since the flip.
1721        let value = interaction.animate(
1722            Id::Save,
1723            true,
1724            duration,
1725            &frame_with_delta(Duration::from_millis(50)),
1726        );
1727        assert_eq!(value, 1.0);
1728    }
1729
1730    #[test]
1731    fn animate_prunes_the_entry_once_it_settles_at_rest() {
1732        let mut interaction = Interaction::<Id>::new();
1733        let duration = Duration::from_millis(100);
1734
1735        // Created at rest (`from == to == 0.0`): `is_finished` only becomes true once `duration`
1736        // has elapsed, exercising the prune path even for a tween that never actually moved.
1737        let _ = interaction.animate(Id::Save, false, duration, &frame_with_delta(Duration::ZERO));
1738        assert_eq!(interaction.tweens.len(), 1);
1739
1740        let _ = interaction.animate(Id::Save, false, duration, &frame_with_delta(duration));
1741        assert!(
1742            interaction.tweens.is_empty(),
1743            "a settled tween must be dropped, not accumulate"
1744        );
1745    }
1746
1747    #[test]
1748    #[allow(clippy::float_cmp)]
1749    fn animate_tracks_each_id_independently() {
1750        let mut interaction = Interaction::<Id>::new();
1751        let duration = Duration::from_millis(100);
1752
1753        let save = interaction.animate(Id::Save, true, duration, &frame_with_delta(Duration::ZERO));
1754        let cancel = interaction.animate(
1755            Id::Cancel,
1756            false,
1757            duration,
1758            &frame_with_delta(Duration::ZERO),
1759        );
1760        assert_eq!(save, 1.0);
1761        assert_eq!(cancel, 0.0);
1762
1763        // Advancing Save must not perturb Cancel's independently tracked tween.
1764        let save = interaction.animate(
1765            Id::Save,
1766            false,
1767            duration,
1768            &frame_with_delta(Duration::from_millis(50)),
1769        );
1770        let cancel = interaction.animate(
1771            Id::Cancel,
1772            false,
1773            duration,
1774            &frame_with_delta(Duration::from_millis(50)),
1775        );
1776        assert_eq!(save, 0.5);
1777        assert_eq!(cancel, 0.0);
1778    }
1779}