Skip to main content

retroglyph_ui/interact/
response.rs

1//! [`Response`]: what [`Interaction::interact`](crate::Interaction::interact)
2//! hands back to a widget call site.
3
4use retroglyph_core::grid::{Pos, Rect};
5
6/// What happened to a widget this frame, as reported by
7/// [`Interaction::interact`](crate::Interaction::interact).
8///
9/// Every field is scoped to *this* frame only (e.g. [`clicked`](Self::clicked)
10/// is `true` for exactly the one frame the release lands on), except
11/// [`focused`](Self::focused), which stays `true` across frames until focus
12/// moves elsewhere. Fields a widget didn't ask for via
13/// [`Sense`](crate::Sense) are always `false`/`0`: a widget sensed with
14/// only [`Sense::HOVER`](crate::Sense::HOVER) never reports
15/// [`clicked`](Self::clicked), for instance.
16// Flat, independent fields by design: `Response` is a per-frame report card, not a state
17// machine: collapsing it into enums would only make `interact`'s construction of it more
18// awkward for no reader benefit.
19//
20// `id` is a bare `Id`, not `Option<Id>`: every `Response` that ever reaches app code comes from
21// `Interaction::interact`, which always has a real id in hand, so wrapping it in `Option` would
22// just make every caller unwrap a value that's never actually absent. The one place this crate
23// builds a `Response` without going through `interact` is [`Response::default`], used as a
24// synthetic "nothing happened" value (see [`Widget`](crate::Widget) impls that share their
25// [`InteractiveWidget`](crate::InteractiveWidget) drawing routine, and this crate's own tests).
26// `Default` is implemented for `Response<()>` specifically, not `impl<Id: Default>` generically:
27// nothing here ever needs a default `Response` under a real app `Id`, since a real `Id` only ever
28// reaches a `Response` through `interact`, so there's no reason to demand every `Id` a caller
29// picks implement `Default` just so `Response<Id>` itself can.
30#[allow(clippy::struct_excessive_bools)]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Response<Id> {
33    pub(crate) id: Id,
34    pub(crate) hovered: bool,
35    pub(crate) pressed: bool,
36    pub(crate) released: bool,
37    pub(crate) clicked: bool,
38    pub(crate) double_clicked: bool,
39    pub(crate) held: bool,
40    pub(crate) dragging: bool,
41    pub(crate) focused: bool,
42    pub(crate) gained_focus: bool,
43    pub(crate) lost_focus: bool,
44    pub(crate) secondary_clicked: bool,
45    pub(crate) disabled: bool,
46    pub(crate) scroll_delta: i32,
47    pub(crate) pointer_pos: Option<Pos>,
48    pub(crate) press_origin: Option<Pos>,
49    pub(crate) drag_delta: Option<(i32, i32)>,
50    pub(crate) rect: Rect,
51}
52
53impl Default for Response<()> {
54    fn default() -> Self {
55        Self {
56            id: (),
57            hovered: false,
58            pressed: false,
59            released: false,
60            clicked: false,
61            double_clicked: false,
62            held: false,
63            dragging: false,
64            focused: false,
65            gained_focus: false,
66            lost_focus: false,
67            secondary_clicked: false,
68            disabled: false,
69            scroll_delta: 0,
70            pointer_pos: None,
71            press_origin: None,
72            drag_delta: None,
73            rect: Rect::default(),
74        }
75    }
76}
77
78impl<Id: Copy> Response<Id> {
79    /// The `id` passed to [`Interaction::interact`](crate::Interaction::interact) this frame,
80    /// echoed back so a call site that only has the resolved `Response` in hand (e.g. one
81    /// returned from a widget it drew in a loop) can still tell which id it belongs to. For a
82    /// [`Response::default`] built directly rather than through `interact`, this is just
83    /// `Id::default()`, not a real widget's id.
84    #[must_use]
85    pub const fn id(&self) -> Id {
86        self.id
87    }
88}
89
90impl<Id> Response<Id> {
91    /// The pointer is over this widget's rect, resolved from last frame's
92    /// hit-test: see [`Interaction`](crate::Interaction) for why there's a
93    /// frame of latency.
94    #[must_use]
95    pub const fn hovered(&self) -> bool {
96        self.hovered
97    }
98
99    /// The primary pointer button went down on this widget this frame, or
100    /// (sensed with [`Sense::FOCUSABLE`](crate::Sense::FOCUSABLE)) Enter or
101    /// Space was pressed while it was focused.
102    #[must_use]
103    pub const fn pressed(&self) -> bool {
104        self.pressed
105    }
106
107    /// The primary pointer button (or an activating key) was released this
108    /// frame while this widget was the active one. Fires whether or not the
109    /// release also counts as a [`clicked`](Self::clicked) (e.g. it doesn't,
110    /// if the gesture crossed the drag threshold first).
111    #[must_use]
112    pub const fn released(&self) -> bool {
113        self.released
114    }
115
116    /// A full press-release cycle landed on this widget this frame: pressed
117    /// and released while still hovered, never crossing the drag threshold.
118    /// Also fires from keyboard activation (Enter/Space while focused) --
119    /// terminals are frequently mouse-less, so [`Sense::click`](crate::Sense::click)
120    /// widgets are keyboard-operable by default.
121    #[must_use]
122    pub const fn clicked(&self) -> bool {
123        self.clicked
124    }
125
126    /// A second [`clicked`](Self::clicked) landed on this widget within
127    /// [`Interaction::with_double_click_window`](crate::Interaction::with_double_click_window)
128    /// frames of the first, e.g. to open a file on double-click while a single click just
129    /// selects it. Implies [`clicked`](Self::clicked) is also `true` this frame. A third click
130    /// starts counting fresh rather than re-firing every frame after the second: each pair of
131    /// qualifying clicks reports exactly one `double_clicked` frame.
132    #[must_use]
133    pub const fn double_clicked(&self) -> bool {
134        self.double_clicked
135    }
136
137    /// The primary pointer button is down *and* the pointer is currently over this widget's
138    /// rect, re-checked live every frame, unlike [`pressed`](Self::pressed), which fires
139    /// once on the down edge and never re-checks position. Automatically cancels (goes
140    /// `false`) the instant the pointer slides off this widget's rect, even before release,
141    /// without waiting for a release event: the same "slide-to-cancel" feedback
142    /// `is_pointer_button_down_on` gives egui widgets and `IsItemHovered() && IsItemActive()`
143    /// gives Dear `ImGui` widgets. Only ever `true` for widgets sensed with
144    /// [`Sense::CLICK`](crate::Sense::CLICK).
145    #[must_use]
146    pub const fn held(&self) -> bool {
147        self.held
148    }
149
150    /// The pointer moved past the drag threshold while pressed on this
151    /// widget. Only ever `true` for widgets sensed with
152    /// [`Sense::DRAG`](crate::Sense::DRAG).
153    #[must_use]
154    pub const fn dragging(&self) -> bool {
155        self.dragging
156    }
157
158    /// This widget holds keyboard focus. Unlike the other fields, this is
159    /// level state, not a one-shot "this happened" flag: it stays `true`
160    /// across frames until focus moves to another widget or is cleared.
161    #[must_use]
162    pub const fn focused(&self) -> bool {
163        self.focused
164    }
165
166    /// This widget just became [`focused`](Self::focused) this frame, having not been focused
167    /// last frame: a one-shot edge for a widget that wants to react only on the transition (e.g.
168    /// select-all-on-focus for a text input), rather than every frame [`focused`](Self::focused)
169    /// happens to be `true`.
170    #[must_use]
171    pub const fn gained_focus(&self) -> bool {
172        self.gained_focus
173    }
174
175    /// This widget was [`focused`](Self::focused) last frame but isn't anymore: the mirror of
176    /// [`gained_focus`](Self::gained_focus), e.g. to commit a text input's edits when focus
177    /// moves away.
178    #[must_use]
179    pub const fn lost_focus(&self) -> bool {
180        self.lost_focus
181    }
182
183    /// The secondary (right) mouse button pressed and released on this
184    /// widget this frame while still hovered. Only ever `true` for widgets
185    /// sensed with [`Sense::SECONDARY_CLICK`](crate::Sense::SECONDARY_CLICK).
186    /// Unlike [`clicked`](Self::clicked), there's no keyboard equivalent --
187    /// a secondary action needs its own trigger (a modifier+Enter, a menu
188    /// key, whatever fits the app) since Enter/Space already means
189    /// "primary activate".
190    #[must_use]
191    pub const fn secondary_clicked(&self) -> bool {
192        self.secondary_clicked
193    }
194
195    /// Scroll wheel delta accumulated this frame while the pointer was
196    /// within this widget's rect (regardless of what else was drawn on top
197    /// of it; see [`Sense::SCROLL`](crate::Sense::SCROLL)): positive
198    /// scrolls forward/down, negative scrolls backward/up. Feeds straight
199    /// into [`ListState::scroll_by`](crate::ListState::scroll_by). Zero
200    /// unless sensed with `SCROLL` and something scrolled.
201    #[must_use]
202    pub const fn scroll_delta(&self) -> i32 {
203        self.scroll_delta
204    }
205
206    /// This widget was interacted with via a [`Sense`](crate::Sense) that
207    /// had [`Sense::DISABLED`](crate::Sense::DISABLED) set.
208    /// [`hovered`](Self::hovered) still works, so a disabled control can
209    /// show a tooltip explaining why; every other field above is `false`
210    /// (or `0`) regardless of what the pointer/keyboard did. Widgets should
211    /// read this instead of threading a parallel `enabled` bool into their
212    /// own draw call.
213    #[must_use]
214    pub const fn disabled(&self) -> bool {
215        self.disabled
216    }
217
218    /// Where the pointer is, in grid coordinates, resolved from the same last-frame snapshot
219    /// [`hovered`](Self::hovered) is computed from: `Some` exactly when this widget's rect
220    /// contains the pointer, `None` when it's elsewhere or this widget wasn't sensed with a
221    /// pointer-registering [`Sense`](crate::Sense). Lets a composite widget shown under a single
222    /// id (a list of rows, a strip of tabs) resolve which of its own parts the pointer is over,
223    /// without needing a distinct id per part.
224    #[must_use]
225    pub const fn pointer_pos(&self) -> Option<Pos> {
226        self.pointer_pos
227    }
228
229    /// Where the pointer was when the press currently active on this widget landed. Unlike
230    /// [`pointer_pos`](Self::pointer_pos), this stays put for the duration of a press or drag
231    /// rather than tracking the pointer's current position: useful for a composite widget (e.g.
232    /// a scrollbar thumb) that needs to measure a drag's total displacement from where it
233    /// started. `None` unless this widget is the one a press is currently active on.
234    #[must_use]
235    pub const fn press_origin(&self) -> Option<Pos> {
236        self.press_origin
237    }
238
239    /// How far the pointer has moved from [`press_origin`](Self::press_origin), signed and in
240    /// grid cells: `(pointer.x - press_origin.x, pointer.y - press_origin.y)`. Unlike
241    /// [`pointer_pos`](Self::pointer_pos), this keeps reporting once the pointer slides outside
242    /// this widget's own rect, which is the common case for a [`Sense::DRAG`](crate::Sense::DRAG)
243    /// widget like a scrollbar thumb or a resizable pane divider: both need the drag's full
244    /// displacement, not just the part of it that stayed over the thumb. `None` under the same
245    /// condition [`press_origin`](Self::press_origin) is `None`: no press is currently active on
246    /// this widget.
247    #[must_use]
248    pub const fn drag_delta(&self) -> Option<(i32, i32)> {
249        self.drag_delta
250    }
251
252    /// The `area` this widget was shown at, as passed to
253    /// [`Interaction::interact`](crate::Interaction::interact) this frame. Useful for anything
254    /// that draws relative to where this widget landed, e.g. a tooltip anchored under its
255    /// trigger.
256    #[must_use]
257    pub const fn rect(&self) -> Rect {
258        self.rect
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn default_is_all_falsy() {
268        let r: Response<()> = Response::default();
269        assert_eq!(r.id(), ());
270        assert!(!r.hovered());
271        assert!(!r.pressed());
272        assert!(!r.released());
273        assert!(!r.clicked());
274        assert!(!r.double_clicked());
275        assert!(!r.held());
276        assert!(!r.dragging());
277        assert!(!r.focused());
278        assert!(!r.gained_focus());
279        assert!(!r.lost_focus());
280        assert!(!r.secondary_clicked());
281        assert!(!r.disabled());
282        assert_eq!(r.scroll_delta(), 0);
283        assert_eq!(r.pointer_pos(), None);
284        assert_eq!(r.press_origin(), None);
285        assert_eq!(r.drag_delta(), None);
286        assert_eq!(r.rect(), Rect::default());
287    }
288}