mirror of
https://github.com/italicsjenga/winit-sonoma-fix.git
synced 2025-01-11 21:31:29 +11:00
macOS: Improve set_cursor
(#740)
* Improve set_cursor on macOS * Check for nil
This commit is contained in:
parent
5a0b4dba47
commit
139686ddce
|
@ -18,6 +18,7 @@
|
||||||
- Added support for generating dummy `DeviceId`s and `WindowId`s to better support unit testing.
|
- 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, fixed unsoundness in drag-and-drop that could result in drops being rejected.
|
||||||
- On macOS, implemented `WindowEvent::Refresh`.
|
- 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)
|
# Version 0.18.0 (2018-11-07)
|
||||||
|
|
||||||
|
|
149
src/platform/macos/util/cursor.rs
Normal file
149
src/platform/macos/util/cursor.rs
Normal 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)
|
||||||
|
)
|
||||||
|
}
|
14
src/platform/macos/util/into_option.rs
Normal file
14
src/platform/macos/util/into_option.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
|
mod cursor;
|
||||||
|
mod into_option;
|
||||||
|
|
||||||
|
pub use self::{cursor::Cursor, into_option::IntoOption};
|
||||||
|
|
||||||
use cocoa::appkit::NSWindowStyleMask;
|
use cocoa::appkit::NSWindowStyleMask;
|
||||||
use cocoa::base::{id, nil};
|
use cocoa::base::{id, nil};
|
||||||
use cocoa::foundation::{NSRect, NSUInteger};
|
use cocoa::foundation::{NSRect, NSUInteger};
|
|
@ -5,7 +5,7 @@ use std::{slice, str};
|
||||||
use std::boxed::Box;
|
use std::boxed::Box;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::os::raw::*;
|
use std::os::raw::*;
|
||||||
use std::sync::Weak;
|
use std::sync::{Arc, Mutex, Weak};
|
||||||
|
|
||||||
use cocoa::base::{id, nil};
|
use cocoa::base::{id, nil};
|
||||||
use cocoa::appkit::{NSEvent, NSView, NSWindow};
|
use cocoa::appkit::{NSEvent, NSView, NSWindow};
|
||||||
|
@ -22,15 +22,19 @@ use platform::platform::window::{get_window_id, IdRef};
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
window: id,
|
window: id,
|
||||||
shared: Weak<Shared>,
|
shared: Weak<Shared>,
|
||||||
|
cursor: Arc<Mutex<util::Cursor>>,
|
||||||
ime_spot: Option<(f64, f64)>,
|
ime_spot: Option<(f64, f64)>,
|
||||||
raw_characters: Option<String>,
|
raw_characters: Option<String>,
|
||||||
is_key_down: bool,
|
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 {
|
let state = ViewState {
|
||||||
window,
|
window,
|
||||||
shared,
|
shared,
|
||||||
|
cursor,
|
||||||
ime_spot: None,
|
ime_spot: None,
|
||||||
raw_characters: None,
|
raw_characters: None,
|
||||||
is_key_down: false,
|
is_key_down: false,
|
||||||
|
@ -39,7 +43,7 @@ pub fn new_view(window: id, shared: Weak<Shared>) -> IdRef {
|
||||||
// This is free'd in `dealloc`
|
// This is free'd in `dealloc`
|
||||||
let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void;
|
let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void;
|
||||||
let view: id = msg_send![VIEW_CLASS.0, alloc];
|
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:),
|
sel!(drawRect:),
|
||||||
draw_rect as extern fn(&Object, Sel, NSRect),
|
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!(hasMarkedText), has_marked_text as extern fn(&Object, Sel) -> BOOL);
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
sel!(markedRange),
|
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 {
|
extern fn has_marked_text(this: &Object, _sel: Sel) -> BOOL {
|
||||||
//println!("hasMarkedText");
|
//println!("hasMarkedText");
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell};
|
||||||
use std::f64;
|
use std::f64;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
use std::sync::Weak;
|
use std::sync::{Mutex, Weak};
|
||||||
use std::sync::atomic::{Ordering, AtomicBool};
|
use std::sync::atomic::{Ordering, AtomicBool};
|
||||||
|
|
||||||
use cocoa::appkit::{
|
use cocoa::appkit::{
|
||||||
|
@ -547,6 +547,7 @@ pub struct Window2 {
|
||||||
pub window: IdRef,
|
pub window: IdRef,
|
||||||
pub delegate: WindowDelegate,
|
pub delegate: WindowDelegate,
|
||||||
pub input_context: IdRef,
|
pub input_context: IdRef,
|
||||||
|
cursor: Weak<Mutex<util::Cursor>>,
|
||||||
cursor_hidden: AtomicBool,
|
cursor_hidden: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,7 +715,7 @@ impl Window2 {
|
||||||
return Err(OsError(format!("Couldn't create NSWindow")));
|
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,
|
Some(view) => view,
|
||||||
None => {
|
None => {
|
||||||
let _: () = unsafe { msg_send![autoreleasepool, drain] };
|
let _: () = unsafe { msg_send![autoreleasepool, drain] };
|
||||||
|
@ -772,6 +773,7 @@ impl Window2 {
|
||||||
window: window,
|
window: window,
|
||||||
delegate: WindowDelegate::new(delegate_state),
|
delegate: WindowDelegate::new(delegate_state),
|
||||||
input_context,
|
input_context,
|
||||||
|
cursor,
|
||||||
cursor_hidden: Default::default(),
|
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 {
|
unsafe {
|
||||||
let view = new_view(window, shared);
|
let (view, cursor) = new_view(window, shared);
|
||||||
view.non_nil().map(|view| {
|
view.non_nil().map(|view| {
|
||||||
view.setWantsBestResolutionOpenGLSurface_(YES);
|
view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||||
|
|
||||||
|
@ -967,7 +969,7 @@ impl Window2 {
|
||||||
|
|
||||||
window.setContentView_(*view);
|
window.setContentView_(*view);
|
||||||
window.makeFirstResponder_(*view);
|
window.makeFirstResponder_(*view);
|
||||||
view
|
(view, cursor)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1074,40 +1076,14 @@ impl Window2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cursor(&self, cursor: MouseCursor) {
|
pub fn set_cursor(&self, cursor: MouseCursor) {
|
||||||
let cursor_name = match cursor {
|
let cursor = util::Cursor::from(cursor);
|
||||||
MouseCursor::Arrow | MouseCursor::Default => "arrowCursor",
|
if let Some(cursor_access) = self.cursor.upgrade() {
|
||||||
MouseCursor::Hand => "pointingHandCursor",
|
*cursor_access.lock().unwrap() = cursor;
|
||||||
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);
|
|
||||||
unsafe {
|
unsafe {
|
||||||
use objc::Message;
|
let _: () = msg_send![*self.window,
|
||||||
let cursor: id = cls.send_message(sel, ()).unwrap();
|
invalidateCursorRectsForView:*self.view
|
||||||
let _: () = msg_send![cursor, set];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue