Skip to main content

kui_core/
cursor.rs

1//! Pointer shape as declared data. A view says what the pointer is over
2//! a node with the `cursor` prop — a button is a hand because it declared
3//! one, a handle a grab because it declared one — and the core resolves
4//! which declaration is under the pointer per frame
5//! ([`crate::runtime::Core::cursor_shape`]), which the frame driver hands
6//! to the real window. Nothing is inferred from what a node *does*: an
7//! `on_click` node with no `cursor` is the plain arrow, as a native
8//! button is, and so is an `on_drag` node. The one shape the core implies
9//! is the I-beam over an editor or a selection scope, the way every
10//! desktop marks text that can be taken. The stock button declares
11//! `Pointer` for itself, so `<button>` is a hand in every binding without
12//! the app saying so.
13//!
14//! The core never touches a device: headless drivers simply never read.
15
16/// The shape the pointer takes. Spelled the way CSS and the platform
17/// toolkits do, so a driver maps it one-to-one (winit's `CursorIcon`, the
18/// Web's `cursor`, GTK's names) instead of interpreting it.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
20pub enum CursorShape {
21    /// The plain arrow: plain boxes, window chrome, anything unclaimed.
22    #[default]
23    Default,
24    /// I-beam: text that can be selected or edited.
25    Text,
26    /// The pointing hand: something that acts when clicked.
27    Pointer,
28    /// Open hand: something that can be dragged, but is not being dragged.
29    Grab,
30    /// Closed hand: a drag is in flight (the pointer is captured).
31    Grabbing,
32    /// Refused: an inert control that wants to say why the click did
33    /// nothing.
34    NotAllowed,
35    /// Horizontal resize (a vertical splitter, the left/right window edge).
36    EwResize,
37    /// Vertical resize (a horizontal splitter, the top/bottom window edge).
38    NsResize,
39    /// Diagonal resize, top-left/bottom-right.
40    NwseResize,
41    /// Diagonal resize, top-right/bottom-left.
42    NeswResize,
43}
44
45impl CursorShape {
46    /// Every shape, in `schema::CURSORS` order (the `cursor` prop's).
47    pub const ALL: &'static [CursorShape] = &[
48        CursorShape::Default,
49        CursorShape::Text,
50        CursorShape::Pointer,
51        CursorShape::Grab,
52        CursorShape::Grabbing,
53        CursorShape::NotAllowed,
54        CursorShape::EwResize,
55        CursorShape::NsResize,
56        CursorShape::NwseResize,
57        CursorShape::NeswResize,
58    ];
59
60    /// The camelCase spelling every binding uses.
61    pub fn name(self) -> &'static str {
62        match self {
63            CursorShape::Default => "default",
64            CursorShape::Text => "text",
65            CursorShape::Pointer => "pointer",
66            CursorShape::Grab => "grab",
67            CursorShape::Grabbing => "grabbing",
68            CursorShape::NotAllowed => "notAllowed",
69            CursorShape::EwResize => "ewResize",
70            CursorShape::NsResize => "nsResize",
71            CursorShape::NwseResize => "nwseResize",
72            CursorShape::NeswResize => "neswResize",
73        }
74    }
75
76    pub fn parse(name: &str) -> Option<Self> {
77        Self::ALL.iter().copied().find(|c| c.name() == name)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    /// The schema's name list is the wire order: an index means the same
86    /// shape in every binding, so the two cannot drift.
87    #[test]
88    fn schema_names_are_all_in_order() {
89        let names: Vec<&str> = CursorShape::ALL.iter().map(|c| c.name()).collect();
90        assert_eq!(names, crate::schema::CURSORS);
91    }
92}