From bc2e33695d423ff51a22ce4ee0299800d356feff Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 10:22:38 +0200 Subject: [PATCH 01/11] wip on objc2 migration --- Cargo.toml | 10 +- src/gl/macos.rs | 114 ++++---- src/gl/mod.rs | 4 +- src/macos/keyboard.rs | 128 ++++----- src/macos/mod.rs | 12 - src/macos/view.rs | 590 +++++++++++++++++++----------------------- src/macos/window.rs | 124 +++++---- 7 files changed, 432 insertions(+), 550 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f397425..d8157b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = [ "Robbert van der Helm ", "Adrien Prokopowicz " ] -edition = "2018" +edition = "2024" license = "MIT OR Apache-2.0" description = "Low-level windowing system geared towards making audio plugin UIs." keywords = ["windowing", "audio", "plugin"] @@ -39,10 +39,12 @@ 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"] } +# TODO: only enable needed features +objc2 = "0.6.4" +objc2-core-foundation = "0.3.2" +objc2-foundation = "0.3.2" +objc2-app-kit = "0.3.2" [dev-dependencies] rtrb = "0.2" diff --git a/src/gl/macos.rs b/src/gl/macos.rs index 9f9456cd..f8c2066f 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -1,30 +1,27 @@ -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::ffi::{id, nil}; +use objc2::rc::Retained; +use objc2::{AllocAnyThread, msg_send}; +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::{NSBundle, NSSize, ns_string}; +use raw_window_handle::RawWindowHandle; +use std::ffi::c_void; +use std::ptr::NonNull; +use std::str::FromStr; pub type CreationFailedError = (); pub struct GlContext { - view: id, - context: id, + view: Retained, + context: Retained, } impl GlContext { @@ -35,11 +32,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 +72,29 @@ impl GlContext { attrs.push(0); - let pixel_format = NSOpenGLPixelFormat::alloc(nil).initWithAttributes_(&attrs); + let pixel_format = NSOpenGLPixelFormat::initWithAttributes( + NSOpenGLPixelFormat::alloc(), + NonNull::new(attrs.as_mut_ptr()).unwrap(), + ) + .ok_or(GlError::CreationFailed(()))?; - if pixel_format == nil { - return Err(GlError::CreationFailed(())); - } + let view = NSOpenGLView::initWithFrame_pixelFormat( + NSOpenGLView::alloc(), + parent_view.frame(), + Some(&pixel_format), + ) + .ok_or(GlError::CreationFailed(()))?; - let view = - NSOpenGLView::alloc(nil).initWithFrame_pixelFormat_(parent_view.frame(), pixel_format); + view.setWantsBestResolutionOpenGLSurface(true); - if view == nil { - return Err(GlError::CreationFailed(())); - } + view.display(); + parent_view.addSubview(&view); - view.setWantsBestResolutionOpenGLSurface_(YES); + let context = view.openGLContext().ok_or(GlError::CreationFailed(()))?; - NSOpenGLView::display_(view); - parent_view.addSubview_(view); + let value = config.vsync as i32; - let context: id = msg_send![view, openGLContext]; - let () = msg_send![context, retain]; - - context.setValues_forParameter_( - &(config.vsync as i32), - NSOpenGLContextParameter::NSOpenGLCPSwapInterval, - ); - - let () = msg_send![pixel_format, release]; + context.setValues_forParameter((&value).into(), NSOpenGLContextParameter::SwapInterval); Ok(GlContext { view, context }) } @@ -112,47 +104,35 @@ impl GlContext { } pub unsafe fn make_not_current(&self) { - NSOpenGLContext::clearCurrentContext(self.context); + if Some(self.context) == NSOpenGLContext::currentContext() { + 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 5cda059a..77b29b25 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 b38c1209..d4c9b3b5 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,65 @@ 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 - } - } 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; - } + pub(crate) fn process_native_event(&self, event: &NSEvent) -> Option { + 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::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 { 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()); + if is_valid_key(&event.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()); + 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 02a0e36f..e1fa7b87 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -3,15 +3,3 @@ mod view; mod window; pub use window::*; - -#[allow(non_upper_case_globals)] -mod consts { - use cocoa::foundation::NSUInteger; - - 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; -} -use consts::*; diff --git a/src/macos/view.rs b/src/macos/view.rs index a12d5a06..44f003a0 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -1,53 +1,40 @@ -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, +use objc2::__framework_prelude::Retained; +use objc2::ffi::{id, objc_disposeClassPair}; +use objc2::rc::Allocated; +use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, ProtocolObject, Sel}; +use objc2::{AllocAnyThread, ClassType, Message, msg_send, sel}; +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::{CStr, CString, c_void}; 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" fn $sel(this: &NSView, _: Sel, _: id){ 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,); }; } @@ -56,28 +43,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" 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); }; } 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" fn $sel(this: &NSView, _: Sel, event: id){ let state = unsafe { WindowState::from_view(this) }; if let Some(key_event) = state.process_native_key_event(event){ @@ -93,208 +75,169 @@ 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); }; } -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 { +pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { let class = create_view_class(); - let view: id = msg_send![class, alloc]; + let view: Allocated = 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: TODO + 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: TODO + let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; + view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); view } -unsafe fn create_view_class() -> &'static Class { +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(); - - 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, - ); - - 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), - ); - - 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!(scrollWheel:), scroll_wheel as extern "C" fn(&Object, Sel, id)); - - class.add_method( - sel!(viewDidChangeBackingProperties:), - view_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - ); - - 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), - ); + let class_name = CString::new(format!("BaseviewNSView_{}", Uuid::new_v4().to_simple())) + // PANIC: This cannot have any NULL bytes + .unwrap(); - 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); + let mut class = ClassBuilder::new(&class_name, NSView::class()).unwrap(); - add_simple_keyboard_class_method!(class, keyDown); - add_simple_keyboard_class_method!(class, keyUp); - add_simple_keyboard_class_method!(class, flagsChanged); + // SAFETY: All of these function signatures are correct + unsafe { + class.add_method(sel!(acceptsFirstResponder), property_yes); + class.add_method(sel!(becomeFirstResponder), become_first_responder); + class.add_method(sel!(resignFirstResponder), resign_first_responder); + class.add_method(sel!(isFlipped), property_yes); + class.add_method(sel!(preservesContentInLiveResize), property_no); + class.add_method(sel!(acceptsFirstMouse:), accepts_first_mouse); + + class.add_method(sel!(windowShouldClose:), window_should_close); + class.add_method(sel!(dealloc), dealloc); + class.add_method(sel!(viewWillMoveToWindow:), view_will_move_to_window); + class.add_method(sel!(hitTest:), hit_test); + class.add_method(sel!(updateTrackingAreas:), update_tracking_areas); + + class.add_method(sel!(mouseMoved:), mouse_moved); + class.add_method(sel!(mouseDragged:), mouse_moved); + class.add_method(sel!(rightMouseDragged:), mouse_moved); + class.add_method(sel!(otherMouseDragged:), mouse_moved); + + class.add_method(sel!(scrollWheel:), scroll_wheel); + + class.add_method(sel!(viewDidChangeBackingProperties:), view_did_change_backing_properties); + + class.add_method(sel!(draggingEntered:), dragging_entered); + class.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation); + class.add_method(sel!(performDragOperation:), perform_drag_operation); + class.add_method(sel!(draggingUpdated:), dragging_updated); + class.add_method(sel!(draggingExited:), dragging_exited); + class.add_method(sel!(handleNotification:), handle_notification); + + 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" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { + Bool::YES } -extern "C" fn property_no(_this: &Object, _sel: Sel) -> BOOL { - NO +extern "C" 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" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: id) -> 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" 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() { + 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" fn resign_first_responder(this: &NSView, _sel: Sel) -> Bool { 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" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool { 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]; +extern "C" fn dealloc(this: &mut NSView, _sel: Sel) { + let class = this.class(); - let superclass = msg_send![this, superclass]; + if let Some(superclass) = class.superclass() { let () = msg_send![super(this, superclass), dealloc]; - - // Delete class - ::objc::runtime::objc_disposeClassPair(class); } + + // Delete class + // SAFETY: TODO: nope, this is NOT sound, as this invalidates any &AnyClass + unsafe { objc_disposeClassPair(class as *const _ as *mut _) } } -extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) { +extern "C" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { unsafe { - let ns_window: *mut Object = msg_send![this, window]; + 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: TODO + let state = unsafe { WindowState::from_view(this) }; let bounds: NSRect = msg_send![this, bounds]; @@ -314,37 +257,28 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) { } } -/// 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 +304,42 @@ 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" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option> { + // SAFETY: TODO + let super_result: Option> = unsafe { msg_send![super(this), 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; + if &*super_result == gl_context.ns_view() { + return Some(this.retain()); } } } - 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" 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 +350,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" 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" 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" 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" 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 +451,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" 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 +467,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" 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" 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 +490,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" 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" 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 first_responder.as_ref() != this { + 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 b94c8398..2998b106 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -4,18 +4,16 @@ use std::ffi::c_void; use std::ptr; use std::rc::Rc; -use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, - NSPasteboard, 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 keyboard_types::KeyboardEvent; -use objc::class; -use objc::{msg_send, runtime::Object, sel, sel_impl}; +use objc2::rc::Retained; +use objc2::runtime::{AnyObject, Ivar}; +use objc2::{MainThreadMarker, MainThreadOnly}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSView, NSWindow, + NSWindowStyleMask, +}; +use objc2_core_foundation::{CFRunLoop, CFRunLoopMode, kCFRunLoopDefaultMode}; +use objc2_foundation::{NSNotificationCenter, NSPoint, NSRect, NSSize, NSString}; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, @@ -27,7 +25,7 @@ use crate::{ }; use super::keyboard::KeyboardState; -use super::view::{create_view, BASEVIEW_STATE_IVAR}; +use super::view::{BASEVIEW_STATE_IVAR, create_view}; #[cfg(feature = "opengl")] use crate::gl::{GlConfig, GlContext}; @@ -57,16 +55,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: Cell>>, /// Only set if we created the parent window, i.e. we are running in /// parentless mode - ns_window: Cell>, + ns_window: Cell>>, /// Only set when running in parented mode. - parent_ns_window: Option, + parent_ns_window: Cell>>, /// Our subclassed NSView - ns_view: id, + ns_view: Cell>>, #[cfg(feature = "opengl")] pub(super) gl_context: Option, @@ -76,6 +74,10 @@ 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); @@ -83,13 +85,12 @@ impl WindowInner { // Cancel the frame timer if let Some(frame_timer) = window_state.frame_timer.take() { - CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode); + CFRunLoop::current()?.remove_timer(&frame_timer, Some(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,13 +100,13 @@ 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(app); } } } @@ -191,75 +192,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: TODO 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: TODO + 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 = unsafe { create_view(&options) }; let window_inner = WindowInner { open: Cell::new(true), - ns_app: Cell::new(Some(app)), - ns_window: Cell::new(Some(ns_window)), + ns_app: Cell::new(Some(app.clone())), parent_ns_window: None, ns_view, #[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: Cell::new(Some(ns_window.clone())), }; let _ = Self::init(window_inner, window_info, build); - unsafe { - ns_window.setContentView_(ns_view); - ns_window.setDelegate_(ns_view); + ns_window.setContentView(Some(&ns_view)); + ns_window.setDelegate(Some(&ns_view)); - let () = msg_send![pool, drain]; - - app.run(); - } + app.run(); } fn init(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle @@ -384,8 +381,9 @@ 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); + pub(super) unsafe fn from_view(view: &NSView) -> Rc { + let state_ptr: *const c_void = + view.class().instance_variable(BASEVIEW_STATE_IVAR).unwrap().load(view); let state_rc = Rc::from_raw(state_ptr as *const WindowState); let state = Rc::clone(&state_rc); From adcc5d1a2698008c766ff14771a3087c0547a614 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 17:15:03 +0200 Subject: [PATCH 02/11] wip --- Cargo.toml | 2 +- src/gl/macos.rs | 5 +- src/macos/mod.rs | 38 ++++++++++ src/macos/view.rs | 6 +- src/macos/window.rs | 169 ++++++++++++++++++++++++-------------------- 5 files changed, 138 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8157b52..31597475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = [ "Robbert van der Helm ", "Adrien Prokopowicz " ] -edition = "2024" +edition = "2021" license = "MIT OR Apache-2.0" description = "Low-level windowing system geared towards making audio plugin UIs." keywords = ["windowing", "audio", "plugin"] diff --git a/src/gl/macos.rs b/src/gl/macos.rs index f8c2066f..96119ade 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -1,9 +1,8 @@ #![allow(deprecated)] // OpenGL is deprecated on macOS use super::{GlConfig, GlError, Profile}; -use objc2::ffi::{id, nil}; use objc2::rc::Retained; -use objc2::{AllocAnyThread, msg_send}; +use objc2::AllocAnyThread; use objc2_app_kit::{ NSOpenGLContext, NSOpenGLContextParameter, NSOpenGLPFAAccelerated, NSOpenGLPFAAlphaSize, NSOpenGLPFAColorSize, NSOpenGLPFADepthSize, NSOpenGLPFADoubleBuffer, NSOpenGLPFAMultisample, @@ -12,7 +11,7 @@ use objc2_app_kit::{ NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView, }; use objc2_core_foundation::{CFBundle, CFString}; -use objc2_foundation::{NSBundle, NSSize, ns_string}; +use objc2_foundation::NSSize; use raw_window_handle::RawWindowHandle; use std::ffi::c_void; use std::ptr::NonNull; diff --git a/src/macos/mod.rs b/src/macos/mod.rs index e1fa7b87..8b733c21 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -2,4 +2,42 @@ mod keyboard; mod view; mod window; +use objc2::rc::Retained; +use objc2::Message; +use std::cell::RefCell; pub use window::*; + +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) => inner.retain(), + } + } + + pub fn set(&self, value: Retained) { + self.inner.replace(Some(value)); + } +} diff --git a/src/macos/view.rs b/src/macos/view.rs index 44f003a0..d2612bf2 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -2,7 +2,7 @@ use objc2::__framework_prelude::Retained; use objc2::ffi::{id, objc_disposeClassPair}; use objc2::rc::Allocated; use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, ProtocolObject, Sel}; -use objc2::{AllocAnyThread, ClassType, Message, msg_send, sel}; +use objc2::{msg_send, sel, AllocAnyThread, ClassType, Message}; use objc2_app_kit::{ NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification, @@ -11,7 +11,7 @@ use objc2_app_kit::{ use objc2_foundation::{ NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, }; -use std::ffi::{CStr, CString, c_void}; +use std::ffi::{c_void, CStr, CString}; use uuid::Uuid; use super::keyboard::make_modifiers; @@ -59,7 +59,7 @@ macro_rules! add_mouse_button_class_method { macro_rules! add_simple_keyboard_class_method { ($class:ident, $sel:ident) => { #[allow(non_snake_case)] - extern "C" fn $sel(this: &NSView, _: Sel, event: id){ + extern "C" 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){ diff --git a/src/macos/window.rs b/src/macos/window.rs index 2998b106..fb050e11 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -6,13 +6,16 @@ use std::rc::Rc; use keyboard_types::KeyboardEvent; use objc2::rc::Retained; -use objc2::runtime::{AnyObject, Ivar}; +use objc2::runtime::{AnyObject, Ivar, NSObjectProtocol}; use objc2::{MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{ - NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSView, NSWindow, - NSWindowStyleMask, + NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSPasteboard, + NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, +}; +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopMode, CFRunLoopTimer, + CFRunLoopTimerContext, }; -use objc2_core_foundation::{CFRunLoop, CFRunLoopMode, kCFRunLoopDefaultMode}; use objc2_foundation::{NSNotificationCenter, NSPoint, NSRect, NSSize, NSString}; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, @@ -25,10 +28,11 @@ use crate::{ }; use super::keyboard::KeyboardState; -use super::view::{BASEVIEW_STATE_IVAR, create_view}; +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, @@ -55,16 +59,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: Cell>>, + parent_ns_window: RetainedCell, /// Our subclassed NSView - ns_view: Cell>>, + ns_view: RetainedCell, #[cfg(feature = "opengl")] pub(super) gl_context: Option, @@ -113,19 +117,23 @@ impl WindowInner { } 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() } } @@ -141,8 +149,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, @@ -157,30 +163,26 @@ impl<'a> Window<'a> { }; let ns_view = unsafe { 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 @@ -239,16 +241,16 @@ impl<'a> Window<'a> { let ns_view = unsafe { create_view(&options) }; let window_inner = WindowInner { open: Cell::new(true), - ns_app: Cell::new(Some(app.clone())), - 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: Cell::new(Some(ns_window.clone())), + ns_window: RetainedCell::new(ns_window.clone()), }; let _ = Self::init(window_inner, window_info, build); @@ -268,13 +270,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(), }); @@ -282,6 +284,7 @@ impl<'a> Window<'a> { let window_state_ptr = Rc::into_raw(Rc::clone(&window_state)); unsafe { + // TODO: Pretty certain this is a cyclic reference (aaaa) (*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void); WindowState::setup_timer(window_state_ptr); @@ -295,26 +298,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)); } } @@ -324,9 +327,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 @@ -338,7 +341,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); } } } @@ -353,10 +356,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") } @@ -367,7 +375,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, @@ -426,12 +434,12 @@ 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) { + extern "C" fn timer_callback(_: *mut CFRunLoopTimer, window_state_ptr: *mut c_void) { unsafe { let window_state = &*(window_state_ptr as *const WindowState); @@ -439,6 +447,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, @@ -447,11 +459,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) { @@ -480,12 +502,9 @@ unsafe impl<'a> HasRawDisplayHandle for Window<'a> { } pub fn copy_to_clipboard(string: &str) { - unsafe { - let pb = NSPasteboard::generalPasteboard(nil); - - let ns_str = NSString::alloc(nil).init_str(string); + let pb = NSPasteboard::generalPasteboard(); + let ns_str = NSString::from_str(string); - pb.clearContents(); - pb.setString_forType(ns_str, cocoa::appkit::NSPasteboardTypeString); - } + pb.clearContents(); + pb.setString_forType(&ns_str, unsafe { NSPasteboardTypeString }); } From 1cc280711dd967d5805eec29a00191e8ae6a5fda Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 18:30:48 +0200 Subject: [PATCH 03/11] fixes --- Cargo.toml | 2 +- src/gl/macos.rs | 6 +- src/macos/keyboard.rs | 15 ++-- src/macos/mod.rs | 4 +- src/macos/view.rs | 158 +++++++++++++++++++++++++----------------- src/macos/window.rs | 48 +++++++++---- 6 files changed, 145 insertions(+), 88 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 31597475..4d0bfbc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ uuid = { version = "0.8", features = ["v4"] } objc2 = "0.6.4" objc2-core-foundation = "0.3.2" objc2-foundation = "0.3.2" -objc2-app-kit = "0.3.2" +objc2-app-kit = { version = "0.3.2", features = ["objc2-open-gl"] } [dev-dependencies] rtrb = "0.2" diff --git a/src/gl/macos.rs b/src/gl/macos.rs index 96119ade..a6d5abfb 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -3,6 +3,7 @@ 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, @@ -15,7 +16,6 @@ use objc2_foundation::NSSize; use raw_window_handle::RawWindowHandle; use std::ffi::c_void; use std::ptr::NonNull; -use std::str::FromStr; pub type CreationFailedError = (); pub struct GlContext { @@ -78,7 +78,7 @@ impl GlContext { .ok_or(GlError::CreationFailed(()))?; let view = NSOpenGLView::initWithFrame_pixelFormat( - NSOpenGLView::alloc(), + NSOpenGLView::alloc(MainThreadMarker::new().unwrap()), parent_view.frame(), Some(&pixel_format), ) @@ -103,7 +103,7 @@ impl GlContext { } pub unsafe fn make_not_current(&self) { - if Some(self.context) == NSOpenGLContext::currentContext() { + if Some(&self.context) == NSOpenGLContext::currentContext().as_ref() { NSOpenGLContext::clearCurrentContext(); } } diff --git a/src/macos/keyboard.rs b/src/macos/keyboard.rs index d4c9b3b5..8e3bdf1a 100644 --- a/src/macos/keyboard.rs +++ b/src/macos/keyboard.rs @@ -267,7 +267,7 @@ impl KeyboardState { } pub(crate) fn process_native_event(&self, event: &NSEvent) -> Option { - let event_type = event.eventType(); + 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); @@ -283,7 +283,11 @@ impl KeyboardState { 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 } + if any_down == 0 { + KeyState::Up + } else { + KeyState::Down + } } else { // HandleFlagsChanged has some logic for this; it might // happen when an app is deactivated by Command-Tab. In @@ -301,11 +305,12 @@ impl KeyboardState { let key = if let Some(key) = code_to_key(code) { key } else { - let characters = event.characters().map(|c| c.to_string()); - if is_valid_key(&event.characters()) { + let characters = event.characters().map(|c| c.to_string()).unwrap_or_default(); + if is_valid_key(&characters) { Key::Character(characters) } else { - let chars_ignoring = event.charactersIgnoringModifiers().map(|c| c.to_string()); + let chars_ignoring = + event.charactersIgnoringModifiers().map(|c| c.to_string()).unwrap_or_default(); if is_valid_key(&chars_ignoring) { Key::Character(chars_ignoring) } else { diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 8b733c21..033b8c8e 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -31,9 +31,9 @@ impl RetainedCell { impl RetainedCell { pub fn get(&self) -> Option> { - match &self.inner.borrow() { + match &*self.inner.borrow() { None => None, - Some(inner) => inner.retain(), + Some(inner) => Some(inner.retain()), } } diff --git a/src/macos/view.rs b/src/macos/view.rs index d2612bf2..619c96d4 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -1,8 +1,12 @@ +#![allow(deprecated)] // Allow use of NSFilenamesPboardType for now + use objc2::__framework_prelude::Retained; -use objc2::ffi::{id, objc_disposeClassPair}; +use objc2::ffi::objc_disposeClassPair; use objc2::rc::Allocated; -use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, ProtocolObject, Sel}; -use objc2::{msg_send, sel, AllocAnyThread, ClassType, Message}; +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, @@ -28,13 +32,13 @@ 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: &NSView, _: Sel, _: id){ + extern "C" 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,); + $class.add_method(sel!($sel:), $sel as extern "C" fn(_, _, _) -> _,); }; } @@ -52,7 +56,7 @@ macro_rules! add_mouse_button_class_method { })); } - $class.add_method(sel!($sel:),$sel); + $class.add_method(sel!($sel:),$sel as extern "C" fn(_, _, _) -> _); }; } @@ -75,14 +79,13 @@ macro_rules! add_simple_keyboard_class_method { } } - $class.add_method(sel!($sel:),$sel); + $class.add_method(sel!($sel:),$sel as extern "C" fn(_, _, _) -> _); }; } pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { let class = create_view_class(); - - let view: Allocated = msg_send![class, alloc]; + let view: Allocated = unsafe { msg_send![class, alloc] }; let size = window_options.size; let view = NSView::initWithFrame( @@ -129,34 +132,67 @@ fn create_view_class() -> &'static AnyClass { // SAFETY: All of these function signatures are correct unsafe { - class.add_method(sel!(acceptsFirstResponder), property_yes); - class.add_method(sel!(becomeFirstResponder), become_first_responder); - class.add_method(sel!(resignFirstResponder), resign_first_responder); - class.add_method(sel!(isFlipped), property_yes); - class.add_method(sel!(preservesContentInLiveResize), property_no); - class.add_method(sel!(acceptsFirstMouse:), accepts_first_mouse); - - class.add_method(sel!(windowShouldClose:), window_should_close); - class.add_method(sel!(dealloc), dealloc); - class.add_method(sel!(viewWillMoveToWindow:), view_will_move_to_window); - class.add_method(sel!(hitTest:), hit_test); - class.add_method(sel!(updateTrackingAreas:), update_tracking_areas); - - class.add_method(sel!(mouseMoved:), mouse_moved); - class.add_method(sel!(mouseDragged:), mouse_moved); - class.add_method(sel!(rightMouseDragged:), mouse_moved); - class.add_method(sel!(otherMouseDragged:), mouse_moved); - - class.add_method(sel!(scrollWheel:), scroll_wheel); - - class.add_method(sel!(viewDidChangeBackingProperties:), view_did_change_backing_properties); - - class.add_method(sel!(draggingEntered:), dragging_entered); - class.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation); - class.add_method(sel!(performDragOperation:), perform_drag_operation); - class.add_method(sel!(draggingUpdated:), dragging_updated); - class.add_method(sel!(draggingExited:), dragging_exited); - class.add_method(sel!(handleNotification:), handle_notification); + class.add_method(sel!(acceptsFirstResponder), property_yes as extern "C" fn(_, _) -> _); + class.add_method( + sel!(becomeFirstResponder), + become_first_responder as extern "C" fn(_, _) -> _, + ); + class.add_method( + sel!(resignFirstResponder), + resign_first_responder as extern "C" fn(_, _) -> _, + ); + class.add_method(sel!(isFlipped), property_yes as extern "C" fn(_, _) -> _); + class.add_method( + sel!(preservesContentInLiveResize), + property_no as extern "C" fn(_, _) -> _, + ); + class.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(_, _, _) -> _, + ); + + class.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(_, _, _) -> _, + ); + class.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); + class.add_method( + sel!(viewWillMoveToWindow:), + view_will_move_to_window as extern "C" fn(_, _, _) -> _, + ); + class.add_method(sel!(hitTest:), hit_test as extern "C" fn(_, _, _) -> _); + class.add_method( + sel!(updateTrackingAreas:), + update_tracking_areas as extern "C" fn(_, _, _) -> _, + ); + + class.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(mouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(rightMouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(otherMouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); + + class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(_, _, _) -> _); + + class.add_method( + sel!(viewDidChangeBackingProperties:), + view_did_change_backing_properties as extern "C" fn(_, _, _) -> _, + ); + + class.add_method(sel!(draggingEntered:), dragging_entered as extern "C" fn(_, _, _) -> _); + class.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation as extern "C" fn(_, _, _) -> _, + ); + class.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(_, _, _) -> _, + ); + class.add_method(sel!(draggingUpdated:), dragging_updated as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(_, _, _) -> _); + class.add_method( + sel!(handleNotification:), + handle_notification as extern "C" fn(_, _, _) -> _, + ); add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left); @@ -185,7 +221,7 @@ extern "C" fn property_no(_this: &NSView, _sel: Sel) -> Bool { Bool::NO } -extern "C" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: id) -> Bool { +extern "C" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { Bool::YES } @@ -218,11 +254,11 @@ extern "C" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool::NO } -extern "C" fn dealloc(this: &mut NSView, _sel: Sel) { +extern "C" fn dealloc(this: &mut AnyObject, _sel: Sel) { let class = this.class(); if let Some(superclass) = class.superclass() { - let () = msg_send![super(this, superclass), dealloc]; + let () = unsafe { msg_send![super(this, superclass), dealloc] }; } // Delete class @@ -231,29 +267,27 @@ extern "C" fn dealloc(this: &mut NSView, _sel: Sel) { } extern "C" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { - unsafe { - let ns_window = this.window(); + let ns_window = this.window(); - let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); + let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - // SAFETY: TODO - let state = unsafe { WindowState::from_view(this) }; + // SAFETY: TODO + 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))); } } @@ -304,17 +338,17 @@ fn new_tracking_area(this: &NSView) -> Retained { /// 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: &NSView, _sel: Sel, point: NSPoint) -> Option> { +extern "C" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { // SAFETY: TODO - let super_result: Option> = unsafe { msg_send![super(this), hitTest: point] }; + let super_result: Option<&NSView> = unsafe { msg_send![super(this), hitTest: point] }; let super_result = super_result?; #[cfg(feature = "opengl")] { 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 Some(this.retain()); + if super_result == gl_context.ns_view() { + return Some(this); } } } @@ -524,7 +558,7 @@ extern "C" fn handle_notification(this: &NSView, _cmd: Sel, notification: &NSNot // 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 first_responder.as_ref() != this { + if !this.isEqual(Some(&first_responder)) { return; } diff --git a/src/macos/window.rs b/src/macos/window.rs index fb050e11..283029a9 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -6,15 +6,14 @@ use std::rc::Rc; use keyboard_types::KeyboardEvent; use objc2::rc::Retained; -use objc2::runtime::{AnyObject, Ivar, NSObjectProtocol}; +use objc2::runtime::NSObjectProtocol; use objc2::{MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSPasteboard, NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, }; use objc2_core_foundation::{ - kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopMode, CFRunLoopTimer, - CFRunLoopTimerContext, + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, }; use objc2_foundation::{NSNotificationCenter, NSPoint, NSRect, NSSize, NSString}; use raw_window_handle::{ @@ -84,12 +83,19 @@ impl WindowInner { 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::current()?.remove_timer(&frame_timer, Some(kCFRunLoopDefaultMode)); + if let Some(run_loop) = CFRunLoop::current() { + run_loop.remove_timer(Some(&frame_timer), kCFRunLoopDefaultMode); + } } // Deregister NSView from NotificationCenter. @@ -110,7 +116,7 @@ impl WindowInner { // 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)); } } } @@ -162,7 +168,7 @@ 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) }; @@ -238,7 +244,7 @@ impl<'a> Window<'a> { ns_window.makeKeyAndOrderFront(None); - let ns_view = unsafe { create_view(&options) }; + let ns_view = create_view(&options); let window_inner = WindowInner { open: Cell::new(true), ns_app: RetainedCell::new(app.clone()), @@ -256,7 +262,8 @@ impl<'a> Window<'a> { let _ = Self::init(window_inner, window_info, build); ns_window.setContentView(Some(&ns_view)); - ns_window.setDelegate(Some(&ns_view)); + // TODO: this was here before, but couldn't have worked?? + //ns_window.setDelegate(Some(&ns_view)); app.run(); } @@ -285,7 +292,12 @@ impl<'a> Window<'a> { unsafe { // TODO: Pretty certain this is a cyclic reference (aaaa) - (*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void); + 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); } @@ -390,10 +402,14 @@ impl WindowState { /// original `Rc` owned by the `NSView` can be dropped at any time /// (including during an event handler). pub(super) unsafe fn from_view(view: &NSView) -> Rc { - let state_ptr: *const c_void = - view.class().instance_variable(BASEVIEW_STATE_IVAR).unwrap().load(view); - - let state_rc = Rc::from_raw(state_ptr as *const WindowState); + 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); @@ -439,7 +455,9 @@ impl WindowState { } 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); From c939c41815bdb9e9a5d8eeafab0738fae0aeb208 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 18:44:58 +0200 Subject: [PATCH 04/11] Use "C-unwind" calling convention --- src/macos/view.rs | 116 +++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/src/macos/view.rs b/src/macos/view.rs index 619c96d4..a46177d5 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -32,13 +32,13 @@ 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: &NSView, _: Sel, _: &AnyObject){ + 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(_, _, _) -> _,); + $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _,); }; } @@ -47,7 +47,7 @@ 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: &NSView, _: Sel, event: &NSEvent){ + extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ let state = unsafe { WindowState::from_view(this) }; state.trigger_event(Event::Mouse($event_ty { @@ -56,14 +56,14 @@ macro_rules! add_mouse_button_class_method { })); } - $class.add_method(sel!($sel:),$sel as extern "C" fn(_, _, _) -> _); + $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: &NSView, _: Sel, event: &NSEvent){ + 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){ @@ -79,7 +79,7 @@ macro_rules! add_simple_keyboard_class_method { } } - $class.add_method(sel!($sel:),$sel as extern "C" fn(_, _, _) -> _); + $class.add_method(sel!($sel:),$sel as extern "C-unwind" fn(_, _, _) -> _); }; } @@ -132,66 +132,84 @@ fn create_view_class() -> &'static AnyClass { // SAFETY: All of these function signatures are correct unsafe { - class.add_method(sel!(acceptsFirstResponder), property_yes as extern "C" fn(_, _) -> _); + class.add_method( + sel!(acceptsFirstResponder), + property_yes as extern "C-unwind" fn(_, _) -> _, + ); class.add_method( sel!(becomeFirstResponder), - become_first_responder as extern "C" fn(_, _) -> _, + become_first_responder as extern "C-unwind" fn(_, _) -> _, ); class.add_method( sel!(resignFirstResponder), - resign_first_responder as extern "C" fn(_, _) -> _, + resign_first_responder as extern "C-unwind" fn(_, _) -> _, ); - class.add_method(sel!(isFlipped), property_yes as extern "C" fn(_, _) -> _); + class.add_method(sel!(isFlipped), property_yes as extern "C-unwind" fn(_, _) -> _); class.add_method( sel!(preservesContentInLiveResize), - property_no as extern "C" fn(_, _) -> _, + property_no as extern "C-unwind" fn(_, _) -> _, ); class.add_method( sel!(acceptsFirstMouse:), - accepts_first_mouse as extern "C" fn(_, _, _) -> _, + accepts_first_mouse as extern "C-unwind" fn(_, _, _) -> _, ); class.add_method( sel!(windowShouldClose:), - window_should_close as extern "C" fn(_, _, _) -> _, + window_should_close as extern "C-unwind" fn(_, _, _) -> _, ); - class.add_method(sel!(dealloc), dealloc as extern "C" 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" fn(_, _, _) -> _, + view_will_move_to_window as extern "C-unwind" fn(_, _, _) -> _, ); - class.add_method(sel!(hitTest:), hit_test as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(hitTest:), hit_test as extern "C-unwind" fn(_, _, _) -> _); class.add_method( sel!(updateTrackingAreas:), - update_tracking_areas as extern "C" fn(_, _, _) -> _, + update_tracking_areas as extern "C-unwind" fn(_, _, _) -> _, ); - class.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(_, _, _) -> _); - class.add_method(sel!(mouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); - class.add_method(sel!(rightMouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); - class.add_method(sel!(otherMouseDragged:), mouse_moved as extern "C" fn(_, _, _) -> _); + 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!(scrollWheel:), scroll_wheel as extern "C" fn(_, _, _) -> _); + class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C-unwind" fn(_, _, _) -> _); class.add_method( sel!(viewDidChangeBackingProperties:), - view_did_change_backing_properties as extern "C" fn(_, _, _) -> _, + view_did_change_backing_properties as extern "C-unwind" fn(_, _, _) -> _, ); - class.add_method(sel!(draggingEntered:), dragging_entered as extern "C" fn(_, _, _) -> _); + class.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C-unwind" fn(_, _, _) -> _, + ); class.add_method( sel!(prepareForDragOperation:), - prepare_for_drag_operation as extern "C" fn(_, _, _) -> _, + prepare_for_drag_operation as extern "C-unwind" fn(_, _, _) -> _, ); class.add_method( sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(_, _, _) -> _, + 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!(draggingUpdated:), dragging_updated as extern "C" fn(_, _, _) -> _); - class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(_, _, _) -> _); class.add_method( sel!(handleNotification:), - handle_notification as extern "C" fn(_, _, _) -> _, + handle_notification as extern "C-unwind" fn(_, _, _) -> _, ); add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left); @@ -213,19 +231,19 @@ fn create_view_class() -> &'static AnyClass { class.register() } -extern "C" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { +extern "C-unwind" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { Bool::YES } -extern "C" fn property_no(_this: &NSView, _sel: Sel) -> Bool { +extern "C-unwind" fn property_no(_this: &NSView, _sel: Sel) -> Bool { Bool::NO } -extern "C" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { +extern "C-unwind" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { Bool::YES } -extern "C" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { +extern "C-unwind" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { let Some(window) = this.window() else { return Bool::YES; }; @@ -238,13 +256,13 @@ extern "C" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { Bool::YES } -extern "C" fn resign_first_responder(this: &NSView, _sel: Sel) -> Bool { +extern "C-unwind" fn resign_first_responder(this: &NSView, _sel: Sel) -> Bool { let state = unsafe { WindowState::from_view(this) }; state.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); Bool::YES } -extern "C" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool { +extern "C-unwind" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool { let state = unsafe { WindowState::from_view(this) }; state.trigger_event(Event::Window(WindowEvent::WillClose)); @@ -254,7 +272,7 @@ extern "C" fn window_should_close(this: &NSView, _: Sel, _sender: &AnyObject) -> Bool::NO } -extern "C" fn dealloc(this: &mut AnyObject, _sel: Sel) { +extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { let class = this.class(); if let Some(superclass) = class.superclass() { @@ -266,7 +284,7 @@ extern "C" fn dealloc(this: &mut AnyObject, _sel: Sel) { unsafe { objc_disposeClassPair(class as *const _ as *mut _) } } -extern "C" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { +extern "C-unwind" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { let ns_window = this.window(); let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); @@ -338,7 +356,7 @@ fn new_tracking_area(this: &NSView) -> Retained { /// 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: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { +extern "C-unwind" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { // SAFETY: TODO let super_result: Option<&NSView> = unsafe { msg_send![super(this), hitTest: point] }; let super_result = super_result?; @@ -356,7 +374,9 @@ extern "C" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSVi Some(super_result) } -extern "C" fn view_will_move_to_window(this: &NSView, _self: Sel, new_window: Option<&NSWindow>) { +extern "C-unwind" fn view_will_move_to_window( + this: &NSView, _self: Sel, new_window: Option<&NSWindow>, +) { let tracking_areas = this.trackingAreas(); match new_window { @@ -384,7 +404,7 @@ extern "C" fn view_will_move_to_window(this: &NSView, _self: Sel, new_window: Op } } -extern "C" fn update_tracking_areas(this: &NSView, _self: Sel, _: &AnyObject) { +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); @@ -396,7 +416,7 @@ extern "C" fn update_tracking_areas(this: &NSView, _self: Sel, _: &AnyObject) { this.addTrackingArea(&tracking_area); } -extern "C" fn mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { +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); @@ -408,7 +428,7 @@ extern "C" fn mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { })); } -extern "C" fn scroll_wheel(this: &NSView, _: Sel, event: &NSEvent) { +extern "C-unwind" fn scroll_wheel(this: &NSView, _: Sel, event: &NSEvent) { let state = unsafe { WindowState::from_view(this) }; let x = event.scrollingDeltaX() as f32; @@ -469,7 +489,7 @@ fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { } } -extern "C" fn dragging_entered( +extern "C-unwind" fn dragging_entered( this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, ) -> NSDragOperation { let state = unsafe { WindowState::from_view(this) }; @@ -485,7 +505,7 @@ extern "C" fn dragging_entered( on_event(&state, event) } -extern "C" fn dragging_updated( +extern "C-unwind" fn dragging_updated( this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, ) -> NSDragOperation { let state = unsafe { WindowState::from_view(this) }; @@ -501,7 +521,7 @@ extern "C" fn dragging_updated( on_event(&state, event) } -extern "C" fn prepare_for_drag_operation( +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 @@ -510,7 +530,7 @@ extern "C" fn prepare_for_drag_operation( Bool::YES } -extern "C" fn perform_drag_operation( +extern "C-unwind" fn perform_drag_operation( this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, ) -> Bool { let state = unsafe { WindowState::from_view(this) }; @@ -531,7 +551,7 @@ extern "C" fn perform_drag_operation( } } -extern "C" fn dragging_exited( +extern "C-unwind" fn dragging_exited( this: &NSView, _sel: Sel, _sender: Option<&ProtocolObject>, ) { let state = unsafe { WindowState::from_view(this) }; @@ -539,7 +559,7 @@ extern "C" fn dragging_exited( on_event(&state, MouseEvent::DragLeft); } -extern "C" fn handle_notification(this: &NSView, _cmd: Sel, notification: &NSNotification) { +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 }; From 6b48cb89554df3e67eb62935b3694c59b9cbfea2 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 19:47:39 +0200 Subject: [PATCH 05/11] Leaner objc2 --- Cargo.toml | 22 +++++++++++++++++----- src/macos/view.rs | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d0bfbc7..94606354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,12 +39,24 @@ winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", " uuid = { version = "0.8", features = ["v4"], optional = true } [target.'cfg(target_os="macos")'.dependencies] -uuid = { version = "0.8", features = ["v4"] } -# TODO: only enable needed features +uuid = { version = "1.23.1", features = ["v4"] } objc2 = "0.6.4" -objc2-core-foundation = "0.3.2" -objc2-foundation = "0.3.2" -objc2-app-kit = { version = "0.3.2", features = ["objc2-open-gl"] } +objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std"] } +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-open-gl", + "objc2-core-foundation" +] } [dev-dependencies] rtrb = "0.2" diff --git a/src/macos/view.rs b/src/macos/view.rs index a46177d5..0ae90907 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -124,7 +124,7 @@ fn create_view_class() -> &'static AnyClass { // 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 = CString::new(format!("BaseviewNSView_{}", Uuid::new_v4().to_simple())) + let class_name = CString::new(format!("BaseviewNSView_{}", Uuid::new_v4().simple())) // PANIC: This cannot have any NULL bytes .unwrap(); From 44b4b5b55db99679dec7143249c14fa3631271f5 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 19:59:43 +0200 Subject: [PATCH 06/11] opengl fixes --- Cargo.toml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94606354..6be3b408 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } @@ -41,7 +48,7 @@ uuid = { version = "0.8", features = ["v4"], optional = true } [target.'cfg(target_os="macos")'.dependencies] uuid = { version = "1.23.1", features = ["v4"] } objc2 = "0.6.4" -objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std"] } +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", @@ -54,7 +61,6 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSTrackingArea", "NSView", "NSWindow", - "objc2-open-gl", "objc2-core-foundation" ] } From 4cabe2f1595402072e196a1f4cf67fb324f94573 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Mon, 4 May 2026 20:04:35 +0200 Subject: [PATCH 07/11] smol fix --- src/macos/view.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/macos/view.rs b/src/macos/view.rs index 0ae90907..45f8d558 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -357,8 +357,10 @@ fn new_tracking_area(this: &NSView) -> Retained { /// collapse, so the override pass-through is equivalent to the /// default implementation. extern "C-unwind" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { + let superclass = this.class().superclass().unwrap(); // SAFETY: TODO - let super_result: Option<&NSView> = unsafe { msg_send![super(this), hitTest: point] }; + let super_result: Option<&NSView> = + unsafe { msg_send![super(this, superclass), hitTest: point] }; let super_result = super_result?; #[cfg(feature = "opengl")] From 31ccb0b758ed19ab72f21fdcc2b69828c255f162 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 5 May 2026 00:56:57 +0200 Subject: [PATCH 08/11] Fix setDelegate call --- src/macos/window.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/macos/window.rs b/src/macos/window.rs index 283029a9..ce6e6315 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -7,7 +7,7 @@ use std::rc::Rc; use keyboard_types::KeyboardEvent; use objc2::rc::Retained; use objc2::runtime::NSObjectProtocol; -use objc2::{MainThreadMarker, MainThreadOnly}; +use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSPasteboard, NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, @@ -262,8 +262,7 @@ impl<'a> Window<'a> { let _ = Self::init(window_inner, window_info, build); ns_window.setContentView(Some(&ns_view)); - // TODO: this was here before, but couldn't have worked?? - //ns_window.setDelegate(Some(&ns_view)); + let () = unsafe { msg_send![&*ns_window, setDelegate: &*ns_view] }; app.run(); } From 7f35382553588df2b6d41d2d4964ec7ce874c529 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 5 May 2026 11:38:24 +0200 Subject: [PATCH 09/11] Extra safety comments --- src/macos/view.rs | 31 ++++++++++++++++++++++--------- src/macos/window.rs | 4 ++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/macos/view.rs b/src/macos/view.rs index 45f8d558..92fd0736 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -84,8 +84,13 @@ macro_rules! add_simple_keyboard_class_method { } pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { - let class = create_view_class(); - let view: Allocated = unsafe { msg_send![class, alloc] }; + 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( @@ -95,7 +100,7 @@ pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained Retained &'static AnyClass { +/// # 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 @@ -249,6 +259,7 @@ extern "C-unwind" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { }; 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)); } @@ -257,12 +268,14 @@ extern "C-unwind" fn become_first_responder(this: &NSView, _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)); Bool::YES } 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)); @@ -279,8 +292,8 @@ extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { let () = unsafe { msg_send![super(this, superclass), dealloc] }; } - // Delete class - // SAFETY: TODO: nope, this is NOT sound, as this invalidates any &AnyClass + // 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 _) } } @@ -289,7 +302,7 @@ extern "C-unwind" fn view_did_change_backing_properties(this: &NSView, _: Sel, _ let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - // SAFETY: TODO + // SAFETY: This is our own view instance let state = unsafe { WindowState::from_view(this) }; let bounds = this.bounds(); @@ -358,7 +371,7 @@ fn new_tracking_area(this: &NSView) -> Retained { /// default implementation. extern "C-unwind" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { let superclass = this.class().superclass().unwrap(); - // SAFETY: TODO + // SAFETY: Our superclass is NSView let super_result: Option<&NSView> = unsafe { msg_send![super(this, superclass), hitTest: point] }; let super_result = super_result?; diff --git a/src/macos/window.rs b/src/macos/window.rs index ce6e6315..2638b3a3 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -400,6 +400,10 @@ 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). + /// + /// # 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() From 1a43f829d83d0af0e4a96e2d7bafd23662ef8d46 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 5 May 2026 12:09:59 +0200 Subject: [PATCH 10/11] More comments --- src/macos/window.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/macos/window.rs b/src/macos/window.rs index 2638b3a3..ab3ab82f 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -290,7 +290,10 @@ impl<'a> Window<'a> { let window_state_ptr = Rc::into_raw(Rc::clone(&window_state)); unsafe { - // TODO: Pretty certain this is a cyclic reference (aaaa) + // 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) From cb976e74677cb85b7ee0256a07b7615afba68277 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 5 May 2026 12:38:19 +0200 Subject: [PATCH 11/11] Fix remaining TODOs --- src/macos/window.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/macos/window.rs b/src/macos/window.rs index ab3ab82f..4d1b95d7 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -221,7 +221,7 @@ impl<'a> Window<'a> { let window_info = WindowInfo::from_logical_size(options.size, scaling); - // SAFETY: TODO + // SAFETY: This is safe because of the setReleasedWhenClosed(false) below let ns_window = unsafe { NSWindow::initWithContentRect_styleMask_backing_defer( NSWindow::alloc(mtm), @@ -234,7 +234,7 @@ impl<'a> Window<'a> { ) }; - // SAFETY: TODO + // SAFETY: setReleasedWhenClosed is always safe to call with `false` (worst case is a memory leak) unsafe { ns_window.setReleasedWhenClosed(false) }; ns_window.center();