winit/
cursor.rs

1use core::fmt;
2use std::error::Error;
3use std::hash::{Hash, Hasher};
4use std::sync::Arc;
5
6use cursor_icon::CursorIcon;
7
8use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorSource};
9
10/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`].
11pub const MAX_CURSOR_SIZE: u16 = 2048;
12
13const PIXEL_SIZE: usize = 4;
14
15/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details.
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
17pub enum Cursor {
18    Icon(CursorIcon),
19    Custom(CustomCursor),
20}
21
22impl Default for Cursor {
23    fn default() -> Self {
24        Self::Icon(CursorIcon::default())
25    }
26}
27
28impl From<CursorIcon> for Cursor {
29    fn from(icon: CursorIcon) -> Self {
30        Self::Icon(icon)
31    }
32}
33
34impl From<CustomCursor> for Cursor {
35    fn from(custom: CustomCursor) -> Self {
36        Self::Custom(custom)
37    }
38}
39
40/// Use a custom image as a cursor (mouse pointer).
41///
42/// Is guaranteed to be cheap to clone.
43///
44/// ## Platform-specific
45///
46/// **Web**: Some browsers have limits on cursor sizes usually at 128x128.
47///
48/// # Example
49///
50/// ```no_run
51/// # use winit::event_loop::ActiveEventLoop;
52/// # use winit::window::Window;
53/// # fn scope(event_loop: &dyn ActiveEventLoop, window: &dyn Window) {
54/// use winit::window::CustomCursor;
55///
56/// let w = 10;
57/// let h = 10;
58/// let rgba = vec![255; (w * h * 4) as usize];
59///
60/// #[cfg(not(target_family = "wasm"))]
61/// let source = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
62///
63/// #[cfg(target_family = "wasm")]
64/// let source = {
65///     use winit::platform::web::CustomCursorExtWeb;
66///     CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0)
67/// };
68///
69/// if let Ok(custom_cursor) = event_loop.create_custom_cursor(source) {
70///     window.set_cursor(custom_cursor.clone().into());
71/// }
72/// # }
73/// ```
74#[derive(Clone, Debug, Eq, Hash, PartialEq)]
75pub struct CustomCursor {
76    /// Platforms should make sure this is cheap to clone.
77    pub(crate) inner: PlatformCustomCursor,
78}
79
80impl CustomCursor {
81    /// Creates a new cursor from an rgba buffer.
82    ///
83    /// The alpha channel is assumed to be **not** premultiplied.
84    pub fn from_rgba(
85        rgba: impl Into<Vec<u8>>,
86        width: u16,
87        height: u16,
88        hotspot_x: u16,
89        hotspot_y: u16,
90    ) -> Result<CustomCursorSource, BadImage> {
91        let _span =
92            tracing::debug_span!("winit::Cursor::from_rgba", width, height, hotspot_x, hotspot_y)
93                .entered();
94
95        Ok(CustomCursorSource {
96            inner: PlatformCustomCursorSource::from_rgba(
97                rgba.into(),
98                width,
99                height,
100                hotspot_x,
101                hotspot_y,
102            )?,
103        })
104    }
105}
106
107/// Source for [`CustomCursor`].
108///
109/// See [`CustomCursor`] for more details.
110#[derive(Debug, Clone, Eq, Hash, PartialEq)]
111pub struct CustomCursorSource {
112    // Some platforms don't support custom cursors.
113    #[allow(dead_code)]
114    pub(crate) inner: PlatformCustomCursorSource,
115}
116
117/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub enum BadImage {
121    /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
122    /// guarantee that the cursor will work, but should avoid many platform and device specific
123    /// limits.
124    TooLarge { width: u16, height: u16 },
125    /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
126    /// safely interpreted as 32bpp RGBA pixels.
127    ByteCountNotDivisibleBy4 { byte_count: usize },
128    /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
129    /// At least one of your arguments is incorrect.
130    DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 },
131    /// Produced when the hotspot is outside the image bounds
132    HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
133}
134
135impl fmt::Display for BadImage {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        match self {
138            BadImage::TooLarge { width, height } => write!(
139                f,
140                "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \
141                 {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
142            ),
143            BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(
144                f,
145                "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \
146                 it impossible to interpret as 32bpp RGBA pixels.",
147            ),
148            BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => {
149                write!(
150                    f,
151                    "The specified dimensions ({width:?}x{height:?}) don't match the number of \
152                     pixels supplied by the `rgba` argument ({pixel_count:?}). For those \
153                     dimensions, the expected pixel count is {width_x_height:?}.",
154                )
155            },
156            BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!(
157                f,
158                "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \
159                 ({width:?}x{height:?}).",
160            ),
161        }
162    }
163}
164
165impl Error for BadImage {}
166
167/// Platforms export this directly as `PlatformCustomCursorSource` if they need to only work with
168/// images.
169#[allow(dead_code)]
170#[derive(Debug, Clone, Eq, Hash, PartialEq)]
171pub(crate) struct OnlyCursorImageSource(pub(crate) CursorImage);
172
173#[allow(dead_code)]
174impl OnlyCursorImageSource {
175    pub(crate) fn from_rgba(
176        rgba: Vec<u8>,
177        width: u16,
178        height: u16,
179        hotspot_x: u16,
180        hotspot_y: u16,
181    ) -> Result<Self, BadImage> {
182        CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self)
183    }
184}
185
186/// Platforms export this directly as `PlatformCustomCursor` if they don't implement caching.
187#[allow(dead_code)]
188#[derive(Debug, Clone)]
189pub(crate) struct OnlyCursorImage(pub(crate) Arc<CursorImage>);
190
191impl Hash for OnlyCursorImage {
192    fn hash<H: Hasher>(&self, state: &mut H) {
193        Arc::as_ptr(&self.0).hash(state);
194    }
195}
196
197impl PartialEq for OnlyCursorImage {
198    fn eq(&self, other: &Self) -> bool {
199        Arc::ptr_eq(&self.0, &other.0)
200    }
201}
202
203impl Eq for OnlyCursorImage {}
204
205#[derive(Debug, Clone, Eq, Hash, PartialEq)]
206#[allow(dead_code)]
207pub(crate) struct CursorImage {
208    pub(crate) rgba: Vec<u8>,
209    pub(crate) width: u16,
210    pub(crate) height: u16,
211    pub(crate) hotspot_x: u16,
212    pub(crate) hotspot_y: u16,
213}
214
215impl CursorImage {
216    pub(crate) fn from_rgba(
217        rgba: Vec<u8>,
218        width: u16,
219        height: u16,
220        hotspot_x: u16,
221        hotspot_y: u16,
222    ) -> Result<Self, BadImage> {
223        if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
224            return Err(BadImage::TooLarge { width, height });
225        }
226
227        if rgba.len() % PIXEL_SIZE != 0 {
228            return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() });
229        }
230
231        let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
232        let width_x_height = width as u64 * height as u64;
233        if pixel_count != width_x_height {
234            return Err(BadImage::DimensionsVsPixelCount {
235                width,
236                height,
237                width_x_height,
238                pixel_count,
239            });
240        }
241
242        if hotspot_x >= width || hotspot_y >= height {
243            return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y });
244        }
245
246        Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y })
247    }
248}
249
250// Platforms that don't support cursors will export this as `PlatformCustomCursor`.
251#[derive(Debug, Clone, Hash, PartialEq, Eq)]
252pub(crate) struct NoCustomCursor;
253
254#[allow(dead_code)]
255impl NoCustomCursor {
256    pub(crate) fn from_rgba(
257        rgba: Vec<u8>,
258        width: u16,
259        height: u16,
260        hotspot_x: u16,
261        hotspot_y: u16,
262    ) -> Result<Self, BadImage> {
263        CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?;
264        Ok(Self)
265    }
266}