cranpose_ui_graphics/pointer_icon.rs
1//! The mouse pointer's appearance: the shape a platform draws under the
2//! pointing device while it hovers a region of the UI.
3//!
4//! A [`PointerIcon`] is either one of the standard shapes every windowing
5//! system and browser already knows ([`CursorIcon`], the CSS cursor
6//! vocabulary) or a [`CustomPointerIcon`] the application draws itself from an
7//! [`ImageBitmap`] and a hotspot. Applications attach one to a region with
8//! `Modifier::pointer_icon`; the shell resolves the topmost hovered region's
9//! icon and the platform layer applies it to the window (winit on desktop, the
10//! canvas's CSS `cursor` on the web). Platforms with no pointing device —
11//! Android and iOS — ignore it.
12
13#[doc(inline)]
14pub use cursor_icon::CursorIcon;
15
16use crate::ImageBitmap;
17
18/// The largest custom pointer icon a platform is asked to draw, in pixels.
19///
20/// Windowing systems reject or silently drop oversized cursors (browsers
21/// commonly cap at 128x128), so a bitmap wider or taller than this is refused
22/// when the icon is built rather than at the point where it would fail to
23/// appear.
24pub const MAX_POINTER_ICON_SIZE: u32 = 128;
25
26/// Errors returned while constructing a [`CustomPointerIcon`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28pub enum PointerIconError {
29 /// The bitmap is larger than [`MAX_POINTER_ICON_SIZE`] in one dimension.
30 #[error(
31 "pointer icon is {width}x{height}, larger than the {MAX_POINTER_ICON_SIZE}px platform limit"
32 )]
33 TooLarge {
34 /// The rejected bitmap's width in pixels.
35 width: u32,
36 /// The rejected bitmap's height in pixels.
37 height: u32,
38 },
39 /// The hotspot lies outside the bitmap.
40 #[error("pointer icon hotspot ({x},{y}) lies outside its {width}x{height} bitmap")]
41 HotspotOutsideBitmap {
42 /// The rejected hotspot's x coordinate.
43 x: u32,
44 /// The rejected hotspot's y coordinate.
45 y: u32,
46 /// The bitmap's width in pixels.
47 width: u32,
48 /// The bitmap's height in pixels.
49 height: u32,
50 },
51}
52
53/// An application-drawn pointer shape: RGBA pixels plus the hotspot, the pixel
54/// inside the image that sits exactly on the pointer's position.
55///
56/// The alpha channel is **not** premultiplied, which is what both winit and the
57/// browser expect of cursor images.
58#[derive(Clone, Debug, PartialEq, Eq, Hash)]
59pub struct CustomPointerIcon {
60 image: ImageBitmap,
61 hotspot_x: u32,
62 hotspot_y: u32,
63}
64
65impl CustomPointerIcon {
66 /// Builds a custom pointer icon from `image`, with its hotspot at
67 /// (`hotspot_x`, `hotspot_y`) pixels from the image's top-left corner.
68 pub fn new(
69 image: ImageBitmap,
70 hotspot_x: u32,
71 hotspot_y: u32,
72 ) -> Result<Self, PointerIconError> {
73 let (width, height) = (image.width(), image.height());
74 if width > MAX_POINTER_ICON_SIZE || height > MAX_POINTER_ICON_SIZE {
75 return Err(PointerIconError::TooLarge { width, height });
76 }
77 if hotspot_x >= width || hotspot_y >= height {
78 return Err(PointerIconError::HotspotOutsideBitmap {
79 x: hotspot_x,
80 y: hotspot_y,
81 width,
82 height,
83 });
84 }
85 Ok(Self {
86 image,
87 hotspot_x,
88 hotspot_y,
89 })
90 }
91
92 /// The icon's pixels, tightly packed RGBA8 with straight (not
93 /// premultiplied) alpha.
94 pub fn image(&self) -> &ImageBitmap {
95 &self.image
96 }
97
98 /// The hotspot's x offset from the image's left edge, in pixels.
99 pub fn hotspot_x(&self) -> u32 {
100 self.hotspot_x
101 }
102
103 /// The hotspot's y offset from the image's top edge, in pixels.
104 pub fn hotspot_y(&self) -> u32 {
105 self.hotspot_y
106 }
107
108 /// A stable identity derived from the pixels and the hotspot. Platform
109 /// backends key their per-window cursor caches on it so an icon that
110 /// reappears across frames is uploaded to the windowing system once.
111 pub fn id(&self) -> u64 {
112 self.image.id().rotate_left(17) ^ ((self.hotspot_x as u64) << 32 | self.hotspot_y as u64)
113 }
114}
115
116/// The pointer's appearance over a region of the UI.
117#[derive(Clone, Debug, PartialEq, Eq, Hash)]
118pub enum PointerIcon {
119 /// One of the standard shapes the platform already draws, named by the CSS
120 /// cursor vocabulary.
121 System(CursorIcon),
122 /// A shape the application draws itself.
123 Custom(CustomPointerIcon),
124}
125
126impl PointerIcon {
127 /// The platform's default pointer, usually an arrow.
128 pub const DEFAULT: Self = Self::System(CursorIcon::Default);
129
130 /// The hand shown over something that can be clicked.
131 pub const POINTER: Self = Self::System(CursorIcon::Pointer);
132
133 /// The I-beam shown over selectable text.
134 pub const TEXT: Self = Self::System(CursorIcon::Text);
135
136 /// Builds a custom icon from `image` with its hotspot at (`hotspot_x`,
137 /// `hotspot_y`).
138 pub fn custom(
139 image: ImageBitmap,
140 hotspot_x: u32,
141 hotspot_y: u32,
142 ) -> Result<Self, PointerIconError> {
143 CustomPointerIcon::new(image, hotspot_x, hotspot_y).map(Self::Custom)
144 }
145
146 /// The CSS `cursor` keyword for a standard shape, or `None` for a custom
147 /// one, which a browser names with a `url()` instead.
148 pub fn css_keyword(&self) -> Option<&'static str> {
149 match self {
150 Self::System(icon) => Some(icon.name()),
151 Self::Custom(_) => None,
152 }
153 }
154}
155
156impl Default for PointerIcon {
157 fn default() -> Self {
158 Self::DEFAULT
159 }
160}
161
162impl From<CursorIcon> for PointerIcon {
163 fn from(icon: CursorIcon) -> Self {
164 Self::System(icon)
165 }
166}
167
168impl From<CustomPointerIcon> for PointerIcon {
169 fn from(icon: CustomPointerIcon) -> Self {
170 Self::Custom(icon)
171 }
172}
173
174#[cfg(test)]
175#[path = "tests/pointer_icon_tests.rs"]
176mod tests;