damascene_core/cursor.rs
1//! Pointer-cursor model.
2//!
3//! Widgets opt into a non-default cursor by setting [`crate::tree::El::cursor`].
4//! Per-frame the host runner reads [`crate::state::UiState::cursor`] and forwards
5//! the resolved value to the windowing backend (winit's
6//! `Window::set_cursor_icon`, the browser's `canvas.style.cursor`).
7//!
8//! The variants line up with the CSS `cursor` property — same names
9//! winit's `CursorIcon` already uses — so backend bridges are dumb
10//! `From` impls.
11//!
12//! # Resolution
13//!
14//! [`crate::state::UiState::cursor`] picks the active cursor each frame:
15//!
16//! 1. If a press is captured (button drag, slider thumb, scrollbar
17//! drag, text selection, …), the cursor follows the *press target*'s
18//! declared cursor and ignores whatever the pointer is currently
19//! over. Drag off a button onto a text input and the cursor stays
20//! [`Cursor::Pointer`] — matches native press-and-hold behaviour.
21//! 2. Else the hovered node and its ancestors are walked root-ward,
22//! returning the first explicit `.cursor(...)`. So a panel that
23//! sets `.cursor(Move)` once propagates to children that don't
24//! override.
25//! 3. Else [`Cursor::Default`].
26//!
27//! Disabled state is *not* auto-mapped to [`Cursor::NotAllowed`] —
28//! widgets that want that affordance branch in their build closure.
29
30crate::event::enum_with_all! {
31/// Pointer cursor. Variant names mirror CSS `cursor` so the
32/// backend mapping is a 1:1 translation.
33///
34/// `#[non_exhaustive]`: hosts map with a wildcard-arm fallback to the
35/// platform default, and test their mapping against [`Cursor::ALL`].
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum Cursor {
39 /// Platform default arrow.
40 #[default]
41 Default,
42 /// Hand / pointing finger — clickable surfaces (buttons, links,
43 /// checkboxes, switches, radios).
44 Pointer,
45 /// I-beam — text inputs and selectable text regions.
46 Text,
47 /// Slashed circle — disabled / unavailable affordances.
48 NotAllowed,
49 /// Open hand — a draggable target at rest.
50 Grab,
51 /// Closed hand — a draggable target while dragging.
52 Grabbing,
53 /// Generic "drag in any direction" (pan handles, view-port grabs).
54 Move,
55 /// Horizontal resize (←→).
56 EwResize,
57 /// Vertical resize (↑↓).
58 NsResize,
59 /// Diagonal resize (↖↘).
60 NwseResize,
61 /// Anti-diagonal resize (↗↙).
62 NeswResize,
63 /// Column boundary resize (table column dividers).
64 ColResize,
65 /// Row boundary resize (table row dividers).
66 RowResize,
67 /// Crosshair — picker / area-select tools.
68 Crosshair,
69}
70}
71
72impl Cursor {
73 /// The CSS `cursor` property value for this cursor (`"default"`,
74 /// `"pointer"`, `"ew-resize"`, …).
75 ///
76 /// This is the backend-neutral bridge for custom hosts:
77 /// - Web hosts assign it to `canvas.style.cursor` directly.
78 /// - winit hosts parse it into a `CursorIcon` without needing any
79 /// damascene host crate (winit's `cursor-icon` types implement
80 /// `FromStr` over exactly these CSS names):
81 /// `cursor.css_name().parse::<CursorIcon>().unwrap_or_default()`.
82 pub const fn css_name(self) -> &'static str {
83 match self {
84 Cursor::Default => "default",
85 Cursor::Pointer => "pointer",
86 Cursor::Text => "text",
87 Cursor::NotAllowed => "not-allowed",
88 Cursor::Grab => "grab",
89 Cursor::Grabbing => "grabbing",
90 Cursor::Move => "move",
91 Cursor::EwResize => "ew-resize",
92 Cursor::NsResize => "ns-resize",
93 Cursor::NwseResize => "nwse-resize",
94 Cursor::NeswResize => "nesw-resize",
95 Cursor::ColResize => "col-resize",
96 Cursor::RowResize => "row-resize",
97 Cursor::Crosshair => "crosshair",
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn default_is_default_variant() {
108 assert_eq!(Cursor::default(), Cursor::Default);
109 }
110
111 #[test]
112 fn css_names_match_the_css_cursor_property() {
113 // Spot-check the kebab-case conversions; the simple one-word
114 // names are self-evidently CSS values.
115 assert_eq!(Cursor::NotAllowed.css_name(), "not-allowed");
116 assert_eq!(Cursor::EwResize.css_name(), "ew-resize");
117 assert_eq!(Cursor::NwseResize.css_name(), "nwse-resize");
118 assert_eq!(Cursor::ColResize.css_name(), "col-resize");
119 }
120}