-
Notifications
You must be signed in to change notification settings - Fork 932
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(all): Custom cursor images for all desktop platforms
There seems to be many PRs relating to this issue, but they don't include all platforms and for some reason lost steam. This PR again tries to make this feature happen, and does it for all desktop platforms (x11, wayland, macos, windows, web). I think the best user of this feature and the reason I'm doing this is Bevy and game engines in general. There non laggy hardware cursors with custom images are very important. Game devs also like their PNGs so supporting platform native cursor files is not that important, but I guess could be added too. Co-authored-by: daxpedda <[email protected]> Co-authored-by: Mads Marquart <[email protected]> Co-authored-by: Kirill Chibisov <[email protected]>
- Loading branch information
1 parent
7f6b16a
commit af93167
Showing
42 changed files
with
1,243 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
#![allow(clippy::single_match, clippy::disallowed_methods)] | ||
|
||
#[cfg(not(wasm_platform))] | ||
use simple_logger::SimpleLogger; | ||
use winit::{ | ||
event::{ElementState, Event, KeyEvent, WindowEvent}, | ||
event_loop::EventLoop, | ||
keyboard::Key, | ||
window::{CustomCursor, WindowBuilder}, | ||
}; | ||
|
||
fn decode_cursor(bytes: &[u8]) -> CustomCursor { | ||
let img = image::load_from_memory(bytes).unwrap().to_rgba8(); | ||
let samples = img.into_flat_samples(); | ||
let (_, w, h) = samples.extents(); | ||
let (w, h) = (w as u16, h as u16); | ||
CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() | ||
} | ||
|
||
#[cfg(not(wasm_platform))] | ||
#[path = "util/fill.rs"] | ||
mod fill; | ||
|
||
fn main() -> Result<(), impl std::error::Error> { | ||
#[cfg(not(wasm_platform))] | ||
SimpleLogger::new() | ||
.with_level(log::LevelFilter::Info) | ||
.init() | ||
.unwrap(); | ||
#[cfg(wasm_platform)] | ||
console_log::init_with_level(log::Level::Debug).unwrap(); | ||
|
||
let event_loop = EventLoop::new().unwrap(); | ||
let builder = WindowBuilder::new().with_title("A fantastic window!"); | ||
#[cfg(wasm_platform)] | ||
let builder = { | ||
use winit::platform::web::WindowBuilderExtWebSys; | ||
builder.with_append(true) | ||
}; | ||
let window = builder.build(&event_loop).unwrap(); | ||
|
||
let mut cursor_idx = 0; | ||
let mut cursor_visible = true; | ||
|
||
let custom_cursors = [ | ||
decode_cursor(include_bytes!("data/cross.png")), | ||
decode_cursor(include_bytes!("data/cross2.png")), | ||
]; | ||
|
||
event_loop.run(move |event, _elwt| match event { | ||
Event::WindowEvent { event, .. } => match event { | ||
WindowEvent::KeyboardInput { | ||
event: | ||
KeyEvent { | ||
state: ElementState::Pressed, | ||
logical_key: key, | ||
.. | ||
}, | ||
.. | ||
} => match key.as_ref() { | ||
Key::Character("1") => { | ||
log::debug!("Setting cursor to {:?}", cursor_idx); | ||
window.set_custom_cursor(&custom_cursors[cursor_idx]); | ||
cursor_idx = (cursor_idx + 1) % 2; | ||
} | ||
Key::Character("2") => { | ||
log::debug!("Setting cursor icon to default"); | ||
window.set_cursor_icon(Default::default()); | ||
} | ||
Key::Character("3") => { | ||
cursor_visible = !cursor_visible; | ||
log::debug!("Setting cursor visibility to {:?}", cursor_visible); | ||
window.set_cursor_visible(cursor_visible); | ||
} | ||
_ => {} | ||
}, | ||
WindowEvent::RedrawRequested => { | ||
#[cfg(not(wasm_platform))] | ||
fill::fill_window(&window); | ||
} | ||
WindowEvent::CloseRequested => { | ||
#[cfg(not(wasm_platform))] | ||
_elwt.exit(); | ||
} | ||
_ => (), | ||
}, | ||
Event::AboutToWait => { | ||
window.request_redraw(); | ||
} | ||
_ => {} | ||
}) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
use core::fmt; | ||
use std::{error::Error, sync::Arc}; | ||
|
||
use crate::platform_impl::PlatformCustomCursor; | ||
|
||
/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. | ||
pub const MAX_CURSOR_SIZE: u16 = 2048; | ||
|
||
const PIXEL_SIZE: usize = 4; | ||
|
||
/// Use a custom image as a cursor (mouse pointer). | ||
/// | ||
/// ## Platform-specific | ||
/// | ||
/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. | ||
/// | ||
/// # Example | ||
/// | ||
/// ``` | ||
/// use winit::window::CustomCursor; | ||
/// | ||
/// let w = 10; | ||
/// let h = 10; | ||
/// let rgba = vec![255; (w * h * 4) as usize]; | ||
/// let custom_cursor = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap(); | ||
/// | ||
/// #[cfg(target_family = "wasm")] | ||
/// let custom_cursor_url = { | ||
/// use winit::platform::web::CustomCursorExtWebSys; | ||
/// CustomCursor::from_url("http://localhost:3000/cursor.png", 0, 0).unwrap() | ||
/// }; | ||
/// ``` | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct CustomCursor { | ||
pub(crate) inner: Arc<PlatformCustomCursor>, | ||
} | ||
|
||
impl CustomCursor { | ||
/// Creates a new cursor from an rgba buffer. | ||
/// | ||
/// ## Platform-specific | ||
/// | ||
/// - **Web:** Setting cursor could be delayed due to the creation of `Blob` objects, | ||
/// which are async by nature. | ||
pub fn from_rgba( | ||
rgba: impl Into<Vec<u8>>, | ||
width: u16, | ||
height: u16, | ||
hotspot_x: u16, | ||
hotspot_y: u16, | ||
) -> Result<Self, BadImage> { | ||
Ok(Self { | ||
inner: PlatformCustomCursor::from_rgba( | ||
rgba.into(), | ||
width, | ||
height, | ||
hotspot_x, | ||
hotspot_y, | ||
)? | ||
.into(), | ||
}) | ||
} | ||
} | ||
|
||
/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. | ||
#[derive(Debug, Clone)] | ||
pub enum BadImage { | ||
/// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't | ||
/// guarantee that the cursor will work, but should avoid many platform and device specific | ||
/// limits. | ||
TooLarge { width: u16, height: u16 }, | ||
/// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be | ||
/// safely interpreted as 32bpp RGBA pixels. | ||
ByteCountNotDivisibleBy4 { byte_count: usize }, | ||
/// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. | ||
/// At least one of your arguments is incorrect. | ||
DimensionsVsPixelCount { | ||
width: u16, | ||
height: u16, | ||
width_x_height: u64, | ||
pixel_count: u64, | ||
}, | ||
/// Produced when the hotspot is outside the image bounds | ||
HotspotOutOfBounds { | ||
width: u16, | ||
height: u16, | ||
hotspot_x: u16, | ||
hotspot_y: u16, | ||
}, | ||
} | ||
|
||
impl fmt::Display for BadImage { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
BadImage::TooLarge { width, height } => write!(f, | ||
"The specified dimensions ({width:?}x{height:?}) are too large. The maximum is {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.", | ||
), | ||
BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(f, | ||
"The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.", | ||
), | ||
BadImage::DimensionsVsPixelCount { | ||
width, | ||
height, | ||
width_x_height, | ||
pixel_count, | ||
} => write!(f, | ||
"The specified dimensions ({width:?}x{height:?}) don't match the number of pixels supplied by the `rgba` argument ({pixel_count:?}). For those dimensions, the expected pixel count is {width_x_height:?}.", | ||
), | ||
BadImage::HotspotOutOfBounds { | ||
width, | ||
height, | ||
hotspot_x, | ||
hotspot_y, | ||
} => write!(f, | ||
"The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds ({width:?}x{height:?}).", | ||
), | ||
} | ||
} | ||
} | ||
|
||
impl Error for BadImage {} | ||
|
||
/// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct CursorImage { | ||
pub(crate) rgba: Vec<u8>, | ||
pub(crate) width: u16, | ||
pub(crate) height: u16, | ||
pub(crate) hotspot_x: u16, | ||
pub(crate) hotspot_y: u16, | ||
} | ||
|
||
#[allow(dead_code)] | ||
impl CursorImage { | ||
pub fn from_rgba( | ||
rgba: Vec<u8>, | ||
width: u16, | ||
height: u16, | ||
hotspot_x: u16, | ||
hotspot_y: u16, | ||
) -> Result<Self, BadImage> { | ||
if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE { | ||
return Err(BadImage::TooLarge { width, height }); | ||
} | ||
|
||
if rgba.len() % PIXEL_SIZE != 0 { | ||
return Err(BadImage::ByteCountNotDivisibleBy4 { | ||
byte_count: rgba.len(), | ||
}); | ||
} | ||
|
||
let pixel_count = (rgba.len() / PIXEL_SIZE) as u64; | ||
let width_x_height = width as u64 * height as u64; | ||
if pixel_count != width_x_height { | ||
return Err(BadImage::DimensionsVsPixelCount { | ||
width, | ||
height, | ||
width_x_height, | ||
pixel_count, | ||
}); | ||
} | ||
|
||
if hotspot_x >= width || hotspot_y >= height { | ||
return Err(BadImage::HotspotOutOfBounds { | ||
width, | ||
height, | ||
hotspot_x, | ||
hotspot_y, | ||
}); | ||
} | ||
|
||
Ok(CursorImage { | ||
rgba, | ||
width, | ||
height, | ||
hotspot_x, | ||
hotspot_y, | ||
}) | ||
} | ||
} | ||
|
||
// Platforms that don't support cursors will export this as `PlatformCustomCursor`. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub(crate) struct NoCustomCursor; | ||
|
||
#[allow(dead_code)] | ||
impl NoCustomCursor { | ||
pub fn from_rgba( | ||
rgba: Vec<u8>, | ||
width: u16, | ||
height: u16, | ||
hotspot_x: u16, | ||
hotspot_y: u16, | ||
) -> Result<Self, BadImage> { | ||
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; | ||
Ok(Self) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.