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}