crcbl_shell/cursor.rs
1//! Pointer capture modes and cursor shapes.
2//!
3//! # Three pointer modes, and why "hidden" is not one of them
4//!
5//! [`PointerMode`] is about *where the pointer may go*; visibility is a
6//! separate axis, set through [`Shell::set_cursor`](crate::Shell::set_cursor)
7//! with `None`. Collapsing the two — a `Hidden` mode — is tempting and wrong:
8//! a text editor hides the cursor while typing without capturing it, and a
9//! strategy game confines the pointer to the window while keeping it perfectly
10//! visible. Two axes, two settings.
11//!
12//! # Cursor shapes are a fixed enum, not an image
13//!
14//! [`CursorIcon`] names shapes rather than carrying pixels because every
15//! platform already has themed, correctly-scaled, user-configured cursors and
16//! shipping our own would mean shipping them at every scale factor and ignoring
17//! the user's theme. Custom cursor images are a real feature for an editor
18//! (drag-and-drop ghosts) and are deliberately out of scope here; they can be
19//! added as a separate call that takes a pixel buffer without disturbing this
20//! enum.
21//!
22//! The names follow the CSS cursor keywords, which is the same vocabulary the
23//! Wayland `cursor-shape-v1` protocol adopted and which every X11 cursor theme
24//! is indexed by.
25
26/// Where the pointer is allowed to go.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
28pub enum PointerMode {
29 /// Normal: the pointer moves freely and can leave the window.
30 #[default]
31 Free,
32
33 /// The pointer stays inside the window but keeps moving visibly.
34 ///
35 /// What a strategy game's edge-scrolling wants. Requires
36 /// [`ShellCaps::POINTER_CONFINE`](crate::ShellCaps::POINTER_CONFINE).
37 Confined,
38
39 /// The pointer is pinned in place and only relative motion is reported.
40 ///
41 /// What a first-person camera wants. Requires
42 /// [`ShellCaps::POINTER_LOCK`](crate::ShellCaps::POINTER_LOCK), and in
43 /// practice also
44 /// [`ShellCaps::RAW_POINTER_MOTION`](crate::ShellCaps::RAW_POINTER_MOTION)
45 /// — see [`ShellCaps::has_mouselook`](crate::ShellCaps::has_mouselook).
46 ///
47 /// While locked, [`ShellEvent::PointerMotion`](crate::ShellEvent::PointerMotion)
48 /// carries `abs: None`: there is no meaningful absolute position, and
49 /// reporting the frozen one would make anything that reads it appear to
50 /// work until the day it does not.
51 Locked,
52}
53
54impl PointerMode {
55 /// Whether this mode constrains the pointer at all.
56 #[inline]
57 #[must_use]
58 pub const fn is_captured(self) -> bool {
59 !matches!(self, Self::Free)
60 }
61
62 /// The capability a shell must report for this mode to be settable.
63 ///
64 /// [`Free`](Self::Free) needs none, so it returns an empty set — which is
65 /// what makes `caps.contains(mode.required_cap())` correct for all three
66 /// without a special case at the call site.
67 #[inline]
68 #[must_use]
69 pub const fn required_cap(self) -> crate::ShellCaps {
70 match self {
71 Self::Free => crate::ShellCaps::empty(),
72 Self::Confined => crate::ShellCaps::POINTER_CONFINE,
73 Self::Locked => crate::ShellCaps::POINTER_LOCK,
74 }
75 }
76
77 /// What this mode is called in a
78 /// [`ShellError::Unsupported`](crate::ShellError::Unsupported).
79 ///
80 /// Every backend needs this string in
81 /// [`set_pointer_mode`](crate::Shell::set_pointer_mode) and each one used
82 /// to spell the table again, with an `unreachable!()` for
83 /// [`Free`](Self::Free) on the grounds that it needs no capability. That
84 /// reasoning is correct today and is a panic the day a mode is added, so
85 /// the function is total instead.
86 #[inline]
87 #[must_use]
88 pub const fn as_str(self) -> &'static str {
89 match self {
90 Self::Free => "a free pointer",
91 Self::Confined => "pointer confinement",
92 Self::Locked => "pointer lock",
93 }
94 }
95}
96
97/// A standard cursor shape, defined in [`crcbl_core::input`] so the UI can name
98/// one without depending on the shell.
99pub use crcbl_core::input::CursorIcon;
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::ShellCaps;
105
106 #[test]
107 fn capture_modes_declare_the_cap_they_need() {
108 assert!(!PointerMode::Free.is_captured());
109 assert!(PointerMode::Confined.is_captured());
110 assert!(PointerMode::Locked.is_captured());
111
112 // The property that makes the check uniform at the call site.
113 for mode in [
114 PointerMode::Free,
115 PointerMode::Confined,
116 PointerMode::Locked,
117 ] {
118 assert!(ShellCaps::DESKTOP.contains(mode.required_cap()));
119 }
120 assert!(ShellCaps::empty().contains(PointerMode::Free.required_cap()));
121 assert!(!ShellCaps::POINTER_CONFINE.contains(PointerMode::Locked.required_cap()));
122 assert_eq!(PointerMode::default(), PointerMode::Free);
123 }
124}