diff --git a/Cargo.toml b/Cargo.toml index 8f39742..6be3b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = [ "Robbert van der Helm ", "Adrien Prokopowicz " ] -edition = "2018" +edition = "2021" license = "MIT OR Apache-2.0" description = "Low-level windowing system geared towards making audio plugin UIs." keywords = ["windowing", "audio", "plugin"] @@ -23,7 +23,14 @@ exclude = [".github"] [features] default = [] -opengl = ["uuid", "x11/glx"] +opengl = [ + "uuid", + "x11/glx", + "objc2-core-foundation/CFBundle", + "objc2-app-kit/NSOpenGL", + "objc2-app-kit/NSOpenGLView", + "objc2-app-kit/objc2-open-gl" +] [dependencies] keyboard-types = { version = "0.6.1", default-features = false } @@ -39,10 +46,23 @@ winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", " uuid = { version = "0.8", features = ["v4"], optional = true } [target.'cfg(target_os="macos")'.dependencies] -cocoa = "0.24.0" -core-foundation = "0.9.1" -objc = "0.2.7" -uuid = { version = "0.8", features = ["v4"] } +uuid = { version = "1.23.1", features = ["v4"] } +objc2 = "0.6.4" +objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std", "CFString"] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSEnumerator"] } +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "NSApplication", + "NSDragging", + "NSEvent", + "NSGraphics", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSTrackingArea", + "NSView", + "NSWindow", + "objc2-core-foundation" +] } [dev-dependencies] rtrb = "0.2" diff --git a/src/gl/macos.rs b/src/gl/macos.rs index 9f9456c..a6d5abf 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -1,30 +1,26 @@ -use std::ffi::c_void; -use std::str::FromStr; - -use raw_window_handle::RawWindowHandle; +#![allow(deprecated)] // OpenGL is deprecated on macOS -use cocoa::appkit::{ +use super::{GlConfig, GlError, Profile}; +use objc2::rc::Retained; +use objc2::AllocAnyThread; +use objc2::{MainThreadMarker, MainThreadOnly}; +use objc2_app_kit::{ NSOpenGLContext, NSOpenGLContextParameter, NSOpenGLPFAAccelerated, NSOpenGLPFAAlphaSize, NSOpenGLPFAColorSize, NSOpenGLPFADepthSize, NSOpenGLPFADoubleBuffer, NSOpenGLPFAMultisample, NSOpenGLPFAOpenGLProfile, NSOpenGLPFASampleBuffers, NSOpenGLPFASamples, NSOpenGLPFAStencilSize, NSOpenGLPixelFormat, NSOpenGLProfileVersion3_2Core, NSOpenGLProfileVersion4_1Core, NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView, }; -use cocoa::base::{id, nil, YES}; -use cocoa::foundation::NSSize; - -use core_foundation::base::TCFType; -use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName}; -use core_foundation::string::CFString; - -use objc::{msg_send, sel, sel_impl}; - -use super::{GlConfig, GlError, Profile}; +use objc2_core_foundation::{CFBundle, CFString}; +use objc2_foundation::NSSize; +use raw_window_handle::RawWindowHandle; +use std::ffi::c_void; +use std::ptr::NonNull; pub type CreationFailedError = (); pub struct GlContext { - view: id, - context: id, + view: Retained, + context: Retained, } impl GlContext { @@ -35,11 +31,10 @@ impl GlContext { return Err(GlError::InvalidWindowHandle); }; - if handle.ns_view.is_null() { + let parent_view = handle.ns_view.cast::(); + let Some(parent_view) = parent_view.as_ref() else { return Err(GlError::InvalidWindowHandle); - } - - let parent_view = handle.ns_view as id; + }; let version = if config.version < (3, 2) && config.profile == Profile::Compatibility { NSOpenGLProfileVersionLegacy @@ -76,33 +71,29 @@ impl GlContext { attrs.push(0); - let pixel_format = NSOpenGLPixelFormat::alloc(nil).initWithAttributes_(&attrs); - - if pixel_format == nil { - return Err(GlError::CreationFailed(())); - } - - let view = - NSOpenGLView::alloc(nil).initWithFrame_pixelFormat_(parent_view.frame(), pixel_format); + let pixel_format = NSOpenGLPixelFormat::initWithAttributes( + NSOpenGLPixelFormat::alloc(), + NonNull::new(attrs.as_mut_ptr()).unwrap(), + ) + .ok_or(GlError::CreationFailed(()))?; - if view == nil { - return Err(GlError::CreationFailed(())); - } + let view = NSOpenGLView::initWithFrame_pixelFormat( + NSOpenGLView::alloc(MainThreadMarker::new().unwrap()), + parent_view.frame(), + Some(&pixel_format), + ) + .ok_or(GlError::CreationFailed(()))?; - view.setWantsBestResolutionOpenGLSurface_(YES); + view.setWantsBestResolutionOpenGLSurface(true); - NSOpenGLView::display_(view); - parent_view.addSubview_(view); + view.display(); + parent_view.addSubview(&view); - let context: id = msg_send![view, openGLContext]; - let () = msg_send![context, retain]; + let context = view.openGLContext().ok_or(GlError::CreationFailed(()))?; - context.setValues_forParameter_( - &(config.vsync as i32), - NSOpenGLContextParameter::NSOpenGLCPSwapInterval, - ); + let value = config.vsync as i32; - let () = msg_send![pixel_format, release]; + context.setValues_forParameter((&value).into(), NSOpenGLContextParameter::SwapInterval); Ok(GlContext { view, context }) } @@ -112,47 +103,35 @@ impl GlContext { } pub unsafe fn make_not_current(&self) { - NSOpenGLContext::clearCurrentContext(self.context); + if Some(&self.context) == NSOpenGLContext::currentContext().as_ref() { + NSOpenGLContext::clearCurrentContext(); + } } pub fn get_proc_address(&self, symbol: &str) -> *const c_void { - let symbol_name = CFString::from_str(symbol).unwrap(); - let framework_name = CFString::from_str("com.apple.opengl").unwrap(); - let framework = - unsafe { CFBundleGetBundleWithIdentifier(framework_name.as_concrete_TypeRef()) }; + let symbol_name = CFString::from_str(symbol); + let framework_name = CFString::from_static_str("com.apple.opengl"); + let framework = CFBundle::bundle_with_identifier(Some(&framework_name)).unwrap(); - unsafe { CFBundleGetFunctionPointerForName(framework, symbol_name.as_concrete_TypeRef()) } + CFBundle::function_pointer_for_name(&framework, Some(&symbol_name)) } pub fn swap_buffers(&self) { - unsafe { - self.context.flushBuffer(); - let () = msg_send![self.view, setNeedsDisplay: YES]; - } + self.context.flushBuffer(); + self.view.setNeedsDisplay(true); } /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. pub(crate) fn resize(&self, size: NSSize) { - unsafe { NSView::setFrameSize(self.view, size) }; - unsafe { - let _: () = msg_send![self.view, setNeedsDisplay: YES]; - } + self.view.setFrameSize(size); + self.view.setNeedsDisplay(true); } /// Pointer to the `NSOpenGLView` this context renders into. Used by /// the parent `NSView`'s `hitTest:` override to collapse hits on the /// render subview to the parent, so AppKit routes `mouseDown:` on /// first click in non-key windows. - pub(crate) fn ns_view(&self) -> id { - self.view - } -} - -impl Drop for GlContext { - fn drop(&mut self) { - unsafe { - let () = msg_send![self.context, release]; - let () = msg_send![self.view, release]; - } + pub(crate) fn ns_view(&self) -> &NSOpenGLView { + &self.view } } diff --git a/src/gl/mod.rs b/src/gl/mod.rs index 5cda059..77b29b2 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -109,7 +109,7 @@ impl GlContext { /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. #[cfg(target_os = "macos")] - pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) { + pub(crate) fn resize(&self, size: objc2_foundation::NSSize) { self.context.resize(size); } @@ -118,7 +118,7 @@ impl GlContext { /// render subview to the parent, so AppKit routes `mouseDown:` on /// first click in non-key windows. #[cfg(target_os = "macos")] - pub(crate) fn ns_view(&self) -> cocoa::base::id { + pub(crate) fn ns_view(&self) -> &objc2_app_kit::NSView { self.context.ns_view() } } diff --git a/src/macos/keyboard.rs b/src/macos/keyboard.rs index b38c120..8e3bdf1 100644 --- a/src/macos/keyboard.rs +++ b/src/macos/keyboard.rs @@ -18,23 +18,10 @@ //! Conversion of platform keyboard event into cross-platform event. -use std::cell::Cell; - -use cocoa::appkit::{NSEvent, NSEventModifierFlags, NSEventType}; -use cocoa::base::id; -use cocoa::foundation::NSString; -use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Modifiers}; -use objc::{msg_send, sel, sel_impl}; - use crate::keyboard::code_to_location; - -pub(crate) fn from_nsstring(s: id) -> String { - unsafe { - let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len()); - let result = std::str::from_utf8_unchecked(slice); - result.into() - } -} +use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Modifiers}; +use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventType}; +use std::cell::Cell; /// State for processing of keyboard events. /// @@ -279,72 +266,70 @@ impl KeyboardState { self.last_mods.get() } - pub(crate) fn process_native_event(&self, event: id) -> Option { - unsafe { - let event_type = event.eventType(); - let key_code = event.keyCode(); - let code = key_code_to_code(key_code); - let location = code_to_location(code); - let raw_mods = event.modifierFlags(); - let modifiers = make_modifiers(raw_mods); - let state = match event_type { - NSEventType::NSKeyDown => KeyState::Down, - NSEventType::NSKeyUp => KeyState::Up, - NSEventType::NSFlagsChanged => { - // We use `bits` here because we want to distinguish the - // device dependent bits (when both left and right keys - // may be pressed, for example). - let any_down = raw_mods.bits() & !self.last_mods.get().bits(); - self.last_mods.set(raw_mods); - if is_modifier_code(code) { - if any_down == 0 { - KeyState::Up - } else { - KeyState::Down - } + pub(crate) fn process_native_event(&self, event: &NSEvent) -> Option { + let event_type = event.r#type(); + let key_code = event.keyCode(); + let code = key_code_to_code(key_code); + let location = code_to_location(code); + let raw_mods = event.modifierFlags(); + let modifiers = make_modifiers(raw_mods); + let state = match event_type { + NSEventType::KeyDown => KeyState::Down, + NSEventType::KeyUp => KeyState::Up, + NSEventType::FlagsChanged => { + // We use `bits` here because we want to distinguish the + // device dependent bits (when both left and right keys + // may be pressed, for example). + let any_down = raw_mods.bits() & !self.last_mods.get().bits(); + self.last_mods.set(raw_mods); + if is_modifier_code(code) { + if any_down == 0 { + KeyState::Up } else { - // HandleFlagsChanged has some logic for this; it might - // happen when an app is deactivated by Command-Tab. In - // that case, the best thing to do is synthesize the event - // from the modifiers. But a challenge there is that we - // might get multiple events. - return None; + KeyState::Down } + } else { + // HandleFlagsChanged has some logic for this; it might + // happen when an app is deactivated by Command-Tab. In + // that case, the best thing to do is synthesize the event + // from the modifiers. But a challenge there is that we + // might get multiple events. + return None; } - // In case another event type ends up here, do not produce any kind of keyboard event. - _ => return None, - }; - let is_composing = false; - let repeat: bool = event_type == NSEventType::NSKeyDown && msg_send![event, isARepeat]; - let key = if let Some(key) = code_to_key(code) { - key + } + // In case another event type ends up here, do not produce any kind of keyboard event. + _ => return None, + }; + let is_composing = false; + let repeat: bool = event_type == NSEventType::KeyDown && event.isARepeat(); + let key = if let Some(key) = code_to_key(code) { + key + } else { + let characters = event.characters().map(|c| c.to_string()).unwrap_or_default(); + if is_valid_key(&characters) { + Key::Character(characters) } else { - let characters = from_nsstring(event.characters()); - if is_valid_key(&characters) { - Key::Character(characters) + let chars_ignoring = + event.charactersIgnoringModifiers().map(|c| c.to_string()).unwrap_or_default(); + if is_valid_key(&chars_ignoring) { + Key::Character(chars_ignoring) } else { - let chars_ignoring = from_nsstring(event.charactersIgnoringModifiers()); - if is_valid_key(&chars_ignoring) { - Key::Character(chars_ignoring) - } else { - // There may be more heroic things we can do here. - Key::Unidentified - } + // There may be more heroic things we can do here. + Key::Unidentified } - }; - let event = - KeyboardEvent { code, key, location, modifiers, state, is_composing, repeat }; - Some(event) - } + } + }; + let event = KeyboardEvent { code, key, location, modifiers, state, is_composing, repeat }; + Some(event) } } const MODIFIER_MAP: &[(NSEventModifierFlags, Modifiers)] = &[ - (NSEventModifierFlags::NSShiftKeyMask, Modifiers::SHIFT), - (NSEventModifierFlags::NSAlternateKeyMask, Modifiers::ALT), - (NSEventModifierFlags::NSControlKeyMask, Modifiers::CONTROL), - (NSEventModifierFlags::NSCommandKeyMask, Modifiers::META), - (NSEventModifierFlags::NSAlphaShiftKeyMask, Modifiers::CAPS_LOCK), + (NSEventModifierFlags::Shift, Modifiers::SHIFT), + (NSEventModifierFlags::Option, Modifiers::ALT), + (NSEventModifierFlags::Control, Modifiers::CONTROL), + (NSEventModifierFlags::Command, Modifiers::META), + (NSEventModifierFlags::CapsLock, Modifiers::CAPS_LOCK), ]; pub(crate) fn make_modifiers(raw: NSEventModifierFlags) -> Modifiers { diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 02a0e36..033b8c8 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -2,16 +2,42 @@ mod keyboard; mod view; mod window; +use objc2::rc::Retained; +use objc2::Message; +use std::cell::RefCell; pub use window::*; -#[allow(non_upper_case_globals)] -mod consts { - use cocoa::foundation::NSUInteger; +pub struct RetainedCell { + inner: RefCell>>, +} + +impl RetainedCell { + pub const fn empty() -> Self { + RetainedCell { inner: RefCell::new(None) } + } + + pub const fn new(value: Retained) -> Self { + RetainedCell { inner: RefCell::new(Some(value)) } + } + + pub const fn with(value: Option>) -> Self { + RetainedCell { inner: RefCell::new(value) } + } + + pub fn take(&self) -> Option> { + self.inner.borrow_mut().take() + } +} + +impl RetainedCell { + pub fn get(&self) -> Option> { + match &*self.inner.borrow() { + None => None, + Some(inner) => Some(inner.retain()), + } + } - pub const NSDragOperationNone: NSUInteger = 0; - pub const NSDragOperationCopy: NSUInteger = 1; - pub const NSDragOperationLink: NSUInteger = 2; - pub const NSDragOperationGeneric: NSUInteger = 4; - pub const NSDragOperationMove: NSUInteger = 16; + pub fn set(&self, value: Retained) { + self.inner.replace(Some(value)); + } } -use consts::*; diff --git a/src/macos/view.rs b/src/macos/view.rs index a12d5a0..92fd073 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -1,53 +1,44 @@ -use std::ffi::c_void; - -use cocoa::appkit::{NSEvent, NSFilenamesPboardType, NSView, NSWindow}; -use cocoa::base::{id, nil, BOOL, NO, YES}; -use cocoa::foundation::{NSArray, NSPoint, NSRect, NSSize, NSUInteger}; - -use objc::{ - class, - declare::ClassDecl, - msg_send, - runtime::{Class, Object, Sel}, - sel, sel_impl, +#![allow(deprecated)] // Allow use of NSFilenamesPboardType for now + +use objc2::__framework_prelude::Retained; +use objc2::ffi::objc_disposeClassPair; +use objc2::rc::Allocated; +use objc2::runtime::{ + AnyClass, AnyObject, Bool, ClassBuilder, NSObjectProtocol, ProtocolObject, Sel, +}; +use objc2::{msg_send, sel, AllocAnyThread, ClassType}; +use objc2_app_kit::{ + NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea, + NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification, + NSWindowDidResignKeyNotification, }; +use objc2_foundation::{ + NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, +}; +use std::ffi::{c_void, CStr, CString}; use uuid::Uuid; +use super::keyboard::make_modifiers; +use super::window::WindowState; use crate::MouseEvent::{ButtonPressed, ButtonReleased}; use crate::{ DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, Point, ScrollDelta, Size, WindowEvent, WindowInfo, WindowOpenOptions, }; -use super::keyboard::{from_nsstring, make_modifiers}; -use super::window::WindowState; -use super::{ - NSDragOperationCopy, NSDragOperationGeneric, NSDragOperationLink, NSDragOperationMove, - NSDragOperationNone, -}; - /// Name of the field used to store the `WindowState` pointer. -pub(super) const BASEVIEW_STATE_IVAR: &str = "baseview_state"; - -#[link(name = "AppKit", kind = "framework")] -extern "C" { - static NSWindowDidBecomeKeyNotification: id; - static NSWindowDidResignKeyNotification: id; -} +pub(super) const BASEVIEW_STATE_IVAR: &CStr = c"baseview_state"; macro_rules! add_simple_mouse_class_method { ($class:ident, $sel:ident, $event:expr) => { #[allow(non_snake_case)] - extern "C" fn $sel(this: &Object, _: Sel, _: id){ + extern "C-unwind" fn $sel(this: &NSView, _: Sel, _: &AnyObject){ let state = unsafe { WindowState::from_view(this) }; state.trigger_event(Event::Mouse($event)); } - $class.add_method( - sel!($sel:), - $sel as extern "C" fn(&Object, Sel, id), - ); + $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _,); }; } @@ -56,28 +47,23 @@ macro_rules! add_simple_mouse_class_method { macro_rules! add_mouse_button_class_method { ($class:ident, $sel:ident, $event_ty:ident, $button:expr) => { #[allow(non_snake_case)] - extern "C" fn $sel(this: &Object, _: Sel, event: id){ + extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ let state = unsafe { WindowState::from_view(this) }; - let modifiers = unsafe { NSEvent::modifierFlags(event) }; - state.trigger_event(Event::Mouse($event_ty { button: $button, - modifiers: make_modifiers(modifiers), + modifiers: make_modifiers(event.modifierFlags()), })); } - $class.add_method( - sel!($sel:), - $sel as extern "C" fn(&Object, Sel, id), - ); + $class.add_method(sel!($sel:),$sel as extern "C-unwind" fn(_, _, _) -> _); }; } macro_rules! add_simple_keyboard_class_method { ($class:ident, $sel:ident) => { #[allow(non_snake_case)] - extern "C" fn $sel(this: &Object, _: Sel, event: id){ + extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ let state = unsafe { WindowState::from_view(this) }; if let Some(key_event) = state.process_native_key_event(event){ @@ -93,258 +79,271 @@ macro_rules! add_simple_keyboard_class_method { } } - $class.add_method( - sel!($sel:), - $sel as extern "C" fn(&Object, Sel, id), - ); + $class.add_method(sel!($sel:),$sel as extern "C-unwind" fn(_, _, _) -> _); }; } -unsafe fn register_notification(observer: id, notification_name: id, object: id) { - let notification_center: id = msg_send![class!(NSNotificationCenter), defaultCenter]; - - let _: () = msg_send![ - notification_center, - addObserver:observer - selector:sel!(handleNotification:) - name:notification_name - object:object - ]; -} - -pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id { - let class = create_view_class(); - - let view: id = msg_send![class, alloc]; +pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { + let view: Allocated = { + // SAFETY: We don't access this reference after calling alloc + let class = unsafe { create_view_class() }; + // SAFETY: This function is valid to call, and Allocated is the correct type for the + // returned pointer + unsafe { msg_send![class, alloc] } + }; let size = window_options.size; + let view = NSView::initWithFrame( + view, + NSRect::new(NSPoint::ZERO, NSSize::new(size.width, size.height)), + ); - view.initWithFrame_(NSRect::new(NSPoint::new(0., 0.), NSSize::new(size.width, size.height))); + let notification_center = NSNotificationCenter::defaultCenter(); - register_notification(view, NSWindowDidBecomeKeyNotification, nil); - register_notification(view, NSWindowDidResignKeyNotification, nil); + // SAFETY: Our NSView class does have a handleNotification: method with the matching signature. + unsafe { + notification_center.addObserver_selector_name_object( + &view, + sel!(handleNotification:), + Some(NSWindowDidBecomeKeyNotification), + None, + ); + notification_center.addObserver_selector_name_object( + &view, + sel!(handleNotification:), + Some(NSWindowDidResignKeyNotification), + None, + ); + } - let _: id = msg_send![ - view, - registerForDraggedTypes: NSArray::arrayWithObjects(nil, &[NSFilenamesPboardType]) - ]; + // SAFETY: This static is a read-only constant + let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; + view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); view } -unsafe fn create_view_class() -> &'static Class { +/// # Safety +/// +/// This class is going to be destroyed when its first instance gets deallocated. +/// +/// The returned reference must NOT be used after that point. +unsafe fn create_view_class() -> &'static AnyClass { // 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(); + let class_name = CString::new(format!("BaseviewNSView_{}", Uuid::new_v4().simple())) + // PANIC: This cannot have any NULL bytes + .unwrap(); - class.add_method( - sel!(acceptsFirstResponder), - property_yes as extern "C" fn(&Object, Sel) -> BOOL, - ); - class.add_method( - sel!(becomeFirstResponder), - become_first_responder as extern "C" fn(&Object, Sel) -> BOOL, - ); - class.add_method( - sel!(resignFirstResponder), - resign_first_responder 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, - ); + let mut class = ClassBuilder::new(&class_name, NSView::class()).unwrap(); - class.add_method( - sel!(windowShouldClose:), - window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel)); - class.add_method( - sel!(viewWillMoveToWindow:), - view_will_move_to_window as extern "C" fn(&Object, Sel, id), - ); - class.add_method(sel!(hitTest:), hit_test as extern "C" fn(&Object, Sel, NSPoint) -> id); - class.add_method( - sel!(updateTrackingAreas:), - update_tracking_areas as extern "C" fn(&Object, Sel, id), - ); + // SAFETY: All of these function signatures are correct + unsafe { + class.add_method( + sel!(acceptsFirstResponder), + property_yes as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(becomeFirstResponder), + become_first_responder as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(resignFirstResponder), + resign_first_responder as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method(sel!(isFlipped), property_yes as extern "C-unwind" fn(_, _) -> _); + class.add_method( + sel!(preservesContentInLiveResize), + property_no as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C-unwind" fn(_, _, _) -> _, + ); - 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!(windowShouldClose:), + window_should_close as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method(sel!(dealloc), dealloc as extern "C-unwind" fn(_, _)); + class.add_method( + sel!(viewWillMoveToWindow:), + view_will_move_to_window as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method(sel!(hitTest:), hit_test as extern "C-unwind" fn(_, _, _) -> _); + class.add_method( + sel!(updateTrackingAreas:), + update_tracking_areas as extern "C-unwind" fn(_, _, _) -> _, + ); - class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(&Object, Sel, id)); + class.add_method(sel!(mouseMoved:), mouse_moved as extern "C-unwind" fn(_, _, _) -> _); + class.add_method(sel!(mouseDragged:), mouse_moved as extern "C-unwind" fn(_, _, _) -> _); + class.add_method( + sel!(rightMouseDragged:), + mouse_moved as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(otherMouseDragged:), + mouse_moved as extern "C-unwind" fn(_, _, _) -> _, + ); - class.add_method( - sel!(viewDidChangeBackingProperties:), - view_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - ); + class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C-unwind" fn(_, _, _) -> _); - class.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(&Object, Sel, id) -> NSUInteger, - ); - class.add_method( - sel!(prepareForDragOperation:), - prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - class.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - class.add_method( - sel!(draggingUpdated:), - dragging_updated as extern "C" fn(&Object, Sel, id) -> NSUInteger, - ); - class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id)); - class.add_method( - sel!(handleNotification:), - handle_notification as extern "C" fn(&Object, Sel, id), - ); + class.add_method( + sel!(viewDidChangeBackingProperties:), + view_did_change_backing_properties as extern "C-unwind" fn(_, _, _) -> _, + ); - add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); - add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left); - add_mouse_button_class_method!(class, rightMouseDown, ButtonPressed, MouseButton::Right); - add_mouse_button_class_method!(class, rightMouseUp, ButtonReleased, MouseButton::Right); - add_mouse_button_class_method!(class, otherMouseDown, ButtonPressed, MouseButton::Middle); - add_mouse_button_class_method!(class, otherMouseUp, ButtonReleased, MouseButton::Middle); - add_simple_mouse_class_method!(class, mouseEntered, MouseEvent::CursorEntered); - add_simple_mouse_class_method!(class, mouseExited, MouseEvent::CursorLeft); + class.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(draggingExited:), + dragging_exited as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(handleNotification:), + handle_notification as extern "C-unwind" fn(_, _, _) -> _, + ); - add_simple_keyboard_class_method!(class, keyDown); - add_simple_keyboard_class_method!(class, keyUp); - add_simple_keyboard_class_method!(class, flagsChanged); + add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); + add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left); + add_mouse_button_class_method!(class, rightMouseDown, ButtonPressed, MouseButton::Right); + add_mouse_button_class_method!(class, rightMouseUp, ButtonReleased, MouseButton::Right); + add_mouse_button_class_method!(class, otherMouseDown, ButtonPressed, MouseButton::Middle); + add_mouse_button_class_method!(class, otherMouseUp, ButtonReleased, MouseButton::Middle); + add_simple_mouse_class_method!(class, mouseEntered, MouseEvent::CursorEntered); + add_simple_mouse_class_method!(class, mouseExited, MouseEvent::CursorLeft); + + add_simple_keyboard_class_method!(class, keyDown); + add_simple_keyboard_class_method!(class, keyUp); + add_simple_keyboard_class_method!(class, flagsChanged); + } class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR); class.register() } -extern "C" fn property_yes(_this: &Object, _sel: Sel) -> BOOL { - YES +extern "C-unwind" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { + Bool::YES } -extern "C" fn property_no(_this: &Object, _sel: Sel) -> BOOL { - NO +extern "C-unwind" fn property_no(_this: &NSView, _sel: Sel) -> Bool { + Bool::NO } -extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL { - YES +extern "C-unwind" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { + Bool::YES } -extern "C" fn become_first_responder(this: &Object, _sel: Sel) -> BOOL { - let state = unsafe { WindowState::from_view(this) }; - let is_key_window = unsafe { - let window: id = msg_send![this, window]; - if window != nil { - let is_key_window: BOOL = msg_send![window, isKeyWindow]; - is_key_window == YES - } else { - false - } +extern "C-unwind" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { + let Some(window) = this.window() else { + return Bool::YES; }; - if is_key_window { + + if window.isKeyWindow() { + // SAFETY: This is our own view instance + let state = unsafe { WindowState::from_view(this) }; state.trigger_deferrable_event(Event::Window(WindowEvent::Focused)); } - YES + + Bool::YES } -extern "C" fn resign_first_responder(this: &Object, _sel: Sel) -> BOOL { +extern "C-unwind" fn resign_first_responder(this: &NSView, _sel: Sel) -> Bool { + // SAFETY: This is our own view instance let state = unsafe { WindowState::from_view(this) }; state.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); - YES + Bool::YES } -extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL { +extern "C-unwind" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool { + // SAFETY: This is our own view instance let state = unsafe { WindowState::from_view(this) }; state.trigger_event(Event::Window(WindowEvent::WillClose)); state.window_inner.close(); - NO + Bool::NO } -extern "C" fn dealloc(this: &mut Object, _sel: Sel) { - unsafe { - let class = msg_send![this, class]; - - let superclass = msg_send![this, superclass]; - let () = msg_send![super(this, superclass), dealloc]; +extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { + let class = this.class(); - // Delete class - ::objc::runtime::objc_disposeClassPair(class); + if let Some(superclass) = class.superclass() { + let () = unsafe { msg_send![super(this, superclass), dealloc] }; } + + // SAFETY: This is safe as long as nobody holds a reference to this class. + // On the Baseview side, this is enforced by the safety contract in `create_view_class` + unsafe { objc_disposeClassPair(class as *const _ as *mut _) } } -extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) { - unsafe { - let ns_window: *mut Object = msg_send![this, window]; +extern "C-unwind" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { + let ns_window = this.window(); - let scale_factor: f64 = - if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) }; + let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - let state = WindowState::from_view(this); + // SAFETY: This is our own view instance + let state = unsafe { WindowState::from_view(this) }; - let bounds: NSRect = msg_send![this, bounds]; + let bounds = this.bounds(); - let new_window_info = WindowInfo::from_logical_size( - Size::new(bounds.size.width, bounds.size.height), - scale_factor, - ); + let new_window_info = WindowInfo::from_logical_size( + Size::new(bounds.size.width, bounds.size.height), + scale_factor, + ); - let window_info = state.window_info.get(); + let window_info = state.window_info.get(); - // Only send the event when the window's size has actually changed to be in line with the - // other platform implementations - if new_window_info.physical_size() != window_info.physical_size() { - state.window_info.set(new_window_info); - state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); - } + // Only send the event when the window's size has actually changed to be in line with the + // other platform implementations + if new_window_info.physical_size() != window_info.physical_size() { + state.window_info.set(new_window_info); + state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); } } -/// 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 - ]; +fn new_tracking_area(this: &NSView) -> Retained { + let options = NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + | NSTrackingAreaOptions::CursorUpdate + | NSTrackingAreaOptions::ActiveInActiveApp + | NSTrackingAreaOptions::InVisibleRect + | NSTrackingAreaOptions::EnabledDuringMouseDrag; + + // SAFETY: `this` is of the correct type (NSView) + unsafe { + NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + this.bounds(), + options, + Some(this), + None, + ) + } } /// `hitTest:` override that collapses hits on baseview's internal @@ -370,53 +369,46 @@ unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object) { /// No-op without the `opengl` feature: there's no GL subview to /// collapse, so the override pass-through is equivalent to the /// default implementation. -extern "C" fn hit_test(this: &Object, _sel: Sel, point: NSPoint) -> id { - let super_result: id = unsafe { - let superclass = msg_send![this, superclass]; - msg_send![super(this, superclass), hitTest: point] - }; - if super_result == nil { - return nil; - } +extern "C-unwind" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { + let superclass = this.class().superclass().unwrap(); + // SAFETY: Our superclass is NSView + let super_result: Option<&NSView> = + unsafe { msg_send![super(this, superclass), hitTest: point] }; + let super_result = super_result?; #[cfg(feature = "opengl")] - unsafe { - let state = WindowState::from_view(this); + { + let state = unsafe { WindowState::from_view(this) }; if let Some(gl_context) = state.window_inner.gl_context.as_ref() { if super_result == gl_context.ns_view() { - return this as *const _ as id; + return Some(this); } } } - super_result + Some(super_result) } -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); - - if new_window == nil { - if tracking_area_count != 0 { - let tracking_area = NSArray::objectAtIndex(tracking_areas, 0); +extern "C-unwind" fn view_will_move_to_window( + this: &NSView, _self: Sel, new_window: Option<&NSWindow>, +) { + let tracking_areas = this.trackingAreas(); - let _: () = msg_send![this, removeTrackingArea: tracking_area]; - let _: () = msg_send![tracking_area, release]; + match new_window { + None => { + if tracking_areas.count() > 0 { + let tracking_area = tracking_areas.objectAtIndex(0); + this.removeTrackingArea(&tracking_area); } - } 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]; + } + Some(new_window) => { + if tracking_areas.is_empty() { + let tracking_area = new_tracking_area(this); + this.addTrackingArea(&tracking_area); } - let _: () = msg_send![new_window, setAcceptsMouseMovedEvents: YES]; - let _: () = msg_send![new_window, makeFirstResponder: this]; + new_window.setAcceptsMouseMovedEvents(true); + new_window.makeFirstResponder(Some(this)); } } @@ -427,95 +419,94 @@ extern "C" fn view_will_move_to_window(this: &Object, _self: Sel, new_window: id } } -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-unwind" fn update_tracking_areas(this: &NSView, _self: Sel, _: &AnyObject) { + let tracking_areas = this.trackingAreas(); + if tracking_areas.count() > 0 { + let tracking_area = tracking_areas.objectAtIndex(0); + this.removeTrackingArea(&tracking_area); } -} -extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) { - let state = unsafe { WindowState::from_view(this) }; + let tracking_area = new_tracking_area(this); - let point: NSPoint = unsafe { - let point = NSEvent::locationInWindow(event); + this.addTrackingArea(&tracking_area); +} - msg_send![this, convertPoint:point fromView:nil] - }; - let modifiers = unsafe { NSEvent::modifierFlags(event) }; +extern "C-unwind" fn mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { + let state = unsafe { WindowState::from_view(this) }; + let point = this.convertPoint_fromView(event.locationInWindow(), None); let position = Point { x: point.x, y: point.y }; state.trigger_event(Event::Mouse(MouseEvent::CursorMoved { position, - modifiers: make_modifiers(modifiers), + modifiers: make_modifiers(event.modifierFlags()), })); } -extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) { +extern "C-unwind" fn scroll_wheel(this: &NSView, _: Sel, event: &NSEvent) { let state = unsafe { WindowState::from_view(this) }; - let delta = unsafe { - let x = NSEvent::scrollingDeltaX(event) as f32; - let y = NSEvent::scrollingDeltaY(event) as f32; + let x = event.scrollingDeltaX() as f32; + let y = event.scrollingDeltaY() as f32; - if NSEvent::hasPreciseScrollingDeltas(event) != NO { - ScrollDelta::Pixels { x, y } - } else { - ScrollDelta::Lines { x, y } - } + let delta = if event.hasPreciseScrollingDeltas() { + ScrollDelta::Pixels { x, y } + } else { + ScrollDelta::Lines { x, y } }; - let modifiers = unsafe { NSEvent::modifierFlags(event) }; - state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { delta, - modifiers: make_modifiers(modifiers), + modifiers: make_modifiers(event.modifierFlags()), })); } -fn get_drag_position(sender: id) -> Point { - let point: NSPoint = unsafe { msg_send![sender, draggingLocation] }; +fn get_drag_position(sender: Option<&ProtocolObject>) -> Point { + let point = match sender { + Some(sender) => sender.draggingLocation(), + None => NSPoint::ZERO, + }; + Point::new(point.x, point.y) } -fn get_drop_data(sender: id) -> DropData { - if sender == nil { +fn get_drop_data(sender: Option<&ProtocolObject>) -> DropData { + let Some(sender) = sender else { return DropData::None; - } + }; - unsafe { - let pasteboard: id = msg_send![sender, draggingPasteboard]; - let file_list: id = msg_send![pasteboard, propertyListForType: NSFilenamesPboardType]; + let pasteboard = sender.draggingPasteboard(); + let Some(file_list) = pasteboard.propertyListForType(unsafe { NSFilenamesPboardType }) else { + return DropData::None; + }; - if file_list == nil { - return DropData::None; - } + let Ok(file_list) = file_list.downcast::() else { + return DropData::None; + }; - let mut files = vec![]; - for i in 0..NSArray::count(file_list) { - let data = NSArray::objectAtIndex(file_list, i); - files.push(from_nsstring(data).into()); - } + let files = file_list + .into_iter() + .filter_map(|i| i.downcast::().ok()) + .map(|s| s.to_string().into()) + .collect(); - DropData::Files(files) - } + DropData::Files(files) } -fn on_event(window_state: &WindowState, event: MouseEvent) -> NSUInteger { +fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { let event_status = window_state.trigger_event(Event::Mouse(event)); match event_status { - EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperationCopy, - EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperationMove, - EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperationLink, - EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperationGeneric, - _ => NSDragOperationNone, + EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperation::Copy, + EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperation::Move, + EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperation::Link, + EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperation::Generic, + _ => NSDragOperation::None, } } -extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteger { +extern "C-unwind" fn dragging_entered( + this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, +) -> NSDragOperation { let state = unsafe { WindowState::from_view(this) }; let modifiers = state.keyboard_state().last_mods(); let drop_data = get_drop_data(sender); @@ -529,7 +520,9 @@ extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteg on_event(&state, event) } -extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteger { +extern "C-unwind" fn dragging_updated( + this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, +) -> NSDragOperation { let state = unsafe { WindowState::from_view(this) }; let modifiers = state.keyboard_state().last_mods(); let drop_data = get_drop_data(sender); @@ -543,14 +536,18 @@ extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteg on_event(&state, event) } -extern "C" fn prepare_for_drag_operation(_this: &Object, _sel: Sel, _sender: id) -> BOOL { +extern "C-unwind" fn prepare_for_drag_operation( + _this: &NSView, _sel: Sel, _sender: Option<&ProtocolObject>, +) -> Bool { // Always accept drag operation if we get this far // This function won't be called unless dragging_entered/updated // has returned an acceptable operation - YES + Bool::YES } -extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BOOL { +extern "C-unwind" fn perform_drag_operation( + this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, +) -> Bool { let state = unsafe { WindowState::from_view(this) }; let modifiers = state.keyboard_state().last_mods(); let drop_data = get_drop_data(sender); @@ -562,41 +559,47 @@ extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BO }; let event_status = state.trigger_event(Event::Mouse(event)); + match event_status { - EventStatus::AcceptDrop(_) => YES, - _ => NO, + EventStatus::AcceptDrop(_) => Bool::YES, + _ => Bool::NO, } } -extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) { +extern "C-unwind" fn dragging_exited( + this: &NSView, _sel: Sel, _sender: Option<&ProtocolObject>, +) { let state = unsafe { WindowState::from_view(this) }; on_event(&state, MouseEvent::DragLeft); } -extern "C" fn handle_notification(this: &Object, _cmd: Sel, notification: id) { - unsafe { - let state = WindowState::from_view(this); - - // The subject of the notication, in this case an NSWindow object. - let notification_object: id = msg_send![notification, object]; - - // The NSWindow object associated with our NSView. - let window: id = msg_send![this, window]; - - let first_responder: id = msg_send![window, firstResponder]; - - // Only trigger focus events if the NSWindow that's being notified about is our window, - // and if the window's first responder is our NSView. - // If the first responder isn't our NSView, the focus events will instead be triggered - // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. - if notification_object == window && first_responder == this as *const Object as id { - let is_key_window: BOOL = msg_send![window, isKeyWindow]; - state.trigger_event(Event::Window(if is_key_window == YES { - WindowEvent::Focused - } else { - WindowEvent::Unfocused - })); - } +extern "C-unwind" fn handle_notification(this: &NSView, _cmd: Sel, notification: &NSNotification) { + let state = unsafe { WindowState::from_view(this) }; + + let Some(window) = this.window() else { return }; + // The subject of the notification, in this case an NSWindow object. + let Some(notification_object) = notification.object().and_then(|o| o.downcast().ok()) else { + return; + }; + + // Only trigger focus events if the NSWindow that's being notified about is our window, + // and if the window's first responder is our NSView. + if window != notification_object { + return; } + + let Some(first_responder) = window.firstResponder() else { return }; + + // If the first responder isn't our NSView, the focus events will instead be triggered + // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. + if !this.isEqual(Some(&first_responder)) { + return; + } + + state.trigger_event(Event::Window(if window.isKeyWindow() { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + })); } diff --git a/src/macos/window.rs b/src/macos/window.rs index b94c839..4d1b95d 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -4,18 +4,18 @@ use std::ffi::c_void; use std::ptr; use std::rc::Rc; -use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, - NSPasteboard, NSView, NSWindow, NSWindowStyleMask, +use keyboard_types::KeyboardEvent; +use objc2::rc::Retained; +use objc2::runtime::NSObjectProtocol; +use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSPasteboard, + NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, }; -use cocoa::base::{id, nil, BOOL, NO, YES}; -use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString}; -use core_foundation::runloop::{ - __CFRunLoopTimer, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, }; -use keyboard_types::KeyboardEvent; -use objc::class; -use objc::{msg_send, runtime::Object, sel, sel_impl}; +use objc2_foundation::{NSNotificationCenter, NSPoint, NSRect, NSSize, NSString}; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, @@ -31,6 +31,7 @@ use super::view::{create_view, BASEVIEW_STATE_IVAR}; #[cfg(feature = "opengl")] use crate::gl::{GlConfig, GlContext}; +use crate::macos::RetainedCell; pub struct WindowHandle { state: Rc, @@ -57,16 +58,16 @@ pub(super) struct WindowInner { /// Only set if we created the parent window, i.e. we are running in /// parentless mode - ns_app: Cell>, + ns_app: RetainedCell, /// Only set if we created the parent window, i.e. we are running in /// parentless mode - ns_window: Cell>, + ns_window: RetainedCell, /// Only set when running in parented mode. - parent_ns_window: Option, + parent_ns_window: RetainedCell, /// Our subclassed NSView - ns_view: id, + ns_view: RetainedCell, #[cfg(feature = "opengl")] pub(super) gl_context: Option, @@ -76,20 +77,30 @@ impl WindowInner { pub(super) fn close(&self) { if self.open.get() { self.open.set(false); + let Some(ns_view) = self.ns_view.take() else { + return; + }; + unsafe { // Take back ownership of the NSView's Rc - let state_ptr: *const c_void = *(*self.ns_view).get_ivar(BASEVIEW_STATE_IVAR); + let state_ptr: *const c_void = *ns_view + .class() + .instance_variable(BASEVIEW_STATE_IVAR) + .unwrap() + .load::<*const c_void>(&ns_view); + let window_state = Rc::from_raw(state_ptr as *mut WindowState); // Cancel the frame timer if let Some(frame_timer) = window_state.frame_timer.take() { - CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode); + if let Some(run_loop) = CFRunLoop::current() { + run_loop.remove_timer(Some(&frame_timer), kCFRunLoopDefaultMode); + } } // Deregister NSView from NotificationCenter. - let notification_center: id = - msg_send![class!(NSNotificationCenter), defaultCenter]; - let () = msg_send![notification_center, removeObserver:self.ns_view]; + let notification_center = NSNotificationCenter::defaultCenter(); + notification_center.removeObserver(&ns_view); drop(window_state); @@ -99,32 +110,36 @@ impl WindowInner { } // Ensure that the NSView is detached from the parent window - self.ns_view.removeFromSuperview(); - let () = msg_send![self.ns_view as id, release]; + ns_view.removeFromSuperview(); + drop(ns_view); // If in non-parented mode, we want to also quit the app altogether let app = self.ns_app.take(); if let Some(app) = app { - app.stop_(app); + app.stop(Some(&app)); } } } } fn raw_window_handle(&self) -> RawWindowHandle { + let mut handle = AppKitWindowHandle::empty(); + if self.open.get() { - let ns_window = - self.ns_window.get().or(self.parent_ns_window).unwrap_or(ptr::null_mut()) - as *mut c_void; + let ns_window = self.ns_window.get().or(self.parent_ns_window.get()); - let mut handle = AppKitWindowHandle::empty(); - handle.ns_window = ns_window; - handle.ns_view = self.ns_view as *mut c_void; + handle.ns_window = match ns_window { + None => ptr::null_mut(), + Some(view) => (&*view as *const NSWindow) as *mut _, + }; - return RawWindowHandle::AppKit(handle); + handle.ns_view = match self.ns_view.get() { + None => ptr::null_mut(), + Some(view) => (&*view as *const NSView) as *mut _, + }; } - RawWindowHandle::AppKit(AppKitWindowHandle::empty()) + handle.into() } } @@ -140,8 +155,6 @@ impl<'a> Window<'a> { B: FnOnce(&mut crate::Window) -> H, B: Send + 'static, { - let pool = unsafe { NSAutoreleasePool::new(nil) }; - let scaling = match options.scale { WindowScalePolicy::ScaleFactor(scale) => scale, WindowScalePolicy::SystemScaleFactor => 1.0, @@ -155,31 +168,27 @@ impl<'a> Window<'a> { panic!("Not a macOS window"); }; - let ns_view = unsafe { create_view(&options) }; + let ns_view = create_view(&options); + let parent_window = unsafe { Retained::retain(handle.ns_window as *mut NSWindow) }; + let parent_view = unsafe { Retained::retain(handle.ns_view as *mut NSView) }; let window_inner = WindowInner { open: Cell::new(true), - ns_app: Cell::new(None), - ns_window: Cell::new(None), - parent_ns_window: if handle.ns_window.is_null() { - None - } else { - Some(handle.ns_window.cast()) - }, - ns_view, + ns_app: RetainedCell::empty(), + ns_window: RetainedCell::empty(), + parent_ns_window: RetainedCell::with(parent_window.clone()), + ns_view: RetainedCell::new(ns_view.clone()), #[cfg(feature = "opengl")] gl_context: options .gl_config - .map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)), + .map(|gl_config| Self::create_gl_context(None, &ns_view, gl_config)), }; let window_handle = Self::init(window_inner, window_info, build); - unsafe { - let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view]; - - let () = msg_send![pool, drain]; + if let Some(parent_view) = parent_view { + parent_view.addSubview(&ns_view); } window_handle @@ -191,75 +200,71 @@ impl<'a> Window<'a> { B: FnOnce(&mut crate::Window) -> H, B: Send + 'static, { - let pool = unsafe { NSAutoreleasePool::new(nil) }; + let Some(mtm) = MainThreadMarker::new() else { + panic!("macOS: open_blocking can only be called on the main thread!") + }; - // 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 - let app = unsafe { NSApp() }; + // Creates the global NSApplication instance, if it doesn't exist yet + let app = NSApplication::sharedApplication(mtm); - unsafe { - app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - } + let _ = app.setActivationPolicy(NSApplicationActivationPolicy::Regular); let scaling = match options.scale { WindowScalePolicy::ScaleFactor(scale) => scale, WindowScalePolicy::SystemScaleFactor => 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, window_info.logical_size().height), + NSPoint::ZERO, + NSSize { width: options.size.width, height: options.size.height }, ); + let window_info = WindowInfo::from_logical_size(options.size, scaling); + + // SAFETY: This is safe because of the setReleasedWhenClosed(false) below let ns_window = unsafe { - let ns_window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_( + NSWindow::initWithContentRect_styleMask_backing_defer( + NSWindow::alloc(mtm), rect, - NSWindowStyleMask::NSTitledWindowMask - | NSWindowStyleMask::NSClosableWindowMask - | NSWindowStyleMask::NSMiniaturizableWindowMask, - NSBackingStoreBuffered, - NO, - ); - ns_window.center(); + NSWindowStyleMask::Titled + | NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable, + NSBackingStoreType::Buffered, + false, + ) + }; - let title = NSString::alloc(nil).init_str(&options.title).autorelease(); - ns_window.setTitle_(title); + // SAFETY: setReleasedWhenClosed is always safe to call with `false` (worst case is a memory leak) + unsafe { ns_window.setReleasedWhenClosed(false) }; - ns_window.makeKeyAndOrderFront_(nil); + ns_window.center(); - ns_window - }; + let title = NSString::from_str(&options.title); + ns_window.setTitle(&title); - let ns_view = unsafe { create_view(&options) }; + ns_window.makeKeyAndOrderFront(None); + let ns_view = create_view(&options); let window_inner = WindowInner { open: Cell::new(true), - ns_app: Cell::new(Some(app)), - ns_window: Cell::new(Some(ns_window)), - parent_ns_window: None, - ns_view, + ns_app: RetainedCell::new(app.clone()), + parent_ns_window: RetainedCell::empty(), + ns_view: RetainedCell::new(ns_view.clone()), #[cfg(feature = "opengl")] gl_context: options .gl_config - .map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)), + .map(|gl_config| Self::create_gl_context(Some(&ns_window), &ns_view, gl_config)), + + ns_window: RetainedCell::new(ns_window.clone()), }; let _ = Self::init(window_inner, window_info, build); - unsafe { - ns_window.setContentView_(ns_view); - ns_window.setDelegate_(ns_view); - - let () = msg_send![pool, drain]; + ns_window.setContentView(Some(&ns_view)); + let () = unsafe { msg_send![&*ns_window, setDelegate: &*ns_view] }; - app.run(); - } + app.run(); } fn init(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle @@ -271,13 +276,13 @@ impl<'a> Window<'a> { let mut window = crate::Window::new(Window { inner: &window_inner }); let window_handler = Box::new(build(&mut window)); - let ns_view = window_inner.ns_view; + let ns_view = window_inner.ns_view.get().unwrap(); let window_state = Rc::new(WindowState { window_inner, window_handler: RefCell::new(window_handler), keyboard_state: KeyboardState::new(), - frame_timer: Cell::new(None), + frame_timer: RetainedCell::empty(), window_info: Cell::new(window_info), deferred_events: RefCell::default(), }); @@ -285,7 +290,16 @@ impl<'a> Window<'a> { let window_state_ptr = Rc::into_raw(Rc::clone(&window_state)); unsafe { - (*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void); + // This creates a cyclic reference: WindowState > WindowInner > NSView > WindowState. + // This cycle gets broken in WindowInner::close and everything is released properly. + // However, this means the cycle holds and the whole leaks if close() is not called. (e.g. if simply dropped) + // This should be refactored at some point to fix this issue. + ns_view + .class() + .instance_variable(BASEVIEW_STATE_IVAR) + .unwrap() + .load_ptr::<*const c_void>(&ns_view) + .write(window_state_ptr as *const c_void); WindowState::setup_timer(window_state_ptr); } @@ -298,26 +312,26 @@ impl<'a> Window<'a> { } pub fn has_focus(&mut self) -> bool { - unsafe { - let view = self.inner.ns_view.as_mut().unwrap(); - let window: id = msg_send![view, window]; - if window == nil { - return false; - }; - let first_responder: id = msg_send![window, firstResponder]; - let is_key_window: BOOL = msg_send![window, isKeyWindow]; - let is_focused: BOOL = msg_send![view, isEqual: first_responder]; - is_key_window == YES && is_focused == YES + let view = self.inner.ns_view.get().unwrap(); + let Some(window) = view.window() else { + return false; + }; + + if !window.isKeyWindow() { + return false; } + + let Some(first_responder) = window.firstResponder() else { + return false; + }; + + view.isEqual(Some(&*first_responder)) } pub fn focus(&mut self) { - unsafe { - let view = self.inner.ns_view.as_mut().unwrap(); - let window: id = msg_send![view, window]; - if window != nil { - msg_send![window, makeFirstResponder:view] - } + let view = self.inner.ns_view.get().unwrap(); + if let Some(window) = view.window() { + window.makeFirstResponder(Some(&view)); } } @@ -327,9 +341,9 @@ impl<'a> Window<'a> { // though the size is in fractional pixels. let size = NSSize::new(size.width.round(), size.height.round()); - unsafe { NSView::setFrameSize(self.inner.ns_view, size) }; - unsafe { - let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES]; + if let Some(view) = self.inner.ns_view.get() { + view.setFrameSize(size); + view.setNeedsDisplay(true); } // When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because @@ -341,7 +355,7 @@ impl<'a> Window<'a> { // If this is a standalone window then we'll also need to resize the window itself if let Some(ns_window) = self.inner.ns_window.get() { - unsafe { NSWindow::setContentSize_(ns_window, size) }; + ns_window.setContentSize(size); } } } @@ -356,10 +370,15 @@ impl<'a> Window<'a> { } #[cfg(feature = "opengl")] - fn create_gl_context(ns_window: Option, ns_view: id, config: GlConfig) -> GlContext { + fn create_gl_context( + ns_window: Option<&NSWindow>, ns_view: &NSView, config: GlConfig, + ) -> GlContext { let mut handle = AppKitWindowHandle::empty(); - handle.ns_window = ns_window.unwrap_or(ptr::null_mut()) as *mut c_void; - handle.ns_view = ns_view as *mut c_void; + handle.ns_window = match ns_window { + Some(ns_window) => ns_window as *const NSWindow as *mut c_void, + None => ptr::null_mut(), + }; + handle.ns_view = ns_view as *const NSView as *mut c_void; let handle = RawWindowHandle::AppKit(handle); unsafe { GlContext::create(&handle, config).expect("Could not create OpenGL context") } @@ -370,7 +389,7 @@ pub(super) struct WindowState { pub(super) window_inner: WindowInner, window_handler: RefCell>, keyboard_state: KeyboardState, - frame_timer: Cell>, + frame_timer: RetainedCell, /// The last known window info for this window. pub window_info: Cell, @@ -384,10 +403,19 @@ impl WindowState { /// This method returns a cloned `Rc` rather than just a `&WindowState`, since the /// original `Rc` owned by the `NSView` can be dropped at any time /// (including during an event handler). - pub(super) unsafe fn from_view(view: &Object) -> Rc { - let state_ptr: *const c_void = *view.get_ivar(BASEVIEW_STATE_IVAR); - - let state_rc = Rc::from_raw(state_ptr as *const WindowState); + /// + /// # Safety + /// + /// `view` MUST be our own NSView, as created by `create_view` + pub(super) unsafe fn from_view(view: &NSView) -> Rc { + let state_ptr = view + .class() + .instance_variable(BASEVIEW_STATE_IVAR) + .unwrap() + .load::<*const c_void>(view) + .cast::(); + + let state_rc = Rc::from_raw(state_ptr); let state = Rc::clone(&state_rc); let _ = Rc::into_raw(state_rc); @@ -428,12 +456,14 @@ impl WindowState { &self.keyboard_state } - pub(super) fn process_native_key_event(&self, event: *mut Object) -> Option { + pub(super) fn process_native_key_event(&self, event: &NSEvent) -> Option { self.keyboard_state.process_native_event(event) } unsafe fn setup_timer(window_state_ptr: *const WindowState) { - extern "C" fn timer_callback(_: *mut __CFRunLoopTimer, window_state_ptr: *mut c_void) { + unsafe extern "C-unwind" fn timer_callback( + _: *mut CFRunLoopTimer, window_state_ptr: *mut c_void, + ) { unsafe { let window_state = &*(window_state_ptr as *const WindowState); @@ -441,6 +471,10 @@ impl WindowState { } } + let Some(current_loop) = CFRunLoop::current() else { + return; + }; + let mut timer_context = CFRunLoopTimerContext { version: 0, info: window_state_ptr as *mut c_void, @@ -449,11 +483,21 @@ impl WindowState { copyDescription: None, }; - let timer = CFRunLoopTimer::new(0.0, 0.015, 0, 0, timer_callback, &mut timer_context); + let Some(timer) = CFRunLoopTimer::new( + kCFAllocatorDefault, + 0.0, + 0.015, + 0, + 0, + Some(timer_callback), + &mut timer_context, + ) else { + return; + }; - CFRunLoop::get_current().add_timer(&timer, kCFRunLoopDefaultMode); + current_loop.add_timer(Some(&timer), kCFRunLoopDefaultMode); - (*window_state_ptr).frame_timer.set(Some(timer)); + (*window_state_ptr).frame_timer.set(timer.into()); } fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) { @@ -482,12 +526,9 @@ unsafe impl<'a> HasRawDisplayHandle for Window<'a> { } pub fn copy_to_clipboard(string: &str) { - unsafe { - let pb = NSPasteboard::generalPasteboard(nil); + let pb = NSPasteboard::generalPasteboard(); + let ns_str = NSString::from_str(string); - let ns_str = NSString::alloc(nil).init_str(string); - - pb.clearContents(); - pb.setString_forType(ns_str, cocoa::appkit::NSPasteboardTypeString); - } + pb.clearContents(); + pb.setString_forType(&ns_str, unsafe { NSPasteboardTypeString }); }