retroglyph_ui/interact/sense.rs
1//! [`Sense`]: what a widget wants [`Interaction::interact`](crate::Interaction::interact)
2//! to compute on its behalf.
3
4use core::ops::{BitOr, BitOrAssign};
5
6/// Which of a [`Response`](crate::Response)'s fields
7/// [`Interaction::interact`](crate::Interaction::interact) should actually
8/// populate for a given widget call.
9///
10/// A manual bitflag over `u8`: mirrors
11/// [`KeyModifiers`](retroglyph_core::event::KeyModifiers)'s shape rather than
12/// pulling in the `bitflags` crate for a handful of bits. Combine raw flags
13/// with `|` (`Sense::HOVER | Sense::FOCUSABLE`), or reach for one of the
14/// named constructors ([`click`](Self::click), [`drag`](Self::drag),
15/// [`hover`](Self::hover), [`scroll`](Self::scroll)) for the common cases.
16/// [`DISABLED`](Self::DISABLED) is the exception: it's a modifier over an
17/// existing sense, not a capability of its own.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
19pub struct Sense(u8);
20
21impl Sense {
22 /// Register the widget's rect for hit-testing and report
23 /// [`Response::hovered`](crate::Response::hovered).
24 pub const HOVER: Self = Self(1 << 0);
25 /// Report [`Response::pressed`](crate::Response::pressed),
26 /// [`Response::released`](crate::Response::released), and
27 /// [`Response::clicked`](crate::Response::clicked).
28 pub const CLICK: Self = Self(1 << 1);
29 /// Report [`Response::dragging`](crate::Response::dragging) once the
30 /// pointer moves past the drag threshold while pressed on this widget.
31 pub const DRAG: Self = Self(1 << 2);
32 /// Register the widget in the [`FocusRing`](crate::FocusRing)'s
33 /// Tab/Shift+Tab order and report
34 /// [`Response::focused`](crate::Response::focused). Combined with
35 /// [`CLICK`](Self::CLICK), Enter/Space also activate the widget while
36 /// it's focused: terminals are frequently mouse-less.
37 pub const FOCUSABLE: Self = Self(1 << 3);
38 /// Report [`Response::scroll_delta`](crate::Response::scroll_delta)
39 /// whenever the pointer is within this widget's rect. Unlike the other
40 /// pointer senses, this is *not* limited to the single
41 /// topmost widget under the pointer: see [`Interaction::interact`](crate::Interaction::interact)'s
42 /// doc comment on `scroll_delta` for why.
43 pub const SCROLL: Self = Self(1 << 4);
44 /// Report [`Response::secondary_clicked`](crate::Response::secondary_clicked):
45 /// the secondary (right) button pressed and released on this widget
46 /// while still hovered. Independent of [`CLICK`](Self::CLICK): combine
47 /// them (`Sense::click() | Sense::SECONDARY_CLICK`) for a widget that
48 /// wants both a primary action and a secondary one (e.g. a context
49 /// menu). Unlike [`CLICK`](Self::CLICK)/[`DRAG`](Self::DRAG), there's no
50 /// drag-threshold suppression for the secondary button: a
51 /// press-and-release on the same widget always counts, since
52 /// secondary-button drags aren't a gesture this module resolves.
53 pub const SECONDARY_CLICK: Self = Self(1 << 5);
54 /// Keeps this call's hit-testing (so [`Response::hovered`](crate::Response::hovered)
55 /// still works, most of the value of showing a disabled control at
56 /// all) but suppresses everything else this sense would otherwise
57 /// register or report: no [`FocusRing`](crate::FocusRing) registration,
58 /// and [`Response::pressed`](crate::Response::pressed),
59 /// [`released`](crate::Response::released), [`clicked`](crate::Response::clicked),
60 /// [`held`](crate::Response::held), [`dragging`](crate::Response::dragging),
61 /// [`focused`](crate::Response::focused), and
62 /// [`secondary_clicked`](crate::Response::secondary_clicked) all report
63 /// `false`, even if the gesture would otherwise satisfy them.
64 /// [`SCROLL`](Self::SCROLL) is suppressed too: a disabled row's own
65 /// [`Sense::SCROLL`] never fires, though this doesn't stop an enclosing
66 /// scrollable list (a separate widget) from scrolling past it.
67 ///
68 /// A modifier, not a capability of its own: combine it with an existing
69 /// sense rather than using it alone, e.g. `Sense::click() |
70 /// Sense::DISABLED`. See [`disabled_if`](Self::disabled_if) for the
71 /// common call-site shape.
72 pub const DISABLED: Self = Self(1 << 6);
73 /// Senses nothing: [`interact`](crate::Interaction::interact) still
74 /// registers the id nowhere and every [`Response`](crate::Response) field reports as if
75 /// nothing happened, matching [`Response::default`](crate::Response), with one exception:
76 /// [`rect`](crate::Response::rect) always echoes back the area passed to `interact`, useful
77 /// for purely decorative widgets that still want layout echo.
78 pub const NONE: Self = Self(0);
79
80 /// A clickable, hoverable, focusable widget: buttons, tabs, list
81 /// rows. Equivalent to `HOVER | CLICK | FOCUSABLE`.
82 #[must_use]
83 pub const fn click() -> Self {
84 Self(Self::HOVER.0 | Self::CLICK.0 | Self::FOCUSABLE.0)
85 }
86
87 /// A draggable widget, e.g. a slider or scrollbar thumb. Equivalent to
88 /// <code>[click](Self::click) | DRAG</code>.
89 #[must_use]
90 pub const fn drag() -> Self {
91 Self(Self::click().0 | Self::DRAG.0)
92 }
93
94 /// A hover-only widget with no click or focus behavior, e.g. a tooltip
95 /// trigger. Equivalent to `HOVER`.
96 #[must_use]
97 pub const fn hover() -> Self {
98 Self::HOVER
99 }
100
101 /// A scrollable region, e.g. a list or log panel. Equivalent to
102 /// `HOVER | SCROLL`.
103 #[must_use]
104 pub const fn scroll() -> Self {
105 Self(Self::HOVER.0 | Self::SCROLL.0)
106 }
107
108 /// A widget with a secondary (right-click) action but no primary click,
109 /// e.g. a context-menu-only trigger. Equivalent to `HOVER | SECONDARY_CLICK`.
110 /// Combine with [`click`](Self::click) (`Sense::click() | Sense::SECONDARY_CLICK`)
111 /// for a widget with both a primary and a secondary action.
112 #[must_use]
113 pub const fn secondary_click() -> Self {
114 Self(Self::HOVER.0 | Self::SECONDARY_CLICK.0)
115 }
116
117 /// `self` with [`DISABLED`](Self::DISABLED) set if `disabled` is
118 /// `true`, unchanged otherwise: the common call-site shape,
119 /// `Sense::click().disabled_if(!save_available)`, replacing what would
120 /// otherwise be a branch between two `Sense` literals.
121 #[must_use]
122 pub const fn disabled_if(self, disabled: bool) -> Self {
123 if disabled {
124 Self(self.0 | Self::DISABLED.0)
125 } else {
126 self
127 }
128 }
129
130 /// `true` if every bit set in `other` is also set in `self`.
131 #[must_use]
132 pub const fn contains(self, other: Self) -> bool {
133 (self.0 & other.0) == other.0
134 }
135
136 /// `true` if [`DISABLED`](Self::DISABLED) is set.
137 #[must_use]
138 pub const fn is_disabled(self) -> bool {
139 self.contains(Self::DISABLED)
140 }
141
142 /// `true` if this sense wants pointer hit-testing at all ([`HOVER`](Self::HOVER),
143 /// [`CLICK`](Self::CLICK), [`DRAG`](Self::DRAG), [`SCROLL`](Self::SCROLL),
144 /// or [`SECONDARY_CLICK`](Self::SECONDARY_CLICK)).
145 #[must_use]
146 pub const fn wants_pointer(self) -> bool {
147 self.0
148 & (Self::HOVER.0
149 | Self::CLICK.0
150 | Self::DRAG.0
151 | Self::SCROLL.0
152 | Self::SECONDARY_CLICK.0)
153 != 0
154 }
155}
156
157impl BitOr for Sense {
158 type Output = Self;
159
160 fn bitor(self, rhs: Self) -> Self {
161 Self(self.0 | rhs.0)
162 }
163}
164
165impl BitOrAssign for Sense {
166 fn bitor_assign(&mut self, rhs: Self) {
167 self.0 |= rhs.0;
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn contains_checks_all_bits() {
177 let s = Sense::HOVER | Sense::FOCUSABLE;
178 assert!(s.contains(Sense::HOVER));
179 assert!(s.contains(Sense::FOCUSABLE));
180 assert!(!s.contains(Sense::CLICK));
181 assert!(s.contains(Sense::NONE)); // vacuously true
182 }
183
184 #[test]
185 fn constructors_match_their_documented_bit_combinations() {
186 assert_eq!(
187 Sense::click(),
188 Sense::HOVER | Sense::CLICK | Sense::FOCUSABLE
189 );
190 assert_eq!(Sense::drag(), Sense::click() | Sense::DRAG);
191 assert_eq!(Sense::hover(), Sense::HOVER);
192 assert_eq!(Sense::scroll(), Sense::HOVER | Sense::SCROLL);
193 }
194
195 #[test]
196 fn wants_pointer_ignores_focusable() {
197 assert!(!Sense::FOCUSABLE.wants_pointer());
198 assert!(Sense::HOVER.wants_pointer());
199 assert!(Sense::CLICK.wants_pointer());
200 assert!(Sense::DRAG.wants_pointer());
201 assert!(Sense::SCROLL.wants_pointer());
202 assert!(!Sense::NONE.wants_pointer());
203 }
204
205 #[test]
206 fn default_is_none() {
207 assert_eq!(Sense::default(), Sense::NONE);
208 }
209
210 #[test]
211 fn disabled_if_sets_the_bit_only_when_true() {
212 assert_eq!(
213 Sense::click().disabled_if(true),
214 Sense::click() | Sense::DISABLED
215 );
216 assert_eq!(Sense::click().disabled_if(false), Sense::click());
217 }
218
219 #[test]
220 fn is_disabled_reads_the_bit() {
221 assert!(!Sense::click().is_disabled());
222 assert!((Sense::click() | Sense::DISABLED).is_disabled());
223 }
224}