macOS: Improve set_cursor (#740)

* Improve set_cursor on macOS

* Check for nil
This commit is contained in:
Francesca Plebani 2018-12-28 15:29:29 -05:00 committed by GitHub
parent 5a0b4dba47
commit 139686ddce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 41 deletions

View file

@ -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)

View file

@ -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<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)
)
}

View file

@ -0,0 +1,14 @@
use cocoa::base::{id, nil};
pub trait IntoOption: Sized {
fn into_option(self) -> Option<Self>;
}
impl IntoOption for id {
fn into_option(self) -> Option<Self> {
match self != nil {
true => Some(self),
false => None,
}
}
}

View file

@ -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};

View file

@ -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<Shared>,
cursor: Arc<Mutex<util::Cursor>>,
ime_spot: Option<(f64, f64)>,
raw_characters: Option<String>,
is_key_down: bool,
}
pub fn new_view(window: id, shared: Weak<Shared>) -> IdRef {
pub fn new_view(window: id, shared: Weak<Shared>) -> (IdRef, Weak<Mutex<util::Cursor>>) {
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<Shared>) -> 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 {

View file

@ -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<Mutex<util::Cursor>>,
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<Shared>) -> Option<IdRef> {
fn create_view(window: id, shared: Weak<Shared>) -> Option<(IdRef, Weak<Mutex<util::Cursor>>)> {
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
];
}
}