2024-02-28 12:28:26 +01:00
|
|
|
use std::ffi::c_uchar;
|
|
|
|
|
use std::slice;
|
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
use objc2::rc::Retained;
|
2023-12-23 20:58:38 +01:00
|
|
|
use objc2::runtime::Sel;
|
2024-12-08 13:01:57 -08:00
|
|
|
use objc2::{msg_send, msg_send_id, sel, ClassType};
|
2024-04-18 17:34:19 +02:00
|
|
|
use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
|
|
|
|
|
use objc2_foundation::{
|
|
|
|
|
ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize,
|
|
|
|
|
NSString,
|
|
|
|
|
};
|
2023-12-23 16:34:32 +01:00
|
|
|
|
2024-02-03 07:27:17 +04:00
|
|
|
use crate::cursor::{CursorImage, OnlyCursorImageSource};
|
2024-09-06 17:20:11 +03:00
|
|
|
use crate::error::RequestError;
|
2023-12-23 20:58:38 +01:00
|
|
|
use crate::window::CursorIcon;
|
2023-12-23 16:34:32 +01:00
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
2024-05-27 14:49:22 +02:00
|
|
|
pub struct CustomCursor(pub(crate) Retained<NSCursor>);
|
2023-12-23 16:34:32 +01:00
|
|
|
|
2023-12-23 20:58:38 +01:00
|
|
|
// SAFETY: NSCursor is immutable and thread-safe
|
2024-04-18 17:34:19 +02:00
|
|
|
// TODO(madsmtm): Put this logic in objc2-app-kit itself
|
2023-12-23 20:58:38 +01:00
|
|
|
unsafe impl Send for CustomCursor {}
|
|
|
|
|
unsafe impl Sync for CustomCursor {}
|
|
|
|
|
|
2023-12-23 16:34:32 +01:00
|
|
|
impl CustomCursor {
|
2024-09-06 17:20:11 +03:00
|
|
|
pub(crate) fn new(cursor: OnlyCursorImageSource) -> Result<CustomCursor, RequestError> {
|
2024-08-06 18:57:03 +02:00
|
|
|
cursor_from_image(&cursor.0).map(Self)
|
2023-12-23 20:58:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 17:20:11 +03:00
|
|
|
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Result<Retained<NSCursor>, RequestError> {
|
2023-12-23 20:58:38 +01:00
|
|
|
let width = cursor.width;
|
|
|
|
|
let height = cursor.height;
|
|
|
|
|
|
|
|
|
|
let bitmap = unsafe {
|
|
|
|
|
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
|
|
|
|
|
NSBitmapImageRep::alloc(),
|
|
|
|
|
std::ptr::null_mut::<*mut c_uchar>(),
|
|
|
|
|
width as isize,
|
|
|
|
|
height as isize,
|
|
|
|
|
8,
|
|
|
|
|
4,
|
|
|
|
|
true,
|
|
|
|
|
false,
|
|
|
|
|
NSDeviceRGBColorSpace,
|
|
|
|
|
width as isize * 4,
|
|
|
|
|
32,
|
2024-08-06 18:57:03 +02:00
|
|
|
)
|
2024-09-06 17:20:11 +03:00
|
|
|
}.ok_or_else(|| os_error!("parent view should be installed in a window"))?;
|
2023-12-23 20:58:38 +01:00
|
|
|
let bitmap_data = unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) };
|
|
|
|
|
bitmap_data.copy_from_slice(&cursor.rgba);
|
|
|
|
|
|
|
|
|
|
let image = unsafe {
|
|
|
|
|
NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into()))
|
|
|
|
|
};
|
|
|
|
|
unsafe { image.addRepresentation(&bitmap) };
|
|
|
|
|
|
|
|
|
|
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
|
|
|
|
|
|
2024-08-06 18:57:03 +02:00
|
|
|
Ok(NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot))
|
2023-12-23 20:58:38 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(crate) fn default_cursor() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
NSCursor::arrowCursor()
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
unsafe fn try_cursor_from_selector(sel: Sel) -> Option<Retained<NSCursor>> {
|
2023-12-23 20:58:38 +01:00
|
|
|
let cls = NSCursor::class();
|
2024-12-08 13:01:57 -08:00
|
|
|
if msg_send![cls, respondsToSelector: sel] {
|
2024-05-27 14:49:22 +02:00
|
|
|
let cursor: Retained<NSCursor> = unsafe { msg_send_id![cls, performSelector: sel] };
|
2023-12-23 20:58:38 +01:00
|
|
|
Some(cursor)
|
|
|
|
|
} else {
|
2024-02-25 19:20:39 -08:00
|
|
|
tracing::warn!("cursor `{sel}` appears to be invalid");
|
2023-12-23 20:58:38 +01:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! def_undocumented_cursor {
|
|
|
|
|
{$(
|
|
|
|
|
$(#[$($m:meta)*])*
|
|
|
|
|
fn $name:ident();
|
|
|
|
|
)*} => {$(
|
|
|
|
|
$(#[$($m)*])*
|
|
|
|
|
#[allow(non_snake_case)]
|
2024-05-27 14:49:22 +02:00
|
|
|
fn $name() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) }
|
|
|
|
|
}
|
|
|
|
|
)*};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def_undocumented_cursor!(
|
|
|
|
|
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
|
|
|
|
|
fn _helpCursor();
|
|
|
|
|
fn _zoomInCursor();
|
|
|
|
|
fn _zoomOutCursor();
|
|
|
|
|
fn _windowResizeNorthEastCursor();
|
|
|
|
|
fn _windowResizeNorthWestCursor();
|
|
|
|
|
fn _windowResizeSouthEastCursor();
|
|
|
|
|
fn _windowResizeSouthWestCursor();
|
|
|
|
|
fn _windowResizeNorthEastSouthWestCursor();
|
|
|
|
|
fn _windowResizeNorthWestSouthEastCursor();
|
|
|
|
|
|
|
|
|
|
// While these two are available, the former just loads a white arrow,
|
|
|
|
|
// and the latter loads an ugly deflated beachball!
|
|
|
|
|
// pub fn _moveCursor();
|
|
|
|
|
// pub fn _waitCursor();
|
|
|
|
|
|
|
|
|
|
// An even more undocumented cursor...
|
|
|
|
|
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
|
|
|
|
|
fn busyButClickableCursor();
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Note that loading `busybutclickable` with this code won't animate
|
|
|
|
|
// the frames; instead you'll just get them all in a column.
|
2024-05-27 14:49:22 +02:00
|
|
|
unsafe fn load_webkit_cursor(name: &NSString) -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
// Snatch a cursor from WebKit; They fit the style of the native
|
|
|
|
|
// cursors, and will seem completely standard to macOS users.
|
|
|
|
|
//
|
|
|
|
|
// https://stackoverflow.com/a/21786835/5435443
|
|
|
|
|
let root = ns_string!(
|
|
|
|
|
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/\
|
|
|
|
|
HIServices.framework/Versions/A/Resources/cursors"
|
|
|
|
|
);
|
|
|
|
|
let cursor_path = root.stringByAppendingPathComponent(name);
|
|
|
|
|
|
|
|
|
|
let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf"));
|
|
|
|
|
let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap();
|
|
|
|
|
|
|
|
|
|
// TODO: Handle PLists better
|
|
|
|
|
let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist"));
|
2024-05-27 14:49:22 +02:00
|
|
|
let info: Retained<NSDictionary<NSObject, NSObject>> = unsafe {
|
2023-12-23 20:58:38 +01:00
|
|
|
msg_send_id![
|
|
|
|
|
<NSDictionary<NSObject, NSObject>>::class(),
|
|
|
|
|
dictionaryWithContentsOfFile: &*info_path,
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
let mut x = 0.0;
|
|
|
|
|
if let Some(n) = info.get(&*ns_string!("hotx")) {
|
|
|
|
|
if n.is_kind_of::<NSNumber>() {
|
|
|
|
|
let ptr: *const NSObject = n;
|
|
|
|
|
let ptr: *const NSNumber = ptr.cast();
|
|
|
|
|
x = unsafe { &*ptr }.as_cgfloat()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut y = 0.0;
|
|
|
|
|
if let Some(n) = info.get(&*ns_string!("hotx")) {
|
|
|
|
|
if n.is_kind_of::<NSNumber>() {
|
|
|
|
|
let ptr: *const NSObject = n;
|
|
|
|
|
let ptr: *const NSNumber = ptr.cast();
|
|
|
|
|
y = unsafe { &*ptr }.as_cgfloat()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let hotspot = NSPoint::new(x, y);
|
|
|
|
|
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
fn webkit_move() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
unsafe { load_webkit_cursor(ns_string!("move")) }
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
fn webkit_cell() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
unsafe { load_webkit_cursor(ns_string!("cell")) }
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(crate) fn invisible_cursor() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
// 16x16 GIF data for invisible cursor
|
|
|
|
|
// You can reproduce this via ImageMagick.
|
|
|
|
|
// $ convert -size 16x16 xc:none cursor.gif
|
|
|
|
|
static CURSOR_BYTES: &[u8] = &[
|
|
|
|
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0e, 0x84, 0x8f, 0xa9, 0xcb, 0xed, 0x0f,
|
|
|
|
|
0xa3, 0x9c, 0xb4, 0xda, 0x8b, 0xb3, 0x3e, 0x05, 0x00, 0x3b,
|
|
|
|
|
];
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
fn new_invisible() -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
// TODO: Consider using `dataWithBytesNoCopy:`
|
|
|
|
|
let data = NSData::with_bytes(CURSOR_BYTES);
|
|
|
|
|
let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap();
|
|
|
|
|
let hotspot = NSPoint::new(0.0, 0.0);
|
2024-02-28 12:28:26 +01:00
|
|
|
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache this for efficiency
|
|
|
|
|
static CURSOR: OnceLock<CustomCursor> = OnceLock::new();
|
|
|
|
|
CURSOR.get_or_init(|| CustomCursor(new_invisible())).0.clone()
|
2023-12-23 20:58:38 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-27 14:49:22 +02:00
|
|
|
pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained<NSCursor> {
|
2023-12-23 20:58:38 +01:00
|
|
|
match icon {
|
|
|
|
|
CursorIcon::Default => default_cursor(),
|
|
|
|
|
CursorIcon::Pointer => NSCursor::pointingHandCursor(),
|
|
|
|
|
CursorIcon::Grab => NSCursor::openHandCursor(),
|
|
|
|
|
CursorIcon::Grabbing => NSCursor::closedHandCursor(),
|
|
|
|
|
CursorIcon::Text => NSCursor::IBeamCursor(),
|
|
|
|
|
CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(),
|
|
|
|
|
CursorIcon::Copy => NSCursor::dragCopyCursor(),
|
|
|
|
|
CursorIcon::Alias => NSCursor::dragLinkCursor(),
|
|
|
|
|
CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(),
|
|
|
|
|
CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(),
|
|
|
|
|
CursorIcon::Crosshair => NSCursor::crosshairCursor(),
|
|
|
|
|
CursorIcon::EResize => NSCursor::resizeRightCursor(),
|
|
|
|
|
CursorIcon::NResize => NSCursor::resizeUpCursor(),
|
|
|
|
|
CursorIcon::WResize => NSCursor::resizeLeftCursor(),
|
|
|
|
|
CursorIcon::SResize => NSCursor::resizeDownCursor(),
|
|
|
|
|
CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(),
|
|
|
|
|
CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(),
|
|
|
|
|
CursorIcon::Help => _helpCursor(),
|
|
|
|
|
CursorIcon::ZoomIn => _zoomInCursor(),
|
|
|
|
|
CursorIcon::ZoomOut => _zoomOutCursor(),
|
|
|
|
|
CursorIcon::NeResize => _windowResizeNorthEastCursor(),
|
|
|
|
|
CursorIcon::NwResize => _windowResizeNorthWestCursor(),
|
|
|
|
|
CursorIcon::SeResize => _windowResizeSouthEastCursor(),
|
|
|
|
|
CursorIcon::SwResize => _windowResizeSouthWestCursor(),
|
|
|
|
|
CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(),
|
|
|
|
|
CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(),
|
|
|
|
|
// This is the wrong semantics for `Wait`, but it's the same as
|
|
|
|
|
// what's used in Safari and Chrome.
|
|
|
|
|
CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(),
|
|
|
|
|
CursorIcon::Move | CursorIcon::AllScroll => webkit_move(),
|
|
|
|
|
CursorIcon::Cell => webkit_cell(),
|
|
|
|
|
_ => default_cursor(),
|
2023-12-23 16:34:32 +01:00
|
|
|
}
|
|
|
|
|
}
|