From 2167a091fad259298da70bfdb274ae2607498dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Wed, 11 Nov 2020 23:04:40 +0100 Subject: [PATCH] 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 --- Cargo.toml | 5 +- src/macos/mod.rs | 2 + src/macos/view.rs | 336 ++++++++++++++++++++++++++++++++++++++++++++ src/macos/window.rs | 219 ++++++++++++++++++++++------- 4 files changed, 511 insertions(+), 51 deletions(-) create mode 100644 src/macos/view.rs diff --git a/Cargo.toml b/Cargo.toml index 27a4447..604d025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ authors = [ "Mirko Covizzi ", "Micah Johnston ", "Billy Messenger ", + "Anton Lazarev ", + "Joakim FrostegÄrd ", ] edition = "2018" @@ -25,5 +27,6 @@ nix = "0.18" winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi"] } [target.'cfg(target_os="macos")'.dependencies] -cocoa = "0.23.0" +cocoa = "0.24.0" objc = "0.2.7" +uuid = { version = "0.8", features = ["v4"] } \ No newline at end of file diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 01e0b8e..c0efe14 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -1,2 +1,4 @@ mod window; +mod view; + pub use window::*; diff --git a/src/macos/view.rs b/src/macos/view.rs new file mode 100644 index 0000000..d14373f --- /dev/null +++ b/src/macos/view.rs @@ -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( + window_options: &WindowOpenOptions, +) -> id { + let class = create_view_class::(); + + 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() -> &'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:: as extern "C" fn(&Object, Sel) -> BOOL + ); + class.add_method( + sel!(isFlipped), + property_yes:: as extern "C" fn(&Object, Sel) -> BOOL + ); + class.add_method( + sel!(preservesContentInLiveResize), + property_no:: as extern "C" fn(&Object, Sel) -> BOOL + ); + class.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse:: as extern "C" fn(&Object, Sel, id) -> BOOL + ); + + class.add_method( + sel!(release), + release:: as extern "C" fn(&Object, Sel) + ); + class.add_method( + sel!(viewWillMoveToWindow:), + view_will_move_to_window:: as extern "C" fn(&Object, Sel, id) + ); + class.add_method( + sel!(updateTrackingAreas:), + update_tracking_areas:: as extern "C" fn(&Object, Sel, id) + ); + + class.add_method( + sel!(mouseMoved:), + mouse_moved:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(mouseDragged:), + mouse_moved:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(rightMouseDragged:), + mouse_moved:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(otherMouseDragged:), + mouse_moved:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_method( + sel!(mouseEntered:), + mouse_entered:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_method( + sel!(mouseExited:), + mouse_exited:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_method( + sel!(mouseDown:), + left_mouse_down:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(mouseUp:), + left_mouse_up:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_method( + sel!(rightMouseDown:), + right_mouse_down:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(rightMouseUp:), + right_mouse_up:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_method( + sel!(otherMouseDown:), + middle_mouse_down:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(otherMouseUp:), + middle_mouse_up:: as extern "C" fn(&Object, Sel, id), + ); + + class.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR_NAME); + + class.register() +} + + +extern "C" fn property_yes( + _this: &Object, + _sel: Sel, +) -> BOOL { + YES +} + + +extern "C" fn property_no( + _this: &Object, + _sel: Sel, +) -> BOOL { + YES +} + + +extern "C" fn accepts_first_mouse( + _this: &Object, + _sel: Sel, + _event: id +) -> BOOL { + YES +} + + +extern "C" fn release(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); + + // 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( + 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( + 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( + 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 = unsafe { + WindowState::from_field(this) + }; + + state.trigger_event(event); +} + + +macro_rules! mouse_simple_extern_fn { + ($fn:ident, $event:expr) => { + extern "C" fn $fn( + this: &Object, + _sel: Sel, + _event: id, + ){ + let state: &mut WindowState = 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); \ No newline at end of file diff --git a/src/macos/window.rs b/src/macos/window.rs index 33815e9..8901260 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -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::sync::Arc; use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivateIgnoringOtherApps, - NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, NSRunningApplication, NSView, - NSWindow, NSWindowStyleMask, + NSApp, NSApplication, NSApplicationActivationPolicyRegular, + NSBackingStoreBuffered, NSWindow, NSWindowStyleMask, }; use cocoa::base::{id, nil, NO}; 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 crate::{ - Event, KeyboardEvent, MouseButton, MouseEvent, ScrollDelta, WindowEvent, WindowHandler, - WindowOpenOptions, WindowScalePolicy, WindowInfo, + Event, Parent, WindowHandler, WindowOpenOptions, WindowScalePolicy, + 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 { - ns_window: id, + /// Only set if we created the parent window, i.e. we are running in + /// parentless mode + ns_window: Option, + /// Our subclassed NSView ns_view: id, } + pub struct WindowHandle; + impl WindowHandle { pub fn app_run_blocking(self) { unsafe { - let app = NSApp(); - app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - app.run(); + // Get reference to already created shared NSApplication object + // and run the main loop + NSApp().run(); } } } @@ -38,62 +57,162 @@ impl Window { B: FnOnce(&mut Window) -> H, B: Send + 'static { + let _pool = unsafe { NSAutoreleasePool::new(nil) }; + + let mut window = match options.parent { + Parent::WithParent(parent) => { + if let RawWindowHandle::MacOS(handle) = parent { + let ns_view = handle.ns_view as *mut objc::runtime::Object; + + unsafe { + let subview = create_view::(&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::(&options) + }; + + 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( + NSPoint::new(0.0, 0.0), + NSSize::new( + window_info.logical_size().width as f64, + window_info.logical_size().height as f64 + ), + ); + + unsafe { + let ns_window = NSWindow::alloc(nil) + .initWithContentRect_styleMask_backing_defer_( + rect, + NSWindowStyleMask::NSTitledWindowMask, + NSBackingStoreBuffered, + NO, + ) + .autorelease(); + ns_window.center(); + + let title = NSString::alloc(nil) + .init_str(&options.title) + .autorelease(); + ns_window.setTitle_(title); + + ns_window.makeKeyAndOrderFront_(nil); + + let subview = create_view::(&options); + + ns_window.setContentView_(subview); + + Window { + ns_window: Some(ns_window), + ns_view: subview, + } + } + }, + }; + + let window_handler = build(&mut window); + + 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 { - let _pool = NSAutoreleasePool::new(nil); - - let scaling = match options.scale { - WindowScalePolicy::SystemScaleFactor => get_scaling().unwrap_or(1.0), - WindowScalePolicy::ScaleFactor(scale) => scale - }; - - let window_info = WindowInfo::from_logical_size(options.size, scaling); - - let rect = NSRect::new( - NSPoint::new(0.0, 0.0), - NSSize::new( - window_info.logical_size().width as f64, - window_info.logical_size().height as f64 - ), + (*window_state_arc.window.ns_view).set_ivar( + WINDOW_STATE_IVAR_NAME, + window_state_pointer ); - - let ns_window = NSWindow::alloc(nil) - .initWithContentRect_styleMask_backing_defer_( - rect, - NSWindowStyleMask::NSTitledWindowMask, - NSBackingStoreBuffered, - NO, - ) - .autorelease(); - ns_window.center(); - ns_window.setTitle_(NSString::alloc(nil).init_str(&options.title)); - ns_window.makeKeyAndOrderFront_(nil); - - let ns_view = NSView::alloc(nil).init(); - ns_window.setContentView_(ns_view); - - let mut window = Window { ns_window, ns_view }; - - let handler = build(&mut window); - - // FIXME: only do this in the unparented case - let current_app = NSRunningApplication::currentApplication(nil); - current_app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps); - - WindowHandle } + + WindowHandle } } + +pub(super) struct WindowState { + window: Window, + window_handler: H, +} + + +impl WindowState { + /// 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 { 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 { - ns_window: self.ns_window as *mut c_void, + ns_window, ns_view: self.ns_view as *mut c_void, ..MacOSHandle::empty() }) } } + fn get_scaling() -> Option { // TODO: find system scaling None