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