1
0
Fork 0

macOS: basic event handling (#52)

* macOS: add basic event handling

* macos: don't store subview pointer in WindowHandle

* macOS: mention inspiration from antonok's vst_window crate, clean up

* Add Anton Lazarev and myself to author list

* macOS: fix event handling issues

- Rename EventDelegate to WindowState
- Make Window.ns_window optional, only set it if parentless
- Put our own NSView subclass in Window.ns_view
- Don't create useless "intermediate" NSView in parentless mode

* macOS: use Arc::from_raw in WindowHandler dealloc fn

* macOS: move subview code own file, handle more mouse events

* macOS: add (non-tested) support for AsIfParented window

* macOS: rename subview module to view

* macOS: rename "mouse_click_extern_fn!" to "mouse_button_extern_fn!"

This avoids confusion with the click event

* macOS: make WindowState Arc wrapping code clearer

* macOS: handle basic key press and release events

* macOS: accept mouseMoved events, don't trigger them on clicks

* macOS: fix cursor movement location conversion

* macOS: add WindowState.trigger_event fn, make fields private

* macOS: in view, set preservesContentInLiveResize to NO

* macOS: add NSTrackingArea, cursor enter/exit events, better window init

* macOS: remove unused WindowState.size field

* macOS: acceptFirstMouse = YES in view

* macOS: rename macro mouse_button_extern_fn to mouse_simple_extern_fn

* macOS: remove key event handling, it will be implemented differently

* macOS: trigger CursorMoved on right and middle mouse drag

* macOS: run NSEvent.setMouseCoalescingEnabled(NO)

* macOS: clean up

* macOS: non-parented mode: don't "activate ignoring other apps"

This is rarely necessary according to
https://developer.apple.com/documentation/appkit/nsapplication/1428468-activate
and I don't see any reason why we would need to do it.

* macOS: call NSApp() before doing more work in non-parented mode

* macOS: don't attempt to declare NSView subclass multiple times

* macOS: add random suffix to name of NSView subclass to prevent issues

* macOS: send tracking area options as a usize (objc UInt)

* macOS: use UUID for class name suffix

* macOS: fix view_will_move_to_window super call

* macOS: drop WindowState when our NSView is released

* macOS: in Window::open, autorelease an NSString that was allocated

* macOS: delete our view class when the view is released

* Upgrade cocoa dependency to version 0.24.0

* macOS: reorder some code in view.rs

* macOS: mark WindowState::from_field as unsafe, update doc comment

* macOS: in HasRawWindowHandle impl, use unwrap_or for ns_window
This commit is contained in:
Joakim Frostegård 2020-11-11 23:04:40 +01:00 committed by GitHub
parent cd5c91f8a8
commit 2167a091fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 511 additions and 51 deletions

View file

@ -7,6 +7,8 @@ authors = [
"Mirko Covizzi <mrkcvzz@gmail.com>", "Mirko Covizzi <mrkcvzz@gmail.com>",
"Micah Johnston <micah@glowcoil.com>", "Micah Johnston <micah@glowcoil.com>",
"Billy Messenger <billydm@protonmail.com>", "Billy Messenger <billydm@protonmail.com>",
"Anton Lazarev <https://antonok.com>",
"Joakim Frostegård <joakim.frostegard@gmail.com>",
] ]
edition = "2018" edition = "2018"
@ -25,5 +27,6 @@ nix = "0.18"
winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi"] } winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi"] }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
cocoa = "0.23.0" cocoa = "0.24.0"
objc = "0.2.7" objc = "0.2.7"
uuid = { version = "0.8", features = ["v4"] }

View file

@ -1,2 +1,4 @@
mod window; mod window;
mod view;
pub use window::*; pub use window::*;

336
src/macos/view.rs Normal file
View file

@ -0,0 +1,336 @@
use std::ffi::c_void;
use std::sync::Arc;
use cocoa::appkit::{NSEvent, NSView};
use cocoa::base::{id, nil, BOOL, YES, NO};
use cocoa::foundation::{NSArray, NSPoint, NSRect, NSSize};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use uuid::Uuid;
use crate::{
Event, MouseButton, MouseEvent, Point, WindowHandler,
WindowOpenOptions
};
use crate::MouseEvent::{ButtonPressed, ButtonReleased};
use super::window::{WindowState, WINDOW_STATE_IVAR_NAME};
pub(super) unsafe fn create_view<H: WindowHandler>(
window_options: &WindowOpenOptions,
) -> id {
let class = create_view_class::<H>();
let view: id = msg_send![class, alloc];
let size = window_options.size;
view.initWithFrame_(NSRect::new(
NSPoint::new(0., 0.),
NSSize::new(size.width, size.height),
));
view
}
unsafe fn create_view_class<H: WindowHandler>() -> &'static Class {
// Use unique class names so that there are no conflicts between different
// instances. The class is deleted when the view is released. Previously,
// the class was stored in a OnceCell after creation. This way, we didn't
// have to recreate it each time a view was opened, but now we don't leave
// any class definitions lying around when the plugin is closed.
let class_name = format!("BaseviewNSView_{}", Uuid::new_v4().to_simple());
let mut class = ClassDecl::new(&class_name, class!(NSView)).unwrap();
class.add_method(
sel!(acceptsFirstResponder),
property_yes::<H> as extern "C" fn(&Object, Sel) -> BOOL
);
class.add_method(
sel!(isFlipped),
property_yes::<H> as extern "C" fn(&Object, Sel) -> BOOL
);
class.add_method(
sel!(preservesContentInLiveResize),
property_no::<H> as extern "C" fn(&Object, Sel) -> BOOL
);
class.add_method(
sel!(acceptsFirstMouse:),
accepts_first_mouse::<H> as extern "C" fn(&Object, Sel, id) -> BOOL
);
class.add_method(
sel!(release),
release::<H> as extern "C" fn(&Object, Sel)
);
class.add_method(
sel!(viewWillMoveToWindow:),
view_will_move_to_window::<H> as extern "C" fn(&Object, Sel, id)
);
class.add_method(
sel!(updateTrackingAreas:),
update_tracking_areas::<H> as extern "C" fn(&Object, Sel, id)
);
class.add_method(
sel!(mouseMoved:),
mouse_moved::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(mouseDragged:),
mouse_moved::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(rightMouseDragged:),
mouse_moved::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(otherMouseDragged:),
mouse_moved::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(mouseEntered:),
mouse_entered::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(mouseExited:),
mouse_exited::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(mouseDown:),
left_mouse_down::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(mouseUp:),
left_mouse_up::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(rightMouseDown:),
right_mouse_down::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(rightMouseUp:),
right_mouse_up::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(otherMouseDown:),
middle_mouse_down::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_method(
sel!(otherMouseUp:),
middle_mouse_up::<H> as extern "C" fn(&Object, Sel, id),
);
class.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR_NAME);
class.register()
}
extern "C" fn property_yes<H: WindowHandler>(
_this: &Object,
_sel: Sel,
) -> BOOL {
YES
}
extern "C" fn property_no<H: WindowHandler>(
_this: &Object,
_sel: Sel,
) -> BOOL {
YES
}
extern "C" fn accepts_first_mouse<H: WindowHandler>(
_this: &Object,
_sel: Sel,
_event: id
) -> BOOL {
YES
}
extern "C" fn release<H: WindowHandler>(this: &Object, _sel: Sel) {
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), release];
}
unsafe {
let retain_count: usize = msg_send![this, retainCount];
if retain_count == 1 {
let state_ptr: *mut c_void = *this.get_ivar(
WINDOW_STATE_IVAR_NAME
);
// Drop WindowState
Arc::from_raw(state_ptr as *mut WindowState<H>);
// Delete class
let class = msg_send![this, class];
::objc::runtime::objc_disposeClassPair(class);
}
}
}
/// Init/reinit tracking area
///
/// Info:
/// https://developer.apple.com/documentation/appkit/nstrackingarea
/// https://developer.apple.com/documentation/appkit/nstrackingarea/options
/// https://developer.apple.com/documentation/appkit/nstrackingareaoptions
unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object){
let options: usize = {
let mouse_entered_and_exited = 0x01;
let tracking_mouse_moved = 0x02;
let tracking_cursor_update = 0x04;
let tracking_active_in_active_app = 0x40;
let tracking_in_visible_rect = 0x200;
let tracking_enabled_during_mouse_drag = 0x400;
mouse_entered_and_exited | tracking_mouse_moved |
tracking_cursor_update | tracking_active_in_active_app |
tracking_in_visible_rect | tracking_enabled_during_mouse_drag
};
let bounds: NSRect = msg_send![this, bounds];
*tracking_area = msg_send![tracking_area,
initWithRect:bounds
options:options
owner:this
userInfo:nil
];
}
extern "C" fn view_will_move_to_window<H: WindowHandler>(
this: &Object,
_self: Sel,
new_window: id
){
unsafe {
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
let tracking_area_count = NSArray::count(tracking_areas);
let _: () = msg_send![class!(NSEvent), setMouseCoalescingEnabled:NO];
if new_window == nil {
if tracking_area_count != 0 {
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
let _: () = msg_send![this, removeTrackingArea:tracking_area];
let _: () = msg_send![tracking_area, release];
}
} else {
if tracking_area_count == 0 {
let class = Class::get("NSTrackingArea").unwrap();
let tracking_area: *mut Object = msg_send![class, alloc];
reinit_tracking_area(this, tracking_area);
let _: () = msg_send![this, addTrackingArea:tracking_area];
}
let _: () = msg_send![new_window, setAcceptsMouseMovedEvents:YES];
let _: () = msg_send![new_window, makeFirstResponder:this];
}
}
unsafe {
let superclass = msg_send![this, superclass];
let () = msg_send![super(this, superclass), viewWillMoveToWindow:new_window];
}
}
extern "C" fn update_tracking_areas<H: WindowHandler>(
this: &Object,
_self: Sel,
_: id
){
unsafe {
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
reinit_tracking_area(this, tracking_area);
}
}
extern "C" fn mouse_moved<H: WindowHandler>(
this: &Object,
_sel: Sel,
event: id
){
let point: NSPoint = unsafe {
let point = NSEvent::locationInWindow(event);
msg_send![this, convertPoint:point fromView:nil]
};
let position = Point {
x: point.x,
y: point.y
};
let event = Event::Mouse(MouseEvent::CursorMoved { position });
let state: &mut WindowState<H> = unsafe {
WindowState::from_field(this)
};
state.trigger_event(event);
}
macro_rules! mouse_simple_extern_fn {
($fn:ident, $event:expr) => {
extern "C" fn $fn<H: WindowHandler>(
this: &Object,
_sel: Sel,
_event: id,
){
let state: &mut WindowState<H> = unsafe {
WindowState::from_field(this)
};
state.trigger_event(Event::Mouse($event));
}
};
}
mouse_simple_extern_fn!(left_mouse_down, ButtonPressed(MouseButton::Left));
mouse_simple_extern_fn!(left_mouse_up, ButtonReleased(MouseButton::Left));
mouse_simple_extern_fn!(right_mouse_down, ButtonPressed(MouseButton::Right));
mouse_simple_extern_fn!(right_mouse_up, ButtonReleased(MouseButton::Right));
mouse_simple_extern_fn!(middle_mouse_down, ButtonPressed(MouseButton::Middle));
mouse_simple_extern_fn!(middle_mouse_up, ButtonReleased(MouseButton::Middle));
mouse_simple_extern_fn!(mouse_entered, MouseEvent::CursorEntered);
mouse_simple_extern_fn!(mouse_exited, MouseEvent::CursorLeft);

View file

@ -1,33 +1,52 @@
/// macOS window handling
///
/// Inspired by implementation in https://github.com/antonok-edm/vst_window
use std::ffi::c_void; use std::ffi::c_void;
use std::sync::Arc;
use cocoa::appkit::{ use cocoa::appkit::{
NSApp, NSApplication, NSApplicationActivateIgnoringOtherApps, NSApp, NSApplication, NSApplicationActivationPolicyRegular,
NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, NSRunningApplication, NSView, NSBackingStoreBuffered, NSWindow, NSWindowStyleMask,
NSWindow, NSWindowStyleMask,
}; };
use cocoa::base::{id, nil, NO}; use cocoa::base::{id, nil, NO};
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString}; use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString};
use objc::{msg_send, runtime::Object, sel, sel_impl};
use raw_window_handle::{macos::MacOSHandle, HasRawWindowHandle, RawWindowHandle}; use raw_window_handle::{macos::MacOSHandle, HasRawWindowHandle, RawWindowHandle};
use crate::{ use crate::{
Event, KeyboardEvent, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, Event, Parent, WindowHandler, WindowOpenOptions, WindowScalePolicy,
WindowOpenOptions, WindowScalePolicy, WindowInfo, WindowInfo
}; };
use super::view::create_view;
/// Name of the field used to store the `WindowState` pointer in the custom
/// view class.
pub(super) const WINDOW_STATE_IVAR_NAME: &str = "WINDOW_STATE_IVAR_NAME";
pub struct Window { pub struct Window {
ns_window: id, /// Only set if we created the parent window, i.e. we are running in
/// parentless mode
ns_window: Option<id>,
/// Our subclassed NSView
ns_view: id, ns_view: id,
} }
pub struct WindowHandle; pub struct WindowHandle;
impl WindowHandle { impl WindowHandle {
pub fn app_run_blocking(self) { pub fn app_run_blocking(self) {
unsafe { unsafe {
let app = NSApp(); // Get reference to already created shared NSApplication object
app.setActivationPolicy_(NSApplicationActivationPolicyRegular); // and run the main loop
app.run(); NSApp().run();
} }
} }
} }
@ -38,15 +57,61 @@ impl Window {
B: FnOnce(&mut Window) -> H, B: FnOnce(&mut Window) -> H,
B: Send + 'static B: Send + 'static
{ {
unsafe { let _pool = unsafe { NSAutoreleasePool::new(nil) };
let _pool = NSAutoreleasePool::new(nil);
let scaling = match options.scale { let mut window = match options.parent {
WindowScalePolicy::SystemScaleFactor => get_scaling().unwrap_or(1.0), Parent::WithParent(parent) => {
WindowScalePolicy::ScaleFactor(scale) => scale if let RawWindowHandle::MacOS(handle) = parent {
let ns_view = handle.ns_view as *mut objc::runtime::Object;
unsafe {
let subview = create_view::<H>(&options);
let _: id = msg_send![ns_view, addSubview: subview];
Window {
ns_window: None,
ns_view: subview,
}
}
} else {
panic!("Not a macOS window");
}
},
Parent::AsIfParented => {
let ns_view = unsafe {
create_view::<H>(&options)
}; };
let window_info = WindowInfo::from_logical_size(options.size, scaling); Window {
ns_window: None,
ns_view,
}
},
Parent::None => {
// It seems prudent to run NSApp() here before doing other
// work. It runs [NSApplication sharedApplication], which is
// what is run at the very start of the Xcode-generated main
// function of a cocoa app according to:
// https://developer.apple.com/documentation/appkit/nsapplication
unsafe {
let app = NSApp();
app.setActivationPolicy_(
NSApplicationActivationPolicyRegular
);
}
let scaling = match options.scale {
WindowScalePolicy::ScaleFactor(scale) => scale,
WindowScalePolicy::SystemScaleFactor => {
get_scaling().unwrap_or(1.0)
},
};
let window_info = WindowInfo::from_logical_size(
options.size,
scaling
);
let rect = NSRect::new( let rect = NSRect::new(
NSPoint::new(0.0, 0.0), NSPoint::new(0.0, 0.0),
@ -56,6 +121,7 @@ impl Window {
), ),
); );
unsafe {
let ns_window = NSWindow::alloc(nil) let ns_window = NSWindow::alloc(nil)
.initWithContentRect_styleMask_backing_defer_( .initWithContentRect_styleMask_backing_defer_(
rect, rect,
@ -65,35 +131,88 @@ impl Window {
) )
.autorelease(); .autorelease();
ns_window.center(); ns_window.center();
ns_window.setTitle_(NSString::alloc(nil).init_str(&options.title));
let title = NSString::alloc(nil)
.init_str(&options.title)
.autorelease();
ns_window.setTitle_(title);
ns_window.makeKeyAndOrderFront_(nil); ns_window.makeKeyAndOrderFront_(nil);
let ns_view = NSView::alloc(nil).init(); let subview = create_view::<H>(&options);
ns_window.setContentView_(ns_view);
let mut window = Window { ns_window, ns_view }; ns_window.setContentView_(subview);
let handler = build(&mut window); Window {
ns_window: Some(ns_window),
ns_view: subview,
}
}
},
};
// FIXME: only do this in the unparented case let window_handler = build(&mut window);
let current_app = NSRunningApplication::currentApplication(nil);
current_app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps); let window_state_arc = Arc::new(WindowState {
window,
window_handler,
});
let window_state_pointer = Arc::into_raw(
window_state_arc.clone()
) as *mut c_void;
unsafe {
(*window_state_arc.window.ns_view).set_ivar(
WINDOW_STATE_IVAR_NAME,
window_state_pointer
);
}
WindowHandle WindowHandle
} }
} }
pub(super) struct WindowState<H: WindowHandler> {
window: Window,
window_handler: H,
} }
impl <H: WindowHandler>WindowState<H> {
/// Returns a mutable reference to a WindowState from an Objective-C field
///
/// Don't use this to create two simulataneous references to a single
/// WindowState. Apparently, macOS blocks for the duration of an event,
/// callback, meaning that this shouldn't be a problem in practice.
pub(super) unsafe fn from_field(obj: &Object) -> &mut Self {
let state_ptr: *mut c_void = *obj.get_ivar(WINDOW_STATE_IVAR_NAME);
&mut *(state_ptr as *mut Self)
}
pub(super) fn trigger_event(&mut self, event: Event){
self.window_handler.on_event(&mut self.window, event);
}
}
unsafe impl HasRawWindowHandle for Window { unsafe impl HasRawWindowHandle for Window {
fn raw_window_handle(&self) -> RawWindowHandle { fn raw_window_handle(&self) -> RawWindowHandle {
let ns_window = self.ns_window.unwrap_or(
::std::ptr::null_mut()
) as *mut c_void;
RawWindowHandle::MacOS(MacOSHandle { RawWindowHandle::MacOS(MacOSHandle {
ns_window: self.ns_window as *mut c_void, ns_window,
ns_view: self.ns_view as *mut c_void, ns_view: self.ns_view as *mut c_void,
..MacOSHandle::empty() ..MacOSHandle::empty()
}) })
} }
} }
fn get_scaling() -> Option<f64> { fn get_scaling() -> Option<f64> {
// TODO: find system scaling // TODO: find system scaling
None None