Skip to content
Merged
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
241 changes: 241 additions & 0 deletions src/platform_impl/macos/appkit/cursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use once_cell::sync::Lazy;

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

use super::NSImage;
use crate::window::CursorIcon;

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

unsafe impl ClassType for NSCursor {
type Super = NSObject;
}
);

// SAFETY: NSCursor is immutable, stated here:
// https://developer.apple.com/documentation/appkit/nscursor/1527062-image?language=objc
unsafe impl Send for NSCursor {}
unsafe impl Sync for NSCursor {}

macro_rules! def_cursor {
{$(
$(#[$($m:meta)*])*
pub fn $name:ident();
)*} => {$(
$(#[$($m)*])*
pub fn $name() -> Id<Self, Shared> {
unsafe { msg_send_id![Self::class(), $name] }
}
)*};
}

macro_rules! def_undocumented_cursor {
{$(
$(#[$($m:meta)*])*
pub fn $name:ident();
)*} => {$(
$(#[$($m)*])*
pub fn $name() -> Id<Self, Shared> {
unsafe { Self::from_selector(sel!($name)).unwrap_or_else(|| Default::default()) }
}
)*};
}

extern_methods!(
/// Documented cursors
unsafe impl NSCursor {
def_cursor!(
pub fn arrowCursor();
pub fn pointingHandCursor();
pub fn openHandCursor();
pub fn closedHandCursor();
pub fn IBeamCursor();
pub fn IBeamCursorForVerticalLayout();
pub fn dragCopyCursor();
pub fn dragLinkCursor();
pub fn operationNotAllowedCursor();
pub fn contextualMenuCursor();
pub fn crosshairCursor();
pub fn resizeRightCursor();
pub fn resizeUpCursor();
pub fn resizeLeftCursor();
pub fn resizeDownCursor();
pub fn resizeLeftRightCursor();
pub fn resizeUpDownCursor();
);

// Creating cursors should be thread-safe, though using them for anything probably isn't.
pub fn new(image: &NSImage, hotSpot: NSPoint) -> Id<Self, Shared> {
let this = unsafe { msg_send_id![Self::class(), alloc] };
unsafe { msg_send_id![this, initWithImage: image, hotSpot: hotSpot] }
}

pub fn invisible() -> Id<Self, Shared> {
// 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,
];

static CURSOR: Lazy<Id<NSCursor, Shared>> = Lazy::new(|| {
// TODO: Consider using `dataWithBytesNoCopy:`
let data = NSData::with_bytes(CURSOR_BYTES);
let image = NSImage::new_with_data(&data);
NSCursor::new(&image, NSPoint::new(0.0, 0.0))
});

CURSOR.clone()
}
}

/// Undocumented cursors
unsafe impl NSCursor {
#[sel(respondsToSelector:)]
fn class_responds_to(sel: Sel) -> bool;

unsafe fn from_selector_unchecked(sel: Sel) -> Id<Self, Shared> {
unsafe { msg_send_id![Self::class(), performSelector: sel] }
}

unsafe fn from_selector(sel: Sel) -> Option<Id<Self, Shared>> {
if Self::class_responds_to(sel) {
Some(unsafe { Self::from_selector_unchecked(sel) })
} else {
warn!("Cursor `{:?}` appears to be invalid", sel);
None
}
}

def_undocumented_cursor!(
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
pub fn _helpCursor();
pub fn _zoomInCursor();
pub fn _zoomOutCursor();
pub fn _windowResizeNorthEastCursor();
pub fn _windowResizeNorthWestCursor();
pub fn _windowResizeSouthEastCursor();
pub fn _windowResizeSouthWestCursor();
pub fn _windowResizeNorthEastSouthWestCursor();
pub 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
pub fn busyButClickableCursor();
);
}

/// Webkit cursors
unsafe impl NSCursor {
// Note that loading `busybutclickable` with this code won't animate
// the frames; instead you'll just get them all in a column.
unsafe fn load_webkit_cursor(name: &NSString) -> Id<Self, Shared> {
// 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.join_path(name);

let pdf_path = cursor_path.join_path(ns_string!("cursor.pdf"));
let image = NSImage::new_by_referencing_file(&pdf_path);

// TODO: Handle PLists better
let info_path = cursor_path.join_path(ns_string!("info.plist"));
let info: Id<NSDictionary<NSObject, NSObject>, Shared> = unsafe {
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);
Self::new(&image, hotspot)
}

pub fn moveCursor() -> Id<Self, Shared> {
unsafe { Self::load_webkit_cursor(ns_string!("move")) }
}

pub fn cellCursor() -> Id<Self, Shared> {
unsafe { Self::load_webkit_cursor(ns_string!("cell")) }
}
}
);

impl NSCursor {
pub fn from_icon(icon: CursorIcon) -> Id<Self, Shared> {
match icon {
CursorIcon::Default => Default::default(),
CursorIcon::Arrow => Self::arrowCursor(),
CursorIcon::Hand => Self::pointingHandCursor(),
CursorIcon::Grab => Self::openHandCursor(),
CursorIcon::Grabbing => Self::closedHandCursor(),
CursorIcon::Text => Self::IBeamCursor(),
CursorIcon::VerticalText => Self::IBeamCursorForVerticalLayout(),
CursorIcon::Copy => Self::dragCopyCursor(),
CursorIcon::Alias => Self::dragLinkCursor(),
CursorIcon::NotAllowed | CursorIcon::NoDrop => Self::operationNotAllowedCursor(),
CursorIcon::ContextMenu => Self::contextualMenuCursor(),
CursorIcon::Crosshair => Self::crosshairCursor(),
CursorIcon::EResize => Self::resizeRightCursor(),
CursorIcon::NResize => Self::resizeUpCursor(),
CursorIcon::WResize => Self::resizeLeftCursor(),
CursorIcon::SResize => Self::resizeDownCursor(),
CursorIcon::EwResize | CursorIcon::ColResize => Self::resizeLeftRightCursor(),
CursorIcon::NsResize | CursorIcon::RowResize => Self::resizeUpDownCursor(),
CursorIcon::Help => Self::_helpCursor(),
CursorIcon::ZoomIn => Self::_zoomInCursor(),
CursorIcon::ZoomOut => Self::_zoomOutCursor(),
CursorIcon::NeResize => Self::_windowResizeNorthEastCursor(),
CursorIcon::NwResize => Self::_windowResizeNorthWestCursor(),
CursorIcon::SeResize => Self::_windowResizeSouthEastCursor(),
CursorIcon::SwResize => Self::_windowResizeSouthWestCursor(),
CursorIcon::NeswResize => Self::_windowResizeNorthEastSouthWestCursor(),
CursorIcon::NwseResize => Self::_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 => Self::busyButClickableCursor(),
CursorIcon::Move | CursorIcon::AllScroll => Self::moveCursor(),
CursorIcon::Cell => Self::cellCursor(),
}
}
}

impl DefaultId for NSCursor {
type Ownership = Shared;

fn default_id() -> Id<Self, Shared> {
Self::arrowCursor()
}
}
37 changes: 37 additions & 0 deletions src/platform_impl/macos/appkit/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use objc2::foundation::{NSData, NSObject, NSString};
use objc2::rc::{Id, Shared};
use objc2::{extern_class, extern_methods, msg_send_id, ClassType};

extern_class!(
// TODO: Can this be mutable?
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) struct NSImage;

unsafe impl ClassType for NSImage {
type Super = NSObject;
}
);

// Documented Thread-Unsafe, but:
// > One thread can create an NSImage object, draw to the image buffer,
// > and pass it off to the main thread for drawing. The underlying image
// > cache is shared among all threads.
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-126728>
//
// So really only unsafe to mutate on several threads.
unsafe impl Send for NSImage {}
unsafe impl Sync for NSImage {}

extern_methods!(
unsafe impl NSImage {
pub fn new_by_referencing_file(path: &NSString) -> Id<Self, Shared> {
let this = unsafe { msg_send_id![Self::class(), alloc] };
unsafe { msg_send_id![this, initByReferencingFile: path] }
}

pub fn new_with_data(data: &NSData) -> Id<Self, Shared> {
let this = unsafe { msg_send_id![Self::class(), alloc] };
unsafe { msg_send_id![this, initWithData: data] }
}
}
);
7 changes: 7 additions & 0 deletions src/platform_impl/macos/appkit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
#![deny(unsafe_op_in_unsafe_fn)]
// Objective-C methods have different conventions, and it's much easier to
// understand if we just use the same names
#![allow(non_snake_case)]

mod application;
mod cursor;
mod image;
mod responder;
mod view;
mod window;

pub(crate) use self::application::NSApplication;
pub(crate) use self::cursor::NSCursor;
pub(crate) use self::image::NSImage;
pub(crate) use self::responder::NSResponder;
pub(crate) use self::view::NSView;
pub(crate) use self::window::NSWindow;
20 changes: 17 additions & 3 deletions src/platform_impl/macos/appkit/view.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use objc2::foundation::NSObject;
use objc2::{extern_class, ClassType};
use objc2::foundation::{NSObject, NSRect};
use objc2::{extern_class, extern_methods, ClassType};

use super::NSResponder;
use super::{NSCursor, NSResponder};

extern_class!(
#[derive(Debug, PartialEq, Eq, Hash)]
Expand All @@ -12,3 +12,17 @@ extern_class!(
type Super = NSResponder;
}
);

extern_methods!(
/// Getter methods
unsafe impl NSView {
#[sel(bounds)]
pub fn bounds(&self) -> NSRect;
}

unsafe impl NSView {
#[sel(addCursorRect:cursor:)]
// NSCursor safe to take by shared reference since it is already immutable
pub fn addCursorRect(&self, rect: NSRect, cursor: &NSCursor);
}
);
Loading