retroglyph_widgets/interact/response.rs
1//! [`Response`]: what [`Interaction::interact`](crate::Interaction::interact)
2//! hands back to a widget call site.
3
4/// What happened to a widget this frame, as reported by
5/// [`Interaction::interact`](crate::Interaction::interact).
6///
7/// Every field is scoped to *this* frame only (e.g. [`clicked`](Self::clicked)
8/// is `true` for exactly the one frame the release lands on), except
9/// [`focused`](Self::focused), which stays `true` across frames until focus
10/// moves elsewhere. Fields a widget didn't ask for via
11/// [`Sense`](crate::Sense) are always `false`/`0`: a widget sensed with
12/// only [`Sense::HOVER`](crate::Sense::HOVER) never reports
13/// [`clicked`](Self::clicked), for instance.
14// Eight flat, independent fields by design: `Response` is a per-frame
15// report card, not a state machine: collapsing it into enums would only
16// make `interact`'s construction of it more awkward for no reader benefit.
17#[allow(clippy::struct_excessive_bools)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct Response {
20 pub(crate) hovered: bool,
21 pub(crate) pressed: bool,
22 pub(crate) released: bool,
23 pub(crate) clicked: bool,
24 pub(crate) held: bool,
25 pub(crate) dragging: bool,
26 pub(crate) focused: bool,
27 pub(crate) secondary_clicked: bool,
28 pub(crate) scroll_delta: i32,
29}
30
31impl Response {
32 /// The pointer is over this widget's rect, resolved from last frame's
33 /// hit-test: see [`Interaction`](crate::Interaction) for why there's a
34 /// frame of latency.
35 #[must_use]
36 pub const fn hovered(&self) -> bool {
37 self.hovered
38 }
39
40 /// The primary pointer button went down on this widget this frame, or
41 /// (sensed with [`Sense::FOCUSABLE`](crate::Sense::FOCUSABLE)) Enter or
42 /// Space was pressed while it was focused.
43 #[must_use]
44 pub const fn pressed(&self) -> bool {
45 self.pressed
46 }
47
48 /// The primary pointer button (or an activating key) was released this
49 /// frame while this widget was the active one. Fires whether or not the
50 /// release also counts as a [`clicked`](Self::clicked) (e.g. it doesn't,
51 /// if the gesture crossed the drag threshold first).
52 #[must_use]
53 pub const fn released(&self) -> bool {
54 self.released
55 }
56
57 /// A full press-release cycle landed on this widget this frame: pressed
58 /// and released while still hovered, never crossing the drag threshold.
59 /// Also fires from keyboard activation (Enter/Space while focused) --
60 /// terminals are frequently mouse-less, so [`Sense::click`](crate::Sense::click)
61 /// widgets are keyboard-operable by default.
62 #[must_use]
63 pub const fn clicked(&self) -> bool {
64 self.clicked
65 }
66
67 /// The primary pointer button is down *and* the pointer is currently over this widget's
68 /// rect, re-checked live every frame, unlike [`pressed`](Self::pressed), which fires
69 /// once on the down edge and never re-checks position. Automatically cancels (goes
70 /// `false`) the instant the pointer slides off this widget's rect, even before release,
71 /// without waiting for a release event: the same "slide-to-cancel" feedback
72 /// `is_pointer_button_down_on` gives egui widgets and `IsItemHovered() && IsItemActive()`
73 /// gives Dear `ImGui` widgets. Only ever `true` for widgets sensed with
74 /// [`Sense::CLICK`](crate::Sense::CLICK).
75 #[must_use]
76 pub const fn held(&self) -> bool {
77 self.held
78 }
79
80 /// The pointer moved past the drag threshold while pressed on this
81 /// widget. Only ever `true` for widgets sensed with
82 /// [`Sense::DRAG`](crate::Sense::DRAG).
83 #[must_use]
84 pub const fn dragging(&self) -> bool {
85 self.dragging
86 }
87
88 /// This widget holds keyboard focus. Unlike the other fields, this is
89 /// level state, not a one-shot "this happened" flag: it stays `true`
90 /// across frames until focus moves to another widget or is cleared.
91 #[must_use]
92 pub const fn focused(&self) -> bool {
93 self.focused
94 }
95
96 /// The secondary (right) mouse button pressed and released on this
97 /// widget this frame while still hovered. Only ever `true` for widgets
98 /// sensed with [`Sense::SECONDARY_CLICK`](crate::Sense::SECONDARY_CLICK).
99 /// Unlike [`clicked`](Self::clicked), there's no keyboard equivalent --
100 /// a secondary action needs its own trigger (a modifier+Enter, a menu
101 /// key, whatever fits the app) since Enter/Space already means
102 /// "primary activate".
103 #[must_use]
104 pub const fn secondary_clicked(&self) -> bool {
105 self.secondary_clicked
106 }
107
108 /// Scroll wheel delta accumulated this frame while the pointer was
109 /// within this widget's rect (regardless of what else was drawn on top
110 /// of it; see [`Sense::SCROLL`](crate::Sense::SCROLL)): positive
111 /// scrolls forward/down, negative scrolls backward/up. Feeds straight
112 /// into [`ListState::scroll_by`](crate::ListState::scroll_by). Zero
113 /// unless sensed with `SCROLL` and something scrolled.
114 #[must_use]
115 pub const fn scroll_delta(&self) -> i32 {
116 self.scroll_delta
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn default_is_all_falsy() {
126 let r = Response::default();
127 assert!(!r.hovered());
128 assert!(!r.pressed());
129 assert!(!r.released());
130 assert!(!r.clicked());
131 assert!(!r.held());
132 assert!(!r.dragging());
133 assert!(!r.focused());
134 assert!(!r.secondary_clicked());
135 assert_eq!(r.scroll_delta(), 0);
136 }
137}