150 lines
6.7 KiB
Rust
150 lines
6.7 KiB
Rust
|
|
use cocoa::{
|
||
|
|
appkit::NSImage, base::{id, nil, YES},
|
||
|
|
foundation::{NSDictionary, NSPoint, NSString},
|
||
|
|
};
|
||
|
|
use objc::runtime::Sel;
|
||
|
|
|
||
|
|
use super::IntoOption;
|
||
|
|
use MouseCursor;
|
||
|
|
|
||
|
|
pub enum Cursor {
|
||
|
|
Native(&'static str),
|
||
|
|
Undocumented(&'static str),
|
||
|
|
WebKit(&'static str),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<MouseCursor> for Cursor {
|
||
|
|
fn from(cursor: MouseCursor) -> Self {
|
||
|
|
match cursor {
|
||
|
|
MouseCursor::Arrow | MouseCursor::Default => Cursor::Native("arrowCursor"),
|
||
|
|
MouseCursor::Hand => Cursor::Native("pointingHandCursor"),
|
||
|
|
MouseCursor::Grabbing | MouseCursor::Grab => Cursor::Native("closedHandCursor"),
|
||
|
|
MouseCursor::Text => Cursor::Native("IBeamCursor"),
|
||
|
|
MouseCursor::VerticalText => Cursor::Native("IBeamCursorForVerticalLayout"),
|
||
|
|
MouseCursor::Copy => Cursor::Native("dragCopyCursor"),
|
||
|
|
MouseCursor::Alias => Cursor::Native("dragLinkCursor"),
|
||
|
|
MouseCursor::NotAllowed | MouseCursor::NoDrop => Cursor::Native("operationNotAllowedCursor"),
|
||
|
|
MouseCursor::ContextMenu => Cursor::Native("contextualMenuCursor"),
|
||
|
|
MouseCursor::Crosshair => Cursor::Native("crosshairCursor"),
|
||
|
|
MouseCursor::EResize => Cursor::Native("resizeRightCursor"),
|
||
|
|
MouseCursor::NResize => Cursor::Native("resizeUpCursor"),
|
||
|
|
MouseCursor::WResize => Cursor::Native("resizeLeftCursor"),
|
||
|
|
MouseCursor::SResize => Cursor::Native("resizeDownCursor"),
|
||
|
|
MouseCursor::EwResize | MouseCursor::ColResize => Cursor::Native("resizeLeftRightCursor"),
|
||
|
|
MouseCursor::NsResize | MouseCursor::RowResize => Cursor::Native("resizeUpDownCursor"),
|
||
|
|
|
||
|
|
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
|
||
|
|
MouseCursor::Help => Cursor::Undocumented("_helpCursor"),
|
||
|
|
MouseCursor::ZoomIn => Cursor::Undocumented("_zoomInCursor"),
|
||
|
|
MouseCursor::ZoomOut => Cursor::Undocumented("_zoomOutCursor"),
|
||
|
|
MouseCursor::NeResize => Cursor::Undocumented("_windowResizeNorthEastCursor"),
|
||
|
|
MouseCursor::NwResize => Cursor::Undocumented("_windowResizeNorthWestCursor"),
|
||
|
|
MouseCursor::SeResize => Cursor::Undocumented("_windowResizeSouthEastCursor"),
|
||
|
|
MouseCursor::SwResize => Cursor::Undocumented("_windowResizeSouthWestCursor"),
|
||
|
|
MouseCursor::NeswResize => Cursor::Undocumented("_windowResizeNorthEastSouthWestCursor"),
|
||
|
|
MouseCursor::NwseResize => Cursor::Undocumented("_windowResizeNorthWestSouthEastCursor"),
|
||
|
|
|
||
|
|
// While these are available, the former just loads a white arrow,
|
||
|
|
// and the latter loads an ugly deflated beachball!
|
||
|
|
// MouseCursor::Move => Cursor::Undocumented("_moveCursor"),
|
||
|
|
// MouseCursor::Wait => Cursor::Undocumented("_waitCursor"),
|
||
|
|
|
||
|
|
// An even more undocumented cursor...
|
||
|
|
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
|
||
|
|
// This is the wrong semantics for `Wait`, but it's the same as
|
||
|
|
// what's used in Safari and Chrome.
|
||
|
|
MouseCursor::Wait | MouseCursor::Progress => Cursor::Undocumented("busyButClickableCursor"),
|
||
|
|
|
||
|
|
// For the rest, we can just snatch the cursors from WebKit...
|
||
|
|
// They fit the style of the native cursors, and will seem
|
||
|
|
// completely standard to macOS users.
|
||
|
|
// https://stackoverflow.com/a/21786835/5435443
|
||
|
|
MouseCursor::Move | MouseCursor::AllScroll => Cursor::WebKit("move"),
|
||
|
|
MouseCursor::Cell => Cursor::WebKit("cell"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for Cursor {
|
||
|
|
fn default() -> Self {
|
||
|
|
Cursor::Native("arrowCursor")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Cursor {
|
||
|
|
pub unsafe fn load(&self) -> id {
|
||
|
|
match self {
|
||
|
|
Cursor::Native(cursor_name) => {
|
||
|
|
let sel = Sel::register(cursor_name);
|
||
|
|
msg_send![class!(NSCursor), performSelector:sel]
|
||
|
|
},
|
||
|
|
Cursor::Undocumented(cursor_name) => {
|
||
|
|
let class = class!(NSCursor);
|
||
|
|
let sel = Sel::register(cursor_name);
|
||
|
|
let sel = if msg_send![class, respondsToSelector:sel] {
|
||
|
|
sel
|
||
|
|
} else {
|
||
|
|
warn!("Cursor `{}` appears to be invalid", cursor_name);
|
||
|
|
sel!(arrowCursor)
|
||
|
|
};
|
||
|
|
msg_send![class, performSelector:sel]
|
||
|
|
},
|
||
|
|
Cursor::WebKit(cursor_name) => load_webkit_cursor(cursor_name)
|
||
|
|
.unwrap_or_else(|message| {
|
||
|
|
warn!("{}", message);
|
||
|
|
Self::default().load()
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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(cursor_name_str: &str) -> Result<id, String> {
|
||
|
|
static CURSOR_ROOT: &'static str = "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors";
|
||
|
|
let cursor_root = NSString::alloc(nil).init_str(CURSOR_ROOT);
|
||
|
|
let cursor_name = NSString::alloc(nil).init_str(cursor_name_str);
|
||
|
|
let cursor_pdf = NSString::alloc(nil).init_str("cursor.pdf");
|
||
|
|
let cursor_plist = NSString::alloc(nil).init_str("info.plist");
|
||
|
|
let key_x = NSString::alloc(nil).init_str("hotx");
|
||
|
|
let key_y = NSString::alloc(nil).init_str("hoty");
|
||
|
|
|
||
|
|
let cursor_path: id = msg_send![cursor_root,
|
||
|
|
stringByAppendingPathComponent:cursor_name
|
||
|
|
];
|
||
|
|
let pdf_path: id = msg_send![cursor_path,
|
||
|
|
stringByAppendingPathComponent:cursor_pdf
|
||
|
|
];
|
||
|
|
let info_path: id = msg_send![cursor_path,
|
||
|
|
stringByAppendingPathComponent:cursor_plist
|
||
|
|
];
|
||
|
|
|
||
|
|
let image = NSImage::alloc(nil)
|
||
|
|
.initByReferencingFile_(pdf_path)
|
||
|
|
// This will probably never be `None`, since images are loaded lazily...
|
||
|
|
.into_option()
|
||
|
|
// because of that, we need to check for validity.
|
||
|
|
.filter(|image| image.isValid() == YES)
|
||
|
|
.ok_or_else(||
|
||
|
|
format!("Failed to read image for `{}` cursor", cursor_name_str)
|
||
|
|
)?;
|
||
|
|
let info = NSDictionary::dictionaryWithContentsOfFile_(nil, info_path)
|
||
|
|
.into_option()
|
||
|
|
.ok_or_else(||
|
||
|
|
format!("Failed to read info for `{}` cursor", cursor_name_str)
|
||
|
|
)?;
|
||
|
|
let x = info.valueForKey_(key_x);
|
||
|
|
let y = info.valueForKey_(key_y);
|
||
|
|
let point = NSPoint::new(
|
||
|
|
msg_send![x, doubleValue],
|
||
|
|
msg_send![y, doubleValue],
|
||
|
|
);
|
||
|
|
let cursor: id = msg_send![class!(NSCursor), alloc];
|
||
|
|
let cursor: id = msg_send![cursor, initWithImage:image hotSpot:point];
|
||
|
|
cursor
|
||
|
|
.into_option()
|
||
|
|
.ok_or_else(||
|
||
|
|
format!("Failed to initialize `{}` cursor", cursor_name_str)
|
||
|
|
)
|
||
|
|
}
|