Skip to main content

retroglyph_widgets/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, Headless, Rect, Terminal};
25//! use retroglyph_widgets::{Interaction, Sense};
26//!
27//! #[derive(Clone, Copy, PartialEq, Eq)]
28//! enum WidgetId {
29//!     SaveButton,
30//! }
31//!
32//! fn draw<B: Backend>(
33//!     term: &mut Terminal<B>,
34//!     interaction: &mut Interaction<WidgetId>,
35//! ) -> bool {
36//!     let area = Rect::new(0, 0, 10, 1);
37//!     let response = interaction.interact(area, WidgetId::SaveButton, Sense::click());
38//!     // ... draw the button, using response.hovered()/focused() to pick a style ...
39//!     response.clicked()
40//! }
41//!
42//! let mut term = Terminal::new(Headless::new(20, 10));
43//! let mut interaction = Interaction::<WidgetId>::new();
44//! interaction.begin_frame();
45//! let saved = draw(&mut term, &mut interaction);
46//! interaction.end_frame();
47//! assert!(!saved); // nothing clicked yet: no input was fed in
48//! ```
49
50mod density;
51mod focus;
52mod hit;
53mod pointer;
54mod response;
55mod sense;
56mod shortcuts;
57
58pub use density::Density;
59pub use focus::FocusRing;
60pub use hit::HitTester;
61pub use pointer::Pointer;
62pub use response::Response;
63pub use sense::Sense;
64pub use shortcuts::Shortcuts;
65
66use retroglyph_core::{Event, KeyCode, MouseButton, Pos, Rect};
67
68/// Default [`Interaction::with_drag_threshold`].
69///
70/// The pointer must move more than one cell from its press-down position
71/// before a [`Sense::DRAG`] widget reports [`Response::dragging`] instead of
72/// a click-in-progress.
73pub const DEFAULT_DRAG_THRESHOLD: u16 = 1;
74
75/// Ties [`Pointer`], [`HitTester`], and [`FocusRing`] together into the one
76/// piece of state a draw pass needs to make its widgets interactive.
77///
78/// # Frame lifecycle
79///
80/// ```text
81/// interaction.begin_frame();                 // 1
82/// for event in poll_events() {
83///     interaction.handle_event(&event);       // 2
84/// }
85/// draw(&mut term, &mut interaction, &state);  // 3: calls interaction.interact(...)
86/// interaction.end_frame();                    // 4
87/// ```
88///
89/// 1. [`begin_frame`](Self::begin_frame) snapshots which id (if any) is
90///    under the pointer, and whether it pressed/released/scrolled, using
91///    *last* frame's hit registrations and pointer events: this frame's
92///    registrations aren't complete until step 3 finishes, and this frame's
93///    events haven't arrived yet (they're step 2), so every [`Response`] in
94///    a given frame is one frame stale relative to what's being drawn/fed in
95///    *this* frame: uniformly for hover, press, release, click, and
96///    scroll, all resolved from that one snapshot. At typical redraw rates
97///    this is imperceptible; it's the same kind of trade-off
98///    [`ListState::ensure_visible`](crate::ListState::ensure_visible)
99///    documents for a different reason (only the caller knows the current
100///    viewport height), applied here because only the *previous* frame
101///    knows the full hit list and the pointer's position as of the input
102///    that's about to be processed. `dragging` and [`Response::held`] are exceptions: both
103///    re-check the pointer's *live* position (via [`Pointer::pos`]/[`Pointer::is_down`]) rather
104///    than the frame-stale snapshot, because a drag-in-progress or a press-cancel needs to react
105///    the instant the pointer moves, not one frame later. Keyboard focus is the remaining
106///    exception: [`Response::focused`] and Enter/Space activation read [`FocusRing`]'s `current`
107///    live, since it's plain level state with no hit-testing involved: no staleness to trade
108///    off.
109/// 2. [`handle_event`](Self::handle_event) updates pointer position/buttons
110///    and, by default, cycles focus on Tab/Shift+Tab.
111/// 3. Each widget calls [`interact`](Self::interact) with its rect, a
112///    caller-chosen id, and a [`Sense`] describing what it cares about; it
113///    gets back a [`Response`] and, as a side effect, registers itself for
114///    step 1 of the *next* frame.
115/// 4. [`end_frame`](Self::end_frame) releases the active widget if step 1
116///    saw the pointer go up.
117///
118/// One consequence worth knowing: a full press-then-release gesture that
119/// arrives as two events in the *same* [`handle_event`](Self::handle_event)
120/// batch (both fed in during step 2 of one frame, e.g. a synthetic test
121/// firing them back to back) takes an extra frame to resolve versus a
122/// realistic press and release arriving in separate frames, because step
123/// 1's hover snapshot for that frame still reflects the pointer's
124/// position from *before* those events. Real input rarely lands this way
125/// (a physical click's down and up are milliseconds apart, i.e. several
126/// frames at typical redraw rates), so this only tends to show up in tests.
127///
128/// # Why `Id` is a type parameter, not a hash
129///
130/// Immediate-mode toolkits like egui derive a widget's identity from its
131/// call-site source location (optionally salted with data) hashed down to
132/// an opaque integer, flexible, but it means two widgets can collide onto
133/// the same id at runtime with no compile-time signal, and the id carries
134/// no meaning a debugger can show you. `Interaction<Id>` instead asks the
135/// app for whatever id type it already has lying around: typically a
136/// small `Copy` enum like the hand-rolled hit-target enum an app would
137/// otherwise define anyway. Collisions become unrepresentable if the enum
138/// is exhaustive, and `{:?}`-printing an id tells you exactly which widget
139/// it is. The cost is one generic parameter; `Id: Copy + PartialEq` is all
140/// any of this module asks for.
141///
142/// Consistently with that: everything here holds its state in a plain,
143/// explicitly-owned struct threaded through `&mut self`, the same
144/// convention [`ListState`](crate::ListState) uses, rather than the
145/// interior-mutability/global-context pattern egui's `Memory` relies on to
146/// keep its implicit ids from needing to be threaded everywhere.
147// Several of these are independent one-shot snapshots (primary/secondary
148// press/release, keyboard activation), not states of a single state
149// machine: see the field-level comment above `resolved_press` for why
150// they're snapshotted individually rather than read live off `pointer`.
151#[allow(clippy::struct_excessive_bools)]
152#[derive(Debug, Clone)]
153pub struct Interaction<Id> {
154    pointer: Pointer,
155    hits: HitTester<Id>,
156    focus: FocusRing<Id>,
157    resolved_hover: Option<Id>,
158    // The pointer position `resolved_hover` was computed from, kept
159    // alongside it so `interact` can independently ask "was *my* rect under
160    // the pointer" (see `scroll_delta` below) without needing `resolved_hover`
161    // to have picked this id as the single topmost winner.
162    resolved_pos: Option<Pos>,
163    // Snapshots of the pointer's one-shot flags, taken once in `begin_frame`
164    // and read by every `interact` call for the rest of this frame. Not read
165    // straight off `pointer` during `interact`: `handle_event` runs *between*
166    // `begin_frame` and `interact` calls (see the frame lifecycle docs), so a
167    // press/release arriving this frame would otherwise be visible to
168    // `interact` immediately while `resolved_hover` (computed before that
169    // event) still reflects last frame's pointer position: `active` would
170    // then latch onto whatever was hovered *last* frame, not the widget the
171    // fresh press actually landed on. Resolving everything from one
172    // consistent snapshot keeps hover/press/release/click/scroll uniformly
173    // one frame behind the input that produced them, matching the docs.
174    resolved_press: bool,
175    resolved_release: bool,
176    resolved_secondary_press: bool,
177    resolved_secondary_release: bool,
178    resolved_scroll: i32,
179    active: Option<Id>,
180    // Tracked separately from `active`: a secondary press can land on one
181    // widget while the primary button is mid-drag on another (or not
182    // pressed at all), so the two buttons need independent "which widget
183    // did this press originate on" state.
184    secondary_active: Option<Id>,
185    drag_origin: Option<Pos>,
186    drag_threshold: u16,
187    activate_focused: bool,
188}
189
190impl<Id> Interaction<Id> {
191    /// A fresh interaction context: nothing hovered, focused, or active.
192    #[must_use]
193    pub const fn new() -> Self {
194        Self {
195            pointer: Pointer::new(),
196            hits: HitTester::new(),
197            focus: FocusRing::new(),
198            resolved_hover: None,
199            resolved_pos: None,
200            resolved_press: false,
201            resolved_release: false,
202            resolved_secondary_press: false,
203            resolved_secondary_release: false,
204            resolved_scroll: 0,
205            active: None,
206            secondary_active: None,
207            drag_origin: None,
208            drag_threshold: DEFAULT_DRAG_THRESHOLD,
209            activate_focused: false,
210        }
211    }
212
213    /// Override how far (in cells) the pointer must move from its press
214    /// origin before a [`Sense::DRAG`] widget reports
215    /// [`Response::dragging`] rather than a click-in-progress. Defaults to
216    /// [`DEFAULT_DRAG_THRESHOLD`].
217    #[must_use]
218    pub const fn with_drag_threshold(mut self, cells: u16) -> Self {
219        self.drag_threshold = cells;
220        self
221    }
222
223    /// Read access to the pointer's current position/button/scroll state,
224    /// e.g. to draw a custom cursor glyph.
225    #[must_use]
226    pub const fn pointer(&self) -> &Pointer {
227        &self.pointer
228    }
229
230    /// Read access to the focus ring, e.g. to render a "press Tab to
231    /// begin" hint when nothing is focused yet.
232    #[must_use]
233    pub const fn focus(&self) -> &FocusRing<Id> {
234        &self.focus
235    }
236
237    /// Mutable access to the focus ring, e.g. to drive it from a gamepad
238    /// shoulder button instead of (or in addition to) Tab/Shift+Tab.
239    pub const fn focus_mut(&mut self) -> &mut FocusRing<Id> {
240        &mut self.focus
241    }
242}
243
244impl<Id: Copy + PartialEq> Interaction<Id> {
245    /// Resolve hover/press against last frame's registrations, finalize the
246    /// focus order, and clear the hit registry for this frame's
247    /// [`interact`](Self::interact) calls. Call once per frame, before
248    /// processing input or drawing.
249    pub fn begin_frame(&mut self) {
250        self.resolved_pos = self.pointer.pos();
251        self.resolved_hover = self.resolved_pos.and_then(|pos| self.hits.topmost_at(pos));
252        self.resolved_press = self.pointer.pressed(MouseButton::Left);
253        self.resolved_release = self.pointer.released(MouseButton::Left);
254        self.resolved_secondary_press = self.pointer.pressed(MouseButton::Right);
255        self.resolved_secondary_release = self.pointer.released(MouseButton::Right);
256        self.resolved_scroll = self.pointer.scroll_delta();
257
258        if self.resolved_press {
259            self.active = self.resolved_hover;
260            self.drag_origin = self.resolved_pos;
261        }
262        if self.resolved_secondary_press {
263            self.secondary_active = self.resolved_hover;
264        }
265
266        self.hits.clear();
267        self.focus.begin_frame();
268        // Now that this frame's snapshot is taken, clear the one-shot flags
269        // so next frame's `handle_event` calls start from a clean slate.
270        self.pointer.end_frame();
271    }
272
273    /// Feed a raw input event: updates the pointer, and (by default) Tab
274    /// cycles focus: see [`FocusRing::handle_event`] if you need to
275    /// override that.
276    pub fn handle_event(&mut self, event: &Event) {
277        self.pointer.handle_event(event);
278        self.focus.handle_event(event);
279        self.activate_focused |= is_activation_key(event);
280    }
281
282    /// Register `id`'s `rect` for whatever `sense` asks for, and report
283    /// what happened to it, resolved from *last* frame's input: see the
284    /// [`Interaction`] docs for the frame lifecycle this implies.
285    pub fn interact(&mut self, rect: Rect, id: Id, sense: Sense) -> Response {
286        if sense.wants_pointer() {
287            self.hits.push(rect, id);
288        }
289        if sense.contains(Sense::FOCUSABLE) {
290            self.focus.register(id);
291        }
292
293        let hovered = sense.wants_pointer() && self.resolved_hover == Some(id);
294        let is_active = self.active == Some(id);
295        let senses_click = sense.contains(Sense::CLICK);
296        let key_activated = senses_click
297            && sense.contains(Sense::FOCUSABLE)
298            && self.focus.is_focused(id)
299            && self.activate_focused;
300        let released_here = is_active && self.resolved_release;
301        // Deliberately not gated on `self.pointer.is_down()`: the release
302        // frame (where `is_down` just went false) must still see `dragging
303        // == true` so `clicked` below correctly stays suppressed for a
304        // drag's terminating release, not just the frames in between.
305        let dragging = is_active && sense.contains(Sense::DRAG) && self.past_drag_threshold();
306
307        // Live re-check, deliberately not gated on `hovered`/`resolved_hover` the way `pressed`
308        // is: those are resolved from *last* frame's hit-test snapshot (see the `Interaction`
309        // frame-lifecycle docs), but a slide-off cancellation needs to see the pointer's
310        // *current* position the instant it leaves this rect, not one frame later. Mirrors how
311        // `dragging` above already reads `self.pointer.pos()` live instead of `resolved_pos`, and
312        // how `scroll_delta` below bypasses the single-topmost-winner rule: same "read live
313        // state, scoped to my own rect" shape, applied a third time.
314        let held = senses_click
315            && is_active
316            && self.pointer.is_down(MouseButton::Left)
317            && self.pointer.pos().is_some_and(|pos| rect.contains_pos(pos));
318
319        if senses_click && released_here && hovered && !dragging {
320            self.focus.request(id);
321        }
322
323        // Scroll deliberately isn't gated on `hovered` (single topmost
324        // winner) the way click/press/release/drag are: a scrollable
325        // container's own rect is usually fully covered by its rows/items
326        // (each independently sensing HOVER | CLICK so they're individually
327        // clickable), which would otherwise shadow the container at every
328        // point inside it and make it un-scrollable. Any rect the resolved
329        // pointer position falls within gets scroll credit, regardless of
330        // what's drawn on top of it, matching how wheel input behaves in
331        // most real UIs (it reaches the nearest scrollable ancestor, not
332        // just whatever's topmost at the exact pixel).
333        let scrollable_here = sense.wants_pointer()
334            && sense.contains(Sense::SCROLL)
335            && self.resolved_pos.is_some_and(|pos| rect.contains_pos(pos));
336
337        // The secondary button gets a narrower resolution than the primary
338        // one: no drag-threshold suppression (secondary-button drags aren't
339        // a gesture this module tracks), and it doesn't drive focus the way
340        // a primary click does (see `Response::secondary_clicked`'s doc
341        // comment).
342        let secondary_is_active = self.secondary_active == Some(id);
343        let secondary_clicked = sense.contains(Sense::SECONDARY_CLICK)
344            && secondary_is_active
345            && self.resolved_secondary_release
346            && hovered;
347
348        Response {
349            hovered,
350            pressed: (is_active && self.resolved_press) || key_activated,
351            released: released_here || key_activated,
352            clicked: (senses_click && released_here && hovered && !dragging) || key_activated,
353            held,
354            dragging,
355            focused: self.focus.is_focused(id),
356            secondary_clicked,
357            scroll_delta: if scrollable_here {
358                self.resolved_scroll
359            } else {
360                0
361            },
362        }
363    }
364
365    /// Release the active widget (both primary and secondary), e.g. so a
366    /// later [`focus_mut`](Self::focus_mut)-driven Tab handling starts
367    /// clean. Call once per frame, after drawing.
368    pub const fn end_frame(&mut self) {
369        if self.resolved_release {
370            self.active = None;
371            self.drag_origin = None;
372        }
373        if self.resolved_secondary_release {
374            self.secondary_active = None;
375        }
376        self.activate_focused = false;
377    }
378
379    fn past_drag_threshold(&self) -> bool {
380        let (Some(origin), Some(pos)) = (self.drag_origin, self.pointer.pos()) else {
381            return false;
382        };
383        origin.x.abs_diff(pos.x).max(origin.y.abs_diff(pos.y)) > self.drag_threshold
384    }
385}
386
387impl<Id> Default for Interaction<Id> {
388    fn default() -> Self {
389        Self::new()
390    }
391}
392
393const fn is_activation_key(event: &Event) -> bool {
394    let Event::Key(key) = event else {
395        return false;
396    };
397    key.is_down() && matches!(key.code, KeyCode::Enter | KeyCode::Char(' '))
398}
399
400#[cfg(test)]
401mod tests {
402    use retroglyph_core::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
403
404    use super::*;
405
406    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
407    enum Id {
408        Save,
409        Cancel,
410    }
411
412    fn click_at(interaction: &mut Interaction<Id>, pos: Pos) {
413        interaction.handle_event(&Event::Mouse(MouseEvent {
414            kind: MouseEventKind::Down(MouseButton::Left),
415            position: pos,
416            pixel_position: None,
417            modifiers: KeyModifiers::NONE,
418        }));
419        interaction.handle_event(&Event::Mouse(MouseEvent {
420            kind: MouseEventKind::Up(MouseButton::Left),
421            position: pos,
422            pixel_position: None,
423            modifiers: KeyModifiers::NONE,
424        }));
425    }
426
427    /// Registers `Save`/`Cancel` at fixed rects and returns their responses,
428    /// modeling one full frame (see the [`Interaction`] docs for the
429    /// lifecycle). `events` are fed in between `begin_frame` and the
430    /// `interact` calls, exactly where the documented lifecycle puts them --
431    /// e.g. a `Tab` press only affects focus registered as of the *start*
432    /// of this call, and a click resolves against hits registered by the
433    /// *previous* `frame`/`frame_with_events` call.
434    fn frame_with_events(
435        interaction: &mut Interaction<Id>,
436        events: &[Event],
437    ) -> (Response, Response) {
438        interaction.begin_frame();
439        for event in events {
440            interaction.handle_event(event);
441        }
442        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
443        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
444        interaction.end_frame();
445        (save, cancel)
446    }
447
448    fn frame(interaction: &mut Interaction<Id>) -> (Response, Response) {
449        frame_with_events(interaction, &[])
450    }
451
452    #[test]
453    fn click_is_resolved_one_frame_after_the_pointer_event() {
454        let mut interaction = Interaction::<Id>::new();
455
456        // Frame 1: nothing registered yet, so nothing can resolve.
457        let (save1, _) = frame(&mut interaction);
458        assert!(!save1.clicked());
459
460        // Click lands between frame 1 and frame 2, over "Save"'s rect.
461        click_at(&mut interaction, Pos::new(2, 0));
462
463        // Frame 2: resolves against frame 1's registrations.
464        let (save2, cancel2) = frame(&mut interaction);
465        assert!(save2.clicked());
466        assert!(!cancel2.clicked());
467    }
468
469    /// Regression test for a real bug caught while building the
470    /// `interaction_demo` example: pointer flags used to get cleared in
471    /// `end_frame` (the same frame `handle_event` set them in), so a press
472    /// recorded by `handle_event` was always gone by the time the *next*
473    /// frame's `begin_frame` went looking for it, and `active` could never
474    /// be set at all. Fixed by moving flag consumption into `begin_frame`
475    /// itself. This mirrors the realistic call pattern (`handle_event`
476    /// between `begin_frame` and drawing, once per frame) rather than
477    /// `click_at`'s frame-boundary-agnostic style above.
478    #[test]
479    fn press_and_release_in_separate_frames_still_resolves_a_click() {
480        let mut interaction = Interaction::<Id>::new();
481        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
482
483        let down = Event::Mouse(MouseEvent {
484            kind: MouseEventKind::Down(MouseButton::Left),
485            position: Pos::new(2, 0),
486            pixel_position: None,
487            modifiers: KeyModifiers::NONE,
488        });
489        // frame 2: press delivered via handle_event, same as a real tick.
490        let (save2, _) = frame_with_events(&mut interaction, &[down]);
491        assert!(!save2.pressed()); // this frame's hover snapshot predates the event
492
493        // frame 3: begin_frame now sees frame 2's press against frame 2's
494        // (correctly positioned) hit registrations.
495        let (save3, _) = frame(&mut interaction);
496        assert!(save3.pressed());
497
498        let up = Event::Mouse(MouseEvent {
499            kind: MouseEventKind::Up(MouseButton::Left),
500            position: Pos::new(2, 0),
501            pixel_position: None,
502            modifiers: KeyModifiers::NONE,
503        });
504        // frame 4: release delivered the same way.
505        let _ = frame_with_events(&mut interaction, &[up]);
506
507        // frame 5: resolves the release.
508        let (save5, _) = frame(&mut interaction);
509        assert!(save5.clicked());
510    }
511
512    #[test]
513    fn hover_follows_the_pointer_without_a_click() {
514        let mut interaction = Interaction::<Id>::new();
515        let _ = frame(&mut interaction);
516
517        interaction.handle_event(&Event::Mouse(MouseEvent {
518            kind: MouseEventKind::Moved,
519            position: Pos::new(7, 0),
520            pixel_position: None,
521            modifiers: KeyModifiers::NONE,
522        }));
523
524        let (save, cancel) = frame(&mut interaction);
525        assert!(!save.hovered());
526        assert!(cancel.hovered());
527        assert!(!cancel.clicked());
528    }
529
530    #[test]
531    fn tab_focuses_then_enter_activates_without_any_pointer() {
532        let mut interaction = Interaction::<Id>::new();
533        let _ = frame(&mut interaction); // registers Save/Cancel as focusable for the *next* frame
534
535        let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
536        let (save, _) = frame_with_events(&mut interaction, &[tab]);
537        assert!(save.focused());
538        assert!(!save.clicked());
539
540        let enter = Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
541        let (save, cancel) = frame_with_events(&mut interaction, &[enter]);
542        assert!(save.clicked());
543        assert!(!cancel.clicked());
544    }
545
546    #[test]
547    fn drag_past_threshold_suppresses_the_click() {
548        let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
549        let _ = frame(&mut interaction);
550
551        interaction.handle_event(&Event::Mouse(MouseEvent {
552            kind: MouseEventKind::Down(MouseButton::Left),
553            position: Pos::new(2, 0),
554            pixel_position: None,
555            modifiers: KeyModifiers::NONE,
556        }));
557        interaction.begin_frame();
558        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
559        assert!(!save.dragging()); // hasn't moved yet
560        interaction.end_frame();
561
562        interaction.handle_event(&Event::Mouse(MouseEvent {
563            kind: MouseEventKind::Moved,
564            position: Pos::new(4, 0), // 2 cells from the press origin
565            pixel_position: None,
566            modifiers: KeyModifiers::NONE,
567        }));
568        interaction.begin_frame();
569        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
570        assert!(save.dragging());
571        interaction.end_frame();
572
573        interaction.handle_event(&Event::Mouse(MouseEvent {
574            kind: MouseEventKind::Up(MouseButton::Left),
575            position: Pos::new(4, 0),
576            pixel_position: None,
577            modifiers: KeyModifiers::NONE,
578        }));
579        interaction.begin_frame();
580        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
581        assert!(!save.clicked()); // released after dragging, not a click
582        assert!(save.released());
583    }
584
585    #[test]
586    fn held_is_true_while_pressed_and_hovering_and_false_once_the_pointer_slides_off() {
587        let mut interaction = Interaction::<Id>::new();
588        let _ = frame(&mut interaction); // frame 1: registers Save/Cancel
589
590        interaction.handle_event(&Event::Mouse(MouseEvent {
591            kind: MouseEventKind::Down(MouseButton::Left),
592            position: Pos::new(2, 0), // over Save
593            pixel_position: None,
594            modifiers: KeyModifiers::NONE,
595        }));
596
597        // frame 2: press resolves against frame 1's registrations, pointer still over Save.
598        let (save, _) = frame(&mut interaction);
599        assert!(save.held());
600
601        interaction.handle_event(&Event::Mouse(MouseEvent {
602            kind: MouseEventKind::Moved,
603            position: Pos::new(20, 0), // outside Save's rect, still held down
604            pixel_position: None,
605            modifiers: KeyModifiers::NONE,
606        }));
607        interaction.begin_frame();
608        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
609        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
610        interaction.end_frame();
611        assert!(!save.held()); // slid off before release: cancels immediately
612
613        interaction.handle_event(&Event::Mouse(MouseEvent {
614            kind: MouseEventKind::Moved,
615            position: Pos::new(2, 0), // back over Save, still held down, before release
616            pixel_position: None,
617            modifiers: KeyModifiers::NONE,
618        }));
619        interaction.begin_frame();
620        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
621        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
622        interaction.end_frame();
623        assert!(save.held()); // back inside: held again
624
625        interaction.handle_event(&Event::Mouse(MouseEvent {
626            kind: MouseEventKind::Up(MouseButton::Left),
627            position: Pos::new(2, 0),
628            pixel_position: None,
629            modifiers: KeyModifiers::NONE,
630        }));
631        interaction.begin_frame();
632        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
633        let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
634        interaction.end_frame();
635        assert!(!save.held());
636        assert!(save.released());
637        assert!(save.clicked());
638    }
639
640    #[test]
641    fn held_requires_click_sense() {
642        let mut interaction = Interaction::<Id>::new();
643        interaction.begin_frame();
644        let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
645        interaction.end_frame();
646
647        interaction.handle_event(&Event::Mouse(MouseEvent {
648            kind: MouseEventKind::Down(MouseButton::Left),
649            position: Pos::new(2, 0), // over Save
650            pixel_position: None,
651            modifiers: KeyModifiers::NONE,
652        }));
653
654        interaction.begin_frame();
655        // `active` is assigned from whichever id was topmost at press time, regardless of that
656        // id's own `Sense` (see `begin_frame`'s `self.active = self.resolved_hover;`), so Save
657        // is `is_active` here even though it only sensed `HOVER`: `held` must still stay false.
658        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
659        interaction.end_frame();
660        assert!(!save.held());
661    }
662
663    #[test]
664    fn scroll_reports_only_while_hovered_and_sensed() {
665        let mut interaction = Interaction::<Id>::new();
666        let _ = frame(&mut interaction);
667
668        interaction.handle_event(&Event::Mouse(MouseEvent {
669            kind: MouseEventKind::Moved,
670            position: Pos::new(2, 0),
671            pixel_position: None,
672            modifiers: KeyModifiers::NONE,
673        }));
674        interaction.handle_event(&Event::Mouse(MouseEvent {
675            kind: MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
676            position: Pos::new(2, 0),
677            pixel_position: None,
678            modifiers: KeyModifiers::NONE,
679        }));
680
681        interaction.begin_frame();
682        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::scroll());
683        let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::scroll());
684        interaction.end_frame();
685
686        assert_eq!(save.scroll_delta(), 1);
687        assert_eq!(cancel.scroll_delta(), 0); // outside Cancel's rect
688    }
689
690    /// Regression test for a real bug caught while building the
691    /// `interaction_demo` example: a scrollable container whose rows are
692    /// individually `Sense::HOVER | Sense::CLICK`-sensed (so they're each
693    /// clickable) covers its own rect completely, so under the old
694    /// "scroll only reports for the single topmost-hovered id" rule the
695    /// container could never win hover against its own rows and would
696    /// never see a scroll. Fixed by making `SCROLL` independent of the
697    /// topmost-hover winner: see [`Sense::SCROLL`]'s doc comment.
698    #[test]
699    fn scroll_reaches_a_container_through_an_overlapping_child() {
700        let mut interaction = Interaction::<Id>::new();
701        interaction.begin_frame();
702        // The child (Cancel, standing in for a list row) is registered
703        // *after* the container (Save), so it's topmost at any point they
704        // share, exactly like a row drawn on top of its list container.
705        let _ = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
706        let _ = interaction.interact(
707            Rect::new(0, 0, 10, 1),
708            Id::Cancel,
709            Sense::HOVER | Sense::CLICK,
710        );
711        interaction.end_frame();
712
713        interaction.handle_event(&Event::Mouse(MouseEvent {
714            kind: MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
715            position: Pos::new(2, 0),
716            pixel_position: None,
717            modifiers: KeyModifiers::NONE,
718        }));
719
720        interaction.begin_frame();
721        let container = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
722        let child = interaction.interact(
723            Rect::new(0, 0, 10, 1),
724            Id::Cancel,
725            Sense::HOVER | Sense::CLICK,
726        );
727        interaction.end_frame();
728
729        assert_eq!(container.scroll_delta(), 1);
730        assert!(child.hovered()); // the child still wins plain hover/click resolution
731    }
732
733    #[test]
734    fn hover_only_sense_never_reports_clicked() {
735        let mut interaction = Interaction::<Id>::new();
736        let _ = frame(&mut interaction);
737        click_at(&mut interaction, Pos::new(2, 0));
738
739        interaction.begin_frame();
740        let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
741        interaction.end_frame();
742
743        assert!(save.hovered());
744        assert!(!save.clicked());
745    }
746
747    fn right_click_at(interaction: &mut Interaction<Id>, pos: Pos) {
748        interaction.handle_event(&Event::Mouse(MouseEvent {
749            kind: MouseEventKind::Down(MouseButton::Right),
750            position: pos,
751            pixel_position: None,
752            modifiers: KeyModifiers::NONE,
753        }));
754        interaction.handle_event(&Event::Mouse(MouseEvent {
755            kind: MouseEventKind::Up(MouseButton::Right),
756            position: pos,
757            pixel_position: None,
758            modifiers: KeyModifiers::NONE,
759        }));
760    }
761
762    #[test]
763    fn secondary_click_is_independent_of_the_primary_button() {
764        fn frame_secondary(interaction: &mut Interaction<Id>) -> (Response, Response) {
765            interaction.begin_frame();
766            let save = interaction.interact(
767                Rect::new(0, 0, 5, 1),
768                Id::Save,
769                Sense::click() | Sense::SECONDARY_CLICK,
770            );
771            let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
772            interaction.end_frame();
773            (save, cancel)
774        }
775
776        let mut interaction = Interaction::<Id>::new();
777        let _ = frame_secondary(&mut interaction); // frame 1: register
778        right_click_at(&mut interaction, Pos::new(2, 0)); // over Save
779
780        let (save, cancel) = frame_secondary(&mut interaction); // frame 2: resolves
781        assert!(save.secondary_clicked());
782        assert!(!save.clicked()); // primary button never touched
783        assert!(!cancel.secondary_clicked());
784    }
785
786    #[test]
787    fn secondary_click_not_sensed_never_reports_even_when_right_clicked() {
788        let mut interaction = Interaction::<Id>::new();
789        let _ = frame(&mut interaction); // Save/Cancel sensed with Sense::click() only
790        right_click_at(&mut interaction, Pos::new(2, 0));
791
792        let (save, _) = frame(&mut interaction);
793        assert!(!save.secondary_clicked()); // not sensed, so never reported
794    }
795}