retroglyph_widgets/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::KeyModifiers)'s shape rather than
12/// pulling in the `bitflags` crate for five bits. Combine raw flags with
13/// `|` (`Sense::HOVER | Sense::FOCUSABLE`), or reach for one of the named
14/// constructors ([`click`](Self::click), [`drag`](Self::drag),
15/// [`hover`](Self::hover), [`scroll`](Self::scroll)) for the common cases.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub struct Sense(u8);
18
19impl Sense {
20 /// Register the widget's rect for hit-testing and report
21 /// [`Response::hovered`](crate::Response::hovered).
22 pub const HOVER: Self = Self(1 << 0);
23 /// Report [`Response::pressed`](crate::Response::pressed),
24 /// [`Response::released`](crate::Response::released), and
25 /// [`Response::clicked`](crate::Response::clicked).
26 pub const CLICK: Self = Self(1 << 1);
27 /// Report [`Response::dragging`](crate::Response::dragging) once the
28 /// pointer moves past the drag threshold while pressed on this widget.
29 pub const DRAG: Self = Self(1 << 2);
30 /// Register the widget in the [`FocusRing`](crate::FocusRing)'s
31 /// Tab/Shift+Tab order and report
32 /// [`Response::focused`](crate::Response::focused). Combined with
33 /// [`CLICK`](Self::CLICK), Enter/Space also activate the widget while
34 /// it's focused: terminals are frequently mouse-less.
35 pub const FOCUSABLE: Self = Self(1 << 3);
36 /// Report [`Response::scroll_delta`](crate::Response::scroll_delta)
37 /// whenever the pointer is within this widget's rect. Unlike the other
38 /// pointer senses, this is deliberately *not* limited to the single
39 /// topmost widget under the pointer: see [`Interaction::interact`](crate::Interaction::interact)'s
40 /// doc comment on `scroll_delta` for why.
41 pub const SCROLL: Self = Self(1 << 4);
42 /// Report [`Response::secondary_clicked`](crate::Response::secondary_clicked):
43 /// the secondary (right) button pressed and released on this widget
44 /// while still hovered. Independent of [`CLICK`](Self::CLICK): combine
45 /// them (`Sense::click() | Sense::SECONDARY_CLICK`) for a widget that
46 /// wants both a primary action and a secondary one (e.g. a context
47 /// menu). Unlike [`CLICK`](Self::CLICK)/[`DRAG`](Self::DRAG), there's no
48 /// drag-threshold suppression for the secondary button: a
49 /// press-and-release on the same widget always counts, since
50 /// secondary-button drags aren't a gesture this module resolves.
51 pub const SECONDARY_CLICK: Self = Self(1 << 5);
52 /// Senses nothing: [`interact`](crate::Interaction::interact) still
53 /// registers the id nowhere and returns [`Response::default`](crate::Response).
54 pub const NONE: Self = Self(0);
55
56 /// A clickable, hoverable, focusable widget: buttons, tabs, list
57 /// rows. Equivalent to `HOVER | CLICK | FOCUSABLE`.
58 #[must_use]
59 pub const fn click() -> Self {
60 Self(Self::HOVER.0 | Self::CLICK.0 | Self::FOCUSABLE.0)
61 }
62
63 /// A draggable widget, e.g. a slider or scrollbar thumb. Equivalent to
64 /// <code>[click](Self::click) | DRAG</code>.
65 #[must_use]
66 pub const fn drag() -> Self {
67 Self(Self::click().0 | Self::DRAG.0)
68 }
69
70 /// A hover-only widget with no click or focus behavior, e.g. a tooltip
71 /// trigger. Equivalent to `HOVER`.
72 #[must_use]
73 pub const fn hover() -> Self {
74 Self::HOVER
75 }
76
77 /// A scrollable region, e.g. a list or log panel. Equivalent to
78 /// `HOVER | SCROLL`.
79 #[must_use]
80 pub const fn scroll() -> Self {
81 Self(Self::HOVER.0 | Self::SCROLL.0)
82 }
83
84 /// A widget with a secondary (right-click) action but no primary click,
85 /// e.g. a context-menu-only trigger. Equivalent to `HOVER | SECONDARY_CLICK`.
86 /// Combine with [`click`](Self::click) (`Sense::click() | Sense::SECONDARY_CLICK`)
87 /// for a widget with both a primary and a secondary action.
88 #[must_use]
89 pub const fn secondary_click() -> Self {
90 Self(Self::HOVER.0 | Self::SECONDARY_CLICK.0)
91 }
92
93 /// `true` if every bit set in `other` is also set in `self`.
94 #[must_use]
95 pub const fn contains(self, other: Self) -> bool {
96 (self.0 & other.0) == other.0
97 }
98
99 /// `true` if this sense wants pointer hit-testing at all ([`HOVER`](Self::HOVER),
100 /// [`CLICK`](Self::CLICK), [`DRAG`](Self::DRAG), [`SCROLL`](Self::SCROLL),
101 /// or [`SECONDARY_CLICK`](Self::SECONDARY_CLICK)).
102 #[must_use]
103 pub const fn wants_pointer(self) -> bool {
104 self.0
105 & (Self::HOVER.0
106 | Self::CLICK.0
107 | Self::DRAG.0
108 | Self::SCROLL.0
109 | Self::SECONDARY_CLICK.0)
110 != 0
111 }
112}
113
114impl BitOr for Sense {
115 type Output = Self;
116
117 fn bitor(self, rhs: Self) -> Self {
118 Self(self.0 | rhs.0)
119 }
120}
121
122impl BitOrAssign for Sense {
123 fn bitor_assign(&mut self, rhs: Self) {
124 self.0 |= rhs.0;
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn contains_checks_all_bits() {
134 let s = Sense::HOVER | Sense::FOCUSABLE;
135 assert!(s.contains(Sense::HOVER));
136 assert!(s.contains(Sense::FOCUSABLE));
137 assert!(!s.contains(Sense::CLICK));
138 assert!(s.contains(Sense::NONE)); // vacuously true
139 }
140
141 #[test]
142 fn constructors_match_their_documented_bit_combinations() {
143 assert_eq!(
144 Sense::click(),
145 Sense::HOVER | Sense::CLICK | Sense::FOCUSABLE
146 );
147 assert_eq!(Sense::drag(), Sense::click() | Sense::DRAG);
148 assert_eq!(Sense::hover(), Sense::HOVER);
149 assert_eq!(Sense::scroll(), Sense::HOVER | Sense::SCROLL);
150 }
151
152 #[test]
153 fn wants_pointer_ignores_focusable() {
154 assert!(!Sense::FOCUSABLE.wants_pointer());
155 assert!(Sense::HOVER.wants_pointer());
156 assert!(Sense::CLICK.wants_pointer());
157 assert!(Sense::DRAG.wants_pointer());
158 assert!(Sense::SCROLL.wants_pointer());
159 assert!(!Sense::NONE.wants_pointer());
160 }
161
162 #[test]
163 fn default_is_none() {
164 assert_eq!(Sense::default(), Sense::NONE);
165 }
166}