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}