winit_core/
cursor.rs

1use core::fmt;
2use std::error::Error;
3use std::hash::Hash;
4use std::ops::Deref;
5use std::sync::Arc;
6use std::time::Duration;
7
8#[doc(inline)]
9pub use cursor_icon::CursorIcon;
10
11use crate::as_any::AsAny;
12
13/// The maximum width and height for a cursor when using [`CustomCursorSource::from_rgba`].
14pub const MAX_CURSOR_SIZE: u16 = 2048;
15
16const PIXEL_SIZE: usize = 4;
17
18/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details.
19#[derive(Clone, Debug, Eq, Hash, PartialEq)]
20pub enum Cursor {
21    Icon(CursorIcon),
22    Custom(CustomCursor),
23}
24
25impl Default for Cursor {
26    fn default() -> Self {
27        Self::Icon(CursorIcon::default())
28    }
29}
30
31impl From<CursorIcon> for Cursor {
32    fn from(icon: CursorIcon) -> Self {
33        Self::Icon(icon)
34    }
35}
36
37impl From<CustomCursor> for Cursor {
38    fn from(custom: CustomCursor) -> Self {
39        Self::Custom(custom)
40    }
41}
42
43/// Use a custom image as a cursor (mouse pointer).
44///
45/// Is guaranteed to be cheap to clone.
46///
47/// ## Platform-specific
48///
49/// **Web**: Some browsers have limits on cursor sizes usually at 128x128.
50///
51/// # Example
52///
53/// ```no_run
54/// # use winit_core::event_loop::ActiveEventLoop;
55/// # use winit_core::window::Window;
56/// # fn scope(event_loop: &dyn ActiveEventLoop, window: &dyn Window) {
57/// use winit_core::cursor::CustomCursorSource;
58///
59/// let w = 10;
60/// let h = 10;
61/// let rgba = vec![255; (w * h * 4) as usize];
62///
63/// #[cfg(not(target_family = "wasm"))]
64/// let source = CustomCursorSource::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
65///
66/// #[cfg(target_family = "wasm")]
67/// let source = CustomCursorSource::Url {
68///     url: String::from("http://localhost:3000/cursor.png"),
69///     hotspot_x: 0,
70///     hotspot_y: 0,
71/// };
72///
73/// if let Ok(custom_cursor) = event_loop.create_custom_cursor(source) {
74///     window.set_cursor(custom_cursor.clone().into());
75/// }
76/// # }
77/// ```
78#[derive(Clone, Debug)]
79pub struct CustomCursor(pub Arc<dyn CustomCursorProvider>);
80
81pub trait CustomCursorProvider: AsAny + fmt::Debug + Send + Sync {
82    /// Whether a cursor was backed by animation.
83    fn is_animated(&self) -> bool;
84}
85
86impl PartialEq for CustomCursor {
87    fn eq(&self, other: &Self) -> bool {
88        Arc::ptr_eq(&self.0, &other.0)
89    }
90}
91
92impl Eq for CustomCursor {}
93
94impl Hash for CustomCursor {
95    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
96        Arc::as_ptr(&self.0).hash(state);
97    }
98}
99
100impl Deref for CustomCursor {
101    type Target = dyn CustomCursorProvider;
102
103    fn deref(&self) -> &Self::Target {
104        self.0.deref()
105    }
106}
107
108impl_dyn_casting!(CustomCursorProvider);
109
110/// Source for [`CustomCursor`].
111///
112/// See [`CustomCursor`] for more details.
113#[derive(Debug, Clone, Eq, Hash, PartialEq)]
114pub enum CustomCursorSource {
115    /// Cursor that is backed by RGBA image.
116    ///
117    /// See [CustomCursorSource::from_rgba] for more.
118    ///
119    /// ## Platform-specific
120    ///
121    /// - **iOS / Android / Orbital:** Unsupported
122    Image(CursorImage),
123    /// Animated cursor.
124    ///
125    /// See [CustomCursorSource::from_animation] for more.
126    ///
127    /// ## Platform-specific
128    ///
129    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
130    Animation(CursorAnimation),
131    /// Creates a new cursor from a URL pointing to an image.
132    /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
133    /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
134    ///
135    /// [PNG]: https://en.wikipedia.org/wiki/PNG
136    ///
137    /// ## Platform-specific
138    ///
139    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
140    Url { hotspot_x: u16, hotspot_y: u16, url: String },
141}
142
143impl CustomCursorSource {
144    /// Creates a new cursor from an rgba buffer.
145    ///
146    /// The alpha channel is assumed to be **not** premultiplied.
147    pub fn from_rgba(
148        rgba: Vec<u8>,
149        width: u16,
150        height: u16,
151        hotspot_x: u16,
152        hotspot_y: u16,
153    ) -> Result<Self, BadImage> {
154        CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self::Image)
155    }
156
157    /// Crates a new animated cursor from multiple [`CustomCursor`]s
158    /// Supplied `cursors` can't be empty or other animations.
159    pub fn from_animation(
160        duration: Duration,
161        cursors: Vec<CustomCursor>,
162    ) -> Result<Self, BadAnimation> {
163        CursorAnimation::new(duration, cursors).map(Self::Animation)
164    }
165}
166
167/// An error produced when using [`CustomCursorSource::from_rgba`] with invalid arguments.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170pub enum BadImage {
171    /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
172    /// guarantee that the cursor will work, but should avoid many platform and device specific
173    /// limits.
174    TooLarge { width: u16, height: u16 },
175    /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
176    /// safely interpreted as 32bpp RGBA pixels.
177    ByteCountNotDivisibleBy4 { byte_count: usize },
178    /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
179    /// At least one of your arguments is incorrect.
180    DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 },
181    /// Produced when the hotspot is outside the image bounds
182    HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
183}
184
185impl fmt::Display for BadImage {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            BadImage::TooLarge { width, height } => write!(
189                f,
190                "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \
191                 {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
192            ),
193            BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(
194                f,
195                "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \
196                 it impossible to interpret as 32bpp RGBA pixels.",
197            ),
198            BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => {
199                write!(
200                    f,
201                    "The specified dimensions ({width:?}x{height:?}) don't match the number of \
202                     pixels supplied by the `rgba` argument ({pixel_count:?}). For those \
203                     dimensions, the expected pixel count is {width_x_height:?}.",
204                )
205            },
206            BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!(
207                f,
208                "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \
209                 ({width:?}x{height:?}).",
210            ),
211        }
212    }
213}
214
215impl Error for BadImage {}
216
217/// An error produced when using [`CustomCursorSource::from_animation`] with invalid arguments.
218#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
219#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
220pub enum BadAnimation {
221    /// Produced when no cursors were supplied.
222    Empty,
223    /// Produced when a supplied cursor is an animation.
224    Animation,
225}
226
227impl fmt::Display for BadAnimation {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match self {
230            Self::Empty => write!(f, "No cursors supplied"),
231            Self::Animation => write!(f, "A supplied cursor is an animation"),
232        }
233    }
234}
235
236impl Error for BadAnimation {}
237
238#[derive(Debug, Clone, Eq, Hash, PartialEq)]
239pub struct CursorImage {
240    pub(crate) rgba: Vec<u8>,
241    pub(crate) width: u16,
242    pub(crate) height: u16,
243    pub(crate) hotspot_x: u16,
244    pub(crate) hotspot_y: u16,
245}
246
247impl CursorImage {
248    pub(crate) fn from_rgba(
249        rgba: Vec<u8>,
250        width: u16,
251        height: u16,
252        hotspot_x: u16,
253        hotspot_y: u16,
254    ) -> Result<Self, BadImage> {
255        if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
256            return Err(BadImage::TooLarge { width, height });
257        }
258
259        if rgba.len() % PIXEL_SIZE != 0 {
260            return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() });
261        }
262
263        let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
264        let width_x_height = width as u64 * height as u64;
265        if pixel_count != width_x_height {
266            return Err(BadImage::DimensionsVsPixelCount {
267                width,
268                height,
269                width_x_height,
270                pixel_count,
271            });
272        }
273
274        if hotspot_x >= width || hotspot_y >= height {
275            return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y });
276        }
277
278        Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y })
279    }
280
281    pub fn buffer(&self) -> &[u8] {
282        self.rgba.as_slice()
283    }
284
285    pub fn buffer_mut(&mut self) -> &mut [u8] {
286        self.rgba.as_mut_slice()
287    }
288
289    pub fn width(&self) -> u16 {
290        self.width
291    }
292
293    pub fn height(&self) -> u16 {
294        self.height
295    }
296
297    pub fn hotspot_x(&self) -> u16 {
298        self.hotspot_x
299    }
300
301    pub fn hotspot_y(&self) -> u16 {
302        self.hotspot_y
303    }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Hash)]
307pub struct CursorAnimation {
308    pub(crate) duration: Duration,
309    pub(crate) cursors: Vec<CustomCursor>,
310}
311
312impl CursorAnimation {
313    pub fn new(duration: Duration, cursors: Vec<CustomCursor>) -> Result<Self, BadAnimation> {
314        if cursors.is_empty() {
315            return Err(BadAnimation::Empty);
316        }
317
318        if cursors.iter().any(|cursor| cursor.is_animated()) {
319            return Err(BadAnimation::Animation);
320        }
321
322        Ok(Self { duration, cursors })
323    }
324
325    pub fn duration(&self) -> Duration {
326        self.duration
327    }
328
329    pub fn cursors(&self) -> &[CustomCursor] {
330        self.cursors.as_slice()
331    }
332
333    pub fn into_raw(self) -> (Duration, Vec<CustomCursor>) {
334        (self.duration, self.cursors)
335    }
336}