-
Notifications
You must be signed in to change notification settings - Fork 925
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement custom cursor images for all desktop platforms
- Loading branch information
1 parent
7bed5ee
commit ef518c9
Showing
35 changed files
with
1,025 additions
and
66 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
#![allow(clippy::single_match, clippy::disallowed_methods)] | ||
|
||
#[cfg(not(wasm_platform))] | ||
use simple_logger::SimpleLogger; | ||
use winit::{ | ||
cursor::CustomCursor, | ||
event::{ElementState, Event, KeyEvent, WindowEvent}, | ||
event_loop::EventLoop, | ||
keyboard::{KeyCode, PhysicalKey}, | ||
window::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 u32, h as u32); | ||
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, | ||
physical_key: PhysicalKey::Code(code), | ||
.. | ||
}, | ||
.. | ||
} => match code { | ||
KeyCode::KeyA => { | ||
log::debug!("Setting cursor to {:?}", cursor_idx); | ||
window.set_custom_cursor(custom_cursors[cursor_idx].clone()); | ||
cursor_idx = (cursor_idx + 1) % 2; | ||
} | ||
KeyCode::KeyS => { | ||
log::debug!("Setting cursor icon to default"); | ||
window.set_cursor_icon(Default::default()); | ||
} | ||
KeyCode::KeyD => { | ||
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,171 @@ | ||
use core::fmt; | ||
use std::{error::Error, sync::Arc}; | ||
|
||
use crate::{icon::PIXEL_SIZE, platform_impl::PlatformCustomCursor}; | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct CustomCursor { | ||
pub(crate) inner: Arc<PlatformCustomCursor>, | ||
} | ||
|
||
impl PartialEq for CustomCursor { | ||
fn eq(&self, other: &Self) -> bool { | ||
Arc::ptr_eq(&self.inner, &other.inner) | ||
} | ||
} | ||
|
||
impl Eq for CustomCursor {} | ||
|
||
impl CustomCursor { | ||
pub fn from_rgba( | ||
rgba: Vec<u8>, | ||
width: u32, | ||
height: u32, | ||
hotspot_x: u32, | ||
hotspot_y: u32, | ||
) -> Result<Self, BadImage> { | ||
Ok(Self { | ||
inner: PlatformCustomCursor::from_rgba(rgba, width, height, hotspot_x, hotspot_y)? | ||
.into(), | ||
}) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub(crate) struct NoCustomCursor; | ||
|
||
#[allow(dead_code)] | ||
impl NoCustomCursor { | ||
pub fn from_image(_image: CursorImage) -> Self { | ||
Self | ||
} | ||
} | ||
|
||
/// Implementation of PlatformCustomCursor for platforms that don't need to work with anything but | ||
/// images. | ||
#[allow(dead_code)] | ||
#[derive(Debug, Clone)] | ||
pub(crate) struct ImageCustomCursor { | ||
pub(crate) image: CursorImage, | ||
} | ||
|
||
#[allow(dead_code)] | ||
impl ImageCustomCursor { | ||
pub fn from_rgba( | ||
rgba: Vec<u8>, | ||
width: u32, | ||
height: u32, | ||
hotspot_x: u32, | ||
hotspot_y: u32, | ||
) -> Result<Self, BadImage> { | ||
Ok(Self { | ||
image: CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?, | ||
}) | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
/// An error produced when using [`Icon::from_rgba`] with invalid arguments. | ||
pub enum BadImage { | ||
/// 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: u32, | ||
height: u32, | ||
width_x_height: usize, | ||
pixel_count: usize, | ||
}, | ||
/// Produced when the hotspot is outside the image bounds | ||
HotspotOutOfBounds { | ||
width: u32, | ||
height: u32, | ||
hotspot_x: u32, | ||
hotspot_y: u32, | ||
}, | ||
} | ||
|
||
impl fmt::Display for BadImage { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
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 { | ||
fn source(&self) -> Option<&(dyn Error + 'static)> { | ||
Some(self) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct CursorImage { | ||
pub(crate) rgba: Vec<u8>, | ||
pub(crate) width: u32, | ||
pub(crate) height: u32, | ||
pub(crate) hotspot_x: u32, | ||
pub(crate) hotspot_y: u32, | ||
} | ||
|
||
impl CursorImage { | ||
pub fn from_rgba( | ||
rgba: Vec<u8>, | ||
width: u32, | ||
height: u32, | ||
hotspot_x: u32, | ||
hotspot_y: u32, | ||
) -> Result<Self, BadImage> { | ||
if rgba.len() % PIXEL_SIZE != 0 { | ||
return Err(BadImage::ByteCountNotDivisibleBy4 { | ||
byte_count: rgba.len(), | ||
}); | ||
} | ||
let pixel_count = rgba.len() / PIXEL_SIZE; | ||
if pixel_count != (width * height) as usize { | ||
return Err(BadImage::DimensionsVsPixelCount { | ||
width, | ||
height, | ||
width_x_height: (width * height) as usize, | ||
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, | ||
}) | ||
} | ||
} |
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
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.