1#[doc(inline)]
14pub use cursor_icon::CursorIcon;
15
16use crate::ImageBitmap;
17
18pub const MAX_POINTER_ICON_SIZE: u32 = 128;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28pub enum PointerIconError {
29 #[error(
31 "pointer icon is {width}x{height}, larger than the {MAX_POINTER_ICON_SIZE}px platform limit"
32 )]
33 TooLarge {
34 width: u32,
36 height: u32,
38 },
39 #[error("pointer icon hotspot ({x},{y}) lies outside its {width}x{height} bitmap")]
41 HotspotOutsideBitmap {
42 x: u32,
44 y: u32,
46 width: u32,
48 height: u32,
50 },
51}
52
53#[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 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 pub fn image(&self) -> &ImageBitmap {
95 &self.image
96 }
97
98 pub fn hotspot_x(&self) -> u32 {
100 self.hotspot_x
101 }
102
103 pub fn hotspot_y(&self) -> u32 {
105 self.hotspot_y
106 }
107
108 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
118pub enum PointerIcon {
119 System(CursorIcon),
122 Custom(CustomPointerIcon),
124}
125
126impl PointerIcon {
127 pub const DEFAULT: Self = Self::System(CursorIcon::Default);
129
130 pub const POINTER: Self = Self::System(CursorIcon::Pointer);
132
133 pub const TEXT: Self = Self::System(CursorIcon::Text);
135
136 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 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)]
175mod tests {
176 use super::*;
177
178 fn bitmap(width: u32, height: u32) -> ImageBitmap {
179 ImageBitmap::from_rgba8(width, height, vec![255; (width * height * 4) as usize])
180 .expect("test bitmap")
181 }
182
183 #[test]
184 fn custom_icon_keeps_image_and_hotspot() {
185 let icon = CustomPointerIcon::new(bitmap(8, 8), 3, 5).expect("icon");
186 assert_eq!(icon.image().width(), 8);
187 assert_eq!(icon.hotspot_x(), 3);
188 assert_eq!(icon.hotspot_y(), 5);
189 }
190
191 #[test]
192 fn custom_icon_rejects_oversized_bitmaps() {
193 let oversized = MAX_POINTER_ICON_SIZE + 1;
194 assert_eq!(
195 CustomPointerIcon::new(bitmap(oversized, 1), 0, 0),
196 Err(PointerIconError::TooLarge {
197 width: oversized,
198 height: 1,
199 })
200 );
201 }
202
203 #[test]
204 fn custom_icon_rejects_a_hotspot_outside_the_bitmap() {
205 assert_eq!(
206 CustomPointerIcon::new(bitmap(4, 4), 4, 0),
207 Err(PointerIconError::HotspotOutsideBitmap {
208 x: 4,
209 y: 0,
210 width: 4,
211 height: 4,
212 })
213 );
214 }
215
216 #[test]
217 fn custom_icon_accepts_the_bitmap_size_limit_and_its_last_pixel() {
218 let size = MAX_POINTER_ICON_SIZE;
219 CustomPointerIcon::new(bitmap(size, size), size - 1, size - 1)
220 .expect("the limit itself is allowed");
221 }
222
223 #[test]
224 fn ids_separate_pixels_and_hotspots() {
225 let a = CustomPointerIcon::new(bitmap(8, 8), 0, 0).expect("icon");
226 let same = CustomPointerIcon::new(bitmap(8, 8), 0, 0).expect("icon");
227 let moved_hotspot = CustomPointerIcon::new(bitmap(8, 8), 1, 0).expect("icon");
228 let other_pixels = CustomPointerIcon::new(bitmap(4, 4), 0, 0).expect("icon");
229
230 assert_eq!(a.id(), same.id());
231 assert_ne!(a.id(), moved_hotspot.id());
232 assert_ne!(a.id(), other_pixels.id());
233 }
234
235 #[test]
236 fn system_icons_name_their_css_keyword() {
237 assert_eq!(PointerIcon::DEFAULT.css_keyword(), Some("default"));
238 assert_eq!(PointerIcon::POINTER.css_keyword(), Some("pointer"));
239 assert_eq!(
240 PointerIcon::System(CursorIcon::NotAllowed).css_keyword(),
241 Some("not-allowed")
242 );
243 }
244
245 #[test]
246 fn custom_icons_have_no_css_keyword() {
247 let icon = PointerIcon::custom(bitmap(8, 8), 0, 0).expect("icon");
248 assert_eq!(icon.css_keyword(), None);
249 }
250
251 #[test]
252 fn the_default_icon_is_the_platform_arrow() {
253 assert_eq!(PointerIcon::default(), PointerIcon::DEFAULT);
254 assert_eq!(PointerIcon::from(CursorIcon::Default), PointerIcon::DEFAULT);
255 }
256}