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 0fafa37a..769b66b5 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -1,103 +1,51 @@ #![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}; +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::CFUUID; use objc2_foundation::{ NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString, }; -use std::ffi::{c_void, CStr, CString}; - -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, -}; - -/// Name of the field used to store the `WindowState` pointer. -pub(super) const BASEVIEW_STATE_IVAR: &CStr = c"baseview_state"; - -macro_rules! add_simple_mouse_class_method { - ($class:ident, $sel:ident, $event:expr) => { - #[allow(non_snake_case)] - extern "C-unwind" fn $sel(this: &NSView, _: Sel, _: &AnyObject){ - let state = unsafe { WindowState::from_view(this) }; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; - state.trigger_event(Event::Mouse($event)); - } - - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _,); - }; -} - -/// 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()), - })); - } - - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _); - }; -} - -macro_rules! add_simple_keyboard_class_method { - ($class:ident, $sel:ident) => { - #[allow(non_snake_case)] - extern "C-unwind" fn $sel(this: &NSView, _: Sel, event: &NSEvent){ - let state = unsafe { WindowState::from_view(this) }; - - if let Some(key_event) = state.process_native_key_event(event){ - let status = state.trigger_event(Event::Keyboard(key_event)); +pub struct BaseviewView { + state: Rc, + window_handler: RefCell>>, - if let EventStatus::Ignored = status { - unsafe { - let superclass = msg_send![this, superclass]; + /// Events that will be triggered at the end of `window_handler`'s borrow. + deferred_events: RefCell>, - let () = msg_send![super(this, superclass), $sel:event]; - } - } - } - } + frame_timer: Cell>, + keyboard_state: KeyboardState, - $class.add_method(sel!($sel:), $sel as extern "C-unwind" fn(_, _, _) -> _); - }; + #[cfg(feature = "opengl")] + gl_context: std::cell::OnceCell, } - -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, inner: V, +) -> 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)), inner); + /* let notification_center = NSNotificationCenter::defaultCenter(); // SAFETY: Our NSView class does have a handleNotification: method with the matching signature. @@ -114,360 +62,471 @@ pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained( + 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 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 handler = builder(&mut view.into()); + view.window_handler.replace(Some(Box::new(handler))); + }); - let class_name = format!("BaseviewNSView_{uuid_str}"); - // PANIC: This cannot have any NULL bytes - CString::new(class_name).unwrap() -} + view + } -/// # 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(_, _, _) -> _, - ); + /// 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 + } - 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(_, _, _) -> _, - ); + /// 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); + } + } - 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 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()); + } - class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C-unwind" fn(_, _, _) -> _); + 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; + } + } + } +} - class.add_method( - sel!(viewDidChangeBackingProperties:), - view_did_change_backing_properties as extern "C-unwind" fn(_, _, _) -> _, - ); +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(); + } + })); + } - 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 become_first_responder(this: ViewRef) -> bool { + let Some(window) = this.view.window() else { + return true; + }; - 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); + if window.isKeyWindow() { + this.trigger_deferrable_event(Event::Window(WindowEvent::Focused)); + } - add_simple_keyboard_class_method!(class, keyDown); - add_simple_keyboard_class_method!(class, keyUp); - add_simple_keyboard_class_method!(class, flagsChanged); + true } - class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR); + fn resign_first_responder(this: ViewRef) -> bool { + this.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused)); + true + } - class.register() -} + fn window_should_close(this: ViewRef) -> bool { + this.trigger_event(Event::Window(WindowEvent::WillClose)); -extern "C-unwind" fn property_yes(_this: &NSView, _sel: Sel) -> Bool { - Bool::YES -} + //state.window_inner.close(); -extern "C-unwind" fn property_no(_this: &NSView, _sel: Sel) -> Bool { - Bool::NO -} + false + } -extern "C-unwind" fn accepts_first_mouse(_this: &NSView, _sel: Sel, _event: &NSEvent) -> Bool { - Bool::YES -} + fn view_did_change_backing_properties(this: ViewRef) { + let ns_window = this.view.window(); -extern "C-unwind" fn become_first_responder(this: &NSView, _sel: Sel) -> Bool { - let Some(window) = this.window() else { - return Bool::YES; - }; + let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); - 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 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))); + } } - Bool::YES -} + /// `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); + } + } + } -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 -} + Some(super_result) + } -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) }; + fn view_will_move_to_window(this: ViewRef, new_window: Option<&NSWindow>) { + let tracking_areas = this.view.trackingAreas(); + + 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); + } - state.trigger_event(Event::Window(WindowEvent::WillClose)); + new_window.setAcceptsMouseMovedEvents(true); + new_window.makeFirstResponder(Some(this.view)); + } + } - state.window_inner.close(); + unsafe { + let superclass = msg_send![this.view, superclass]; - Bool::NO -} + let () = msg_send![super(this.view, superclass), viewWillMoveToWindow: new_window]; + } + } -extern "C-unwind" fn dealloc(this: &mut AnyObject, _sel: Sel) { - let class = this.class(); + 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); + } - if let Some(superclass) = class.superclass() { - let () = unsafe { msg_send![super(this, superclass), dealloc] }; + let tracking_area = new_tracking_area(this.view); + + this.view.addTrackingArea(&tracking_area); } - // 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 mouse_moved(this: ViewRef, event: &NSEvent) { + let point = this.view.convertPoint_fromView(event.locationInWindow(), None); -extern "C-unwind" fn view_did_change_backing_properties(this: &NSView, _: Sel, _: &AnyObject) { - let ns_window = this.window(); + let position = Point { x: point.x, y: point.y }; - let scale_factor: f64 = ns_window.map(|w| w.backingScaleFactor()).unwrap_or(1.0); + this.trigger_event(Event::Mouse(MouseEvent::CursorMoved { + position, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - // SAFETY: This is our own view instance - let state = unsafe { WindowState::from_view(this) }; + fn scroll_wheel(this: ViewRef, event: &NSEvent) { + let x = event.scrollingDeltaX() as f32; + let y = event.scrollingDeltaY() as f32; - let bounds = this.bounds(); + let delta = if event.hasPreciseScrollingDeltas() { + ScrollDelta::Pixels { x, y } + } else { + ScrollDelta::Lines { x, y } + }; - let new_window_info = WindowInfo::from_logical_size( - Size::new(bounds.size.width, bounds.size.height), - scale_factor, - ); + this.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { + delta, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - let window_info = state.window_info.get(); + fn dragging_entered( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation { + let modifiers = this.keyboard_state.last_mods(); + let drop_data = get_drop_data(sender); - // 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))); + let event = MouseEvent::DragEntered { + position: get_drag_position(sender), + modifiers: make_modifiers(modifiers), + data: drop_data, + }; + + on_event(&this, event) } -} -/// Info: -/// https://developer.apple.com/documentation/appkit/nstrackingarea -/// https://developer.apple.com/documentation/appkit/nstrackingarea/options -/// https://developer.apple.com/documentation/appkit/nstrackingareaoptions -fn new_tracking_area(this: &NSView) -> Retained { - let options = NSTrackingAreaOptions::MouseEnteredAndExited - | NSTrackingAreaOptions::MouseMoved - | NSTrackingAreaOptions::CursorUpdate - | NSTrackingAreaOptions::ActiveInActiveApp - | NSTrackingAreaOptions::InVisibleRect - | NSTrackingAreaOptions::EnabledDuringMouseDrag; + fn dragging_updated( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> NSDragOperation { + let modifiers = this.keyboard_state.last_mods(); + let drop_data = get_drop_data(sender); - // SAFETY: `this` is of the correct type (NSView) - unsafe { - NSTrackingArea::initWithRect_options_owner_userInfo( - NSTrackingArea::alloc(), - this.bounds(), - options, - Some(this), - None, - ) + let event = MouseEvent::DragMoved { + position: get_drag_position(sender), + modifiers: make_modifiers(modifiers), + data: drop_data, + }; + + on_event(&this, event) } -} -/// `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?; + 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 + } - #[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); - } + fn perform_drag_operation( + this: ViewRef, sender: Option<&ProtocolObject>, + ) -> bool { + let modifiers = this.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 = this.trigger_event(Event::Mouse(event)); + + match event_status { + EventStatus::AcceptDrop(_) => true, + _ => false, } } - 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(); + fn dragging_exited(this: ViewRef, _sender: Option<&ProtocolObject>) { + on_event(&this, MouseEvent::DragLeft); + } - match new_window { - None => { - if tracking_areas.count() > 0 { - let tracking_area = tracking_areas.objectAtIndex(0); - this.removeTrackingArea(&tracking_area); - } + 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; } - 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)); + 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.view.isEqual(Some(&first_responder)) { + return; } + + this.trigger_event(Event::Window(if window.isKeyWindow() { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + })); } - unsafe { - let superclass = msg_send![this, superclass]; + fn mouse_down(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonPressed { + button: MouseButton::Left, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - let () = msg_send![super(this, superclass), viewWillMoveToWindow: new_window]; + 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 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); + fn right_mouse_down(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonPressed { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + })); } - let tracking_area = new_tracking_area(this); + fn right_mouse_up(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonReleased { + button: MouseButton::Right, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - this.addTrackingArea(&tracking_area); -} + 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 mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { - let state = unsafe { WindowState::from_view(this) }; - let point = this.convertPoint_fromView(event.locationInWindow(), None); + fn other_mouse_up(this: ViewRef, event: &NSEvent) { + this.trigger_event(Event::Mouse(ButtonReleased { + button: MouseButton::Middle, + modifiers: make_modifiers(event.modifierFlags()), + })); + } - let position = Point { x: point.x, y: point.y }; + fn mouse_entered(this: ViewRef) { + this.trigger_event(Event::Mouse(MouseEvent::CursorEntered)); + } - state.trigger_event(Event::Mouse(MouseEvent::CursorMoved { - position, - modifiers: make_modifiers(event.modifierFlags()), - })); -} + fn mouse_exited(this: ViewRef) { + this.trigger_event(Event::Mouse(MouseEvent::CursorLeft)); + } -extern "C-unwind" fn scroll_wheel(this: &NSView, _: Sel, event: &NSEvent) { - let state = unsafe { WindowState::from_view(this) }; + 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 x = event.scrollingDeltaX() as f32; - let y = event.scrollingDeltaY() as f32; + if let EventStatus::Ignored = status { + unsafe { + let superclass = msg_send![this.view, superclass]; - let delta = if event.hasPreciseScrollingDeltas() { - ScrollDelta::Pixels { x, y } - } else { - ScrollDelta::Lines { x, y } - }; + let () = msg_send![super(this.view, superclass), keyDown:event]; + } + } + } + } + + 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]; + } + } + } + } +} + +/// Info: +/// https://developer.apple.com/documentation/appkit/nstrackingarea +/// https://developer.apple.com/documentation/appkit/nstrackingarea/options +/// https://developer.apple.com/documentation/appkit/nstrackingareaoptions +fn new_tracking_area(this: &NSView) -> Retained { + let options = NSTrackingAreaOptions::MouseEnteredAndExited + | NSTrackingAreaOptions::MouseMoved + | NSTrackingAreaOptions::CursorUpdate + | NSTrackingAreaOptions::ActiveInActiveApp + | NSTrackingAreaOptions::InVisibleRect + | NSTrackingAreaOptions::EnabledDuringMouseDrag; - state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { - delta, - modifiers: make_modifiers(event.modifierFlags()), - })); + // SAFETY: `this` is of the correct type (NSView) + unsafe { + NSTrackingArea::initWithRect_options_owner_userInfo( + NSTrackingArea::alloc(), + this.bounds(), + options, + Some(this), + None, + ) + } } fn get_drag_position(sender: Option<&ProtocolObject>) -> Point { @@ -502,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, @@ -512,103 +571,3 @@ fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation { _ => NSDragOperation::None, } } - -extern "C-unwind" fn dragging_entered( - this: &NSView, _sel: Sel, sender: Option<&ProtocolObject>, -) -> NSDragOperation { - let state = unsafe { WindowState::from_view(this) }; - let modifiers = state.keyboard_state().last_mods(); - let drop_data = get_drop_data(sender); - - 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 90a94ed5..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, 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(); } @@ -67,61 +64,10 @@ pub(super) struct WindowInner { parent_ns_window: RetainedCell, /// Our subclassed NSView - ns_view: RetainedCell, - - #[cfg(feature = "opengl")] - pub(super) gl_context: Option, + ns_view: RetainedCell, } 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(); @@ -135,7 +81,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 _, }; } @@ -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: &NSView) -> Rc { - let state_ptr = view - .class() - .instance_variable(BASEVIEW_STATE_IVAR) - .unwrap() - .load::<*const c_void>(view) - .cast::(); - - let state_rc = Rc::from_raw(state_ptr); - let state = Rc::clone(&state_rc); - let _ = Rc::into_raw(state_rc); - - 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); +}