From 61a34389ddf829c8b124501afe77cb5293458b05 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Tue, 12 May 2026 05:02:53 +0200 Subject: [PATCH 1/3] wip --- src/macos/view.rs | 67 +++++++++++++++++++++++++++++++++++---------- src/macos/window.rs | 8 +++--- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/macos/view.rs b/src/macos/view.rs index 0fafa37a..65f1319f 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -6,17 +6,19 @@ use objc2::rc::Allocated; use objc2::runtime::{ AnyClass, AnyObject, Bool, ClassBuilder, NSObjectProtocol, ProtocolObject, Sel, }; -use objc2::{msg_send, sel, AllocAnyThread, ClassType}; +use objc2::{msg_send, sel, AllocAnyThread, ClassType, Encoding, Message, RefEncode}; use objc2_app_kit::{ NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification, NSWindowDidResignKeyNotification, }; +use objc2_core_foundation::CGRect; use objc2_core_foundation::CFUUID; use objc2_foundation::{ NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, }; use std::ffi::{c_void, CStr, CString}; +use std::ops::Deref; use super::keyboard::make_modifiers; use super::window::WindowState; @@ -29,6 +31,52 @@ use crate::{ /// Name of the field used to store the `WindowState` pointer. pub(super) const BASEVIEW_STATE_IVAR: &CStr = c"baseview_state"; +#[repr(C)] +pub struct View { + parent: NSView, +} + +// SAFETY: TODO +unsafe impl RefEncode for View { + const ENCODING_REF: Encoding = NSView::ENCODING_REF; +} + +// SAFETY: TODO +unsafe impl Message for View {} + +impl Deref for View { + type Target = NSView; + + fn deref(&self) -> &Self::Target { + &self.parent + } +} + +impl View { + pub fn new(frame: CGRect) -> Retained { + // SAFETY: We don't access this reference after this function + let class = unsafe { create_view_class() }; + + // SAFETY: This function is valid to call, and Allocated is the correct type for the + // returned pointer + let view: Allocated = unsafe { msg_send![class, alloc] }; + Self::set_inner(&view, class, ViewInner {}); + + let view: Retained = unsafe { msg_send![view, initWithFrame: frame] }; + view + } + + fn set_inner(view: &Allocated, class: &AnyClass, inner: ViewInner) { + let inner = Box::new(inner); + let ivar = class.instance_variable(BASEVIEW_STATE_IVAR).unwrap(); + let ivar_target = unsafe { &*Allocated::as_ptr(&view).cast() }; + let ivar = unsafe { ivar.load_ptr::<*mut c_void>(ivar_target) }; + unsafe { ivar.write(Box::into_raw(inner).cast()) }; + } +} + +pub struct ViewInner {} + macro_rules! add_simple_mouse_class_method { ($class:ident, $sel:ident, $event:expr) => { #[allow(non_snake_case)] @@ -83,20 +131,9 @@ macro_rules! add_simple_keyboard_class_method { }; } -pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { - let view: Allocated = { - // SAFETY: We don't access this reference after calling alloc - let class = unsafe { create_view_class() }; - // SAFETY: This function is valid to call, and Allocated is the correct type for the - // returned pointer - unsafe { msg_send![class, alloc] } - }; - +pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { let size = window_options.size; - let view = NSView::initWithFrame( - view, - NSRect::new(NSPoint::ZERO, NSSize::new(size.width, size.height)), - ); + let view = View::new(NSRect::new(NSPoint::ZERO, NSSize::new(size.width, size.height))); let notification_center = NSNotificationCenter::defaultCenter(); @@ -514,7 +551,7 @@ fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { } extern "C-unwind" fn dragging_entered( - this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, ) -> NSDragOperation { let state = unsafe { WindowState::from_view(this) }; let modifiers = state.keyboard_state().last_mods(); diff --git a/src/macos/window.rs b/src/macos/window.rs index 90a94ed5..df7cb35f 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -27,7 +27,7 @@ use crate::{ }; use super::keyboard::KeyboardState; -use super::view::{create_view, BASEVIEW_STATE_IVAR}; +use super::view::{create_view, View, BASEVIEW_STATE_IVAR}; #[cfg(feature = "opengl")] use crate::gl::{GlConfig, GlContext}; @@ -67,7 +67,7 @@ pub(super) struct WindowInner { parent_ns_window: RetainedCell, /// Our subclassed NSView - ns_view: RetainedCell, + ns_view: RetainedCell, #[cfg(feature = "opengl")] pub(super) gl_context: Option, @@ -135,7 +135,7 @@ impl WindowInner { handle.ns_view = match self.ns_view.get() { None => ptr::null_mut(), - Some(view) => (&*view as *const NSView) as *mut _, + Some(view) => (&*view as *const _) as *mut _, }; } @@ -401,7 +401,7 @@ impl WindowState { /// # Safety /// /// `view` MUST be our own NSView, as created by `create_view` - pub(super) unsafe fn from_view(view: &NSView) -> Rc { + pub(super) unsafe fn from_view(view: &View) -> Rc { let state_ptr = view .class() .instance_variable(BASEVIEW_STATE_IVAR) From 7db647d8129f5e92f0b936931bdd4c620adeccad Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Fri, 29 May 2026 14:17:25 +0200 Subject: [PATCH 2/3] wip --- Cargo.toml | 3 +- src/macos/view.rs | 920 ++++++++++----------- src/macos/window.rs | 284 +------ src/wrappers.rs | 3 + src/wrappers/appkit.rs | 5 + src/wrappers/appkit/timer.rs | 40 + src/wrappers/appkit/view.rs | 167 ++++ src/wrappers/appkit/view/implementation.rs | 285 +++++++ 8 files changed, 952 insertions(+), 755 deletions(-) create mode 100644 src/wrappers/appkit.rs create mode 100644 src/wrappers/appkit/timer.rs create mode 100644 src/wrappers/appkit/view.rs create mode 100644 src/wrappers/appkit/view/implementation.rs diff --git a/Cargo.toml b/Cargo.toml index 05a09fa2..b1bbc74e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,8 @@ uuid = { version = "0.8", features = ["v4"], optional = true } [target.'cfg(target_os="macos")'.dependencies] objc2 = "0.6.4" -objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std", "CFString", "CFUUID"] } +objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std", "CFString", "CFUUID", "block2", "objc2"] } +block2 = "0.6.2" objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSEnumerator"] } objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSApplication", diff --git a/src/macos/view.rs b/src/macos/view.rs index 65f1319f..769b66b5 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -1,370 +1,507 @@ #![allow(deprecated)] // Allow use of NSFilenamesPboardType for now -use objc2::__framework_prelude::Retained; -use objc2::ffi::objc_disposeClassPair; -use objc2::rc::Allocated; -use objc2::runtime::{ - AnyClass, AnyObject, Bool, ClassBuilder, NSObjectProtocol, ProtocolObject, Sel, +use super::keyboard::{make_modifiers, KeyboardState}; +use super::window::WindowState; +use crate::macos::Window; +use crate::wrappers::appkit::*; +use crate::MouseEvent::{ButtonPressed, ButtonReleased}; +use crate::{ + DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, Point, ScrollDelta, Size, + WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, }; -use objc2::{msg_send, sel, AllocAnyThread, ClassType, Encoding, Message, RefEncode}; +use objc2::__framework_prelude::Retained; +use objc2::rc::Weak; +use objc2::runtime::{NSObjectProtocol, ProtocolObject}; +use objc2::{msg_send, sel, AllocAnyThread}; use objc2_app_kit::{ NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification, NSWindowDidResignKeyNotification, }; -use objc2_core_foundation::CGRect; -use objc2_core_foundation::CFUUID; use objc2_foundation::{ NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, }; -use std::ffi::{c_void, CStr, CString}; -use std::ops::Deref; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; -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, -}; +pub struct BaseviewView { + state: Rc, + window_handler: RefCell>>, -/// Name of the field used to store the `WindowState` pointer. -pub(super) const BASEVIEW_STATE_IVAR: &CStr = c"baseview_state"; + /// Events that will be triggered at the end of `window_handler`'s borrow. + deferred_events: RefCell>, -#[repr(C)] -pub struct View { - parent: NSView, -} + frame_timer: Cell>, + keyboard_state: KeyboardState, -// SAFETY: TODO -unsafe impl RefEncode for View { - const ENCODING_REF: Encoding = NSView::ENCODING_REF; + #[cfg(feature = "opengl")] + gl_context: std::cell::OnceCell, } +/* +pub(super) fn create_view( + window_options: &WindowOpenOptions, inner: V, +) -> Retained> { + let size = window_options.size; + let view = View::new(NSRect::new(NSPoint::ZERO, NSSize::new(size.width, size.height)), inner); + + /* + let notification_center = NSNotificationCenter::defaultCenter(); + + // SAFETY: Our NSView class does have a handleNotification: method with the matching signature. + unsafe { + notification_center.addObserver_selector_name_object( + &view, + sel!(handleNotification:), + Some(NSWindowDidBecomeKeyNotification), + None, + ); + notification_center.addObserver_selector_name_object( + &view, + sel!(handleNotification:), + Some(NSWindowDidResignKeyNotification), + None, + ); + }*/ + + /* + // SAFETY: This static is a read-only constant + let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; + view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); -// SAFETY: TODO -unsafe impl Message for View {} + */ -impl Deref for View { - type Target = NSView; + view +}*/ + +impl BaseviewView { + pub fn new( + options: WindowOpenOptions, builder: impl FnOnce(&mut crate::Window) -> H, + ) -> Retained> { + let view_rect = + NSRect::new(NSPoint::ZERO, NSSize::new(options.size.width, options.size.height)); + + let inner = BaseviewView { + state: Rc::new(WindowState::new()), + + deferred_events: RefCell::default(), + keyboard_state: KeyboardState::new(), + frame_timer: None.into(), + window_handler: None.into(), + + #[cfg(feature = "opengl")] + gl_context: std::cell::OnceCell::new(), + }; + + let view = View::new(view_rect, inner, |view| { + #[cfg(feature = "opengl")] + if let Some(gl_config) = options.gl_config { + let gl_context = crate::gl::GlContext::create(&view.view, gl_config).unwrap(); + let _ = view.gl_context.set(gl_context); + } - fn deref(&self) -> &Self::Target { - &self.parent + let handler = builder(&mut view.into()); + view.window_handler.replace(Some(Box::new(handler))); + }); + + view } -} -impl View { - pub fn new(frame: CGRect) -> Retained { - // SAFETY: We don't access this reference after this function - let class = unsafe { create_view_class() }; + /// Trigger the event immediately and return the event status. + /// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`). + pub(super) fn trigger_event(&self, event: Event) -> EventStatus { + let mut window = crate::Window::new(Window { inner: &self.window_inner }); + let mut window_handler = self.window_handler.borrow_mut(); + let status = window_handler.on_event(&mut window, event); + self.send_deferred_events(window_handler.as_mut()); + status + } - // SAFETY: This function is valid to call, and Allocated is the correct type for the - // returned pointer - let view: Allocated = unsafe { msg_send![class, alloc] }; - Self::set_inner(&view, class, ViewInner {}); + /// Trigger the event immediately if `window_handler` can be borrowed mutably, + /// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends. + /// As this method might result in the event triggering asynchronously, it can't reliably return the event status. + pub(super) fn trigger_deferrable_event(&self, event: Event) { + if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() { + let mut window = crate::Window::new(Window { inner: &self.window_inner }); + window_handler.on_event(&mut window, event); + self.send_deferred_events(window_handler.as_mut()); + } else { + self.deferred_events.borrow_mut().push_back(event); + } + } - let view: Retained = unsafe { msg_send![view, initWithFrame: frame] }; - view + fn trigger_frame(&self) { + let mut window = crate::Window::new(Window { inner: &self.window_inner }); + let mut window_handler = self.window_handler.borrow_mut(); + window_handler.on_frame(&mut window); + self.send_deferred_events(window_handler.as_mut()); } - fn set_inner(view: &Allocated, class: &AnyClass, inner: ViewInner) { - let inner = Box::new(inner); - let ivar = class.instance_variable(BASEVIEW_STATE_IVAR).unwrap(); - let ivar_target = unsafe { &*Allocated::as_ptr(&view).cast() }; - let ivar = unsafe { ivar.load_ptr::<*mut c_void>(ivar_target) }; - unsafe { ivar.write(Box::into_raw(inner).cast()) }; + fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) { + let mut window = crate::Window::new(Window { inner: &self.window_inner }); + loop { + let next_event = self.deferred_events.borrow_mut().pop_front(); + if let Some(event) = next_event { + window_handler.on_event(&mut window, event); + } else { + break; + } + } } } -pub struct ViewInner {} +impl ViewImpl for BaseviewView { + fn init(&self, view: &Retained>) { + let timer_view = Weak::from_retained(view); + self.frame_timer.set(TimerHandle::new(0.015, move || { + if let Some(view) = timer_view.load() { + view.inner().trigger_frame(); + } + })); + } -macro_rules! add_simple_mouse_class_method { - ($class:ident, $sel:ident, $event:expr) => { - #[allow(non_snake_case)] - extern "C-unwind" fn $sel(this: &NSView, _: Sel, _: &AnyObject){ - let state = unsafe { WindowState::from_view(this) }; + fn become_first_responder(this: ViewRef) -> bool { + let Some(window) = this.view.window() else { + return true; + }; - state.trigger_event(Event::Mouse($event)); + if window.isKeyWindow() { + this.trigger_deferrable_event(Event::Window(WindowEvent::Focused)); } - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _,); - }; -} + true + } -/// Similar to [add_simple_mouse_class_method!], but this creates its own event object for the -/// press/release event and adds the active modifier keys to that event. -macro_rules! add_mouse_button_class_method { - ($class:ident, $sel:ident, $event_ty:ident, $button:expr) => { - #[allow(non_snake_case)] - extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ - let state = unsafe { WindowState::from_view(this) }; - - state.trigger_event(Event::Mouse($event_ty { - button: $button, - modifiers: make_modifiers(event.modifierFlags()), - })); - } + fn resign_first_responder(this: ViewRef) -> bool { + this.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); + true + } - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _); - }; -} + fn window_should_close(this: ViewRef) -> bool { + this.trigger_event(Event::Window(WindowEvent::WillClose)); -macro_rules! add_simple_keyboard_class_method { - ($class:ident, $sel:ident) => { - #[allow(non_snake_case)] - extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ - let state = unsafe { WindowState::from_view(this) }; + //state.window_inner.close(); - if let Some(key_event) = state.process_native_key_event(event){ - let status = state.trigger_event(Event::Keyboard(key_event)); + false + } + + fn view_did_change_backing_properties(this: ViewRef) { + let ns_window = this.view.window(); - if let EventStatus::Ignored = status { - unsafe { - let superclass = msg_send![this, superclass]; + let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - let () = msg_send![super(this, superclass), $sel:event]; - } + // SAFETY: This is our own view instance + let state = &this.state; + + let bounds = this.view.bounds(); + + 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(); + + // 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); + this.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); + } + } + + /// `hitTest:` override that collapses hits on baseview's internal + /// OpenGL render subview to this NSView. + /// + /// `src/gl/macos.rs` attaches an `NSOpenGLView` as a subview of this + /// view so the GL context is isolated from event handling. The side + /// effect is that `[NSView hitTest:]` returns the GL subview for + /// every click inside our frame — `NSOpenGLView` inherits the + /// default `acceptsFirstMouse:` which returns `NO`, so AppKit treats + /// the first click in a non-key window as an activation click and + /// never dispatches `mouseDown:`. That's the "first click dead zone" + /// symptom reported in baseview#129 / #202 / #169. + /// + /// Fix: if the hit lands on our own GL render subview (pointer + /// equality against the `NSOpenGLView` stored in `GlContext`), + /// collapse the result to `self`. AppKit then asks US about + /// `acceptsFirstMouse:` (we return `YES`), and `mouseDown:` is + /// dispatched on the first click. Hits on any other subview pass + /// through unchanged — we only redirect our own render child, not + /// anything the consumer may add. + /// + /// No-op without the `opengl` feature: there's no GL subview to + /// collapse, so the override pass-through is equivalent to the + /// default implementation. + fn hit_test(this: ViewRef, point: NSPoint) -> Option<&NSView> { + let superclass = this.view.class().superclass().unwrap(); + + // SAFETY: Our superclass is NSView + let super_result: Option<&NSView> = + unsafe { msg_send![super(this.view, superclass), hitTest: point] }; + let super_result = super_result?; + + #[cfg(feature = "opengl")] + { + if let Some(gl_context) = this.gl_context.get() { + if super_result == gl_context.ns_view() { + return Some(this.view); } } } - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _); - }; -} + Some(super_result) + } -pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained { - let size = window_options.size; - let view = View::new(NSRect::new(NSPoint::ZERO, NSSize::new(size.width, size.height))); + fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>) { + let tracking_areas = this.view.trackingAreas(); - let notification_center = NSNotificationCenter::defaultCenter(); + match new_window { + None => { + if tracking_areas.count() > 0 { + let tracking_area = tracking_areas.objectAtIndex(0); + this.view.removeTrackingArea(&tracking_area); + } + } + Some(new_window) => { + if tracking_areas.is_empty() { + let tracking_area = new_tracking_area(this.view); + this.view.addTrackingArea(&tracking_area); + } - // SAFETY: Our NSView class does have a handleNotification: method with the matching signature. - unsafe { - notification_center.addObserver_selector_name_object( - &view, - sel!(handleNotification:), - Some(NSWindowDidBecomeKeyNotification), - None, - ); - notification_center.addObserver_selector_name_object( - &view, - sel!(handleNotification:), - Some(NSWindowDidResignKeyNotification), - None, - ); + new_window.setAcceptsMouseMovedEvents(true); + new_window.makeFirstResponder(Some(this.view)); + } + } + + unsafe { + let superclass = msg_send![this.view, superclass]; + + let () = msg_send![super(this.view, superclass), viewWillMoveToWindow: new_window]; + } } - // SAFETY: This static is a read-only constant - let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType }; - view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type])); + fn update_tracking_areas(this: ViewRef) { + let tracking_areas = this.view.trackingAreas(); + if tracking_areas.count() > 0 { + let tracking_area = tracking_areas.objectAtIndex(0); + this.view.removeTrackingArea(&tracking_area); + } - view -} + let tracking_area = new_tracking_area(this.view); -fn new_class_name() -> CString { - // PANIC: CFUUIDCreate is not documented to return NULL. - let uuid = CFUUID::new(None).unwrap(); - // PANIC: CFUUIDCreateString is not documented to return NULL. - let uuid_str = CFUUID::new_string(None, Some(&uuid)).unwrap(); + this.view.addTrackingArea(&tracking_area); + } - let class_name = format!("BaseviewNSView_{uuid_str}"); - // PANIC: This cannot have any NULL bytes - CString::new(class_name).unwrap() -} + fn mouse_moved(this: ViewRef, event: &NSEvent) { + let point = this.view.convertPoint_fromView(event.locationInWindow(), None); -/// # Safety -/// -/// This class is going to be destroyed when its first instance gets deallocated. -/// -/// The returned reference must NOT be used after that point. -unsafe fn create_view_class() -> &'static AnyClass { - // Use unique class names so that there are no conflicts between different - // instances. The class is deleted when the view is released. Previously, - // the class was stored in a OnceCell after creation. This way, we didn't - // have to recreate it each time a view was opened, but now we don't leave - // any class definitions lying around when the plugin is closed. - let class_name = new_class_name(); - - let mut class = ClassBuilder::new(&class_name, NSView::class()).unwrap(); - - // SAFETY: All of these function signatures are correct - unsafe { - class.add_method( - sel!(acceptsFirstResponder), - property_yes as extern "C-unwind" fn(_, _) -> _, - ); - class.add_method( - sel!(becomeFirstResponder), - become_first_responder as extern "C-unwind" fn(_, _) -> _, - ); - class.add_method( - sel!(resignFirstResponder), - resign_first_responder as extern "C-unwind" fn(_, _) -> _, - ); - class.add_method(sel!(isFlipped), property_yes as extern "C-unwind" fn(_, _) -> _); - class.add_method( - sel!(preservesContentInLiveResize), - property_no as extern "C-unwind" fn(_, _) -> _, - ); - class.add_method( - sel!(acceptsFirstMouse:), - accepts_first_mouse as extern "C-unwind" fn(_, _, _) -> _, - ); + let position = Point { x: point.x, y: point.y }; - class.add_method( - sel!(windowShouldClose:), - window_should_close as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method(sel!(dealloc), dealloc as extern "C-unwind" fn(_, _)); - class.add_method( - sel!(viewWillMoveToWindow:), - view_will_move_to_window as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method(sel!(hitTest:), hit_test as extern "C-unwind" fn(_, _, _) -> _); - class.add_method( - sel!(updateTrackingAreas:), - update_tracking_areas as extern "C-unwind" fn(_, _, _) -> _, - ); + this.trigger_event(Event::Mouse(MouseEvent::CursorMoved { + position, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - 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(_, _, _) -> _, - ); + fn scroll_wheel(this: ViewRef, event: &NSEvent) { + let x = event.scrollingDeltaX() as f32; + let y = event.scrollingDeltaY() as f32; - class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C-unwind" fn(_, _, _) -> _); + let delta = if event.hasPreciseScrollingDeltas() { + ScrollDelta::Pixels { x, y } + } else { + ScrollDelta::Lines { x, y } + }; - class.add_method( - sel!(viewDidChangeBackingProperties:), - view_did_change_backing_properties as extern "C-unwind" fn(_, _, _) -> _, - ); + this.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { + delta, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - class.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method( - sel!(prepareForDragOperation:), - prepare_for_drag_operation as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method( - sel!(draggingUpdated:), - dragging_updated as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method( - sel!(draggingExited:), - dragging_exited as extern "C-unwind" fn(_, _, _) -> _, - ); - class.add_method( - sel!(handleNotification:), - handle_notification as extern "C-unwind" fn(_, _, _) -> _, - ); + fn dragging_entered( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation { + let modifiers = this.keyboard_state.last_mods(); + let drop_data = get_drop_data(sender); - 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 event = MouseEvent::DragEntered { + position: get_drag_position(sender), + modifiers: make_modifiers(modifiers), + data: drop_data, + }; - add_simple_keyboard_class_method!(class, keyDown); - add_simple_keyboard_class_method!(class, keyUp); - add_simple_keyboard_class_method!(class, flagsChanged); + on_event(&this, event) } - class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR); + fn dragging_updated( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation { + let modifiers = this.keyboard_state.last_mods(); + let drop_data = get_drop_data(sender); - class.register() -} + let event = MouseEvent::DragMoved { + position: get_drag_position(sender), + modifiers: make_modifiers(modifiers), + data: drop_data, + }; -extern "C-unwind" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { - Bool::YES -} + on_event(&this, event) + } -extern "C-unwind" fn property_no(_this: &NSView, _sel: Sel) -> Bool { - Bool::NO -} + fn prepare_for_drag_operation( + _this: ViewRef, _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 + true + } -extern "C-unwind" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { - Bool::YES -} + fn perform_drag_operation( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> bool { + let modifiers = this.keyboard_state.last_mods(); + let drop_data = get_drop_data(sender); -extern "C-unwind" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { - let Some(window) = this.window() else { - return Bool::YES; - }; + let event = MouseEvent::DragDropped { + position: get_drag_position(sender), + modifiers: make_modifiers(modifiers), + data: drop_data, + }; - 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)); + let event_status = this.trigger_event(Event::Mouse(event)); + + match event_status { + EventStatus::AcceptDrop(_) => true, + _ => false, + } } - Bool::YES -} + fn dragging_exited(this: ViewRef, _sender: Option<&ProtocolObject>) { + on_event(&this, MouseEvent::DragLeft); + } -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 -} + fn handle_notification(this: ViewRef, notification: &NSNotification) { + let Some(window) = this.view.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 }; -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) }; + // If the first responder isn't our NSView, the focus events will instead be triggered + // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. + if !this.view.isEqual(Some(&first_responder)) { + return; + } - state.trigger_event(Event::Window(WindowEvent::WillClose)); + this.trigger_event(Event::Window(if window.isKeyWindow() { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + })); + } - state.window_inner.close(); + fn mouse_down(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonPressed { + button: MouseButton::Left, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - Bool::NO -} + fn mouse_up(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonReleased { + button: MouseButton::Left, + modifiers: make_modifiers(event.modifierFlags()), + })); + } -extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { - let class = this.class(); + fn right_mouse_down(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonPressed { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - if let Some(superclass) = class.superclass() { - let () = unsafe { msg_send![super(this, superclass), dealloc] }; + fn right_mouse_up(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonReleased { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + })); } - // 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 _) } -} + fn other_mouse_down(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonPressed { + button: MouseButton::Middle, + modifiers: make_modifiers(event.modifierFlags()), + })); + } -extern "C-unwind" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { - let ns_window = this.window(); + fn other_mouse_up(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonReleased { + button: MouseButton::Middle, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); + fn mouse_entered(this: ViewRef) { + this.trigger_event(Event::Mouse(MouseEvent::CursorEntered)); + } - // SAFETY: This is our own view instance - let state = unsafe { WindowState::from_view(this) }; + fn mouse_exited(this: ViewRef) { + this.trigger_event(Event::Mouse(MouseEvent::CursorLeft)); + } - let bounds = this.bounds(); + fn key_down(this: ViewRef, event: &NSEvent) { + if let Some(key_event) = this.keyboard_state.process_native_event(event) { + let status = this.trigger_event(Event::Keyboard(key_event)); - let new_window_info = WindowInfo::from_logical_size( - Size::new(bounds.size.width, bounds.size.height), - scale_factor, - ); + if let EventStatus::Ignored = status { + unsafe { + let superclass = msg_send![this.view, superclass]; - let window_info = state.window_info.get(); + let () = msg_send![super(this.view, superclass), keyDown:event]; + } + } + } + } - // 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))); + fn key_up(this: ViewRef, event: &NSEvent) { + if let Some(key_event) = this.keyboard_state.process_native_event(event) { + let status = this.trigger_event(Event::Keyboard(key_event)); + + if let EventStatus::Ignored = status { + unsafe { + let superclass = msg_send![this.view, superclass]; + + let () = msg_send![super(this.view, superclass), keyUp:event]; + } + } + } + } + + fn flags_changed(this: ViewRef, event: &NSEvent) { + if let Some(key_event) = this.keyboard_state.process_native_event(event) { + let status = this.trigger_event(Event::Keyboard(key_event)); + + if let EventStatus::Ignored = status { + unsafe { + let superclass = msg_send![this.view, superclass]; + + let () = msg_send![super(this.view, superclass), flagsChanged:event]; + } + } + } } } @@ -392,121 +529,6 @@ fn new_tracking_area(this: &NSView) -> Retained { } } -/// `hitTest:` override that collapses hits on baseview's internal -/// OpenGL render subview to this NSView. -/// -/// `src/gl/macos.rs` attaches an `NSOpenGLView` as a subview of this -/// view so the GL context is isolated from event handling. The side -/// effect is that `[NSView hitTest:]` returns the GL subview for -/// every click inside our frame — `NSOpenGLView` inherits the -/// default `acceptsFirstMouse:` which returns `NO`, so AppKit treats -/// the first click in a non-key window as an activation click and -/// never dispatches `mouseDown:`. That's the "first click dead zone" -/// symptom reported in baseview#129 / #202 / #169. -/// -/// Fix: if the hit lands on our own GL render subview (pointer -/// equality against the `NSOpenGLView` stored in `GlContext`), -/// collapse the result to `self`. AppKit then asks US about -/// `acceptsFirstMouse:` (we return `YES`), and `mouseDown:` is -/// dispatched on the first click. Hits on any other subview pass -/// through unchanged — we only redirect our own render child, not -/// anything the consumer may add. -/// -/// 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-unwind" fn hit_test(this: &NSView, _sel: Sel, point: NSPoint) -> Option<&NSView> { - let superclass = this.class().superclass().unwrap(); - // SAFETY: Our superclass is NSView - let super_result: Option<&NSView> = - unsafe { msg_send![super(this, superclass), hitTest: point] }; - let super_result = super_result?; - - #[cfg(feature = "opengl")] - { - 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); - } - } - } - - Some(super_result) -} - -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 { - None => { - if tracking_areas.count() > 0 { - let tracking_area = tracking_areas.objectAtIndex(0); - this.removeTrackingArea(&tracking_area); - } - } - Some(new_window) => { - if tracking_areas.is_empty() { - let tracking_area = new_tracking_area(this); - this.addTrackingArea(&tracking_area); - } - - new_window.setAcceptsMouseMovedEvents(true); - new_window.makeFirstResponder(Some(this)); - } - } - - unsafe { - let superclass = msg_send![this, superclass]; - - let () = msg_send![super(this, superclass), viewWillMoveToWindow: new_window]; - } -} - -extern "C-unwind" fn update_tracking_areas(this: &NSView, _self: Sel, _: &AnyObject) { - let tracking_areas = this.trackingAreas(); - if tracking_areas.count() > 0 { - let tracking_area = tracking_areas.objectAtIndex(0); - this.removeTrackingArea(&tracking_area); - } - - let tracking_area = new_tracking_area(this); - - this.addTrackingArea(&tracking_area); -} - -extern "C-unwind" fn mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { - let state = unsafe { WindowState::from_view(this) }; - let point = this.convertPoint_fromView(event.locationInWindow(), None); - - let position = Point { x: point.x, y: point.y }; - - state.trigger_event(Event::Mouse(MouseEvent::CursorMoved { - position, - modifiers: make_modifiers(event.modifierFlags()), - })); -} - -extern "C-unwind" fn scroll_wheel(this: &NSView, _: Sel, event: &NSEvent) { - let state = unsafe { WindowState::from_view(this) }; - - let x = event.scrollingDeltaX() as f32; - let y = event.scrollingDeltaY() as f32; - - let delta = if event.hasPreciseScrollingDeltas() { - ScrollDelta::Pixels { x, y } - } else { - ScrollDelta::Lines { x, y } - }; - - state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { - delta, - modifiers: make_modifiers(event.modifierFlags()), - })); -} - fn get_drag_position(sender: Option<&ProtocolObject>) -> Point { let point = match sender { Some(sender) => sender.draggingLocation(), @@ -539,7 +561,7 @@ fn get_drop_data(sender: Option<&ProtocolObject>) -> DropDat DropData::Files(files) } -fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { +fn on_event(window_state: &BaseviewView, event: MouseEvent) -> NSDragOperation { let event_status = window_state.trigger_event(Event::Mouse(event)); match event_status { EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperation::Copy, @@ -549,103 +571,3 @@ fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { _ => NSDragOperation::None, } } - -extern "C-unwind" fn dragging_entered( - this: &View, _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); - - let event = MouseEvent::DragEntered { - position: get_drag_position(sender), - modifiers: make_modifiers(modifiers), - data: drop_data, - }; - - on_event(&state, event) -} - -extern "C-unwind" fn dragging_updated( - this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, -) -> NSDragOperation { - let state = unsafe { WindowState::from_view(this) }; - let modifiers = state.keyboard_state().last_mods(); - let drop_data = get_drop_data(sender); - - let event = MouseEvent::DragMoved { - position: get_drag_position(sender), - modifiers: make_modifiers(modifiers), - data: drop_data, - }; - - on_event(&state, event) -} - -extern "C-unwind" fn prepare_for_drag_operation( - _this: &NSView, _sel: Sel, _sender: Option<&ProtocolObject>, -) -> Bool { - // Always accept drag operation if we get this far - // This function won't be called unless dragging_entered/updated - // has returned an acceptable operation - Bool::YES -} - -extern "C-unwind" fn perform_drag_operation( - this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, -) -> Bool { - let state = unsafe { WindowState::from_view(this) }; - let modifiers = state.keyboard_state().last_mods(); - let drop_data = get_drop_data(sender); - - let event = MouseEvent::DragDropped { - position: get_drag_position(sender), - modifiers: make_modifiers(modifiers), - data: drop_data, - }; - - let event_status = state.trigger_event(Event::Mouse(event)); - - match event_status { - EventStatus::AcceptDrop(_) => Bool::YES, - _ => Bool::NO, - } -} - -extern "C-unwind" fn dragging_exited( - this: &NSView, _sel: Sel, _sender: Option<&ProtocolObject>, -) { - let state = unsafe { WindowState::from_view(this) }; - - on_event(&state, MouseEvent::DragLeft); -} - -extern "C-unwind" fn handle_notification(this: &NSView, _cmd: Sel, notification: &NSNotification) { - let state = unsafe { WindowState::from_view(this) }; - - let Some(window) = this.window() else { return }; - // The subject of the notification, in this case an NSWindow object. - let Some(notification_object) = notification.object().and_then(|o| o.downcast().ok()) else { - return; - }; - - // Only trigger focus events if the NSWindow that's being notified about is our window, - // and if the window's first responder is our NSView. - if window != notification_object { - return; - } - - let Some(first_responder) = window.firstResponder() else { return }; - - // If the first responder isn't our NSView, the focus events will instead be triggered - // by the becomeFirstResponder and resignFirstResponder methods on the NSView itself. - if !this.isEqual(Some(&first_responder)) { - return; - } - - state.trigger_event(Event::Window(if window.isKeyWindow() { - WindowEvent::Focused - } else { - WindowEvent::Unfocused - })); -} diff --git a/src/macos/window.rs b/src/macos/window.rs index df7cb35f..20eb2e1b 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -1,44 +1,41 @@ -use std::cell::{Cell, RefCell}; -use std::collections::VecDeque; -use std::ffi::c_void; +use std::cell::Cell; use std::ptr; use std::rc::Rc; -use keyboard_types::KeyboardEvent; -use objc2::rc::{autoreleasepool, Retained}; +use objc2::rc::{autoreleasepool, Retained, Weak}; use objc2::runtime::NSObjectProtocol; use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{ - NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSEvent, NSPasteboard, + NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSPasteboard, NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, }; -use objc2_core_foundation::{ - kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, -}; -use objc2_foundation::{NSNotificationCenter, NSPoint, NSRect, NSSize, NSString}; +use objc2_foundation::{NSPoint, NSRect, NSSize, NSString}; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, }; -use crate::{ - Event, EventStatus, MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions, - WindowScalePolicy, -}; - -use super::keyboard::KeyboardState; -use super::view::{create_view, View, BASEVIEW_STATE_IVAR}; +use crate::{MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy}; #[cfg(feature = "opengl")] use crate::gl::{GlConfig, GlContext}; +use crate::macos::view::BaseviewView; use crate::macos::RetainedCell; +use crate::wrappers::appkit::{View, ViewRef}; pub struct WindowHandle { + view: Option>>, state: Rc, } impl WindowHandle { pub fn close(&mut self) { + let Some(view) = self.view.take().and_then(|w| w.load()) else { + return; + }; + + view.removeFromSuperview(); + self.state.window_inner.close(); } @@ -68,60 +65,9 @@ pub(super) struct WindowInner { /// Our subclassed NSView ns_view: RetainedCell, - - #[cfg(feature = "opengl")] - pub(super) gl_context: Option, } 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 = *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() { - if let Some(run_loop) = CFRunLoop::current() { - run_loop.remove_timer(Some(&frame_timer), kCFRunLoopDefaultMode); - } - } - - // Deregister NSView from NotificationCenter. - let notification_center = NSNotificationCenter::defaultCenter(); - notification_center.removeObserver(&ns_view); - - drop(window_state); - - // Close the window if in non-parented mode - if let Some(ns_window) = self.ns_window.take() { - ns_window.close(); - } - - // Ensure that the NSView is detached from the parent window - 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(Some(&app)); - } - } - } - } - fn raw_window_handle(&self) -> RawWindowHandle { let mut handle = AppKitWindowHandle::empty(); @@ -144,7 +90,13 @@ impl WindowInner { } pub struct Window<'a> { - inner: &'a WindowInner, + view: &'a View, +} + +impl<'a> From> for crate::Window<'a> { + fn from(value: ViewRef<'a, BaseviewView>) -> Self { + crate::Window::new(Window { view: value.view }) + } } impl<'a> Window<'a> { @@ -169,7 +121,7 @@ impl<'a> Window<'a> { panic!("Not a macOS window"); }; - let ns_view = create_view(&options); + let ns_view = BaseviewView::new(options, build); let parent_window = unsafe { Retained::retain(handle.ns_window as *mut NSWindow) }; let parent_view = unsafe { Retained::retain(handle.ns_view as *mut NSView) }; @@ -179,11 +131,6 @@ impl<'a> Window<'a> { 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(&ns_view, gl_config)), }; let window_handle = Self::init(window_inner, window_info, build); @@ -247,22 +194,7 @@ impl<'a> Window<'a> { ns_window.makeKeyAndOrderFront(None); - let ns_view = create_view(&options); - let window_inner = WindowInner { - open: Cell::new(true), - 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(&ns_view, gl_config)), - - ns_window: RetainedCell::new(ns_window.clone()), - }; - - let _ = Self::init(window_inner, window_info, build); + let ns_view = BaseviewView::new(options, build); ns_window.setContentView(Some(&ns_view)); let () = unsafe { msg_send![&*ns_window, setDelegate: &*ns_view] }; @@ -271,53 +203,12 @@ impl<'a> Window<'a> { }) } - fn init(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle - where - H: WindowHandler + 'static, - B: FnOnce(&mut crate::Window) -> H, - B: Send + 'static, - { - 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.get().unwrap(); - - let window_state = Rc::new(WindowState { - window_inner, - window_handler: RefCell::new(window_handler), - keyboard_state: KeyboardState::new(), - frame_timer: RetainedCell::empty(), - window_info: Cell::new(window_info), - deferred_events: RefCell::default(), - }); - - let window_state_ptr = Rc::into_raw(Rc::clone(&window_state)); - - unsafe { - // This creates a cyclic reference: WindowState > WindowInner > NSView > WindowState. - // This cycle gets broken in WindowInner::close and everything is released properly. - // However, this means the cycle holds and the whole leaks if close() is not called. (e.g. if simply dropped) - // This should be refactored at some point to fix this issue. - ns_view - .class() - .instance_variable(BASEVIEW_STATE_IVAR) - .unwrap() - .load_ptr::<*const c_void>(&ns_view) - .write(window_state_ptr as *const c_void); - - WindowState::setup_timer(window_state_ptr); - } - - WindowHandle { state: window_state } - } - pub fn close(&mut self) { self.inner.close(); } pub fn has_focus(&mut self) -> bool { - let view = self.inner.ns_view.get().unwrap(); - let Some(window) = view.window() else { + let Some(window) = self.view.window() else { return false; }; @@ -329,13 +220,12 @@ impl<'a> Window<'a> { return false; }; - view.isEqual(Some(&*first_responder)) + self.view.isEqual(Some(&*first_responder)) } pub fn focus(&mut self) { - let view = self.inner.ns_view.get().unwrap(); - if let Some(window) = view.window() { - window.makeFirstResponder(Some(&view)); + if let Some(window) = self.view.window() { + window.makeFirstResponder(Some(self.view)); } } @@ -381,129 +271,13 @@ impl<'a> Window<'a> { pub(super) struct WindowState { pub(super) window_inner: WindowInner, - window_handler: RefCell>, - keyboard_state: KeyboardState, - frame_timer: RetainedCell, /// The last known window info for this window. pub window_info: Cell, - - /// Events that will be triggered at the end of `window_handler`'s borrow. - deferred_events: RefCell>, } impl WindowState { - /// Gets the `WindowState` held by a given `NSView`. - /// - /// 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: &View) -> Rc { - let state_ptr = view - .class() - .instance_variable(BASEVIEW_STATE_IVAR) - .unwrap() - .load::<*const c_void>(view) - .cast::(); - - let state_rc = Rc::from_raw(state_ptr); - let state = Rc::clone(&state_rc); - let _ = Rc::into_raw(state_rc); - - state - } - - /// Trigger the event immediately and return the event status. - /// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`). - pub(super) fn trigger_event(&self, event: Event) -> EventStatus { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - let mut window_handler = self.window_handler.borrow_mut(); - let status = window_handler.on_event(&mut window, event); - self.send_deferred_events(window_handler.as_mut()); - status - } - - /// Trigger the event immediately if `window_handler` can be borrowed mutably, - /// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends. - /// As this method might result in the event triggering asynchronously, it can't reliably return the event status. - pub(super) fn trigger_deferrable_event(&self, event: Event) { - if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - window_handler.on_event(&mut window, event); - self.send_deferred_events(window_handler.as_mut()); - } else { - self.deferred_events.borrow_mut().push_back(event); - } - } - - pub(super) fn trigger_frame(&self) { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - let mut window_handler = self.window_handler.borrow_mut(); - window_handler.on_frame(&mut window); - self.send_deferred_events(window_handler.as_mut()); - } - - pub(super) fn keyboard_state(&self) -> &KeyboardState { - &self.keyboard_state - } - - 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) { - 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); - - window_state.trigger_frame(); - } - } - - let Some(current_loop) = CFRunLoop::current() else { - return; - }; - - let mut timer_context = CFRunLoopTimerContext { - version: 0, - info: window_state_ptr as *mut c_void, - retain: None, - release: None, - copyDescription: None, - }; - - let Some(timer) = CFRunLoopTimer::new( - kCFAllocatorDefault, - 0.0, - 0.015, - 0, - 0, - Some(timer_callback), - &mut timer_context, - ) else { - return; - }; - - current_loop.add_timer(Some(&timer), kCFRunLoopDefaultMode); - - (*window_state_ptr).frame_timer.set(timer.into()); - } - - fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - loop { - let next_event = self.deferred_events.borrow_mut().pop_front(); - if let Some(event) = next_event { - window_handler.on_event(&mut window, event); - } else { - break; - } - } + pub fn new() -> Self { + todo!() } } diff --git a/src/wrappers.rs b/src/wrappers.rs index 3865dcee..6c0f5bce 100644 --- a/src/wrappers.rs +++ b/src/wrappers.rs @@ -22,3 +22,6 @@ pub mod glx; /// Wrappers and utilities around the Win32 API #[cfg(target_os = "windows")] pub mod win32; + +#[cfg(target_os = "macos")] +pub mod appkit; diff --git a/src/wrappers/appkit.rs b/src/wrappers/appkit.rs new file mode 100644 index 00000000..020761fc --- /dev/null +++ b/src/wrappers/appkit.rs @@ -0,0 +1,5 @@ +mod timer; +mod view; + +pub use timer::TimerHandle; +pub use view::*; diff --git a/src/wrappers/appkit/timer.rs b/src/wrappers/appkit/timer.rs new file mode 100644 index 00000000..f9c47293 --- /dev/null +++ b/src/wrappers/appkit/timer.rs @@ -0,0 +1,40 @@ +use block2::RcBlock; +use objc2::rc::Weak; +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFRetained, CFRunLoop, CFRunLoopTimer, + CFTimeInterval, +}; + +pub struct TimerHandle { + run_loop: Weak, + timer: CFRetained, +} + +impl TimerHandle { + pub fn new(interval: CFTimeInterval, closure: impl Fn() + 'static) -> Option { + let run_loop = CFRunLoop::current()?; + + let block = RcBlock::new(move |_| closure()); + + let allocator = unsafe { kCFAllocatorDefault }; + let timer = + unsafe { CFRunLoopTimer::with_handler(allocator, 0.0, interval, 0, 0, Some(&block)) }?; + + let loop_mode = unsafe { kCFRunLoopDefaultMode }; + run_loop.add_timer(Some(&timer), loop_mode); + + Some(Self { run_loop: Weak::from_retained(&run_loop.into()), timer }) + } +} + +impl Drop for TimerHandle { + fn drop(&mut self) { + let Some(run_loop) = self.run_loop.load() else { + return; + }; + + let loop_mode = unsafe { kCFRunLoopDefaultMode }; + + run_loop.remove_timer(Some(&self.timer), loop_mode); + } +} diff --git a/src/wrappers/appkit/view.rs b/src/wrappers/appkit/view.rs new file mode 100644 index 00000000..aaf0db70 --- /dev/null +++ b/src/wrappers/appkit/view.rs @@ -0,0 +1,167 @@ +use crate::WindowOpenOptions; +use objc2::__framework_prelude::{Allocated, AnyClass, ProtocolObject, Retained}; +use objc2::runtime::AnyObject; +use objc2::{msg_send, sel, Encoding, Message, RefEncode}; +use objc2_app_kit::{ + NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSView, NSWindow, + NSWindowDidBecomeKeyNotification, NSWindowDidResignKeyNotification, +}; +use objc2_core_foundation::{CGRect, CFUUID}; +use objc2_foundation::{NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize}; +use raw_window_handle::{AppKitWindowHandle, HasRawWindowHandle, RawWindowHandle}; +use std::ffi::{c_void, CStr, CString}; +use std::marker::PhantomData; +use std::ops::Deref; + +mod implementation; + +/// Name of the field used to store the `WindowState` pointer. +const BASEVIEW_STATE_IVAR: &CStr = c"baseview_state"; + +#[repr(C)] +pub struct View { + parent: NSView, + _inner: PhantomData>, +} + +// SAFETY: TODO +unsafe impl RefEncode for View { + const ENCODING_REF: Encoding = NSView::ENCODING_REF; +} + +// SAFETY: TODO +unsafe impl Message for View {} + +impl Deref for View { + type Target = NSView; + + fn deref(&self) -> &Self::Target { + &self.parent + } +} + +impl View { + pub fn new(frame: CGRect, inner: V, init: impl FnOnce(ViewRef)) -> Retained> { + // SAFETY: We don't access this reference after this function + let class = unsafe { implementation::create_view_class::() }; + + // SAFETY: This function is valid to call, and Allocated is the correct type for the + // returned pointer + let view: Allocated> = unsafe { msg_send![class, alloc] }; + Self::set_inner(&view, class, ViewInner { inner }); + + let view: Retained> = unsafe { msg_send![view, initWithFrame: frame] }; + + init(view.inner_ref()); + + view + } + + fn set_inner(view: &Allocated>, class: &AnyClass, inner: ViewInner) { + let inner = Box::new(inner); + let ivar = class.instance_variable(BASEVIEW_STATE_IVAR).unwrap(); + let ivar_target = unsafe { &*Allocated::as_ptr(&view).cast() }; + let ivar = unsafe { ivar.load_ptr::<*mut c_void>(ivar_target) }; + unsafe { ivar.write(Box::into_raw(inner).cast()) }; + } + + fn free_inner(this: &AnyObject, class: &AnyClass) { + let ivar = class.instance_variable(BASEVIEW_STATE_IVAR).unwrap(); + let ivar = unsafe { ivar.load_ptr::<*mut c_void>(this) }; + let raw = unsafe { ivar.read() }; + let inner = unsafe { Box::>::from_raw(raw.cast()) }; + unsafe { ivar.write(core::ptr::null_mut()) }; + drop(inner); + } + + fn get_inner(&self) -> &ViewInner { + let ivar = self.class().instance_variable(BASEVIEW_STATE_IVAR).unwrap(); + let ivar = unsafe { ivar.load::<*mut c_void>(self) }; + unsafe { ivar.cast::>().as_ref() }.unwrap() + } + + pub fn inner(&self) -> &V { + &self.get_inner().inner + } + + pub fn inner_ref(&self) -> ViewRef { + ViewRef { view: self, inner: self.inner() } + } +} + +pub struct ViewInner { + inner: V, +} + +fn new_class_name() -> CString { + // PANIC: CFUUIDCreate is not documented to return NULL. + let uuid = CFUUID::new(None).unwrap(); + // PANIC: CFUUIDCreateString is not documented to return NULL. + let uuid_str = CFUUID::new_string(None, Some(&uuid)).unwrap(); + + let class_name = format!("BaseviewNSView_{uuid_str}"); + // PANIC: This cannot have any NULL bytes + CString::new(class_name).unwrap() +} + +pub struct ViewRef<'a, V> { + pub view: &'a View, + pub inner: &'a V, +} + +impl<'a, V> Clone for ViewRef<'a, V> { + fn clone(&self) -> Self { + Self { view: self.view, inner: self.inner } + } +} + +impl<'a, V> Copy for ViewRef<'a, V> {} + +impl Deref for ViewRef<'_, V> { + type Target = V; + + fn deref(&self) -> &Self::Target { + self.inner + } +} + +pub trait ViewImpl: Sized { + fn init(&self, view: &Retained>); + fn become_first_responder(this: ViewRef) -> bool; + fn resign_first_responder(this: ViewRef) -> bool; + fn window_should_close(this: ViewRef) -> bool; + fn view_did_change_backing_properties(this: ViewRef); + fn hit_test(this: ViewRef, point: NSPoint) -> Option<&NSView>; + fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>); + fn update_tracking_areas(this: ViewRef); + fn mouse_moved(this: ViewRef, event: &NSEvent); + fn scroll_wheel(this: ViewRef, event: &NSEvent); + fn dragging_entered( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation; + fn dragging_updated( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation; + fn prepare_for_drag_operation( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> bool; + fn perform_drag_operation( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> bool; + fn dragging_exited(this: ViewRef, sender: Option<&ProtocolObject>); + fn handle_notification(this: ViewRef, notification: &NSNotification); + + fn mouse_down(this: ViewRef, event: &NSEvent); + fn mouse_up(this: ViewRef, event: &NSEvent); + fn right_mouse_down(this: ViewRef, event: &NSEvent); + fn right_mouse_up(this: ViewRef, event: &NSEvent); + fn other_mouse_down(this: ViewRef, event: &NSEvent); + fn other_mouse_up(this: ViewRef, event: &NSEvent); + + fn mouse_entered(this: ViewRef); + fn mouse_exited(this: ViewRef); + + fn key_down(this: ViewRef, event: &NSEvent); + fn key_up(this: ViewRef, event: &NSEvent); + fn flags_changed(this: ViewRef, event: &NSEvent); +} diff --git a/src/wrappers/appkit/view/implementation.rs b/src/wrappers/appkit/view/implementation.rs new file mode 100644 index 00000000..613e61f8 --- /dev/null +++ b/src/wrappers/appkit/view/implementation.rs @@ -0,0 +1,285 @@ +use super::*; +use objc2::__framework_prelude::{AnyClass, AnyObject, Bool, Sel}; +use objc2::ffi::objc_disposeClassPair; +use objc2::runtime::ClassBuilder; +use objc2::{msg_send, sel, ClassType}; +use objc2_app_kit::{NSEvent, NSView}; +use std::ffi::c_void; + +/// # Safety +/// +/// This class is going to be destroyed when its first instance gets deallocated. +/// +/// The returned reference must NOT be used after that point. +pub unsafe fn create_view_class() -> &'static AnyClass { + // Use unique class names so that there are no conflicts between different + // instances. The class is deleted when the view is released. Previously, + // the class was stored in a OnceCell after creation. This way, we didn't + // have to recreate it each time a view was opened, but now we don't leave + // any class definitions lying around when the plugin is closed. + let class_name = new_class_name(); + + let mut class = ClassBuilder::new(&class_name, NSView::class()).unwrap(); + + // SAFETY: All of these function signatures are correct + unsafe { + class.add_method( + sel!(acceptsFirstResponder), + property_yes as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(becomeFirstResponder), + become_first_responder:: as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(resignFirstResponder), + resign_first_responder:: as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method(sel!(isFlipped), property_yes as extern "C-unwind" fn(_, _) -> _); + class.add_method( + sel!(preservesContentInLiveResize), + property_no as extern "C-unwind" fn(_, _) -> _, + ); + class.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C-unwind" fn(_, _, _) -> _, + ); + + class.add_method( + sel!(windowShouldClose:), + window_should_close:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method(sel!(dealloc), dealloc:: as extern "C-unwind" fn(_, _)); + class.add_method( + sel!(viewWillMoveToWindow:), + view_will_move_to_window:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method(sel!(hitTest:), hit_test:: as extern "C-unwind" fn(_, _, _) -> _); + class.add_method( + sel!(updateTrackingAreas:), + update_tracking_areas:: as extern "C-unwind" fn(_, _, _) -> _, + ); + + class.add_method(sel!(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-unwind" fn(_, _, _) -> _, + ); + + class.add_method( + sel!(viewDidChangeBackingProperties:), + view_did_change_backing_properties:: as extern "C-unwind" 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-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(performDragOperation:), + perform_drag_operation:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(draggingUpdated:), + dragging_updated:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(draggingExited:), + dragging_exited:: as extern "C-unwind" fn(_, _, _) -> _, + ); + class.add_method( + sel!(handleNotification:), + handle_notification:: as extern "C-unwind" fn(_, _, _) -> _, + ); + + class.add_method(sel!(mouseDown:), mouse_down:: as extern "C-unwind" fn(_, _, _)); + class.add_method(sel!(mouseUp:), mouse_up:: as extern "C-unwind" fn(_, _, _)); + class.add_method( + sel!(rightMouseDown:), + right_mouse_down:: as extern "C-unwind" fn(_, _, _), + ); + class.add_method(sel!(rightMouseUp:), right_mouse_up:: as extern "C-unwind" fn(_, _, _)); + class.add_method( + sel!(otherMouseDown:), + other_mouse_down:: as extern "C-unwind" fn(_, _, _), + ); + class.add_method(sel!(otherMouseUp:), other_mouse_up:: as extern "C-unwind" fn(_, _, _)); + + class.add_method(sel!(mouseEntered:), mouse_entered:: as extern "C-unwind" fn(_, _, _)); + class.add_method(sel!(mouseExited:), mouse_exited:: as extern "C-unwind" fn(_, _, _)); + + class.add_method(sel!(keyDown:), key_down:: as extern "C-unwind" fn(_, _, _)); + class.add_method(sel!(keyUp:), key_up:: as extern "C-unwind" fn(_, _, _)); + class.add_method(sel!(flagsChanged:), flags_changed:: as extern "C-unwind" fn(_, _, _)); + } + + class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR); + + class.register() +} + +pub extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { + let class = this.class(); + View::::free_inner(this, class); + + if let Some(superclass) = class.superclass() { + let () = unsafe { msg_send![super(this, superclass), dealloc] }; + } + + // SAFETY: This is safe as long as nobody holds a reference to this class. + // On the Baseview side, this is enforced by the safety contract in `create_view_class` + unsafe { objc_disposeClassPair(class as *const _ as *mut _) } +} + +extern "C-unwind" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { + Bool::YES +} + +extern "C-unwind" fn property_no(_this: &NSView, _sel: Sel) -> Bool { + Bool::NO +} + +extern "C-unwind" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { + Bool::YES +} + +extern "C-unwind" fn become_first_responder(this: &View, _sel: Sel) -> Bool { + V::become_first_responder(this.inner_ref()).into() +} + +extern "C-unwind" fn resign_first_responder(this: &View, _sel: Sel) -> Bool { + V::resign_first_responder(this.inner_ref()).into() +} + +extern "C-unwind" fn window_should_close( + this: &View, _: Sel, _sender: &AnyObject, +) -> Bool { + V::window_should_close(this.inner_ref()).into() +} + +extern "C-unwind" fn view_did_change_backing_properties( + this: &View, _: Sel, _: &AnyObject, +) { + V::view_did_change_backing_properties(this.inner_ref()); +} + +extern "C-unwind" fn hit_test( + this: &View, _sel: Sel, point: NSPoint, +) -> Option<&NSView> { + V::hit_test(this.inner_ref(), point) +} + +extern "C-unwind" fn view_will_move_to_window( + this: &View, _self: Sel, new_window: Option<&NSWindow>, +) { + V::view_will_move_to_window(this.inner_ref(), new_window); +} + +extern "C-unwind" fn update_tracking_areas(this: &View, _self: Sel, _: &AnyObject) { + V::update_tracking_areas(this.inner_ref()); +} + +extern "C-unwind" fn mouse_moved(this: &View, _sel: Sel, event: &NSEvent) { + V::mouse_moved(this.inner_ref(), event); +} + +extern "C-unwind" fn scroll_wheel(this: &View, _: Sel, event: &NSEvent) { + V::scroll_wheel(this.inner_ref(), event); +} + +extern "C-unwind" fn dragging_entered( + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, +) -> NSDragOperation { + V::dragging_entered(this.inner_ref(), sender) +} + +extern "C-unwind" fn dragging_updated( + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, +) -> NSDragOperation { + V::dragging_updated(this.inner_ref(), sender) +} + +extern "C-unwind" fn prepare_for_drag_operation( + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, +) -> Bool { + V::prepare_for_drag_operation(this.inner_ref(), sender).into() +} + +extern "C-unwind" fn perform_drag_operation( + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, +) -> Bool { + V::perform_drag_operation(this.inner_ref(), sender).into() +} + +extern "C-unwind" fn dragging_exited( + this: &View, _sel: Sel, sender: Option<&ProtocolObject>, +) { + V::dragging_exited(this.inner_ref(), sender) +} + +extern "C-unwind" fn handle_notification( + this: &View, _cmd: Sel, notification: &NSNotification, +) { + V::handle_notification(this.inner_ref(), notification) +} + +extern "C-unwind" fn mouse_entered(this: &View, _: Sel, _: &AnyObject) { + V::mouse_entered(this.inner_ref()); +} + +extern "C-unwind" fn mouse_exited(this: &View, _: Sel, _: &AnyObject) { + V::mouse_exited(this.inner_ref()); +} + +extern "C-unwind" fn key_down(this: &View, _: Sel, event: &NSEvent) { + V::key_down(this.inner_ref(), event); +} + +extern "C-unwind" fn key_up(this: &View, _: Sel, event: &NSEvent) { + V::key_up(this.inner_ref(), event); +} + +extern "C-unwind" fn flags_changed(this: &View, _: Sel, event: &NSEvent) { + V::flags_changed(this.inner_ref(), event); +} + +extern "C-unwind" fn mouse_down(this: &View, _sel: Sel, event: &NSEvent) { + V::mouse_down(this.inner_ref(), event); +} + +extern "C-unwind" fn mouse_up(this: &View, _sel: Sel, event: &NSEvent) { + V::mouse_up(this.inner_ref(), event); +} + +extern "C-unwind" fn right_mouse_down(this: &View, _sel: Sel, event: &NSEvent) { + V::right_mouse_down(this.inner_ref(), event); +} + +extern "C-unwind" fn right_mouse_up(this: &View, _sel: Sel, event: &NSEvent) { + V::right_mouse_up(this.inner_ref(), event); +} + +extern "C-unwind" fn other_mouse_down(this: &View, _sel: Sel, event: &NSEvent) { + V::other_mouse_down(this.inner_ref(), event); +} + +extern "C-unwind" fn other_mouse_up(this: &View, _sel: Sel, event: &NSEvent) { + V::other_mouse_up(this.inner_ref(), event); +} From 239cc8f5c3e6ac49eea4a0bd4a01deba331814bd Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Sat, 30 May 2026 20:54:16 +0200 Subject: [PATCH 3/3] wip --- src/macos/view.rs | 222 +++++++++++++++++++--------------- src/macos/window.rs | 167 +++++++------------------ src/wrappers/appkit.rs | 19 +++ src/wrappers/appkit/view.rs | 31 +++-- src/wrappers/appkit/window.rs | 32 +++++ 5 files changed, 237 insertions(+), 234 deletions(-) create mode 100644 src/wrappers/appkit/window.rs diff --git a/src/macos/view.rs b/src/macos/view.rs index 769b66b5..a0f2bfe5 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -2,7 +2,6 @@ use super::keyboard::{make_modifiers, KeyboardState}; use super::window::WindowState; -use crate::macos::Window; use crate::wrappers::appkit::*; use crate::MouseEvent::{ButtonPressed, ButtonReleased}; use crate::{ @@ -12,20 +11,17 @@ use crate::{ use objc2::__framework_prelude::Retained; use objc2::rc::Weak; use objc2::runtime::{NSObjectProtocol, ProtocolObject}; -use objc2::{msg_send, sel, AllocAnyThread}; +use objc2::{msg_send, AllocAnyThread}; use objc2_app_kit::{ NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea, - NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification, - NSWindowDidResignKeyNotification, -}; -use objc2_foundation::{ - NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, + NSTrackingAreaOptions, NSView, NSWindow, }; +use objc2_foundation::{NSArray, NSNotification, NSPoint, NSRect, NSSize, NSString}; use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::rc::Rc; -pub struct BaseviewView { +pub(crate) struct BaseviewView { state: Rc, window_handler: RefCell>>, @@ -36,8 +32,9 @@ pub struct BaseviewView { keyboard_state: KeyboardState, #[cfg(feature = "opengl")] - gl_context: std::cell::OnceCell, + pub(crate) gl_context: std::cell::OnceCell, } +// TODO /* pub(super) fn create_view( window_options: &WindowOpenOptions, inner: V, @@ -77,12 +74,14 @@ pub(super) fn create_view( impl BaseviewView { pub fn new( options: WindowOpenOptions, builder: impl FnOnce(&mut crate::Window) -> H, - ) -> Retained> { + ) -> (Retained>, Rc) { let view_rect = NSRect::new(NSPoint::ZERO, NSSize::new(options.size.width, options.size.height)); + let state = Rc::new(WindowState::new(&options)); + let inner = BaseviewView { - state: Rc::new(WindowState::new()), + state: state.clone(), deferred_events: RefCell::default(), keyboard_state: KeyboardState::new(), @@ -96,51 +95,57 @@ impl BaseviewView { let view = View::new(view_rect, inner, |view| { #[cfg(feature = "opengl")] if let Some(gl_config) = options.gl_config { - let gl_context = crate::gl::GlContext::create(&view.view, gl_config).unwrap(); - let _ = view.gl_context.set(gl_context); + let gl_context = crate::gl::GlContext::create(view.view, gl_config).unwrap(); + let Ok(()) = view.gl_context.set(gl_context) else { unreachable!() }; } let handler = builder(&mut view.into()); view.window_handler.replace(Some(Box::new(handler))); }); - view + (view, state) } /// Trigger the event immediately and return the event status. /// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`). - pub(super) fn trigger_event(&self, event: Event) -> EventStatus { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - let mut window_handler = self.window_handler.borrow_mut(); - let status = window_handler.on_event(&mut window, event); - self.send_deferred_events(window_handler.as_mut()); + fn trigger_event(this: ViewRef, event: Event) -> EventStatus { + let mut handler = this.window_handler.borrow_mut(); + let Some(handler) = handler.as_mut() else { + return EventStatus::Ignored; + }; + + let status = handler.on_event(&mut this.into(), event); + Self::send_deferred_events(this, handler.as_mut()); status } /// Trigger the event immediately if `window_handler` can be borrowed mutably, /// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends. /// As this method might result in the event triggering asynchronously, it can't reliably return the event status. - pub(super) fn trigger_deferrable_event(&self, event: Event) { - if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - window_handler.on_event(&mut window, event); - self.send_deferred_events(window_handler.as_mut()); - } else { - self.deferred_events.borrow_mut().push_back(event); - } + fn trigger_deferrable_event(this: ViewRef, event: Event) { + let Ok(mut handler) = this.window_handler.try_borrow_mut() else { + this.deferred_events.borrow_mut().push_back(event); + return; + }; + + let Some(handler) = handler.as_mut() else { return }; + + handler.on_event(&mut this.into(), event); + Self::send_deferred_events(this, handler.as_mut()); } - fn trigger_frame(&self) { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); - let mut window_handler = self.window_handler.borrow_mut(); - window_handler.on_frame(&mut window); - self.send_deferred_events(window_handler.as_mut()); + fn trigger_frame(this: ViewRef) { + let mut handler = this.window_handler.borrow_mut(); + let Some(handler) = handler.as_mut() else { return }; + + handler.on_frame(&mut this.into()); + Self::send_deferred_events(this, handler.as_mut()); } - fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) { - let mut window = crate::Window::new(Window { inner: &self.window_inner }); + fn send_deferred_events(this: ViewRef, window_handler: &mut dyn WindowHandler) { + let mut window = this.into(); loop { - let next_event = self.deferred_events.borrow_mut().pop_front(); + let next_event = { this.deferred_events.borrow_mut().pop_front() }; if let Some(event) = next_event { window_handler.on_event(&mut window, event); } else { @@ -155,7 +160,7 @@ impl ViewImpl for BaseviewView { let timer_view = Weak::from_retained(view); self.frame_timer.set(TimerHandle::new(0.015, move || { if let Some(view) = timer_view.load() { - view.inner().trigger_frame(); + BaseviewView::trigger_frame(view.inner_ref()); } })); } @@ -166,19 +171,19 @@ impl ViewImpl for BaseviewView { }; if window.isKeyWindow() { - this.trigger_deferrable_event(Event::Window(WindowEvent::Focused)); + BaseviewView::trigger_deferrable_event(this, Event::Window(WindowEvent::Focused)); } true } fn resign_first_responder(this: ViewRef) -> bool { - this.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); + BaseviewView::trigger_deferrable_event(this, Event::Window(WindowEvent::Unfocused)); true } fn window_should_close(this: ViewRef) -> bool { - this.trigger_event(Event::Window(WindowEvent::WillClose)); + BaseviewView::trigger_event(this, Event::Window(WindowEvent::WillClose)); //state.window_inner.close(); @@ -190,9 +195,6 @@ impl ViewImpl for BaseviewView { let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - // SAFETY: This is our own view instance - let state = &this.state; - let bounds = this.view.bounds(); let new_window_info = WindowInfo::from_logical_size( @@ -200,13 +202,13 @@ impl ViewImpl for BaseviewView { scale_factor, ); - let window_info = state.window_info.get(); + let window_info = this.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); - this.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); + this.state.window_info.set(new_window_info); + BaseviewView::trigger_event(this, Event::Window(WindowEvent::Resized(new_window_info))); } } @@ -233,7 +235,7 @@ impl ViewImpl for BaseviewView { /// No-op without the `opengl` feature: there's no GL subview to /// collapse, so the override pass-through is equivalent to the /// default implementation. - fn hit_test(this: ViewRef, point: NSPoint) -> Option<&NSView> { + fn hit_test(this: ViewRef<'_, Self>, point: NSPoint) -> Option<&NSView> { let superclass = this.view.class().superclass().unwrap(); // SAFETY: Our superclass is NSView @@ -298,10 +300,13 @@ impl ViewImpl for BaseviewView { let position = Point { x: point.x, y: point.y }; - this.trigger_event(Event::Mouse(MouseEvent::CursorMoved { - position, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(MouseEvent::CursorMoved { + position, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn scroll_wheel(this: ViewRef, event: &NSEvent) { @@ -314,10 +319,13 @@ impl ViewImpl for BaseviewView { ScrollDelta::Lines { x, y } }; - this.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { - delta, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(MouseEvent::WheelScrolled { + delta, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn dragging_entered( @@ -332,7 +340,7 @@ impl ViewImpl for BaseviewView { data: drop_data, }; - on_event(&this, event) + on_event(this, event) } fn dragging_updated( @@ -347,7 +355,7 @@ impl ViewImpl for BaseviewView { data: drop_data, }; - on_event(&this, event) + on_event(this, event) } fn prepare_for_drag_operation( @@ -371,16 +379,13 @@ impl ViewImpl for BaseviewView { data: drop_data, }; - let event_status = this.trigger_event(Event::Mouse(event)); + let event_status = BaseviewView::trigger_event(this, Event::Mouse(event)); - match event_status { - EventStatus::AcceptDrop(_) => true, - _ => false, - } + matches!(event_status, EventStatus::AcceptDrop(_)) } fn dragging_exited(this: ViewRef, _sender: Option<&ProtocolObject>) { - on_event(&this, MouseEvent::DragLeft); + on_event(this, MouseEvent::DragLeft); } fn handle_notification(this: ViewRef, notification: &NSNotification) { @@ -405,66 +410,87 @@ impl ViewImpl for BaseviewView { return; } - this.trigger_event(Event::Window(if window.isKeyWindow() { - WindowEvent::Focused - } else { - WindowEvent::Unfocused - })); + BaseviewView::trigger_event( + this, + Event::Window(if window.isKeyWindow() { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + }), + ); } fn mouse_down(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonPressed { - button: MouseButton::Left, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonPressed { + button: MouseButton::Left, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn mouse_up(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonReleased { - button: MouseButton::Left, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonReleased { + button: MouseButton::Left, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn right_mouse_down(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonPressed { - button: MouseButton::Right, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonPressed { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn right_mouse_up(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonReleased { - button: MouseButton::Right, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonReleased { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn other_mouse_down(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonPressed { - button: MouseButton::Middle, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonPressed { + button: MouseButton::Middle, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn other_mouse_up(this: ViewRef, event: &NSEvent) { - this.trigger_event(Event::Mouse(ButtonReleased { - button: MouseButton::Middle, - modifiers: make_modifiers(event.modifierFlags()), - })); + BaseviewView::trigger_event( + this, + Event::Mouse(ButtonReleased { + button: MouseButton::Middle, + modifiers: make_modifiers(event.modifierFlags()), + }), + ); } fn mouse_entered(this: ViewRef) { - this.trigger_event(Event::Mouse(MouseEvent::CursorEntered)); + BaseviewView::trigger_event(this, Event::Mouse(MouseEvent::CursorEntered)); } fn mouse_exited(this: ViewRef) { - this.trigger_event(Event::Mouse(MouseEvent::CursorLeft)); + BaseviewView::trigger_event(this, Event::Mouse(MouseEvent::CursorLeft)); } fn key_down(this: ViewRef, event: &NSEvent) { if let Some(key_event) = this.keyboard_state.process_native_event(event) { - let status = this.trigger_event(Event::Keyboard(key_event)); + let status = BaseviewView::trigger_event(this, Event::Keyboard(key_event)); if let EventStatus::Ignored = status { unsafe { @@ -478,7 +504,7 @@ impl ViewImpl for BaseviewView { fn key_up(this: ViewRef, event: &NSEvent) { if let Some(key_event) = this.keyboard_state.process_native_event(event) { - let status = this.trigger_event(Event::Keyboard(key_event)); + let status = BaseviewView::trigger_event(this, Event::Keyboard(key_event)); if let EventStatus::Ignored = status { unsafe { @@ -492,7 +518,7 @@ impl ViewImpl for BaseviewView { fn flags_changed(this: ViewRef, event: &NSEvent) { if let Some(key_event) = this.keyboard_state.process_native_event(event) { - let status = this.trigger_event(Event::Keyboard(key_event)); + let status = BaseviewView::trigger_event(this, Event::Keyboard(key_event)); if let EventStatus::Ignored = status { unsafe { @@ -561,8 +587,8 @@ fn get_drop_data(sender: Option<&ProtocolObject>) -> DropDat DropData::Files(files) } -fn on_event(window_state: &BaseviewView, event: MouseEvent) -> NSDragOperation { - let event_status = window_state.trigger_event(Event::Mouse(event)); +fn on_event(this: ViewRef, event: MouseEvent) -> NSDragOperation { + let event_status = BaseviewView::trigger_event(this, Event::Mouse(event)); match event_status { EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperation::Copy, EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperation::Move, diff --git a/src/macos/window.rs b/src/macos/window.rs index 20eb2e1b..651cfbec 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -1,27 +1,26 @@ use std::cell::Cell; -use std::ptr; use std::rc::Rc; -use objc2::rc::{autoreleasepool, Retained, Weak}; +use objc2::rc::{autoreleasepool, Weak}; use objc2::runtime::NSObjectProtocol; -use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; +use objc2::MainThreadMarker; use objc2_app_kit::{ - NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSPasteboard, - NSPasteboardTypeString, NSView, NSWindow, NSWindowStyleMask, + NSApplication, NSApplicationActivationPolicy, NSPasteboard, NSPasteboardTypeString, }; -use objc2_foundation::{NSPoint, NSRect, NSSize, NSString}; +use objc2_foundation::NSString; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, }; -use crate::{MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy}; +use crate::{MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions}; #[cfg(feature = "opengl")] -use crate::gl::{GlConfig, GlContext}; +use crate::gl::GlContext; use crate::macos::view::BaseviewView; -use crate::macos::RetainedCell; -use crate::wrappers::appkit::{View, ViewRef}; +use crate::wrappers::appkit::{ + create_window, extract_raw_window_handle, set_delegate, View, ViewRef, +}; pub struct WindowHandle { view: Option>>, @@ -36,66 +35,35 @@ impl WindowHandle { view.removeFromSuperview(); - self.state.window_inner.close(); + // TODO + + // self.state.window_inner.close(); } pub fn is_open(&self) -> bool { - self.state.window_inner.open.get() + todo!() + //self.state.window_inner.open.get() } } unsafe impl HasRawWindowHandle for WindowHandle { fn raw_window_handle(&self) -> RawWindowHandle { - self.state.window_inner.raw_window_handle() - } -} - -pub(super) struct WindowInner { - open: Cell, - - /// Only set if we created the parent window, i.e. we are running in - /// parentless mode - ns_app: RetainedCell, - /// Only set if we created the parent window, i.e. we are running in - /// parentless mode - ns_window: RetainedCell, - - /// Only set when running in parented mode. - parent_ns_window: RetainedCell, - - /// Our subclassed NSView - ns_view: RetainedCell, -} - -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.get()); - - handle.ns_window = match ns_window { - None => ptr::null_mut(), - Some(view) => (&*view as *const NSWindow) as *mut _, - }; - - handle.ns_view = match self.ns_view.get() { - None => ptr::null_mut(), - Some(view) => (&*view as *const _) as *mut _, - }; - } + let Some(view) = self.view.as_ref().and_then(|w| w.load()) else { + return AppKitWindowHandle::empty().into(); + }; - handle.into() + view.raw_window_handle() } } pub struct Window<'a> { view: &'a View, + inner: &'a BaseviewView, } impl<'a> From> for crate::Window<'a> { fn from(value: ViewRef<'a, BaseviewView>) -> Self { - crate::Window::new(Window { view: value.view }) + crate::Window::new(Window { view: value.view, inner: value.inner }) } } @@ -108,38 +76,16 @@ impl<'a> Window<'a> { B: Send + 'static, { autoreleasepool(|_| { - let scaling = match options.scale { - WindowScalePolicy::ScaleFactor(scale) => scale, - WindowScalePolicy::SystemScaleFactor => 1.0, - }; + let (_parent_window, parent_view) = + extract_raw_window_handle(parent.raw_window_handle()); - let window_info = WindowInfo::from_logical_size(options.size, scaling); - - let handle = if let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() { - handle - } else { - panic!("Not a macOS window"); - }; - - let ns_view = BaseviewView::new(options, build); - 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: RetainedCell::empty(), - ns_window: RetainedCell::empty(), - parent_ns_window: RetainedCell::with(parent_window.clone()), - ns_view: RetainedCell::new(ns_view.clone()), - }; - - let window_handle = Self::init(window_inner, window_info, build); + let (ns_view, state) = BaseviewView::new(options, build); if let Some(parent_view) = parent_view { parent_view.addSubview(&ns_view); } - window_handle + WindowHandle { view: Some(Weak::from_retained(&ns_view)), state } }) } @@ -159,52 +105,25 @@ impl<'a> Window<'a> { let _ = app.setActivationPolicy(NSApplicationActivationPolicy::Regular); - let scaling = match options.scale { - WindowScalePolicy::ScaleFactor(scale) => scale, - WindowScalePolicy::SystemScaleFactor => 1.0, - }; - - let rect = NSRect::new( - NSPoint::ZERO, - NSSize { width: options.size.width, height: options.size.height }, - ); - - let window_info = WindowInfo::from_logical_size(options.size, scaling); - - // SAFETY: This is safe because of the setReleasedWhenClosed(false) below - let ns_window = unsafe { - NSWindow::initWithContentRect_styleMask_backing_defer( - NSWindow::alloc(mtm), - rect, - NSWindowStyleMask::Titled - | NSWindowStyleMask::Closable - | NSWindowStyleMask::Miniaturizable, - NSBackingStoreType::Buffered, - false, - ) - }; - - // SAFETY: setReleasedWhenClosed is always safe to call with `false` (worst case is a memory leak) - unsafe { ns_window.setReleasedWhenClosed(false) }; - - ns_window.center(); + let window = create_window(options.size, mtm); + window.center(); let title = NSString::from_str(&options.title); - ns_window.setTitle(&title); - - ns_window.makeKeyAndOrderFront(None); + window.setTitle(&title); + window.makeKeyAndOrderFront(None); - let ns_view = BaseviewView::new(options, build); + let (view, _) = BaseviewView::new(options, build); - ns_window.setContentView(Some(&ns_view)); - let () = unsafe { msg_send![&*ns_window, setDelegate: &*ns_view] }; + window.setContentView(Some(&view)); + set_delegate(&window, &view); app.run(); }) } pub fn close(&mut self) { - self.inner.close(); + // self.inner.close(); + todo!() } pub fn has_focus(&mut self) -> bool { @@ -230,6 +149,8 @@ impl<'a> Window<'a> { } pub fn resize(&mut self, size: Size) { + todo!() + /* if self.inner.open.get() { // NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even // though the size is in fractional pixels. @@ -251,7 +172,7 @@ impl<'a> Window<'a> { if let Some(ns_window) = self.inner.ns_window.get() { ns_window.setContentSize(size); } - } + }*/ } pub fn set_mouse_cursor(&mut self, _mouse_cursor: MouseCursor) { @@ -260,30 +181,24 @@ impl<'a> Window<'a> { #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&GlContext> { - self.inner.gl_context.as_ref() - } - - #[cfg(feature = "opengl")] - fn create_gl_context(ns_view: &NSView, config: GlConfig) -> GlContext { - GlContext::create(ns_view, config).expect("Could not create OpenGL context") + self.view.inner().gl_context.get() } } -pub(super) struct WindowState { - pub(super) window_inner: WindowInner, +pub(crate) struct WindowState { /// The last known window info for this window. pub window_info: Cell, } impl WindowState { - pub fn new() -> Self { - todo!() + pub fn new(options: &WindowOpenOptions) -> Self { + Self { window_info: WindowInfo::from_logical_size(options.size, 1.0).into() } } } unsafe impl<'a> HasRawWindowHandle for Window<'a> { fn raw_window_handle(&self) -> RawWindowHandle { - self.inner.raw_window_handle() + self.view.raw_window_handle() } } diff --git a/src/wrappers/appkit.rs b/src/wrappers/appkit.rs index 020761fc..ef2dcfba 100644 --- a/src/wrappers/appkit.rs +++ b/src/wrappers/appkit.rs @@ -1,5 +1,24 @@ mod timer; mod view; +mod window; +use objc2::rc::Retained; +use objc2_app_kit::{NSView, NSWindow}; pub use timer::TimerHandle; pub use view::*; +pub use window::*; + +use raw_window_handle::RawWindowHandle; + +pub fn extract_raw_window_handle( + handle: RawWindowHandle, +) -> (Option>, Option>) { + let RawWindowHandle::AppKit(handle) = handle else { + panic!("Not a macOS window"); + }; + + let parent_window = unsafe { Retained::retain(handle.ns_window as *mut NSWindow) }; + let parent_view = unsafe { Retained::retain(handle.ns_view as *mut NSView) }; + + (parent_window, parent_view) +} diff --git a/src/wrappers/appkit/view.rs b/src/wrappers/appkit/view.rs index aaf0db70..0586849d 100644 --- a/src/wrappers/appkit/view.rs +++ b/src/wrappers/appkit/view.rs @@ -1,13 +1,10 @@ -use crate::WindowOpenOptions; use objc2::__framework_prelude::{Allocated, AnyClass, ProtocolObject, Retained}; +use objc2::rc::Weak; use objc2::runtime::AnyObject; -use objc2::{msg_send, sel, Encoding, Message, RefEncode}; -use objc2_app_kit::{ - NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSView, NSWindow, - NSWindowDidBecomeKeyNotification, NSWindowDidResignKeyNotification, -}; +use objc2::{msg_send, Encoding, Message, RefEncode}; +use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSEvent, NSView, NSWindow}; use objc2_core_foundation::{CGRect, CFUUID}; -use objc2_foundation::{NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize}; +use objc2_foundation::{NSNotification, NSPoint}; use raw_window_handle::{AppKitWindowHandle, HasRawWindowHandle, RawWindowHandle}; use std::ffi::{c_void, CStr, CString}; use std::marker::PhantomData; @@ -84,7 +81,7 @@ impl View { &self.get_inner().inner } - pub fn inner_ref(&self) -> ViewRef { + pub fn inner_ref(&self) -> ViewRef<'_, V> { ViewRef { view: self, inner: self.inner() } } } @@ -111,7 +108,7 @@ pub struct ViewRef<'a, V> { impl<'a, V> Clone for ViewRef<'a, V> { fn clone(&self) -> Self { - Self { view: self.view, inner: self.inner } + *self } } @@ -131,7 +128,7 @@ pub trait ViewImpl: Sized { fn resign_first_responder(this: ViewRef) -> bool; fn window_should_close(this: ViewRef) -> bool; fn view_did_change_backing_properties(this: ViewRef); - fn hit_test(this: ViewRef, point: NSPoint) -> Option<&NSView>; + fn hit_test(this: ViewRef<'_, Self>, point: NSPoint) -> Option<&NSView>; fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>); fn update_tracking_areas(this: ViewRef); fn mouse_moved(this: ViewRef, event: &NSEvent); @@ -165,3 +162,17 @@ pub trait ViewImpl: Sized { fn key_up(this: ViewRef, event: &NSEvent); fn flags_changed(this: ViewRef, event: &NSEvent); } + +unsafe impl HasRawWindowHandle for View { + fn raw_window_handle(&self) -> RawWindowHandle { + let mut handle = AppKitWindowHandle::empty(); + + handle.ns_view = (&self.parent as *const NSView).cast_mut().cast(); + + if let Some(window) = self.window() { + handle.ns_window = Retained::as_ptr(&window).cast_mut().cast() + } + + handle.into() + } +} diff --git a/src/wrappers/appkit/window.rs b/src/wrappers/appkit/window.rs new file mode 100644 index 00000000..9699c65b --- /dev/null +++ b/src/wrappers/appkit/window.rs @@ -0,0 +1,32 @@ +use crate::wrappers::appkit::{View, ViewImpl}; +use crate::Size; +use objc2::rc::Retained; +use objc2::{msg_send, MainThreadMarker, MainThreadOnly}; +use objc2_app_kit::{NSBackingStoreType, NSWindow, NSWindowStyleMask}; +use objc2_foundation::{NSPoint, NSRect, NSSize}; + +pub fn create_window(size: Size, mtm: MainThreadMarker) -> Retained { + let rect = NSRect::new(NSPoint::ZERO, NSSize { width: size.width, height: size.height }); + + // SAFETY: This is safe because of the setReleasedWhenClosed(false) below + let ns_window = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + NSWindow::alloc(mtm), + rect, + NSWindowStyleMask::Titled + | NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable, + NSBackingStoreType::Buffered, + false, + ) + }; + + // SAFETY: setReleasedWhenClosed is always safe to call with `false` (worst case is a memory leak) + unsafe { ns_window.setReleasedWhenClosed(false) }; + + ns_window +} + +pub fn set_delegate(window: &NSWindow, delegate: &View) { + let () = unsafe { msg_send![&*window, setDelegate: &*delegate] }; +}