From d67c928120421d54c20962e118b8aace908085a5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 2 Sep 2022 18:46:18 +0200 Subject: [PATCH] Clean up macOS class declaration (#2458) * Begin abstraction over AppKit * Clean up NSApplication delegate declaration * Clean up NSApplication override declaration * Clean up NSWindow delegate declaration * Clean up NSWindow override declaration * Clean up NSView delegate declaration --- src/lib.rs | 2 + src/platform_impl/macos/app.rs | 75 +- src/platform_impl/macos/app_delegate.rs | 135 +- src/platform_impl/macos/app_state.rs | 53 +- src/platform_impl/macos/appkit/application.rs | 14 + src/platform_impl/macos/appkit/mod.rs | 11 + src/platform_impl/macos/appkit/responder.rs | 11 + src/platform_impl/macos/appkit/view.rs | 14 + src/platform_impl/macos/appkit/window.rs | 14 + src/platform_impl/macos/event_loop.rs | 39 +- src/platform_impl/macos/mod.rs | 2 +- src/platform_impl/macos/util/mod.rs | 8 +- src/platform_impl/macos/view.rs | 1173 ++++++++--------- src/platform_impl/macos/window.rs | 57 +- src/platform_impl/macos/window_delegate.rs | 278 ++-- 15 files changed, 873 insertions(+), 1013 deletions(-) create mode 100644 src/platform_impl/macos/appkit/application.rs create mode 100644 src/platform_impl/macos/appkit/mod.rs create mode 100644 src/platform_impl/macos/appkit/responder.rs create mode 100644 src/platform_impl/macos/appkit/view.rs create mode 100644 src/platform_impl/macos/appkit/window.rs diff --git a/src/lib.rs b/src/lib.rs index 4871bb3c..30fea96b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,6 +148,8 @@ extern crate bitflags; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] extern crate objc; +#[cfg(target_os = "macos")] +extern crate objc as objc2; pub mod dpi; #[macro_use] diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index b477fd3a..2b6e1ecf 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -4,53 +4,50 @@ use cocoa::{ appkit::{self, NSEvent}, base::id, }; -use objc::{ - declare::ClassBuilder, - runtime::{Class, Object, Sel}, -}; -use once_cell::sync::Lazy; +use objc2::foundation::NSObject; +use objc2::{declare_class, ClassType}; +use super::appkit::{NSApplication, NSResponder}; use super::{app_state::AppState, event::EventWrapper, util, DEVICE_ID}; use crate::event::{DeviceEvent, ElementState, Event}; -pub struct AppClass(pub *const Class); -unsafe impl Send for AppClass {} -unsafe impl Sync for AppClass {} +declare_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(super) struct WinitApplication {} -pub static APP_CLASS: Lazy = Lazy::new(|| unsafe { - let superclass = class!(NSApplication); - let mut decl = ClassBuilder::new("WinitApp", superclass).unwrap(); + unsafe impl ClassType for WinitApplication { + #[inherits(NSResponder, NSObject)] + type Super = NSApplication; + } - decl.add_method(sel!(sendEvent:), send_event as extern "C" fn(_, _, _)); - - AppClass(decl.register()) -}); - -// Normally, holding Cmd + any key never sends us a `keyUp` event for that key. -// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) -// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) -extern "C" fn send_event(this: &Object, _sel: Sel, event: id) { - unsafe { - // For posterity, there are some undocumented event types - // (https://github.com/servo/cocoa-rs/issues/155) - // but that doesn't really matter here. - let event_type = event.eventType(); - let modifier_flags = event.modifierFlags(); - if event_type == appkit::NSKeyUp - && util::has_flag( - modifier_flags, - appkit::NSEventModifierFlags::NSCommandKeyMask, - ) - { - let key_window: id = msg_send![this, keyWindow]; - let _: () = msg_send![key_window, sendEvent: event]; - } else { - maybe_dispatch_device_event(event); - let superclass = util::superclass(this); - let _: () = msg_send![super(this, superclass), sendEvent: event]; + unsafe impl WinitApplication { + // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. + // Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) + // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) + #[sel(sendEvent:)] + fn send_event(&self, event: id) { + unsafe { + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = event.eventType(); + let modifier_flags = event.modifierFlags(); + if event_type == appkit::NSKeyUp + && util::has_flag( + modifier_flags, + appkit::NSEventModifierFlags::NSCommandKeyMask, + ) + { + let key_window: id = msg_send![self, keyWindow]; + let _: () = msg_send![key_window, sendEvent: event]; + } else { + maybe_dispatch_device_event(event); + let _: () = msg_send![super(self), sendEvent: event]; + } + } } } -} +); unsafe fn maybe_dispatch_device_event(event: id) { let event_type = event.eventType(); diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 4c7adc7c..68132741 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,89 +1,64 @@ -use std::{ - cell::{RefCell, RefMut}, - os::raw::c_void, -}; +use cocoa::appkit::NSApplicationActivationPolicy; +use objc2::foundation::NSObject; +use objc2::rc::{Id, Shared}; +use objc2::runtime::Object; +use objc2::{declare_class, ClassType}; -use cocoa::base::id; -use objc::{ - declare::ClassBuilder, - runtime::{Class, Object, Sel}, -}; -use once_cell::sync::Lazy; +use super::app_state::AppState; -use crate::{platform::macos::ActivationPolicy, platform_impl::platform::app_state::AppState}; +declare_class!( + #[derive(Debug)] + pub(super) struct ApplicationDelegate { + activation_policy: NSApplicationActivationPolicy, + default_menu: bool, + } -static AUX_DELEGATE_STATE_NAME: &str = "auxState"; + unsafe impl ClassType for ApplicationDelegate { + type Super = NSObject; + const NAME: &'static str = "WinitApplicationDelegate"; + } -pub struct AuxDelegateState { - pub activation_policy: ActivationPolicy, - pub default_menu: bool, -} + unsafe impl ApplicationDelegate { + #[sel(initWithActivationPolicy:defaultMenu:)] + fn init( + &mut self, + activation_policy: NSApplicationActivationPolicy, + default_menu: bool, + ) -> Option<&mut Self> { + let this: Option<&mut Self> = unsafe { msg_send![super(self), init] }; + this.map(|this| { + *this.activation_policy = activation_policy; + *this.default_menu = default_menu; + this + }) + } -pub struct AppDelegateClass(pub *const Class); -unsafe impl Send for AppDelegateClass {} -unsafe impl Sync for AppDelegateClass {} + #[sel(applicationDidFinishLaunching:)] + fn did_finish_launching(&self, _sender: *const Object) { + trace_scope!("applicationDidFinishLaunching:"); + AppState::launched(*self.activation_policy, *self.default_menu); + } -pub static APP_DELEGATE_CLASS: Lazy = Lazy::new(|| unsafe { - let superclass = class!(NSResponder); - let mut decl = ClassBuilder::new("WinitAppDelegate", superclass).unwrap(); + #[sel(applicationWillTerminate:)] + fn will_terminate(&self, _sender: *const Object) { + trace_scope!("applicationWillTerminate:"); + // TODO: Notify every window that it will be destroyed, like done in iOS? + AppState::exit(); + } + } +); - decl.add_class_method(sel!(new), new as extern "C" fn(_, _) -> _); - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); - - decl.add_method( - sel!(applicationDidFinishLaunching:), - did_finish_launching as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(applicationWillTerminate:), - will_terminate as extern "C" fn(_, _, _), - ); - - decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); - - AppDelegateClass(decl.register()) -}); - -/// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS -pub unsafe fn get_aux_state_mut(this: &Object) -> RefMut<'_, AuxDelegateState> { - let ptr: *mut c_void = *this.ivar(AUX_DELEGATE_STATE_NAME); - // Watch out that this needs to be the correct type - (*(ptr as *mut RefCell)).borrow_mut() -} - -extern "C" fn new(class: &Class, _: Sel) -> id { - unsafe { - let this: id = msg_send![class, alloc]; - let this: id = msg_send![this, init]; - // TODO: Remove the need for this initialization here - (*this).set_ivar( - AUX_DELEGATE_STATE_NAME, - Box::into_raw(Box::new(RefCell::new(AuxDelegateState { - activation_policy: ActivationPolicy::Regular, - default_menu: true, - }))) as *mut c_void, - ); - this +impl ApplicationDelegate { + pub(super) fn new( + activation_policy: NSApplicationActivationPolicy, + default_menu: bool, + ) -> Id { + unsafe { + msg_send_id![ + msg_send_id![Self::class(), alloc], + initWithActivationPolicy: activation_policy, + defaultMenu: default_menu, + ] + } } } - -extern "C" fn dealloc(this: &Object, _: Sel) { - unsafe { - let state_ptr: *mut c_void = *(this.ivar(AUX_DELEGATE_STATE_NAME)); - // As soon as the box is constructed it is immediately dropped, releasing the underlying - // memory - drop(Box::from_raw(state_ptr as *mut RefCell)); - } -} - -extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) { - trace_scope!("applicationDidFinishLaunching:"); - AppState::launched(this); -} - -extern "C" fn will_terminate(_this: &Object, _: Sel, _: id) { - trace!("Triggered `applicationWillTerminate`"); - // TODO: Notify every window that it will be destroyed, like done in iOS? - AppState::exit(); - trace!("Completed `applicationWillTerminate`"); -} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index d5b2c7b2..e9b9f6e2 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -13,32 +13,26 @@ use std::{ }; use cocoa::{ - appkit::{NSApp, NSApplication, NSWindow}, + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSWindow}, base::{id, nil}, foundation::NSSize, }; -use objc::{ - foundation::is_main_thread, - rc::autoreleasepool, - runtime::{Bool, Object}, -}; +use objc::foundation::is_main_thread; +use objc::rc::autoreleasepool; +use objc::runtime::Bool; use once_cell::sync::Lazy; use crate::{ dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, - platform::macos::ActivationPolicy, - platform_impl::{ - get_aux_state_mut, - platform::{ - event::{EventProxy, EventWrapper}, - event_loop::{post_dummy_event, PanicInfo}, - menu, - observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, - util::{IdRef, Never}, - window::get_window_id, - }, + platform_impl::platform::{ + event::{EventProxy, EventWrapper}, + event_loop::{post_dummy_event, PanicInfo}, + menu, + observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, + util::{IdRef, Never}, + window::get_window_id, }, window::WindowId, }; @@ -283,17 +277,21 @@ impl AppState { } } - pub fn launched(app_delegate: &Object) { - apply_activation_policy(app_delegate); + pub fn launched(activation_policy: NSApplicationActivationPolicy, create_default_menu: bool) { unsafe { let ns_app = NSApp(); + + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. + ns_app.setActivationPolicy_(activation_policy); + window_activation_hack(ns_app); // TODO: Consider allowing the user to specify they don't want their application activated ns_app.activateIgnoringOtherApps_(Bool::YES.as_raw()); }; HANDLER.set_ready(); HANDLER.waker().start(); - let create_default_menu = unsafe { get_aux_state_mut(app_delegate).default_menu }; if create_default_menu { // The menubar initialization should be before the `NewEvents` event, to allow // overriding of the default menu even if it's created @@ -450,18 +448,3 @@ unsafe fn window_activation_hack(ns_app: id) { } } } -fn apply_activation_policy(app_delegate: &Object) { - unsafe { - use cocoa::appkit::NSApplicationActivationPolicy::*; - let ns_app = NSApp(); - // We need to delay setting the activation policy and activating the app - // until `applicationDidFinishLaunching` has been called. Otherwise the - // menu bar is initially unresponsive on macOS 10.15. - let act_pol = get_aux_state_mut(app_delegate).activation_policy; - ns_app.setActivationPolicy_(match act_pol { - ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, - ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory, - ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited, - }); - } -} diff --git a/src/platform_impl/macos/appkit/application.rs b/src/platform_impl/macos/appkit/application.rs new file mode 100644 index 00000000..6ec74981 --- /dev/null +++ b/src/platform_impl/macos/appkit/application.rs @@ -0,0 +1,14 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +use super::NSResponder; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSApplication; + + unsafe impl ClassType for NSApplication { + #[inherits(NSObject)] + type Super = NSResponder; + } +); diff --git a/src/platform_impl/macos/appkit/mod.rs b/src/platform_impl/macos/appkit/mod.rs new file mode 100644 index 00000000..9690ae43 --- /dev/null +++ b/src/platform_impl/macos/appkit/mod.rs @@ -0,0 +1,11 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +mod application; +mod responder; +mod view; +mod window; + +pub(crate) use self::application::NSApplication; +pub(crate) use self::responder::NSResponder; +pub(crate) use self::view::NSView; +pub(crate) use self::window::NSWindow; diff --git a/src/platform_impl/macos/appkit/responder.rs b/src/platform_impl/macos/appkit/responder.rs new file mode 100644 index 00000000..5181b986 --- /dev/null +++ b/src/platform_impl/macos/appkit/responder.rs @@ -0,0 +1,11 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSResponder; + + unsafe impl ClassType for NSResponder { + type Super = NSObject; + } +); diff --git a/src/platform_impl/macos/appkit/view.rs b/src/platform_impl/macos/appkit/view.rs new file mode 100644 index 00000000..9f64f5d6 --- /dev/null +++ b/src/platform_impl/macos/appkit/view.rs @@ -0,0 +1,14 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +use super::NSResponder; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSView; + + unsafe impl ClassType for NSView { + #[inherits(NSObject)] + type Super = NSResponder; + } +); diff --git a/src/platform_impl/macos/appkit/window.rs b/src/platform_impl/macos/appkit/window.rs new file mode 100644 index 00000000..c73b6b8a --- /dev/null +++ b/src/platform_impl/macos/appkit/window.rs @@ -0,0 +1,14 @@ +use objc2::foundation::NSObject; +use objc2::{extern_class, ClassType}; + +use super::NSResponder; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSWindow; + + unsafe impl ClassType for NSWindow { + #[inherits(NSObject)] + type Super = NSResponder; + } +); diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 2f445b6f..ae253554 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -16,8 +16,9 @@ use cocoa::{ base::{id, nil}, foundation::{NSPoint, NSTimeInterval}, }; -use objc::foundation::is_main_thread; -use objc::rc::autoreleasepool; +use objc2::foundation::is_main_thread; +use objc2::rc::{autoreleasepool, Id, Shared}; +use objc2::ClassType; use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle}; use crate::{ @@ -25,16 +26,12 @@ use crate::{ event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, monitor::MonitorHandle as RootMonitorHandle, platform::macos::ActivationPolicy, - platform_impl::{ - get_aux_state_mut, - platform::{ - app::APP_CLASS, - app_delegate::APP_DELEGATE_CLASS, - app_state::{AppState, Callback}, - monitor::{self, MonitorHandle}, - observer::*, - util::IdRef, - }, + platform_impl::platform::{ + app::WinitApplication, + app_delegate::ApplicationDelegate, + app_state::{AppState, Callback}, + monitor::{self, MonitorHandle}, + observer::*, }, }; @@ -113,7 +110,7 @@ impl EventLoopWindowTarget { pub struct EventLoop { /// The delegate is only weakly referenced by NSApplication, so we keep /// it around here as well. - _delegate: IdRef, + _delegate: Id, window_target: Rc>, panic_info: Rc, @@ -153,16 +150,18 @@ impl EventLoop { // `sharedApplication`) is called anywhere else, or we'll end up // with the wrong `NSApplication` class and the wrong thread could // be marked as main. - let app: id = msg_send![APP_CLASS.0, sharedApplication]; + let app: id = msg_send![WinitApplication::class(), sharedApplication]; - let delegate = IdRef::new(msg_send![APP_DELEGATE_CLASS.0, new]); - - let mut aux_state = get_aux_state_mut(&**delegate); - aux_state.activation_policy = attributes.activation_policy; - aux_state.default_menu = attributes.default_menu; + use cocoa::appkit::NSApplicationActivationPolicy::*; + let activation_policy = match attributes.activation_policy { + ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, + ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited, + }; + let delegate = ApplicationDelegate::new(activation_policy, attributes.default_menu); autoreleasepool(|_| { - let _: () = msg_send![app, setDelegate:*delegate]; + let _: () = msg_send![app, setDelegate: &*delegate]; }); delegate diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 902565cc..8b278885 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -7,6 +7,7 @@ mod util; mod app; mod app_delegate; mod app_state; +mod appkit; mod event; mod event_loop; mod ffi; @@ -20,7 +21,6 @@ mod window_delegate; use std::{fmt, ops::Deref, sync::Arc}; pub(crate) use self::{ - app_delegate::get_aux_state_mut, event_loop::{ EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes, }, diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index f37bafc5..48988beb 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -12,8 +12,7 @@ use cocoa::{ foundation::{NSPoint, NSRect, NSString}, }; use core_graphics::display::CGDisplay; -use objc::foundation::{NSRange, NSUInteger}; -use objc::runtime::{Class, Object}; +use objc2::foundation::{NSRange, NSUInteger}; use crate::dpi::LogicalPosition; use crate::platform_impl::platform::ffi; @@ -144,11 +143,6 @@ pub unsafe fn app_name() -> Option { } } -pub unsafe fn superclass(this: &Object) -> &Class { - let superclass: *const Class = msg_send![this, superclass]; - &*superclass -} - #[allow(dead_code)] pub unsafe fn open_emoji_picker() { let _: () = msg_send![NSApp(), orderFrontCharacterPalette: nil]; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index df7d50df..afef01df 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -14,13 +14,11 @@ use cocoa::{ base::{id, nil}, foundation::{NSPoint, NSRect, NSSize, NSString}, }; -use objc::{ - declare::ClassBuilder, - foundation::{NSInteger, NSRange, NSUInteger}, - runtime::{Bool, Class, Object, Protocol, Sel}, -}; -use once_cell::sync::Lazy; +use objc2::foundation::{NSInteger, NSObject, NSRange, NSUInteger}; +use objc2::runtime::{Bool, Object, Sel}; +use objc2::{declare_class, ClassType}; +use super::appkit::{NSResponder, NSView as NSViewClass}; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{ @@ -116,7 +114,7 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak>) { unsafe { // This is free'd in `dealloc` let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; - let ns_view: id = msg_send![VIEW_CLASS.0, alloc]; + let ns_view: id = msg_send![WinitView::class(), alloc]; ( IdRef::new(msg_send![ns_view, initWithWinit: state_ptr]), cursor_access, @@ -159,498 +157,6 @@ pub unsafe fn set_ime_allowed(ns_view: id, ime_allowed: bool) { } } -struct ViewClass(*const Class); -unsafe impl Send for ViewClass {} -unsafe impl Sync for ViewClass {} - -static VIEW_CLASS: Lazy = Lazy::new(|| unsafe { - let superclass = class!(NSView); - let mut decl = ClassBuilder::new("WinitView", superclass).unwrap(); - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); - decl.add_method( - sel!(initWithWinit:), - init_with_winit as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(viewDidMoveToWindow), - view_did_move_to_window as extern "C" fn(_, _), - ); - decl.add_method(sel!(drawRect:), draw_rect as extern "C" fn(_, _, _)); - decl.add_method( - sel!(acceptsFirstResponder), - accepts_first_responder as extern "C" fn(_, _) -> _, - ); - decl.add_method(sel!(touchBar), touch_bar as extern "C" fn(_, _) -> _); - decl.add_method( - sel!(resetCursorRects), - reset_cursor_rects as extern "C" fn(_, _), - ); - - // ------------------------------------------------------------------ - // NSTextInputClient - decl.add_method( - sel!(hasMarkedText), - has_marked_text as extern "C" fn(_, _) -> _, - ); - decl.add_method(sel!(markedRange), marked_range as extern "C" fn(_, _) -> _); - decl.add_method( - sel!(selectedRange), - selected_range as extern "C" fn(_, _) -> _, - ); - decl.add_method( - sel!(setMarkedText:selectedRange:replacementRange:), - set_marked_text as extern "C" fn(_, _, _, _, _), - ); - decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(_, _)); - decl.add_method( - sel!(validAttributesForMarkedText), - valid_attributes_for_marked_text as extern "C" fn(_, _) -> _, - ); - decl.add_method( - sel!(attributedSubstringForProposedRange:actualRange:), - attributed_substring_for_proposed_range as extern "C" fn(_, _, _, _) -> _, - ); - decl.add_method( - sel!(insertText:replacementRange:), - insert_text as extern "C" fn(_, _, _, _), - ); - decl.add_method( - sel!(characterIndexForPoint:), - character_index_for_point as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(firstRectForCharacterRange:actualRange:), - first_rect_for_character_range as extern "C" fn(_, _, _, _) -> _, - ); - decl.add_method( - sel!(doCommandBySelector:), - do_command_by_selector as extern "C" fn(_, _, _), - ); - // ------------------------------------------------------------------ - - decl.add_method(sel!(keyDown:), key_down as extern "C" fn(_, _, _)); - decl.add_method(sel!(keyUp:), key_up as extern "C" fn(_, _, _)); - decl.add_method(sel!(flagsChanged:), flags_changed as extern "C" fn(_, _, _)); - decl.add_method(sel!(insertTab:), insert_tab as extern "C" fn(_, _, _)); - decl.add_method( - sel!(insertBackTab:), - insert_back_tab as extern "C" fn(_, _, _), - ); - decl.add_method(sel!(mouseDown:), mouse_down as extern "C" fn(_, _, _)); - decl.add_method(sel!(mouseUp:), mouse_up as extern "C" fn(_, _, _)); - decl.add_method( - sel!(rightMouseDown:), - right_mouse_down as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(rightMouseUp:), - right_mouse_up as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(otherMouseDown:), - other_mouse_down as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(otherMouseUp:), - other_mouse_up as extern "C" fn(_, _, _), - ); - decl.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(_, _, _)); - decl.add_method(sel!(mouseDragged:), mouse_dragged as extern "C" fn(_, _, _)); - decl.add_method( - sel!(rightMouseDragged:), - right_mouse_dragged as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(otherMouseDragged:), - other_mouse_dragged as extern "C" fn(_, _, _), - ); - decl.add_method(sel!(mouseEntered:), mouse_entered as extern "C" fn(_, _, _)); - decl.add_method(sel!(mouseExited:), mouse_exited as extern "C" fn(_, _, _)); - decl.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(_, _, _)); - decl.add_method( - sel!(magnifyWithEvent:), - magnify_with_event as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(rotateWithEvent:), - rotate_with_event as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(pressureChangeWithEvent:), - pressure_change_with_event as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(_wantsKeyDownForEvent:), - wants_key_down_for_event as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(cancelOperation:), - cancel_operation as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(frameDidChange:), - frame_did_change as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(acceptsFirstMouse:), - accepts_first_mouse as extern "C" fn(_, _, _) -> _, - ); - decl.add_ivar::<*mut c_void>("winitState"); - decl.add_ivar::("markedText"); - let protocol = Protocol::get("NSTextInputClient").unwrap(); - decl.add_protocol(protocol); - ViewClass(decl.register()) -}); - -extern "C" fn dealloc(this: &Object, _sel: Sel) { - unsafe { - let marked_text: id = *this.ivar("markedText"); - let _: () = msg_send![marked_text, release]; - let state: *mut c_void = *this.ivar("winitState"); - drop(Box::from_raw(state as *mut ViewState)); - } -} - -extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id { - unsafe { - let this: id = msg_send![this, init]; - if this != nil { - (*this).set_ivar("winitState", state); - let marked_text = - ::init(NSMutableAttributedString::alloc(nil)); - (*this).set_ivar("markedText", marked_text); - let _: () = msg_send![this, setPostsFrameChangedNotifications: true]; - - let notification_center: &Object = - msg_send![class!(NSNotificationCenter), defaultCenter]; - // About frame change - let frame_did_change_notification_name = - IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification")); - let _: () = msg_send![ - notification_center, - addObserver: this - selector: sel!(frameDidChange:) - name: *frame_did_change_notification_name - object: this - ]; - - let winit_state = &mut *(state as *mut ViewState); - winit_state.input_source = current_input_source(this); - } - this - } -} - -extern "C" fn view_did_move_to_window(this: &Object, _sel: Sel) { - trace_scope!("viewDidMoveToWindow"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - if let Some(tracking_rect) = state.tracking_rect.take() { - let _: () = msg_send![this, removeTrackingRect: tracking_rect]; - } - - let rect: NSRect = msg_send![this, visibleRect]; - let tracking_rect: NSInteger = msg_send![ - this, - addTrackingRect: rect, - owner: this, - userData: ptr::null_mut::(), - assumeInside: false - ]; - state.tracking_rect = Some(tracking_rect); - } -} - -extern "C" fn frame_did_change(this: &Object, _sel: Sel, _event: id) { - trace_scope!("frameDidChange:"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - if let Some(tracking_rect) = state.tracking_rect.take() { - let _: () = msg_send![this, removeTrackingRect: tracking_rect]; - } - - let rect: NSRect = msg_send![this, visibleRect]; - let tracking_rect: NSInteger = msg_send![ - this, - addTrackingRect: rect, - owner: this, - userData: ptr::null_mut::(), - assumeInside: false, - ]; - state.tracking_rect = Some(tracking_rect); - - // Emit resize event here rather than from windowDidResize because: - // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. - // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). - let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); - let size = logical_size.to_physical::(state.get_scale_factor()); - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::Resized(size), - })); - } -} - -extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { - trace_scope!("drawRect:"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - AppState::handle_redraw(WindowId(get_window_id(state.ns_window))); - - let superclass = util::superclass(this); - let _: () = msg_send![super(this, superclass), drawRect: rect]; - } -} - -extern "C" fn accepts_first_responder(_this: &Object, _sel: Sel) -> Bool { - trace_scope!("acceptsFirstResponder"); - Bool::YES -} - -// This is necessary to prevent a beefy terminal error on MacBook Pros: -// IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem -// TODO: Add an API extension for using `NSTouchBar` -extern "C" fn touch_bar(_this: &Object, _sel: Sel) -> Bool { - trace_scope!("touchBar"); - Bool::NO -} - -extern "C" fn reset_cursor_rects(this: &Object, _sel: Sel) { - trace_scope!("resetCursorRects"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - let bounds: NSRect = msg_send![this, bounds]; - let cursor_state = state.cursor_state.lock().unwrap(); - let cursor = if cursor_state.visible { - cursor_state.cursor.load() - } else { - util::invisible_cursor() - }; - let _: () = msg_send![this, - addCursorRect:bounds - cursor:cursor - ]; - } -} - -extern "C" fn has_marked_text(this: &Object, _sel: Sel) -> Bool { - trace_scope!("hasMarkedText"); - unsafe { - let marked_text: id = *this.ivar("markedText"); - Bool::new(marked_text.length() > 0) - } -} - -extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange { - trace_scope!("markedRange"); - unsafe { - let marked_text: id = *this.ivar("markedText"); - let length = marked_text.length(); - if length > 0 { - NSRange::new(0, length) - } else { - util::EMPTY_RANGE - } - } -} - -extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange { - trace_scope!("selectedRange"); - util::EMPTY_RANGE -} - -/// Safety: Assumes that `view` is an instance of `VIEW_CLASS` from winit. -unsafe fn current_input_source(view: *const Object) -> String { - let input_context: id = msg_send![view, inputContext]; - let input_source: id = msg_send![input_context, selectedKeyboardInputSource]; - id_to_string_lossy(input_source) -} - -extern "C" fn set_marked_text( - this: &mut Object, - _sel: Sel, - string: id, - _selected_range: NSRange, - _replacement_range: NSRange, -) { - trace_scope!("setMarkedText:selectedRange:replacementRange:"); - unsafe { - // Get pre-edit text - let marked_text_ref: &mut id = this.ivar_mut("markedText"); - - // Update markedText - let _: () = msg_send![*marked_text_ref, release]; - let marked_text = NSMutableAttributedString::alloc(nil); - let has_attr = msg_send![string, isKindOfClass: class!(NSAttributedString)]; - if has_attr { - marked_text.initWithAttributedString(string); - } else { - marked_text.initWithString(string); - }; - *marked_text_ref = marked_text; - - // Update ViewState with new marked text - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - let preedit_string = id_to_string_lossy(string); - - // Notify IME is active if application still doesn't know it. - if state.ime_state == ImeState::Disabled { - state.input_source = current_input_source(this); - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::Ime(Ime::Enabled), - })); - } - - // Don't update state to preedit when we've just commited a string, since the following - // preedit string will be None anyway. - if state.ime_state != ImeState::Commited { - state.ime_state = ImeState::Preedit; - } - - // Empty string basically means that there's no preedit, so indicate that by sending - // `None` cursor range. - let cursor_range = if preedit_string.is_empty() { - None - } else { - Some((preedit_string.len(), preedit_string.len())) - }; - - // Send WindowEvent for updating marked text - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::Ime(Ime::Preedit(preedit_string, cursor_range)), - })); - } -} - -extern "C" fn unmark_text(this: &Object, _sel: Sel) { - trace_scope!("unmarkText"); - unsafe { - let marked_text: id = *this.ivar("markedText"); - let mutable_string = marked_text.mutableString(); - let s: id = msg_send![class!(NSString), new]; - let _: () = msg_send![mutable_string, setString: s]; - let _: () = msg_send![s, release]; - let input_context: id = msg_send![this, inputContext]; - let _: () = msg_send![input_context, discardMarkedText]; - - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), - })); - if state.is_ime_enabled() { - // Leave the Preedit state - state.ime_state = ImeState::Enabled; - } else { - warn!("Expected to have IME enabled when receiving unmarkText"); - } - } -} - -extern "C" fn valid_attributes_for_marked_text(_this: &Object, _sel: Sel) -> id { - trace_scope!("validAttributesForMarkedText"); - unsafe { msg_send![class!(NSArray), array] } -} - -extern "C" fn attributed_substring_for_proposed_range( - _this: &Object, - _sel: Sel, - _range: NSRange, - _actual_range: *mut c_void, // *mut NSRange -) -> id { - trace_scope!("attributedSubstringForProposedRange:actualRange:"); - nil -} - -extern "C" fn character_index_for_point(_this: &Object, _sel: Sel, _point: NSPoint) -> NSUInteger { - trace_scope!("characterIndexForPoint:"); - 0 -} - -extern "C" fn first_rect_for_character_range( - this: &Object, - _sel: Sel, - _range: NSRange, - _actual_range: *mut c_void, // *mut NSRange -) -> NSRect { - trace_scope!("firstRectForCharacterRange:actualRange:"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - let content_rect = - NSWindow::contentRectForFrameRect_(state.ns_window, NSWindow::frame(state.ns_window)); - let base_x = content_rect.origin.x as f64; - let base_y = (content_rect.origin.y + content_rect.size.height) as f64; - let x = base_x + state.ime_position.x; - let y = base_y - state.ime_position.y; - // This is not ideal: We _should_ return a different position based on - // the currently selected character (which varies depending on the type - // and size of the character), but in the current `winit` API there is - // no way to express this. Same goes for the `NSSize`. - NSRect::new(NSPoint::new(x as _, y as _), NSSize::new(0.0, 0.0)) - } -} - -extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_range: NSRange) { - trace_scope!("insertText:replacementRange:"); - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - let string = id_to_string_lossy(string); - - let is_control = string.chars().next().map_or(false, |c| c.is_control()); - - // We don't need this now, but it's here if that changes. - //let event: id = msg_send![NSApp(), currentEvent]; - - if state.is_ime_enabled() && !is_control { - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::Ime(Ime::Commit(string)), - })); - state.ime_state = ImeState::Commited; - } - } -} - -extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _command: Sel) { - trace_scope!("doCommandBySelector:"); - // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human - // readable" character happens, i.e. newlines, tabs, and Ctrl+C. - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - // We shouldn't forward any character from just commited text, since we'll end up sending - // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, - // which is not desired given it was used to confirm IME input. - if state.ime_state == ImeState::Commited { - return; - } - - state.forward_key_to_app = true; - - let has_marked_text = msg_send![this, hasMarkedText]; - if has_marked_text && state.ime_state == ImeState::Preedit { - // Leave preedit so that we also report the keyup for this key - state.ime_state = ImeState::Enabled; - } - } -} - fn get_characters(event: id, ignore_modifiers: bool) -> String { unsafe { let characters: id = if ignore_modifiers { @@ -719,14 +225,456 @@ fn update_potentially_stale_modifiers(state: &mut ViewState, event: id) { } } -extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { - trace_scope!("keyDown:"); +fn mouse_click(this: &Object, event: id, button: MouseButton, button_state: ElementState) { unsafe { let state_ptr: *mut c_void = *this.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::MouseInput { + device_id: DEVICE_ID, + state: button_state, + button, + modifiers: event_mods(event), + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + +fn mouse_motion(this: &Object, event: id) { + unsafe { + let state_ptr: *mut c_void = *this.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + // We have to do this to have access to the `NSView` trait... + let view: id = this as *const _ as *mut _; + + let window_point = event.locationInWindow(); + let view_point = view.convertPoint_fromView_(window_point, nil); + let view_rect = NSView::frame(view); + + if view_point.x.is_sign_negative() + || view_point.y.is_sign_negative() + || view_point.x > view_rect.size.width + || view_point.y > view_rect.size.height + { + let mouse_buttons_down: NSUInteger = msg_send![class!(NSEvent), pressedMouseButtons]; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } + } + + let x = view_point.x as f64; + let y = view_rect.size.height as f64 - view_point.y as f64; + let logical_position = LogicalPosition::new(x, y); + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::CursorMoved { + device_id: DEVICE_ID, + position: logical_position.to_physical(state.get_scale_factor()), + modifiers: event_mods(event), + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + +declare_class! { + #[derive(Debug)] + #[allow(non_snake_case)] + struct WinitView { + winitState: *mut c_void, + markedText: id, + } + + unsafe impl ClassType for WinitView { + #[inherits(NSResponder, NSObject)] + type Super = NSViewClass; + } + + unsafe impl WinitView { +#[sel(dealloc)] +fn dealloc(&mut self) { + unsafe { + let marked_text: id = *self.ivar("markedText"); + let _: () = msg_send![marked_text, release]; + let state: *mut c_void = *self.ivar("winitState"); + drop(Box::from_raw(state as *mut ViewState)); + } +} + +#[sel(initWithWinit:)] +fn init_with_winit(&mut self, state: *mut c_void) -> Option<&mut Self> { + let this: Option<&mut Self> = unsafe { msg_send![self, init] }; + this.map(|this| unsafe { + (*this).set_ivar("winitState", state); + let marked_text = + ::init(NSMutableAttributedString::alloc(nil)); + (*this).set_ivar("markedText", marked_text); + let _: () = msg_send![&mut *this, setPostsFrameChangedNotifications: true]; + + let notification_center: &Object = + msg_send![class!(NSNotificationCenter), defaultCenter]; + // About frame change + let frame_did_change_notification_name = + IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification")); + let _: () = msg_send![ + notification_center, + addObserver: &*this + selector: sel!(frameDidChange:) + name: *frame_did_change_notification_name + object: &*this + ]; + + let winit_state = &mut *(state as *mut ViewState); + winit_state.input_source = this.current_input_source(); + this + }) +} + } + + unsafe impl WinitView { +#[sel(viewDidMoveToWindow)] +fn view_did_move_to_window(&self) { + trace_scope!("viewDidMoveToWindow"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![self, removeTrackingRect: tracking_rect]; + } + + let rect: NSRect = msg_send![self, visibleRect]; + let tracking_rect: NSInteger = msg_send![ + self, + addTrackingRect: rect, + owner: self, + userData: ptr::null_mut::(), + assumeInside: false, + ]; + state.tracking_rect = Some(tracking_rect); + } +} + +#[sel(frameDidChange:)] +fn frame_did_change(&self, _event: id) { + trace_scope!("frameDidChange:"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![self, removeTrackingRect: tracking_rect]; + } + + let rect: NSRect = msg_send![self, visibleRect]; + let tracking_rect: NSInteger = msg_send![ + self, + addTrackingRect: rect, + owner: self, + userData: ptr::null_mut::(), + assumeInside: false, + ]; + state.tracking_rect = Some(tracking_rect); + + // Emit resize event here rather than from windowDidResize because: + // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. + // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical::(state.get_scale_factor()); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Resized(size), + })); + } +} + +#[sel(drawRect:)] +fn draw_rect(&self, rect: NSRect) { + trace_scope!("drawRect:"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + AppState::handle_redraw(WindowId(get_window_id(state.ns_window))); + + let _: () = msg_send![super(self), drawRect: rect]; + } +} + +#[sel(acceptsFirstResponder)] +fn accepts_first_responder(&self) -> bool { + trace_scope!("acceptsFirstResponder"); + true +} + +// This is necessary to prevent a beefy terminal error on MacBook Pros: +// IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem +// TODO: Add an API extension for using `NSTouchBar` +#[sel(touchBar)] +fn touch_bar(&self) -> bool { + trace_scope!("touchBar"); + false +} + +#[sel(resetCursorRects)] +fn reset_cursor_rects(&self) { + trace_scope!("resetCursorRects"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let bounds: NSRect = msg_send![self, bounds]; + let cursor_state = state.cursor_state.lock().unwrap(); + let cursor = if cursor_state.visible { + cursor_state.cursor.load() + } else { + util::invisible_cursor() + }; + let _: () = msg_send![self, + addCursorRect:bounds + cursor:cursor + ]; + } +} + } + + unsafe impl Protocol for WinitView { + +#[sel(hasMarkedText)] +fn has_marked_text(&self) -> bool { + trace_scope!("hasMarkedText"); + unsafe { + let marked_text: id = *self.ivar("markedText"); + marked_text.length() > 0 + } +} + +#[sel(markedRange)] +fn marked_range(&self) -> NSRange { + trace_scope!("markedRange"); + unsafe { + let marked_text: id = *self.ivar("markedText"); + let length = marked_text.length(); + if length > 0 { + NSRange::new(0, length) + } else { + util::EMPTY_RANGE + } + } +} + +#[sel(selectedRange)] +fn selected_range(&self) -> NSRange { + trace_scope!("selectedRange"); + util::EMPTY_RANGE +} + +#[sel(setMarkedText:selectedRange:replacementRange:)] +fn set_marked_text( + &mut self, + string: id, + _selected_range: NSRange, + _replacement_range: NSRange, +) { + trace_scope!("setMarkedText:selectedRange:replacementRange:"); + unsafe { + // Get pre-edit text + let marked_text_ref: &mut id = self.ivar_mut("markedText"); + + // Update markedText + let _: () = msg_send![*marked_text_ref, release]; + let marked_text = NSMutableAttributedString::alloc(nil); + let has_attr = msg_send![string, isKindOfClass: class!(NSAttributedString)]; + if has_attr { + marked_text.initWithAttributedString(string); + } else { + marked_text.initWithString(string); + }; + *marked_text_ref = marked_text; + + // Update ViewState with new marked text + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let preedit_string = id_to_string_lossy(string); + + // Notify IME is active if application still doesn't know it. + if state.ime_state == ImeState::Disabled { + state.input_source = self.current_input_source(); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Enabled), + })); + } + + // Don't update state to preedit when we've just commited a string, since the following + // preedit string will be None anyway. + if state.ime_state != ImeState::Commited { + state.ime_state = ImeState::Preedit; + } + + // Empty string basically means that there's no preedit, so indicate that by sending + // `None` cursor range. + let cursor_range = if preedit_string.is_empty() { + None + } else { + Some((preedit_string.len(), preedit_string.len())) + }; + + // Send WindowEvent for updating marked text + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit(preedit_string, cursor_range)), + })); + } +} + +#[sel(unmarkText)] +fn unmark_text(&self) { + trace_scope!("unmarkText"); + unsafe { + let marked_text: id = *self.ivar("markedText"); + let mutable_string = marked_text.mutableString(); + let s: id = msg_send![class!(NSString), new]; + let _: () = msg_send![mutable_string, setString: s]; + let _: () = msg_send![s, release]; + let input_context: &Object = msg_send![self, inputContext]; + let _: () = msg_send![input_context, discardMarkedText]; + + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + })); + if state.is_ime_enabled() { + // Leave the Preedit state + state.ime_state = ImeState::Enabled; + } else { + warn!("Expected to have IME enabled when receiving unmarkText"); + } + } +} + +#[sel(validAttributesForMarkedText)] +fn valid_attributes_for_marked_text(&self) -> id { + trace_scope!("validAttributesForMarkedText"); + unsafe { msg_send![class!(NSArray), array] } +} + +#[sel(attributedSubstringForProposedRange:actualRange:)] +fn attributed_substring_for_proposed_range( + &self, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> id { + trace_scope!("attributedSubstringForProposedRange:actualRange:"); + nil +} + +#[sel(characterIndexForPoint:)] +fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger { + trace_scope!("characterIndexForPoint:"); + 0 +} + +#[sel(firstRectForCharacterRange:actualRange:)] +fn first_rect_for_character_range( + &self, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> NSRect { + trace_scope!("firstRectForCharacterRange:actualRange:"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + let content_rect = + NSWindow::contentRectForFrameRect_(state.ns_window, NSWindow::frame(state.ns_window)); + let base_x = content_rect.origin.x as f64; + let base_y = (content_rect.origin.y + content_rect.size.height) as f64; + let x = base_x + state.ime_position.x; + let y = base_y - state.ime_position.y; + // This is not ideal: We _should_ return a different position based on + // the currently selected character (which varies depending on the type + // and size of the character), but in the current `winit` API there is + // no way to express this. Same goes for the `NSSize`. + NSRect::new(NSPoint::new(x as _, y as _), NSSize::new(0.0, 0.0)) + } +} + +#[sel(insertText:replacementRange:)] +fn insert_text(&self, string: id, _replacement_range: NSRange) { + trace_scope!("insertText:replacementRange:"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + let string = id_to_string_lossy(string); + + let is_control = string.chars().next().map_or(false, |c| c.is_control()); + + // We don't need this now, but it's here if that changes. + //let event: id = msg_send![NSApp(), currentEvent]; + + if state.is_ime_enabled() && !is_control { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(state.ns_window)), + event: WindowEvent::Ime(Ime::Commit(string)), + })); + state.ime_state = ImeState::Commited; + } + } +} + +#[sel(doCommandBySelector:)] +fn do_command_by_selector(&self, _command: Sel) { + trace_scope!("doCommandBySelector:"); + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human + // readable" character happens, i.e. newlines, tabs, and Ctrl+C. + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); + + // We shouldn't forward any character from just commited text, since we'll end up sending + // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, + // which is not desired given it was used to confirm IME input. + if state.ime_state == ImeState::Commited { + return; + } + + state.forward_key_to_app = true; + + let has_marked_text = msg_send![self, hasMarkedText]; + if has_marked_text && state.ime_state == ImeState::Preedit { + // Leave preedit so that we also report the keyup for this key + state.ime_state = ImeState::Enabled; + } + } +} + } + + unsafe impl WinitView { + +#[sel(keyDown:)] +fn key_down(&self, event: id) { + trace_scope!("keyDown:"); + unsafe { + let state_ptr: *mut c_void = *self.ivar("winitState"); + let state = &mut *(state_ptr as *mut ViewState); let window_id = WindowId(get_window_id(state.ns_window)); - let input_source = current_input_source(this); + let input_source = self.current_input_source(); if state.input_source != input_source && state.is_ime_enabled() { state.ime_state = ImeState::Disabled; state.input_source = input_source; @@ -749,7 +697,7 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { let mut text_commited = false; if state.ime_allowed { let events_for_nsview: id = msg_send![class!(NSArray), arrayWithObject: event]; - let _: () = msg_send![this, interpretKeyEvents: events_for_nsview]; + let _: () = msg_send![self, interpretKeyEvents: events_for_nsview]; // Using a compiler fence because `interpretKeyEvents` might call // into functions that modify the `ViewState`, but the compiler @@ -802,10 +750,11 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { +#[sel(keyUp:)] +fn key_up(&self, event: id) { trace_scope!("keyUp:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let scancode = get_scancode(event) as u32; @@ -835,10 +784,11 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) { +#[sel(flagsChanged:)] +fn flags_changed(&self, event: id) { trace_scope!("flagsChanged:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let mut events = VecDeque::with_capacity(4); @@ -895,36 +845,39 @@ extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn insert_tab(this: &Object, _sel: Sel, _sender: id) { +#[sel(insertTab:)] +fn insert_tab(&self, _sender: id) { trace_scope!("insertTab:"); unsafe { - let window: id = msg_send![this, window]; + let window: id = msg_send![self, window]; let first_responder: id = msg_send![window, firstResponder]; - let this_ptr = this as *const _ as *mut _; - if first_responder == this_ptr { - let _: () = msg_send![window, selectNextKeyView: this]; + let self_ptr = self as *const _ as *mut _; + if first_responder == self_ptr { + let _: () = msg_send![window, selectNextKeyView: self]; } } } -extern "C" fn insert_back_tab(this: &Object, _sel: Sel, _sender: id) { +#[sel(insertBackTab:)] +fn insert_back_tab(&self, _sender: id) { trace_scope!("insertBackTab:"); unsafe { - let window: id = msg_send![this, window]; + let window: id = msg_send![self, window]; let first_responder: id = msg_send![window, firstResponder]; - let this_ptr = this as *const _ as *mut _; - if first_responder == this_ptr { - let _: () = msg_send![window, selectPreviousKeyView: this]; + let self_ptr = self as *const _ as *mut _; + if first_responder == self_ptr { + let _: () = msg_send![window, selectPreviousKeyView: self]; } } } // Allows us to receive Cmd-. (the shortcut for closing a dialog) // https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 -extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { +#[sel(cancelOperation:)] +fn cancel_operation(&self, _sender: id) { trace_scope!("cancelOperation:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let scancode = 0x2f; @@ -954,128 +907,75 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { } } -fn mouse_click(this: &Object, event: id, button: MouseButton, button_state: ElementState) { - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - update_potentially_stale_modifiers(state, event); - - let window_event = Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::MouseInput { - device_id: DEVICE_ID, - state: button_state, - button, - modifiers: event_mods(event), - }, - }; - - AppState::queue_event(EventWrapper::StaticEvent(window_event)); - } -} - -extern "C" fn mouse_down(this: &Object, _sel: Sel, event: id) { +#[sel(mouseDown:)] +fn mouse_down(&self, event: id) { trace_scope!("mouseDown:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Left, ElementState::Pressed); + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Left, ElementState::Pressed); } -extern "C" fn mouse_up(this: &Object, _sel: Sel, event: id) { +#[sel(mouseUp:)] +fn mouse_up(&self, event: id) { trace_scope!("mouseUp:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Left, ElementState::Released); + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Left, ElementState::Released); } -extern "C" fn right_mouse_down(this: &Object, _sel: Sel, event: id) { +#[sel(rightMouseDown:)] +fn right_mouse_down(&self, event: id) { trace_scope!("rightMouseDown:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Right, ElementState::Pressed); + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Right, ElementState::Pressed); } -extern "C" fn right_mouse_up(this: &Object, _sel: Sel, event: id) { +#[sel(rightMouseUp:)] +fn right_mouse_up(&self, event: id) { trace_scope!("rightMouseUp:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Right, ElementState::Released); + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Right, ElementState::Released); } -extern "C" fn other_mouse_down(this: &Object, _sel: Sel, event: id) { +#[sel(otherMouseDown:)] +fn other_mouse_down(&self, event: id) { trace_scope!("otherMouseDown:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Middle, ElementState::Pressed); + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Middle, ElementState::Pressed); } -extern "C" fn other_mouse_up(this: &Object, _sel: Sel, event: id) { +#[sel(otherMouseUp:)] +fn other_mouse_up(&self, event: id) { trace_scope!("otherMouseUp:"); - mouse_motion(this, event); - mouse_click(this, event, MouseButton::Middle, ElementState::Released); -} - -fn mouse_motion(this: &Object, event: id) { - unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - let state = &mut *(state_ptr as *mut ViewState); - - // We have to do this to have access to the `NSView` trait... - let view: id = this as *const _ as *mut _; - - let window_point = event.locationInWindow(); - let view_point = view.convertPoint_fromView_(window_point, nil); - let view_rect = NSView::frame(view); - - if view_point.x.is_sign_negative() - || view_point.y.is_sign_negative() - || view_point.x > view_rect.size.width - || view_point.y > view_rect.size.height - { - let mouse_buttons_down: NSUInteger = msg_send![class!(NSEvent), pressedMouseButtons]; - if mouse_buttons_down == 0 { - // Point is outside of the client area (view) and no buttons are pressed - return; - } - } - - let x = view_point.x as f64; - let y = view_rect.size.height as f64 - view_point.y as f64; - let logical_position = LogicalPosition::new(x, y); - - update_potentially_stale_modifiers(state, event); - - let window_event = Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::CursorMoved { - device_id: DEVICE_ID, - position: logical_position.to_physical(state.get_scale_factor()), - modifiers: event_mods(event), - }, - }; - - AppState::queue_event(EventWrapper::StaticEvent(window_event)); - } + mouse_motion(self, event); + mouse_click(self, event, MouseButton::Middle, ElementState::Released); } // No tracing on these because that would be overly verbose -extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) { - mouse_motion(this, event); +#[sel(mouseMoved:)] +fn mouse_moved(&self, event: id) { + mouse_motion(self, event); } -extern "C" fn mouse_dragged(this: &Object, _sel: Sel, event: id) { - mouse_motion(this, event); +#[sel(mouseDragged:)] +fn mouse_dragged(&self, event: id) { + mouse_motion(self, event); } -extern "C" fn right_mouse_dragged(this: &Object, _sel: Sel, event: id) { - mouse_motion(this, event); +#[sel(rightMouseDragged:)] +fn right_mouse_dragged(&self, event: id) { + mouse_motion(self, event); } -extern "C" fn other_mouse_dragged(this: &Object, _sel: Sel, event: id) { - mouse_motion(this, event); +#[sel(otherMouseDragged:)] +fn other_mouse_dragged(&self, event: id) { + mouse_motion(self, event); } -extern "C" fn mouse_entered(this: &Object, _sel: Sel, _event: id) { +#[sel(mouseEntered:)] +fn mouse_entered(&self, _event: id) { trace_scope!("mouseEntered:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let enter_event = Event::WindowEvent { @@ -1089,10 +989,11 @@ extern "C" fn mouse_entered(this: &Object, _sel: Sel, _event: id) { } } -extern "C" fn mouse_exited(this: &Object, _sel: Sel, _event: id) { +#[sel(mouseExited:)] +fn mouse_exited(&self, _event: id) { trace_scope!("mouseExited:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let window_event = Event::WindowEvent { @@ -1106,13 +1007,14 @@ extern "C" fn mouse_exited(this: &Object, _sel: Sel, _event: id) { } } -extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { +#[sel(scrollWheel:)] +fn scroll_wheel(&self, event: id) { trace_scope!("scrollWheel:"); - mouse_motion(this, event); + mouse_motion(self, event); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let delta = { @@ -1152,7 +1054,7 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { event: DeviceEvent::MouseWheel { delta }, }; - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); update_potentially_stale_modifiers(state, event); @@ -1172,11 +1074,12 @@ extern "C" fn scroll_wheel(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn magnify_with_event(this: &Object, _sel: Sel, event: id) { +#[sel(magnifyWithEvent:)] +fn magnify_with_event(&self, event: id) { trace_scope!("magnifyWithEvent:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let delta = event.magnification(); @@ -1201,11 +1104,12 @@ extern "C" fn magnify_with_event(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn rotate_with_event(this: &Object, _sel: Sel, event: id) { +#[sel(rotateWithEvent:)] +fn rotate_with_event(&self, event: id) { trace_scope!("rotateWithEvent:"); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let delta = event.rotation(); @@ -1230,13 +1134,14 @@ extern "C" fn rotate_with_event(this: &Object, _sel: Sel, event: id) { } } -extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { +#[sel(pressureChangeWithEvent:)] +fn pressure_change_with_event(&self, event: id) { trace_scope!("pressureChangeWithEvent:"); - mouse_motion(this, event); + mouse_motion(self, event); unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); + let state_ptr: *mut c_void = *self.ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); let pressure = event.pressure(); @@ -1258,12 +1163,24 @@ extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { // Allows us to receive Ctrl-Tab and Ctrl-Esc. // Note that this *doesn't* help with any missing Cmd inputs. // https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816 -extern "C" fn wants_key_down_for_event(_this: &Object, _sel: Sel, _event: id) -> Bool { +#[sel(_wantsKeyDownForEvent:)] +fn wants_key_down_for_event(&self, _event: id) -> bool { trace_scope!("_wantsKeyDownForEvent:"); - Bool::YES + true } -extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> Bool { +#[sel(acceptsFirstMouse:)] +fn accepts_first_mouse(&self, _event: id) -> bool { trace_scope!("acceptsFirstMouse:"); - Bool::YES + true +} + } +} + +impl WinitView { + fn current_input_source(&self) -> String { + let input_context: id = unsafe { msg_send![self, inputContext] }; + let input_source: id = unsafe { msg_send![input_context, selectedKeyboardInputSource] }; + unsafe { id_to_string_lossy(input_source) } + } } diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 459ee058..f043cc85 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -45,13 +45,12 @@ use cocoa::{ foundation::{NSDictionary, NSPoint, NSRect, NSSize}, }; use core_graphics::display::{CGDisplay, CGDisplayMode}; -use objc::{ - declare::ClassBuilder, - foundation::{is_main_thread, NSUInteger}, - rc::autoreleasepool, - runtime::{Bool, Class, Object, Sel}, -}; -use once_cell::sync::Lazy; +use objc2::foundation::{is_main_thread, NSObject, NSUInteger}; +use objc2::rc::autoreleasepool; +use objc2::runtime::{Bool, Object}; +use objc2::{declare_class, ClassType}; + +use super::appkit::{NSResponder, NSWindow as NSWindowClass}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId(pub usize); @@ -204,7 +203,7 @@ fn create_window( masks |= NSWindowStyleMask::NSFullSizeContentViewWindowMask; } - let ns_window: id = msg_send![WINDOW_CLASS.0, alloc]; + let ns_window: id = msg_send![WinitWindow::class(), alloc]; let ns_window = IdRef::new(ns_window.initWithContentRect_styleMask_backing_defer_( frame, masks, @@ -262,34 +261,28 @@ fn create_window( }) } -struct WindowClass(*const Class); -unsafe impl Send for WindowClass {} -unsafe impl Sync for WindowClass {} +declare_class!( + struct WinitWindow {} -static WINDOW_CLASS: Lazy = Lazy::new(|| unsafe { - let window_superclass = class!(NSWindow); - let mut decl = ClassBuilder::new("WinitWindow", window_superclass).unwrap(); - - pub extern "C" fn can_become_main_window(_: &Object, _: Sel) -> Bool { - trace_scope!("canBecomeMainWindow"); - Bool::YES + unsafe impl ClassType for WinitWindow { + #[inherits(NSResponder, NSObject)] + type Super = NSWindowClass; } - pub extern "C" fn can_become_key_window(_: &Object, _: Sel) -> Bool { - trace_scope!("canBecomeKeyWindow"); - Bool::YES - } + unsafe impl WinitWindow { + #[sel(canBecomeMainWindow)] + fn can_become_main_window(&self) -> bool { + trace_scope!("canBecomeMainWindow"); + true + } - decl.add_method( - sel!(canBecomeMainWindow), - can_become_main_window as extern "C" fn(_, _) -> _, - ); - decl.add_method( - sel!(canBecomeKeyWindow), - can_become_key_window as extern "C" fn(_, _) -> _, - ); - WindowClass(decl.register()) -}); + #[sel(canBecomeKeyWindow)] + fn can_become_key_window(&self) -> bool { + trace_scope!("canBecomeKeyWindow"); + true + } + } +); #[derive(Default)] pub struct SharedState { diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index 5c4449bb..076b27a5 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -8,13 +8,10 @@ use cocoa::{ appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow, NSWindowOcclusionState}, base::{id, nil}, }; -use objc::{ - declare::ClassBuilder, - foundation::NSUInteger, - rc::autoreleasepool, - runtime::{Bool, Class, Object, Sel}, -}; -use once_cell::sync::Lazy; +use objc2::foundation::{NSObject, NSUInteger}; +use objc2::rc::autoreleasepool; +use objc2::runtime::Object; +use objc2::{declare_class, ClassType}; use crate::{ dpi::{LogicalPosition, LogicalSize}, @@ -29,7 +26,7 @@ use crate::{ window::{Fullscreen, WindowId}, }; -pub struct WindowDelegateState { +struct WindowDelegateState { ns_window: IdRef, // never changes ns_view: IdRef, // never changes @@ -50,7 +47,7 @@ pub struct WindowDelegateState { } impl WindowDelegateState { - pub fn new(window: &Arc, initial_fullscreen: bool) -> Self { + fn new(window: &Arc, initial_fullscreen: bool) -> Self { let scale_factor = window.scale_factor(); let mut delegate_state = WindowDelegateState { ns_window: window.ns_window.clone(), @@ -75,7 +72,7 @@ impl WindowDelegateState { self.window.upgrade().map(|ref window| callback(window)) } - pub fn emit_event(&mut self, event: WindowEvent<'static>) { + fn emit_event(&mut self, event: WindowEvent<'static>) { let event = Event::WindowEvent { window_id: WindowId(get_window_id(*self.ns_window)), event, @@ -83,7 +80,7 @@ impl WindowDelegateState { AppState::queue_event(EventWrapper::StaticEvent(event)); } - pub fn emit_static_scale_factor_changed_event(&mut self) { + fn emit_static_scale_factor_changed_event(&mut self) { let scale_factor = self.get_scale_factor(); if scale_factor == self.previous_scale_factor { return; @@ -126,146 +123,55 @@ pub fn new_delegate(window: &Arc, initial_fullscreen: bool) -> Id unsafe { // This is free'd in `dealloc` let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; - let delegate: id = msg_send![WINDOW_DELEGATE_CLASS.0, alloc]; + let delegate: id = msg_send![WinitWindowDelegate::class(), alloc]; IdRef::new(msg_send![delegate, initWithWinit: state_ptr]) } } -struct WindowDelegateClass(*const Class); -unsafe impl Send for WindowDelegateClass {} -unsafe impl Sync for WindowDelegateClass {} +declare_class! { + #[derive(Debug)] + struct WinitWindowDelegate { + state: *mut c_void, + } -static WINDOW_DELEGATE_CLASS: Lazy = Lazy::new(|| unsafe { - let superclass = class!(NSResponder); - let mut decl = ClassBuilder::new("WinitWindowDelegate", superclass).unwrap(); + unsafe impl ClassType for WinitWindowDelegate { + type Super = NSObject; + } - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); - decl.add_method( - sel!(initWithWinit:), - init_with_winit as extern "C" fn(_, _, _) -> _, - ); - - decl.add_method( - sel!(windowShouldClose:), - window_should_close as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(windowWillClose:), - window_will_close as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidResize:), - window_did_resize as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidMove:), - window_did_move as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidChangeBackingProperties:), - window_did_change_backing_properties as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidBecomeKey:), - window_did_become_key as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidResignKey:), - window_did_resign_key as extern "C" fn(_, _, _), - ); - - decl.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(prepareForDragOperation:), - prepare_for_drag_operation as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(_, _, _) -> _, - ); - decl.add_method( - sel!(concludeDragOperation:), - conclude_drag_operation as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(draggingExited:), - dragging_exited as extern "C" fn(_, _, _), - ); - - decl.add_method( - sel!(window:willUseFullScreenPresentationOptions:), - window_will_use_fullscreen_presentation_options as extern "C" fn(_, _, _, _) -> _, - ); - decl.add_method( - sel!(windowDidEnterFullScreen:), - window_did_enter_fullscreen as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowWillEnterFullScreen:), - window_will_enter_fullscreen as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidExitFullScreen:), - window_did_exit_fullscreen as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowWillExitFullScreen:), - window_will_exit_fullscreen as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidFailToEnterFullScreen:), - window_did_fail_to_enter_fullscreen as extern "C" fn(_, _, _), - ); - decl.add_method( - sel!(windowDidChangeOcclusionState:), - window_did_change_occlusion_state as extern "C" fn(_, _, _), - ); - - decl.add_ivar::<*mut c_void>("winitState"); - WindowDelegateClass(decl.register()) -}); - -// This function is definitely unsafe, but labeling that would increase -// boilerplate and wouldn't really clarify anything... -fn with_state T, T>(this: &Object, callback: F) { - let state_ptr = unsafe { - let state_ptr: *mut c_void = *this.ivar("winitState"); - &mut *(state_ptr as *mut WindowDelegateState) - }; - callback(state_ptr); -} - -extern "C" fn dealloc(this: &Object, _sel: Sel) { - with_state(this, |state| unsafe { + unsafe impl WinitWindowDelegate { +#[sel(dealloc)] +fn dealloc(&mut self) { + self.with_state(|state| unsafe { drop(Box::from_raw(state as *mut WindowDelegateState)); }); } -extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id { - unsafe { - let this: id = msg_send![this, init]; - if this != nil { - (*this).set_ivar("winitState", state); - with_state(&*this, |state| { - let _: () = msg_send![*state.ns_window, setDelegate: this]; - }); - } +#[sel(initWithWinit:)] +fn init_with_winit(&mut self, state: *mut c_void) -> Option<&mut Self> { + let this: Option<&mut Self> = unsafe { msg_send![self, init] }; + this.map(|this| { + *this.state = state; + this.with_state(|state| { + let _: () = unsafe { msg_send![*state.ns_window, setDelegate: &*this] }; + }); this + }) +} } -} -extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> Bool { + // NSWindowDelegate + NSDraggingDestination protocols + unsafe impl WinitWindowDelegate { +#[sel(windowShouldClose:)] +fn window_should_close(&self, _: id) -> bool { trace_scope!("windowShouldClose:"); - with_state(this, |state| state.emit_event(WindowEvent::CloseRequested)); - Bool::NO + self.with_state(|state| state.emit_event(WindowEvent::CloseRequested)); + false } -extern "C" fn window_will_close(this: &Object, _: Sel, _: id) { +#[sel(windowWillClose:)] +fn window_will_close(&self, _: id) { trace_scope!("windowWillClose:"); - with_state(this, |state| unsafe { + self.with_state(|state| unsafe { // `setDelegate:` retains the previous value and then autoreleases it autoreleasepool(|_| { // Since El Capitan, we need to be careful that delegate methods can't @@ -276,41 +182,46 @@ extern "C" fn window_will_close(this: &Object, _: Sel, _: id) { }); } -extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { +#[sel(windowDidResize:)] +fn window_did_resize(&self, _: id) { trace_scope!("windowDidResize:"); - with_state(this, |state| { + self.with_state(|state| { // NOTE: WindowEvent::Resized is reported in frameDidChange. state.emit_move_event(); }); } // This won't be triggered if the move was part of a resize. -extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { +#[sel(windowDidMove:)] +fn window_did_move(&self, _: id) { trace_scope!("windowDidMove:"); - with_state(this, |state| { + self.with_state(|state| { state.emit_move_event(); }); } -extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) { +#[sel(windowDidChangeBackingProperties:)] +fn window_did_change_backing_properties(&self, _: id) { trace_scope!("windowDidChangeBackingProperties:"); - with_state(this, |state| { + self.with_state(|state| { state.emit_static_scale_factor_changed_event(); }); } -extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) { +#[sel(windowDidBecomeKey:)] +fn window_did_become_key(&self, _: id) { trace_scope!("windowDidBecomeKey:"); - with_state(this, |state| { + self.with_state(|state| { // TODO: center the cursor if the window had mouse grab when it // lost focus state.emit_event(WindowEvent::Focused(true)); }); } -extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) { +#[sel(windowDidResignKey:)] +fn window_did_resign_key(&self, _: id) { trace_scope!("windowDidResignKey:"); - with_state(this, |state| { + self.with_state(|state| { // It happens rather often, e.g. when the user is Cmd+Tabbing, that the // NSWindowDelegate will receive a didResignKey event despite no event // being received when the modifiers are released. This is because @@ -339,7 +250,8 @@ extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) { } /// Invoked when the dragged image enters destination bounds or frame -extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> Bool { +#[sel(draggingEntered:)] +fn dragging_entered(&self, sender: id) -> bool { trace_scope!("draggingEntered:"); use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration}; @@ -356,23 +268,25 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> Bool { let f = NSString::UTF8String(file); let path = CStr::from_ptr(f).to_string_lossy().into_owned(); - with_state(this, |state| { + self.with_state(|state| { state.emit_event(WindowEvent::HoveredFile(PathBuf::from(path))); }); } } - Bool::YES + true } /// Invoked when the image is released -extern "C" fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> Bool { +#[sel(prepareForDragOperation:)] +fn prepare_for_drag_operation(&self, _: id) -> bool { trace_scope!("prepareForDragOperation:"); - Bool::YES + true } /// Invoked after the released image has been removed from the screen -extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> Bool { +#[sel(performDragOperation:)] +fn perform_drag_operation(&self, sender: id) -> bool { trace_scope!("performDragOperation:"); use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration}; @@ -389,33 +303,36 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> Bool let f = NSString::UTF8String(file); let path = CStr::from_ptr(f).to_string_lossy().into_owned(); - with_state(this, |state| { + self.with_state(|state| { state.emit_event(WindowEvent::DroppedFile(PathBuf::from(path))); }); } } - Bool::YES + true } /// Invoked when the dragging operation is complete -extern "C" fn conclude_drag_operation(_: &Object, _: Sel, _: id) { +#[sel(concludeDragOperation:)] +fn conclude_drag_operation(&self, _: id) { trace_scope!("concludeDragOperation:"); } /// Invoked when the dragging operation is cancelled -extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { +#[sel(draggingExited:)] +fn dragging_exited(&self, _: id) { trace_scope!("draggingExited:"); - with_state(this, |state| { + self.with_state(|state| { state.emit_event(WindowEvent::HoveredFileCancelled) }); } /// Invoked when before enter fullscreen -extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { +#[sel(windowWillEnterFullscreen:)] +fn window_will_enter_fullscreen(&self, _: id) { trace_scope!("windowWillEnterFullscreen:"); - with_state(this, |state| { + self.with_state(|state| { state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_will_enter_fullscreen"); shared_state.maximized = window.is_zoomed(); @@ -442,10 +359,11 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { } /// Invoked when before exit fullscreen -extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { +#[sel(windowWillExitFullScreen:)] +fn window_will_exit_fullscreen(&self, _: id) { trace_scope!("windowWillExitFullScreen:"); - with_state(this, |state| { + self.with_state(|state| { state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_will_exit_fullscreen"); shared_state.in_fullscreen_transition = true; @@ -453,9 +371,9 @@ extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { }); } -extern "C" fn window_will_use_fullscreen_presentation_options( - this: &Object, - _: Sel, +#[sel(window:willUseFullScreenPresentationOptions:)] +fn window_will_use_fullscreen_presentation_options( + &self, _: id, proposed_options: NSUInteger, ) -> NSUInteger { @@ -469,7 +387,7 @@ extern "C" fn window_will_use_fullscreen_presentation_options( // we don't, for consistency. If we do, it should be documented that the // user-provided options are ignored in exclusive fullscreen. let mut options: NSUInteger = proposed_options; - with_state(this, |state| { + self.with_state(|state| { state.with_window(|window| { let shared_state = window.lock_shared_state("window_will_use_fullscreen_presentation_options"); @@ -486,9 +404,10 @@ extern "C" fn window_will_use_fullscreen_presentation_options( } /// Invoked when entered fullscreen -extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { +#[sel(windowDidEnterFullscreen:)] +fn window_did_enter_fullscreen(&self, _: id) { trace_scope!("windowDidEnterFullscreen:"); - with_state(this, |state| { + self.with_state(|state| { state.initial_fullscreen = false; state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_did_enter_fullscreen"); @@ -503,10 +422,11 @@ extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { } /// Invoked when exited fullscreen -extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { +#[sel(windowDidExitFullscreen:)] +fn window_did_exit_fullscreen(&self, _: id) { trace_scope!("windowDidExitFullscreen:"); - with_state(this, |state| { + self.with_state(|state| { state.with_window(|window| { window.restore_state_from_fullscreen(); let mut shared_state = window.lock_shared_state("window_did_exit_fullscreen"); @@ -536,9 +456,10 @@ extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { /// due to being in the midst of handling some other animation or user gesture. /// This method indicates that there was an error, and you should clean up any /// work you may have done to prepare to enter full-screen mode. -extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { +#[sel(windowDidFailToEnterFullscreen:)] +fn window_did_fail_to_enter_fullscreen(&self, _: id) { trace_scope!("windowDidFailToEnterFullscreen:"); - with_state(this, |state| { + self.with_state(|state| { state.with_window(|window| { let mut shared_state = window.lock_shared_state("window_did_fail_to_enter_fullscreen"); shared_state.in_fullscreen_transition = false; @@ -559,10 +480,11 @@ extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) } // Invoked when the occlusion state of the window changes -extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { +#[sel(windowDidChangeOcclusionState:)] +fn window_did_change_occlusion_state(&self, _: id) { trace_scope!("windowDidChangeOcclusionState:"); unsafe { - with_state(this, |state| { + self.with_state(|state| { state.emit_event(WindowEvent::Occluded( !state .ns_window @@ -571,4 +493,18 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { )) }); } +} + } +} + +impl WinitWindowDelegate { + // This function is definitely unsafe (&self -> &mut state), but labeling that + // would increase boilerplate and wouldn't really clarify anything... + fn with_state T, T>(&self, callback: F) { + let state_ptr = unsafe { + let state_ptr: *mut c_void = *self.state; + &mut *(state_ptr as *mut WindowDelegateState) + }; + callback(state_ptr); + } }