Skip to main content

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}