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