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}