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