Skip to main content

uzor_core/input/
cursor.rs

1//! Cursor icon system for uzor
2//!
3//! Provides cursor icon types and state management based on CSS cursor specification.
4//! Supports priority-based cursor requests to handle overlapping widgets.
5
6/// Priority constants for cursor requests
7pub const PRIORITY_DEFAULT: u8 = 0;
8pub const PRIORITY_WIDGET: u8 = 100;
9pub const PRIORITY_DRAG: u8 = 150;
10pub const PRIORITY_MODAL: u8 = 200;
11pub const PRIORITY_SYSTEM: u8 = 255;
12
13/// Cursor icon types (based on CSS cursor specification)
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15pub enum CursorIcon {
16    /// Default cursor (usually an arrow)
17    #[default]
18    Default,
19    /// No cursor displayed
20    None,
21
22    // Links and help
23    /// Context menu available
24    ContextMenu,
25    /// Help information available
26    Help,
27    /// Pointing hand (typically for links)
28    PointingHand,
29
30    // Progress
31    /// Progress indicator (work in background)
32    Progress,
33    /// Wait/busy indicator
34    Wait,
35
36    // Selection
37    /// Cell or table selection
38    Cell,
39    /// Crosshair for precise selection
40    Crosshair,
41    /// Text selection cursor (I-beam)
42    Text,
43    /// Vertical text selection
44    VerticalText,
45
46    // Drag and drop
47    /// Alias/shortcut will be created
48    Alias,
49    /// Copy operation
50    Copy,
51    /// Move operation
52    Move,
53    /// Drop not allowed
54    NoDrop,
55    /// Action not allowed
56    NotAllowed,
57    /// Grabbable item
58    Grab,
59    /// Currently grabbing
60    Grabbing,
61
62    // Resizing
63    /// Omni-directional resize
64    AllScroll,
65    /// Horizontal resize (↔)
66    ResizeHorizontal,
67    /// Vertical resize (↕)
68    ResizeVertical,
69    /// Diagonal resize (↗↙)
70    ResizeNeSw,
71    /// Diagonal resize (↖↘)
72    ResizeNwSe,
73    /// Resize east (right)
74    ResizeEast,
75    /// Resize west (left)
76    ResizeWest,
77    /// Resize north (up)
78    ResizeNorth,
79    /// Resize south (down)
80    ResizeSouth,
81    /// Resize north-east
82    ResizeNorthEast,
83    /// Resize north-west
84    ResizeNorthWest,
85    /// Resize south-east
86    ResizeSouthEast,
87    /// Resize south-west
88    ResizeSouthWest,
89    /// Column resize
90    ResizeColumn,
91    /// Row resize
92    ResizeRow,
93
94    // Zoom
95    /// Zoom in cursor
96    ZoomIn,
97    /// Zoom out cursor
98    ZoomOut,
99}
100
101impl CursorIcon {
102    /// Returns true if this cursor is a resize variant
103    pub fn is_resize(&self) -> bool {
104        matches!(
105            self,
106            CursorIcon::AllScroll
107                | CursorIcon::ResizeHorizontal
108                | CursorIcon::ResizeVertical
109                | CursorIcon::ResizeNeSw
110                | CursorIcon::ResizeNwSe
111                | CursorIcon::ResizeEast
112                | CursorIcon::ResizeWest
113                | CursorIcon::ResizeNorth
114                | CursorIcon::ResizeSouth
115                | CursorIcon::ResizeNorthEast
116                | CursorIcon::ResizeNorthWest
117                | CursorIcon::ResizeSouthEast
118                | CursorIcon::ResizeSouthWest
119                | CursorIcon::ResizeColumn
120                | CursorIcon::ResizeRow
121        )
122    }
123
124    /// Returns true if this cursor represents a drag operation
125    pub fn is_drag(&self) -> bool {
126        matches!(self, CursorIcon::Grab | CursorIcon::Grabbing | CursorIcon::Move)
127    }
128
129    /// Returns the CSS cursor name for this icon
130    pub fn css_name(&self) -> &'static str {
131        match self {
132            CursorIcon::Default => "default",
133            CursorIcon::None => "none",
134            CursorIcon::ContextMenu => "context-menu",
135            CursorIcon::Help => "help",
136            CursorIcon::PointingHand => "pointer",
137            CursorIcon::Progress => "progress",
138            CursorIcon::Wait => "wait",
139            CursorIcon::Cell => "cell",
140            CursorIcon::Crosshair => "crosshair",
141            CursorIcon::Text => "text",
142            CursorIcon::VerticalText => "vertical-text",
143            CursorIcon::Alias => "alias",
144            CursorIcon::Copy => "copy",
145            CursorIcon::Move => "move",
146            CursorIcon::NoDrop => "no-drop",
147            CursorIcon::NotAllowed => "not-allowed",
148            CursorIcon::Grab => "grab",
149            CursorIcon::Grabbing => "grabbing",
150            CursorIcon::AllScroll => "all-scroll",
151            CursorIcon::ResizeHorizontal => "ew-resize",
152            CursorIcon::ResizeVertical => "ns-resize",
153            CursorIcon::ResizeNeSw => "nesw-resize",
154            CursorIcon::ResizeNwSe => "nwse-resize",
155            CursorIcon::ResizeEast => "e-resize",
156            CursorIcon::ResizeWest => "w-resize",
157            CursorIcon::ResizeNorth => "n-resize",
158            CursorIcon::ResizeSouth => "s-resize",
159            CursorIcon::ResizeNorthEast => "ne-resize",
160            CursorIcon::ResizeNorthWest => "nw-resize",
161            CursorIcon::ResizeSouthEast => "se-resize",
162            CursorIcon::ResizeSouthWest => "sw-resize",
163            CursorIcon::ResizeColumn => "col-resize",
164            CursorIcon::ResizeRow => "row-resize",
165            CursorIcon::ZoomIn => "zoom-in",
166            CursorIcon::ZoomOut => "zoom-out",
167        }
168    }
169}
170
171/// Cursor state for the current frame
172///
173/// Manages cursor icon requests with priority system. Higher priority requests
174/// override lower priority ones. Reset at the start of each frame.
175#[derive(Clone, Debug)]
176pub struct CursorState {
177    /// Currently requested cursor icon
178    requested: CursorIcon,
179    /// Priority level (higher overrides lower)
180    priority: u8,
181}
182
183impl Default for CursorState {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl CursorState {
190    /// Create a new cursor state with default cursor
191    pub fn new() -> Self {
192        Self {
193            requested: CursorIcon::Default,
194            priority: PRIORITY_DEFAULT,
195        }
196    }
197
198    /// Set cursor icon with default widget priority
199    ///
200    /// Uses `PRIORITY_WIDGET` (100) as the default priority level.
201    pub fn set(&mut self, icon: CursorIcon) {
202        self.set_with_priority(icon, PRIORITY_WIDGET);
203    }
204
205    /// Set cursor icon with explicit priority
206    ///
207    /// Only updates the cursor if the new priority is greater than or equal
208    /// to the current priority. This allows higher priority requests to
209    /// override lower priority ones.
210    pub fn set_with_priority(&mut self, icon: CursorIcon, priority: u8) {
211        if priority >= self.priority {
212            self.requested = icon;
213            self.priority = priority;
214        }
215    }
216
217    /// Get the current cursor icon
218    pub fn get(&self) -> CursorIcon {
219        self.requested
220    }
221
222    /// Reset cursor state to default (call at frame start)
223    ///
224    /// Resets both the cursor icon and priority to default values.
225    pub fn reset(&mut self) {
226        self.requested = CursorIcon::Default;
227        self.priority = PRIORITY_DEFAULT;
228    }
229
230    /// Check if cursor is currently set to default
231    pub fn is_default(&self) -> bool {
232        self.requested == CursorIcon::Default
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_cursor_state_new() {
242        let state = CursorState::new();
243        assert_eq!(state.get(), CursorIcon::Default);
244        assert!(state.is_default());
245    }
246
247    #[test]
248    fn test_cursor_state_set() {
249        let mut state = CursorState::new();
250        state.set(CursorIcon::PointingHand);
251        assert_eq!(state.get(), CursorIcon::PointingHand);
252        assert!(!state.is_default());
253    }
254
255    #[test]
256    fn test_cursor_state_reset() {
257        let mut state = CursorState::new();
258        state.set(CursorIcon::Grab);
259        assert!(!state.is_default());
260
261        state.reset();
262        assert_eq!(state.get(), CursorIcon::Default);
263        assert!(state.is_default());
264    }
265
266    #[test]
267    fn test_cursor_priority_override() {
268        let mut state = CursorState::new();
269
270        // Set with widget priority
271        state.set_with_priority(CursorIcon::PointingHand, PRIORITY_WIDGET);
272        assert_eq!(state.get(), CursorIcon::PointingHand);
273
274        // Higher priority overrides
275        state.set_with_priority(CursorIcon::Grab, PRIORITY_DRAG);
276        assert_eq!(state.get(), CursorIcon::Grab);
277
278        // Lower priority does not override
279        state.set_with_priority(CursorIcon::Text, PRIORITY_WIDGET);
280        assert_eq!(state.get(), CursorIcon::Grab);
281
282        // Equal priority overrides
283        state.set_with_priority(CursorIcon::Grabbing, PRIORITY_DRAG);
284        assert_eq!(state.get(), CursorIcon::Grabbing);
285
286        // System priority always wins
287        state.set_with_priority(CursorIcon::Wait, PRIORITY_SYSTEM);
288        assert_eq!(state.get(), CursorIcon::Wait);
289    }
290
291    #[test]
292    fn test_cursor_icon_is_resize() {
293        assert!(CursorIcon::ResizeHorizontal.is_resize());
294        assert!(CursorIcon::ResizeVertical.is_resize());
295        assert!(CursorIcon::ResizeNeSw.is_resize());
296        assert!(CursorIcon::ResizeNwSe.is_resize());
297        assert!(CursorIcon::ResizeEast.is_resize());
298        assert!(CursorIcon::ResizeWest.is_resize());
299        assert!(CursorIcon::ResizeNorth.is_resize());
300        assert!(CursorIcon::ResizeSouth.is_resize());
301        assert!(CursorIcon::ResizeNorthEast.is_resize());
302        assert!(CursorIcon::ResizeNorthWest.is_resize());
303        assert!(CursorIcon::ResizeSouthEast.is_resize());
304        assert!(CursorIcon::ResizeSouthWest.is_resize());
305        assert!(CursorIcon::ResizeColumn.is_resize());
306        assert!(CursorIcon::ResizeRow.is_resize());
307        assert!(CursorIcon::AllScroll.is_resize());
308
309        assert!(!CursorIcon::Default.is_resize());
310        assert!(!CursorIcon::PointingHand.is_resize());
311        assert!(!CursorIcon::Grab.is_resize());
312        assert!(!CursorIcon::Text.is_resize());
313    }
314
315    #[test]
316    fn test_cursor_icon_is_drag() {
317        assert!(CursorIcon::Grab.is_drag());
318        assert!(CursorIcon::Grabbing.is_drag());
319        assert!(CursorIcon::Move.is_drag());
320
321        assert!(!CursorIcon::Default.is_drag());
322        assert!(!CursorIcon::PointingHand.is_drag());
323        assert!(!CursorIcon::ResizeHorizontal.is_drag());
324        assert!(!CursorIcon::Copy.is_drag());
325    }
326
327    #[test]
328    fn test_cursor_icon_css_name() {
329        assert_eq!(CursorIcon::Default.css_name(), "default");
330        assert_eq!(CursorIcon::None.css_name(), "none");
331        assert_eq!(CursorIcon::PointingHand.css_name(), "pointer");
332        assert_eq!(CursorIcon::Grab.css_name(), "grab");
333        assert_eq!(CursorIcon::Grabbing.css_name(), "grabbing");
334        assert_eq!(CursorIcon::Move.css_name(), "move");
335        assert_eq!(CursorIcon::Text.css_name(), "text");
336        assert_eq!(CursorIcon::ResizeHorizontal.css_name(), "ew-resize");
337        assert_eq!(CursorIcon::ResizeVertical.css_name(), "ns-resize");
338        assert_eq!(CursorIcon::ResizeNeSw.css_name(), "nesw-resize");
339        assert_eq!(CursorIcon::ResizeNwSe.css_name(), "nwse-resize");
340        assert_eq!(CursorIcon::Wait.css_name(), "wait");
341        assert_eq!(CursorIcon::Progress.css_name(), "progress");
342        assert_eq!(CursorIcon::NotAllowed.css_name(), "not-allowed");
343        assert_eq!(CursorIcon::ZoomIn.css_name(), "zoom-in");
344        assert_eq!(CursorIcon::ZoomOut.css_name(), "zoom-out");
345    }
346
347    #[test]
348    fn test_default_priority_constant() {
349        let mut state = CursorState::new();
350        state.set(CursorIcon::PointingHand);
351        assert_eq!(state.priority, PRIORITY_WIDGET);
352    }
353
354    #[test]
355    fn test_cursor_state_default_trait() {
356        let state = CursorState::default();
357        assert_eq!(state.get(), CursorIcon::Default);
358        assert!(state.is_default());
359    }
360
361    #[test]
362    fn test_cursor_icon_default_trait() {
363        let icon = CursorIcon::default();
364        assert_eq!(icon, CursorIcon::Default);
365    }
366}