1 use cocoa::{
2 appkit::NSImage, base::{id, nil, YES},
3 foundation::{NSDictionary, NSPoint, NSString},
4 };
5 use objc::runtime::Sel;
6
7 use super::IntoOption;
8 use MouseCursor;
9
10 pub enum Cursor {
11 Native(&'static str),
12 Undocumented(&'static str),
13 WebKit(&'static str),
14 }
15
16 impl From<MouseCursor> for Cursor {
from(cursor: MouseCursor) -> Self17 fn from(cursor: MouseCursor) -> Self {
18 match cursor {
19 MouseCursor::Arrow | MouseCursor::Default => Cursor::Native("arrowCursor"),
20 MouseCursor::Hand => Cursor::Native("pointingHandCursor"),
21 MouseCursor::Grabbing | MouseCursor::Grab => Cursor::Native("closedHandCursor"),
22 MouseCursor::Text => Cursor::Native("IBeamCursor"),
23 MouseCursor::VerticalText => Cursor::Native("IBeamCursorForVerticalLayout"),
24 MouseCursor::Copy => Cursor::Native("dragCopyCursor"),
25 MouseCursor::Alias => Cursor::Native("dragLinkCursor"),
26 MouseCursor::NotAllowed | MouseCursor::NoDrop => Cursor::Native("operationNotAllowedCursor"),
27 MouseCursor::ContextMenu => Cursor::Native("contextualMenuCursor"),
28 MouseCursor::Crosshair => Cursor::Native("crosshairCursor"),
29 MouseCursor::EResize => Cursor::Native("resizeRightCursor"),
30 MouseCursor::NResize => Cursor::Native("resizeUpCursor"),
31 MouseCursor::WResize => Cursor::Native("resizeLeftCursor"),
32 MouseCursor::SResize => Cursor::Native("resizeDownCursor"),
33 MouseCursor::EwResize | MouseCursor::ColResize => Cursor::Native("resizeLeftRightCursor"),
34 MouseCursor::NsResize | MouseCursor::RowResize => Cursor::Native("resizeUpDownCursor"),
35
36 // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
37 MouseCursor::Help => Cursor::Undocumented("_helpCursor"),
38 MouseCursor::ZoomIn => Cursor::Undocumented("_zoomInCursor"),
39 MouseCursor::ZoomOut => Cursor::Undocumented("_zoomOutCursor"),
40 MouseCursor::NeResize => Cursor::Undocumented("_windowResizeNorthEastCursor"),
41 MouseCursor::NwResize => Cursor::Undocumented("_windowResizeNorthWestCursor"),
42 MouseCursor::SeResize => Cursor::Undocumented("_windowResizeSouthEastCursor"),
43 MouseCursor::SwResize => Cursor::Undocumented("_windowResizeSouthWestCursor"),
44 MouseCursor::NeswResize => Cursor::Undocumented("_windowResizeNorthEastSouthWestCursor"),
45 MouseCursor::NwseResize => Cursor::Undocumented("_windowResizeNorthWestSouthEastCursor"),
46
47 // While these are available, the former just loads a white arrow,
48 // and the latter loads an ugly deflated beachball!
49 // MouseCursor::Move => Cursor::Undocumented("_moveCursor"),
50 // MouseCursor::Wait => Cursor::Undocumented("_waitCursor"),
51
52 // An even more undocumented cursor...
53 // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
54 // This is the wrong semantics for `Wait`, but it's the same as
55 // what's used in Safari and Chrome.
56 MouseCursor::Wait | MouseCursor::Progress => Cursor::Undocumented("busyButClickableCursor"),
57
58 // For the rest, we can just snatch the cursors from WebKit...
59 // They fit the style of the native cursors, and will seem
60 // completely standard to macOS users.
61 // https://stackoverflow.com/a/21786835/5435443
62 MouseCursor::Move | MouseCursor::AllScroll => Cursor::WebKit("move"),
63 MouseCursor::Cell => Cursor::WebKit("cell"),
64 }
65 }
66 }
67
68 impl Default for Cursor {
default() -> Self69 fn default() -> Self {
70 Cursor::Native("arrowCursor")
71 }
72 }
73
74 impl Cursor {
load(&self) -> id75 pub unsafe fn load(&self) -> id {
76 match self {
77 Cursor::Native(cursor_name) => {
78 let sel = Sel::register(cursor_name);
79 msg_send![class!(NSCursor), performSelector:sel]
80 },
81 Cursor::Undocumented(cursor_name) => {
82 let class = class!(NSCursor);
83 let sel = Sel::register(cursor_name);
84 let sel = if msg_send![class, respondsToSelector:sel] {
85 sel
86 } else {
87 warn!("Cursor `{}` appears to be invalid", cursor_name);
88 sel!(arrowCursor)
89 };
90 msg_send![class, performSelector:sel]
91 },
92 Cursor::WebKit(cursor_name) => load_webkit_cursor(cursor_name)
93 .unwrap_or_else(|message| {
94 warn!("{}", message);
95 Self::default().load()
96 }),
97 }
98 }
99 }
100
101 // Note that loading `busybutclickable` with this code won't animate the frames;
102 // instead you'll just get them all in a column.
load_webkit_cursor(cursor_name_str: &str) -> Result<id, String>103 unsafe fn load_webkit_cursor(cursor_name_str: &str) -> Result<id, String> {
104 static CURSOR_ROOT: &'static str = "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors";
105 let cursor_root = NSString::alloc(nil).init_str(CURSOR_ROOT);
106 let cursor_name = NSString::alloc(nil).init_str(cursor_name_str);
107 let cursor_pdf = NSString::alloc(nil).init_str("cursor.pdf");
108 let cursor_plist = NSString::alloc(nil).init_str("info.plist");
109 let key_x = NSString::alloc(nil).init_str("hotx");
110 let key_y = NSString::alloc(nil).init_str("hoty");
111
112 let cursor_path: id = msg_send![cursor_root,
113 stringByAppendingPathComponent:cursor_name
114 ];
115 let pdf_path: id = msg_send![cursor_path,
116 stringByAppendingPathComponent:cursor_pdf
117 ];
118 let info_path: id = msg_send![cursor_path,
119 stringByAppendingPathComponent:cursor_plist
120 ];
121
122 let image = NSImage::alloc(nil)
123 .initByReferencingFile_(pdf_path)
124 // This will probably never be `None`, since images are loaded lazily...
125 .into_option()
126 // because of that, we need to check for validity.
127 .filter(|image| image.isValid() == YES)
128 .ok_or_else(||
129 format!("Failed to read image for `{}` cursor", cursor_name_str)
130 )?;
131 let info = NSDictionary::dictionaryWithContentsOfFile_(nil, info_path)
132 .into_option()
133 .ok_or_else(||
134 format!("Failed to read info for `{}` cursor", cursor_name_str)
135 )?;
136 let x = info.valueForKey_(key_x);
137 let y = info.valueForKey_(key_y);
138 let point = NSPoint::new(
139 msg_send![x, doubleValue],
140 msg_send![y, doubleValue],
141 );
142 let cursor: id = msg_send![class!(NSCursor), alloc];
143 let cursor: id = msg_send![cursor, initWithImage:image hotSpot:point];
144 cursor
145 .into_option()
146 .ok_or_else(||
147 format!("Failed to initialize `{}` cursor", cursor_name_str)
148 )
149 }
150