From 139686ddce870614f12eb3a0ed33a9adfee1e47a Mon Sep 17 00:00:00 2001 From: Francesca Plebani Date: Fri, 28 Dec 2018 15:29:29 -0500 Subject: [PATCH] macOS: Improve `set_cursor` (#740) * Improve set_cursor on macOS * Check for nil --- CHANGELOG.md | 1 + src/platform/macos/util/cursor.rs | 149 ++++++++++++++++++++ src/platform/macos/util/into_option.rs | 14 ++ src/platform/macos/{util.rs => util/mod.rs} | 5 + src/platform/macos/view.rs | 28 +++- src/platform/macos/window.rs | 52 ++----- 6 files changed, 208 insertions(+), 41 deletions(-) create mode 100644 src/platform/macos/util/cursor.rs create mode 100644 src/platform/macos/util/into_option.rs rename src/platform/macos/{util.rs => util/mod.rs} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e261d43..89e3315f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added support for generating dummy `DeviceId`s and `WindowId`s to better support unit testing. - On macOS, fixed unsoundness in drag-and-drop that could result in drops being rejected. - On macOS, implemented `WindowEvent::Refresh`. +- On macOS, all `MouseCursor` variants are now implemented and the cursor will no longer reset after unfocusing. # Version 0.18.0 (2018-11-07) diff --git a/src/platform/macos/util/cursor.rs b/src/platform/macos/util/cursor.rs new file mode 100644 index 00000000..e7815d78 --- /dev/null +++ b/src/platform/macos/util/cursor.rs @@ -0,0 +1,149 @@ +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 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 { + 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) + ) +} diff --git a/src/platform/macos/util/into_option.rs b/src/platform/macos/util/into_option.rs new file mode 100644 index 00000000..4fd32e73 --- /dev/null +++ b/src/platform/macos/util/into_option.rs @@ -0,0 +1,14 @@ +use cocoa::base::{id, nil}; + +pub trait IntoOption: Sized { + fn into_option(self) -> Option; +} + +impl IntoOption for id { + fn into_option(self) -> Option { + match self != nil { + true => Some(self), + false => None, + } + } +} diff --git a/src/platform/macos/util.rs b/src/platform/macos/util/mod.rs similarity index 95% rename from src/platform/macos/util.rs rename to src/platform/macos/util/mod.rs index b080660e..baa0e6e0 100644 --- a/src/platform/macos/util.rs +++ b/src/platform/macos/util/mod.rs @@ -1,3 +1,8 @@ +mod cursor; +mod into_option; + +pub use self::{cursor::Cursor, into_option::IntoOption}; + use cocoa::appkit::NSWindowStyleMask; use cocoa::base::{id, nil}; use cocoa::foundation::{NSRect, NSUInteger}; diff --git a/src/platform/macos/view.rs b/src/platform/macos/view.rs index fc410486..97372d10 100644 --- a/src/platform/macos/view.rs +++ b/src/platform/macos/view.rs @@ -5,7 +5,7 @@ use std::{slice, str}; use std::boxed::Box; use std::collections::VecDeque; use std::os::raw::*; -use std::sync::Weak; +use std::sync::{Arc, Mutex, Weak}; use cocoa::base::{id, nil}; use cocoa::appkit::{NSEvent, NSView, NSWindow}; @@ -22,15 +22,19 @@ use platform::platform::window::{get_window_id, IdRef}; struct ViewState { window: id, shared: Weak, + cursor: Arc>, ime_spot: Option<(f64, f64)>, raw_characters: Option, is_key_down: bool, } -pub fn new_view(window: id, shared: Weak) -> IdRef { +pub fn new_view(window: id, shared: Weak) -> (IdRef, Weak>) { + let cursor = Default::default(); + let cursor_access = Arc::downgrade(&cursor); let state = ViewState { window, shared, + cursor, ime_spot: None, raw_characters: None, is_key_down: false, @@ -39,7 +43,7 @@ pub fn new_view(window: id, shared: Weak) -> IdRef { // This is free'd in `dealloc` let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; let view: id = msg_send![VIEW_CLASS.0, alloc]; - IdRef::new(msg_send![view, initWithWinit:state_ptr]) + (IdRef::new(msg_send![view, initWithWinit:state_ptr]), cursor_access) } } @@ -75,6 +79,10 @@ lazy_static! { sel!(drawRect:), draw_rect as extern fn(&Object, Sel, NSRect), ); + decl.add_method( + sel!(resetCursorRects), + reset_cursor_rects as extern fn(&Object, Sel), + ); decl.add_method(sel!(hasMarkedText), has_marked_text as extern fn(&Object, Sel) -> BOOL); decl.add_method( sel!(markedRange), @@ -179,6 +187,20 @@ extern fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { } } +extern fn reset_cursor_rects(this: &Object, _sel: Sel) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let bounds: NSRect = msg_send![this, bounds]; + let cursor = state.cursor.lock().unwrap().load(); + let _: () = msg_send![this, + addCursorRect:bounds + cursor:cursor + ]; + } +} + extern fn has_marked_text(this: &Object, _sel: Sel) -> BOOL { //println!("hasMarkedText"); unsafe { diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index a3a9db01..4fc27328 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell}; use std::f64; use std::ops::Deref; use std::os::raw::c_void; -use std::sync::Weak; +use std::sync::{Mutex, Weak}; use std::sync::atomic::{Ordering, AtomicBool}; use cocoa::appkit::{ @@ -547,6 +547,7 @@ pub struct Window2 { pub window: IdRef, pub delegate: WindowDelegate, pub input_context: IdRef, + cursor: Weak>, cursor_hidden: AtomicBool, } @@ -714,7 +715,7 @@ impl Window2 { return Err(OsError(format!("Couldn't create NSWindow"))); }, }; - let view = match Window2::create_view(*window, Weak::clone(&shared)) { + let (view, cursor) = match Window2::create_view(*window, Weak::clone(&shared)) { Some(view) => view, None => { let _: () = unsafe { msg_send![autoreleasepool, drain] }; @@ -772,6 +773,7 @@ impl Window2 { window: window, delegate: WindowDelegate::new(delegate_state), input_context, + cursor, cursor_hidden: Default::default(), }; @@ -950,9 +952,9 @@ impl Window2 { } } - fn create_view(window: id, shared: Weak) -> Option { + fn create_view(window: id, shared: Weak) -> Option<(IdRef, Weak>)> { unsafe { - let view = new_view(window, shared); + let (view, cursor) = new_view(window, shared); view.non_nil().map(|view| { view.setWantsBestResolutionOpenGLSurface_(YES); @@ -967,7 +969,7 @@ impl Window2 { window.setContentView_(*view); window.makeFirstResponder_(*view); - view + (view, cursor) }) } } @@ -1074,40 +1076,14 @@ impl Window2 { } pub fn set_cursor(&self, cursor: MouseCursor) { - let cursor_name = match cursor { - MouseCursor::Arrow | MouseCursor::Default => "arrowCursor", - MouseCursor::Hand => "pointingHandCursor", - MouseCursor::Grabbing | MouseCursor::Grab => "closedHandCursor", - MouseCursor::Text => "IBeamCursor", - MouseCursor::VerticalText => "IBeamCursorForVerticalLayout", - MouseCursor::Copy => "dragCopyCursor", - MouseCursor::Alias => "dragLinkCursor", - MouseCursor::NotAllowed | MouseCursor::NoDrop => "operationNotAllowedCursor", - MouseCursor::ContextMenu => "contextualMenuCursor", - MouseCursor::Crosshair => "crosshairCursor", - MouseCursor::EResize => "resizeRightCursor", - MouseCursor::NResize => "resizeUpCursor", - MouseCursor::WResize => "resizeLeftCursor", - MouseCursor::SResize => "resizeDownCursor", - MouseCursor::EwResize | MouseCursor::ColResize => "resizeLeftRightCursor", - MouseCursor::NsResize | MouseCursor::RowResize => "resizeUpDownCursor", - - // TODO: Find appropriate OSX cursors - MouseCursor::NeResize | MouseCursor::NwResize | - MouseCursor::SeResize | MouseCursor::SwResize | - MouseCursor::NwseResize | MouseCursor::NeswResize | - - MouseCursor::Cell | - MouseCursor::Wait | MouseCursor::Progress | MouseCursor::Help | - MouseCursor::Move | MouseCursor::AllScroll | MouseCursor::ZoomIn | - MouseCursor::ZoomOut => "arrowCursor", - }; - let sel = Sel::register(cursor_name); - let cls = class!(NSCursor); + let cursor = util::Cursor::from(cursor); + if let Some(cursor_access) = self.cursor.upgrade() { + *cursor_access.lock().unwrap() = cursor; + } unsafe { - use objc::Message; - let cursor: id = cls.send_message(sel, ()).unwrap(); - let _: () = msg_send![cursor, set]; + let _: () = msg_send![*self.window, + invalidateCursorRectsForView:*self.view + ]; } }