Skip to content

Commit

Permalink
Implement custom cursor images for all desktop platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
eero-lehtinen committed Nov 17, 2023
1 parent 7bed5ee commit ef518c9
Show file tree
Hide file tree
Showing 35 changed files with 1,025 additions and 66 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optiona
x11-dl = { version = "2.18.5", optional = true }
x11rb = { version = "0.12.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true }
xkbcommon-dl = "0.4.0"
memfd = "0.6.4"

[target.'cfg(target_os = "redox")'.dependencies]
orbclient = { version = "0.3.42", default-features = false }
Expand Down Expand Up @@ -211,7 +212,7 @@ web-time = "0.2"

[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_log = "1"
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d', 'Url', 'ImageData'] }

[workspace]
members = [
Expand Down
93 changes: 93 additions & 0 deletions examples/custom_cursors.rs
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();
}
_ => {}
})
}
Binary file added examples/data/cross.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/data/cross2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 171 additions & 0 deletions src/cursor.rs
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,
})
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ extern crate bitflags;
pub mod dpi;
#[macro_use]
pub mod error;
pub mod cursor;
pub mod event;
pub mod event_loop;
mod icon;
Expand Down
19 changes: 19 additions & 0 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding

use crate::cursor::CustomCursor;
use crate::event::Event;
use crate::event_loop::EventLoop;
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursor;
use crate::window::{Window, WindowBuilder};
use crate::SendSyncWrapper;

Expand Down Expand Up @@ -200,3 +202,20 @@ pub enum PollStrategy {
#[default]
Scheduler,
}

pub trait CustomCursorExtWebSys {
fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self;
}

impl CustomCursorExtWebSys for CustomCursor {
fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self {
Self {
inner: PlatformCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
}
.into(),
}
}
}
4 changes: 4 additions & 0 deletions src/platform_impl/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use android_activity::{
use once_cell::sync::Lazy;

use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error,
event::{self, Force, InnerSizeWriter, StartCause},
Expand Down Expand Up @@ -906,6 +907,8 @@ impl Window {

pub fn set_cursor_icon(&self, _: window::CursorIcon) {}

pub fn set_custom_cursor(&self, _: CustomCursor) {}

pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> {
Err(error::ExternalError::NotSupported(
error::NotSupportedError::new(),
Expand Down Expand Up @@ -1031,6 +1034,7 @@ impl Display for OsError {
}
}

pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
Expand Down
1 change: 1 addition & 0 deletions src/platform_impl/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub(crate) use self::{
};

use self::uikit::UIScreen;
pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor;
pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/ios/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::app_state::EventWrapper;
use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation};
use super::view::{WinitUIWindow, WinitView, WinitViewController};
use crate::{
cursor::CustomCursor,
dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
event::{Event, WindowEvent},
Expand Down Expand Up @@ -177,6 +178,10 @@ impl Inner {
debug!("`Window::set_cursor_icon` ignored on iOS")
}

pub fn set_custom_cursor(&self, _: CustomCursor) {
debug!("`Window::set_custom_cursor` ignored on iOS")
}

pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> {
Err(ExternalError::NotSupported(NotSupportedError::new()))
}
Expand Down
Loading

0 comments on commit ef518c9

Please sign in to comment.