Skip to main content

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}