Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RGBA cursor icon support on Windows/macOS/Linux #2117

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 90 additions & 39 deletions examples/cursor.rs

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use crate::{
ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW,
},
icon::Icon,
window::{CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes},
window::{CursorGrabMode, CursorIcon, CursorRgba, UserAttentionType, WindowAttributes},
};

pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
Expand Down Expand Up @@ -416,6 +416,11 @@ impl Window {
x11_or_wayland!(match self; Window(w) => w.set_cursor_icon(cursor))
}

#[inline]
pub fn set_cursor_rgba(&self, cursor: CursorRgba) {
x11_or_wayland!(match self; Window(w) => w.set_cursor_rgba(cursor))
}

#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode))
Expand Down
7 changes: 6 additions & 1 deletion src/platform_impl/linux/wayland/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::platform_impl::{
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError,
PlatformSpecificWindowBuilderAttributes as PlatformAttributes,
};
use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes};
use crate::window::{
CursorGrabMode, CursorIcon, CursorRgba, Theme, UserAttentionType, WindowAttributes,
};

use super::env::WindowingFeatures;
use super::event_loop::WinitState;
Expand Down Expand Up @@ -502,6 +504,9 @@ impl Window {
self.send_request(WindowRequest::NewCursorIcon(cursor));
}

#[inline]
pub fn set_cursor_rgba(&self, _cursor: CursorRgba) {}

#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
self.send_request(WindowRequest::ShowCursor(visible));
Expand Down
42 changes: 41 additions & 1 deletion src/platform_impl/linux/x11/util/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::window::CursorIcon;
use std::slice;

use crate::window::{CursorIcon, CursorRgba};

use super::*;

Expand All @@ -14,6 +16,44 @@ impl XConnection {
self.update_cursor(window, cursor);
}

pub fn set_cursor_rgba(&self, window: ffi::Window, cursor: CursorRgba) {
// Create new cursor
let new_cursor = unsafe {
let image =
(self.xcursor.XcursorImageCreate)(cursor.width as i32, cursor.height as i32);
if image.is_null() {
panic!("failed to allocate image for cursor");
}

(*image).xhot = cursor.xhot;
(*image).yhot = cursor.yhot;
(*image).delay = 0;

let dst =
slice::from_raw_parts_mut((*image).pixels, (cursor.width * cursor.height) as usize);

dst.copy_from_slice(cursor.data.as_slice());

let cursor = (self.xcursor.XcursorImageLoadCursor)(self.display, image);
(self.xcursor.XcursorImageDestroy)(image);

cursor
};

// Drop old cursor if required, and store the new one
let mut cursor_rgba = self.cursor_rgba.lock().unwrap();
if let Some(old_cursor) = cursor_rgba.take() {
unsafe {
(self.xlib.XFreeCursor)(self.display, old_cursor);
}
}

*cursor_rgba = Some(new_cursor);

// Display
self.update_cursor(window, new_cursor);
}

fn create_empty_cursor(&self) -> ffi::Cursor {
let data = 0;
let pixmap = unsafe {
Expand Down
9 changes: 8 additions & 1 deletion src/platform_impl/linux/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError,
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{CursorGrabMode, CursorIcon, Icon, UserAttentionType, WindowAttributes},
window::{CursorGrabMode, CursorIcon, CursorRgba, Icon, UserAttentionType, WindowAttributes},
};

use super::{
Expand Down Expand Up @@ -1292,6 +1292,13 @@ impl UnownedWindow {
}
}

#[inline]
pub fn set_cursor_rgba(&self, cursor: CursorRgba) {
if *self.cursor_visible.lock().unwrap() {
self.xconn.set_cursor_rgba(self.xwindow, cursor);
}
}

#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap();
Expand Down
8 changes: 8 additions & 0 deletions src/platform_impl/linux/x11/xdisplay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct XConnection {
pub x11_fd: c_int,
pub latest_error: Mutex<Option<XError>>,
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, ffi::Cursor>>,
pub cursor_rgba: Mutex<Option<ffi::Cursor>>,
}

unsafe impl Send for XConnection {}
Expand Down Expand Up @@ -65,6 +66,7 @@ impl XConnection {
x11_fd: fd,
latest_error: Mutex::new(None),
cursor_cache: Default::default(),
cursor_rgba: Mutex::new(None),
})
}

Expand Down Expand Up @@ -95,6 +97,12 @@ impl fmt::Debug for XConnection {
impl Drop for XConnection {
#[inline]
fn drop(&mut self) {
if let Some(cursor_rgba) = self.cursor_rgba.lock().unwrap().take() {
unsafe {
(self.xlib.XFreeCursor)(self.display, cursor_rgba);
}
}

unsafe { (self.xlib.XCloseDisplay)(self.display) };
}
}
Expand Down
47 changes: 47 additions & 0 deletions src/platform_impl/macos/appkit/bitmap_image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use objc2::foundation::{NSInteger, NSString};
use objc2::rc::{Id, Shared};
use objc2::runtime::Bool;
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, ClassType};

use super::NSImageRep;

extern "C" {
static NSDeviceRGBColorSpace: &'static NSString;
}

extern_class!(
/// <https://developer.apple.com/documentation/appkit/nsbitmapimagerep?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSBitmapImageRep;

unsafe impl ClassType for NSBitmapImageRep {
type Super = NSImageRep;
}
);

extern_methods!(
unsafe impl NSBitmapImageRep {
pub fn initAbgr(width: NSInteger, height: NSInteger) -> Id<Self, Shared> {
unsafe {
let this = msg_send_id![Self::class(), alloc];

msg_send_id![this,
initWithBitmapDataPlanes: std::ptr::null_mut() as *mut u8,
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8 as NSInteger,
samplesPerPixel: 4 as NSInteger,
hasAlpha: Bool::new(true),
isPlanar: Bool::new(false),
colorSpaceName: NSDeviceRGBColorSpace,
bytesPerRow: width * 4,
bitsPerPixel: 32 as NSInteger,
]
}
}

pub fn bitmapData(&self) -> *mut u8 {
unsafe { msg_send![self, bitmapData] }
}
}
);
39 changes: 36 additions & 3 deletions src/platform_impl/macos/appkit/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use once_cell::sync::Lazy;

use objc2::foundation::{NSData, NSDictionary, NSNumber, NSObject, NSPoint, NSString};
use objc2::foundation::{NSData, NSDictionary, NSNumber, NSObject, NSPoint, NSSize, NSString};
use objc2::rc::{DefaultId, Id, Shared};
use objc2::runtime::Sel;
use objc2::{extern_class, extern_methods, msg_send_id, ns_string, sel, ClassType};

use super::NSImage;
use crate::window::CursorIcon;
use super::{NSBitmapImageRep, NSImage};
use crate::window::{CursorIcon, CursorRgba};

extern_class!(
/// <https://developer.apple.com/documentation/appkit/nscursor?language=objc>
Expand Down Expand Up @@ -230,6 +230,39 @@ impl NSCursor {
CursorIcon::Cell => Self::cellCursor(),
}
}

pub fn from_rgba_cursor(cursor: CursorRgba) -> Id<Self, Shared> {
let rep = NSBitmapImageRep::initAbgr(cursor.width as isize, cursor.height as isize);
let pixels = rep.bitmapData();

let data: Vec<_> = cursor
.data
.into_iter()
.map(|v| {
let mut out = 0;

out |= v & 0xFF00FF00;
out |= (v >> 16) & 0xFF;
out |= (v & 0xFF) << 16;

out
})
.collect();

unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr() as *const u8,
pixels,
cursor.width as usize * cursor.height as usize * std::mem::size_of::<u32>(),
)
};

let image =
NSImage::init_with_size(&NSSize::new(cursor.width.into(), cursor.height.into()));
image.add_representation(&rep);

Self::new(&image, NSPoint::new(cursor.xhot.into(), cursor.yhot.into()))
}
}

impl DefaultId for NSCursor {
Expand Down
25 changes: 23 additions & 2 deletions src/platform_impl/macos/appkit/image.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use objc2::foundation::{NSData, NSObject, NSString};
use objc2::foundation::{NSData, NSObject, NSSize, NSString};
use objc2::rc::{Id, Shared};
use objc2::{extern_class, extern_methods, msg_send_id, ClassType};
use objc2::{extern_class, extern_methods, msg_send, msg_send_id, ClassType};

use super::NSBitmapImageRep;

extern_class!(
// TODO: Can this be mutable?
Expand Down Expand Up @@ -33,5 +35,24 @@ extern_methods!(
let this = unsafe { msg_send_id![Self::class(), alloc] };
unsafe { msg_send_id![this, initWithData: data] }
}

pub fn init_with_size(size: &NSSize) -> Id<Self, Shared> {
let this = unsafe { msg_send_id![Self::class(), alloc] };
unsafe { msg_send_id![this, initWithSize: size] }
}

pub fn add_representation(&self, representation: &NSBitmapImageRep) {
unsafe { msg_send![self, addRepresentation: representation] }
}
}
);

extern_class!(
/// <https://developer.apple.com/documentation/appkit/nsimagerep?language=objc>
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSImageRep;

unsafe impl ClassType for NSImageRep {
type Super = NSObject;
}
);
4 changes: 3 additions & 1 deletion src/platform_impl/macos/appkit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#![allow(non_upper_case_globals)]

mod application;
mod bitmap_image;
mod button;
mod color;
mod control;
Expand All @@ -32,6 +33,7 @@ pub(crate) use self::application::{
NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions,
NSRequestUserAttentionType,
};
pub(crate) use self::bitmap_image::NSBitmapImageRep;
pub(crate) use self::button::NSButton;
pub(crate) use self::color::NSColor;
pub(crate) use self::control::NSControl;
Expand All @@ -40,7 +42,7 @@ pub(crate) use self::cursor::NSCursor;
pub(crate) use self::event::{
NSEvent, NSEventModifierFlags, NSEventPhase, NSEventSubtype, NSEventType,
};
pub(crate) use self::image::NSImage;
pub(crate) use self::image::{NSImage, NSImageRep};
pub(crate) use self::menu::NSMenu;
pub(crate) use self::menu_item::NSMenuItem;
pub(crate) use self::pasteboard::{NSFilenamesPboardType, NSPasteboard, NSPasteboardType};
Expand Down
16 changes: 13 additions & 3 deletions src/platform_impl/macos/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ use crate::{
Fullscreen, OsError,
},
window::{
CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
CursorGrabMode, CursorIcon, CursorRgba, UserAttentionType, WindowAttributes,
WindowId as RootWindowId,
},
};
use core_graphics::display::{CGDisplay, CGPoint};
Expand Down Expand Up @@ -606,14 +607,23 @@ impl WinitWindow {
self.isResizable()
}

pub fn set_cursor_icon(&self, icon: CursorIcon) {
fn set_cursor_internal(&self, cursor: Id<NSCursor, Shared>) {
let view = self.view();
let mut cursor_state = view.state.cursor_state.lock().unwrap();
cursor_state.cursor = NSCursor::from_icon(icon);
cursor_state.cursor = cursor;
drop(cursor_state);
self.invalidateCursorRectsForView(&view);
}

pub fn set_cursor_icon(&self, cursor: CursorIcon) {
self.set_cursor_internal(NSCursor::from_icon(cursor));
}

#[inline]
pub fn set_cursor_rgba(&self, cursor: CursorRgba) {
self.set_cursor_internal(NSCursor::from_rgba_cursor(cursor));
}

#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let associate_mouse_cursor = match mode {
Expand Down
Loading