Skip to main content

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
30/// Pointer cursor. Variant names mirror CSS `cursor` so the
31/// backend mapping is a 1:1 translation.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33#[non_exhaustive]
34pub enum Cursor {
35    /// Platform default arrow.
36    #[default]
37    Default,
38    /// Hand / pointing finger — clickable surfaces (buttons, links,
39    /// checkboxes, switches, radios).
40    Pointer,
41    /// I-beam — text inputs and selectable text regions.
42    Text,
43    /// Slashed circle — disabled / unavailable affordances.
44    NotAllowed,
45    /// Open hand — a draggable target at rest.
46    Grab,
47    /// Closed hand — a draggable target while dragging.
48    Grabbing,
49    /// Generic "drag in any direction" (pan handles, view-port grabs).
50    Move,
51    /// Horizontal resize (←→).
52    EwResize,
53    /// Vertical resize (↑↓).
54    NsResize,
55    /// Diagonal resize (↖↘).
56    NwseResize,
57    /// Anti-diagonal resize (↗↙).
58    NeswResize,
59    /// Column boundary resize (table column dividers).
60    ColResize,
61    /// Row boundary resize (table row dividers).
62    RowResize,
63    /// Crosshair — picker / area-select tools.
64    Crosshair,
65}
66
67impl Cursor {
68    /// The CSS `cursor` property value for this cursor (`"default"`,
69    /// `"pointer"`, `"ew-resize"`, …).
70    ///
71    /// This is the backend-neutral bridge for custom hosts:
72    /// - Web hosts assign it to `canvas.style.cursor` directly.
73    /// - winit hosts parse it into a `CursorIcon` without needing any
74    ///   damascene host crate (winit's `cursor-icon` types implement
75    ///   `FromStr` over exactly these CSS names):
76    ///   `cursor.css_name().parse::<CursorIcon>().unwrap_or_default()`.
77    pub const fn css_name(self) -> &'static str {
78        match self {
79            Cursor::Default => "default",
80            Cursor::Pointer => "pointer",
81            Cursor::Text => "text",
82            Cursor::NotAllowed => "not-allowed",
83            Cursor::Grab => "grab",
84            Cursor::Grabbing => "grabbing",
85            Cursor::Move => "move",
86            Cursor::EwResize => "ew-resize",
87            Cursor::NsResize => "ns-resize",
88            Cursor::NwseResize => "nwse-resize",
89            Cursor::NeswResize => "nesw-resize",
90            Cursor::ColResize => "col-resize",
91            Cursor::RowResize => "row-resize",
92            Cursor::Crosshair => "crosshair",
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn default_is_default_variant() {
103        assert_eq!(Cursor::default(), Cursor::Default);
104    }
105
106    #[test]
107    fn css_names_match_the_css_cursor_property() {
108        // Spot-check the kebab-case conversions; the simple one-word
109        // names are self-evidently CSS values.
110        assert_eq!(Cursor::NotAllowed.css_name(), "not-allowed");
111        assert_eq!(Cursor::EwResize.css_name(), "ew-resize");
112        assert_eq!(Cursor::NwseResize.css_name(), "nwse-resize");
113        assert_eq!(Cursor::ColResize.css_name(), "col-resize");
114    }
115}