Skip to main content

winit_core/
cursor.rs

1use core::fmt;
2use std::any::Any;
3use std::error::Error;
4use std::hash::Hash;
5use std::ops::Deref;
6use std::sync::Arc;
7use std::time::Duration;
8
9#[doc(inline)]
10pub use cursor_icon::CursorIcon;
11
12/// The maximum width and height for a cursor when using [`CustomCursorSource::from_rgba`].
13pub const MAX_CURSOR_SIZE: u16 = 2048;
14
15const PIXEL_SIZE: usize = 4;
16
17/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details.
18#[derive(Clone, Debug, Eq, Hash, PartialEq)]
19#[allow(clippy::exhaustive_enums)]
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: Any + 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)]
114#[non_exhaustive]
115pub enum CustomCursorSource {
116    /// Cursor that is backed by RGBA image.
117    ///
118    /// See [CustomCursorSource::from_rgba] for more.
119    ///
120    /// ## Platform-specific
121    ///
122    /// - **iOS / Android / Orbital:** Unsupported
123    Image(CursorImage),
124    /// Animated cursor.
125    ///
126    /// See [CustomCursorSource::from_animation] for more.
127    ///
128    /// ## Platform-specific
129    ///
130    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
131    Animation(CursorAnimation),
132    /// Creates a new cursor from a URL pointing to an image.
133    /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
134    /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
135    ///
136    /// [PNG]: https://en.wikipedia.org/wiki/PNG
137    ///
138    /// ## Platform-specific
139    ///
140    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
141    Url { hotspot_x: u16, hotspot_y: u16, url: String },
142}
143
144impl CustomCursorSource {
145    /// Creates a new cursor from an rgba buffer.
146    ///
147    /// The alpha channel is assumed to be **not** premultiplied.
148    pub fn from_rgba(
149        rgba: Vec<u8>,
150        width: u16,
151        height: u16,
152        hotspot_x: u16,
153        hotspot_y: u16,
154    ) -> Result<Self, BadImage> {
155        CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self::Image)
156    }
157
158    /// Crates a new animated cursor from multiple [`CustomCursor`]s
159    /// Supplied `cursors` can't be empty or other animations.
160    pub fn from_animation(
161        duration: Duration,
162        cursors: Vec<CustomCursor>,
163    ) -> Result<Self, BadAnimation> {
164        CursorAnimation::new(duration, cursors).map(Self::Animation)
165    }
166}
167
168/// An error produced when using [`CustomCursorSource::from_rgba`] with invalid arguments.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
171#[non_exhaustive]
172pub enum BadImage {
173    /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
174    /// guarantee that the cursor will work, but should avoid many platform and device specific
175    /// limits.
176    TooLarge { width: u16, height: u16 },
177    /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
178    /// safely interpreted as 32bpp RGBA pixels.
179    ByteCountNotDivisibleBy4 { byte_count: usize },
180    /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
181    /// At least one of your arguments is incorrect.
182    DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 },
183    /// Produced when the hotspot is outside the image bounds
184    HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
185}
186
187impl fmt::Display for BadImage {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            BadImage::TooLarge { width, height } => write!(
191                f,
192                "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \
193                 {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
194            ),
195            BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(
196                f,
197                "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \
198                 it impossible to interpret as 32bpp RGBA pixels.",
199            ),
200            BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => {
201                write!(
202                    f,
203                    "The specified dimensions ({width:?}x{height:?}) don't match the number of \
204                     pixels supplied by the `rgba` argument ({pixel_count:?}). For those \
205                     dimensions, the expected pixel count is {width_x_height:?}.",
206                )
207            },
208            BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!(
209                f,
210                "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \
211                 ({width:?}x{height:?}).",
212            ),
213        }
214    }
215}
216
217impl Error for BadImage {}
218
219/// An error produced when using [`CustomCursorSource::from_animation`] with invalid arguments.
220#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
221#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
222#[non_exhaustive]
223pub enum BadAnimation {
224    /// Produced when no cursors were supplied.
225    Empty,
226    /// Produced when a supplied cursor is an animation.
227    Animation,
228}
229
230impl fmt::Display for BadAnimation {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        match self {
233            Self::Empty => write!(f, "No cursors supplied"),
234            Self::Animation => write!(f, "A supplied cursor is an animation"),
235        }
236    }
237}
238
239impl Error for BadAnimation {}
240
241#[derive(Debug, Clone, Eq, Hash, PartialEq)]
242pub struct CursorImage {
243    pub(crate) rgba: Vec<u8>,
244    pub(crate) width: u16,
245    pub(crate) height: u16,
246    pub(crate) hotspot_x: u16,
247    pub(crate) hotspot_y: u16,
248}
249
250impl CursorImage {
251    pub(crate) fn from_rgba(
252        rgba: Vec<u8>,
253        width: u16,
254        height: u16,
255        hotspot_x: u16,
256        hotspot_y: u16,
257    ) -> Result<Self, BadImage> {
258        if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
259            return Err(BadImage::TooLarge { width, height });
260        }
261
262        if rgba.len() % PIXEL_SIZE != 0 {
263            return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() });
264        }
265
266        let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
267        let width_x_height = width as u64 * height as u64;
268        if pixel_count != width_x_height {
269            return Err(BadImage::DimensionsVsPixelCount {
270                width,
271                height,
272                width_x_height,
273                pixel_count,
274            });
275        }
276
277        if hotspot_x >= width || hotspot_y >= height {
278            return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y });
279        }
280
281        Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y })
282    }
283
284    pub fn buffer(&self) -> &[u8] {
285        self.rgba.as_slice()
286    }
287
288    pub fn buffer_mut(&mut self) -> &mut [u8] {
289        self.rgba.as_mut_slice()
290    }
291
292    pub fn width(&self) -> u16 {
293        self.width
294    }
295
296    pub fn height(&self) -> u16 {
297        self.height
298    }
299
300    pub fn hotspot_x(&self) -> u16 {
301        self.hotspot_x
302    }
303
304    pub fn hotspot_y(&self) -> u16 {
305        self.hotspot_y
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Hash)]
310pub struct CursorAnimation {
311    pub(crate) duration: Duration,
312    pub(crate) cursors: Vec<CustomCursor>,
313}
314
315impl CursorAnimation {
316    pub fn new(duration: Duration, cursors: Vec<CustomCursor>) -> Result<Self, BadAnimation> {
317        if cursors.is_empty() {
318            return Err(BadAnimation::Empty);
319        }
320
321        if cursors.iter().any(|cursor| cursor.is_animated()) {
322            return Err(BadAnimation::Animation);
323        }
324
325        Ok(Self { duration, cursors })
326    }
327
328    pub fn duration(&self) -> Duration {
329        self.duration
330    }
331
332    pub fn cursors(&self) -> &[CustomCursor] {
333        self.cursors.as_slice()
334    }
335
336    pub fn into_raw(self) -> (Duration, Vec<CustomCursor>) {
337        (self.duration, self.cursors)
338    }
339}