From d3f10bdf2961d4584cb83c2666b1bb4dcbf6aa10 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 4 Sep 2024 08:16:36 +0200 Subject: [PATCH] Use `objc2` and its framework crates (#15) This makes the memory management very clear, and uses a type-safe API to access everything. --- CHANGELOG.md | 3 ++ Cargo.toml | 36 +++++++++++++++++--- src/appkit.rs | 93 +++++++++++++++++++++++++++++---------------------- src/lib.rs | 58 ++++++++++++++++++++++++++------ src/uikit.rs | 69 +++++++++++++++++++++++--------------- 5 files changed, 178 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60da0c0..b4fc5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased - Bump Rust Edition from 2018 to 2021. +- Make `Layer`'s implementation details private; it is now a struct with `as_ptr` and `is_existing` accessor methods. +- Add support for tvOS, watchOS and visionOS. +- Use `objc2` internally. # 0.4.0 (2023-10-31) - Update `raw-window-handle` dep to `0.6.0`. diff --git a/Cargo.toml b/Cargo.toml index fa76cfa..a729d21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,42 @@ exclude = [".github/*"] [dependencies] raw-window-handle = "0.6.0" -objc = "0.2" -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -cocoa = "0.25" -core-graphics = "0.23" +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.5.2" +objc2-foundation = { version = "0.2.2", features = [ + "NSObjCRuntime", + "NSGeometry", +] } +objc2-quartz-core = { version = "0.2.2", features = [ + "CALayer", + "CAMetalLayer", + "objc2-metal", +] } + +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = { version = "0.2.2", features = [ + "NSResponder", + "NSView", + "NSWindow", + "objc2-quartz-core", +] } + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +objc2-ui-kit = { version = "0.2.2", features = [ + "UIResponder", + "UIView", + "UIWindow", + "UIScreen", + "objc2-quartz-core", +] } [package.metadata.docs.rs] targets = [ "x86_64-apple-darwin", + "aarch64-apple-darwin", "aarch64-apple-ios", + "aarch64-apple-ios-macabi", + "x86_64-apple-ios", ] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/appkit.rs b/src/appkit.rs index d3fd2a8..eeae971 100644 --- a/src/appkit.rs +++ b/src/appkit.rs @@ -1,57 +1,70 @@ -use crate::{CAMetalLayer, Layer}; use core::ffi::c_void; -use core_graphics::{base::CGFloat, geometry::CGRect}; -use objc::{ - msg_send, - runtime::{BOOL, YES}, -}; +use objc2::rc::Retained; +use objc2::ClassType; +use objc2_foundation::{NSObject, NSObjectProtocol}; +use objc2_quartz_core::CAMetalLayer; use raw_window_handle::AppKitWindowHandle; use std::ptr::NonNull; +use crate::Layer; + +/// Get or create a new [`Layer`] associated with the given +/// [`AppKitWindowHandle`]. +/// +/// # Safety /// +/// The handle must be valid. pub unsafe fn metal_layer_from_handle(handle: AppKitWindowHandle) -> Layer { - metal_layer_from_ns_view(handle.ns_view) + unsafe { metal_layer_from_ns_view(handle.ns_view) } } +/// Get or create a new [`Layer`] associated with the given `NSView`. /// +/// # Safety +/// +/// The view must be a valid instance of `NSView`. pub unsafe fn metal_layer_from_ns_view(view: NonNull) -> Layer { - let view: cocoa::base::id = view.cast().as_ptr(); + // SAFETY: Caller ensures that the view is valid. + let obj = unsafe { view.cast::().as_ref() }; - // Check if the view is a CAMetalLayer - let class = class!(CAMetalLayer); - let is_actually_layer: BOOL = msg_send![view, isKindOfClass: class]; - if is_actually_layer == YES { - return Layer::Existing(view); + // Check if the view is a `CAMetalLayer`. + if obj.is_kind_of::() { + // SAFETY: Just checked that the view is a `CAMetalLayer`. + let layer = unsafe { view.cast::().as_ref() }; + return Layer { + layer: layer.retain(), + pre_existing: true, + }; } + // Otherwise assume the view is `NSView`. + let view = unsafe { view.cast::().as_ref() }; - // Check if the view contains a valid CAMetalLayer - let existing: CAMetalLayer = msg_send![view, layer]; - let use_current = if existing.is_null() { - false - } else { - let result: BOOL = msg_send![existing, isKindOfClass: class]; - result == YES - }; - - let render_layer = if use_current { - Layer::Existing(existing) - } else { - // Allocate a new CAMetalLayer for the current view - let layer: CAMetalLayer = msg_send![class, new]; - let () = msg_send![view, setLayer: layer]; - let () = msg_send![view, setWantsLayer: YES]; - let bounds: CGRect = msg_send![view, bounds]; - let () = msg_send![layer, setBounds: bounds]; - - let window: cocoa::base::id = msg_send![view, window]; - if !window.is_null() { - let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; - let () = msg_send![layer, setContentsScale: scale_factor]; + // Check if the view contains a valid `CAMetalLayer`. + let existing = unsafe { view.layer() }; + if let Some(existing) = existing { + if existing.is_kind_of::() { + // SAFETY: Just checked that the layer is a `CAMetalLayer`. + let layer = unsafe { Retained::cast::(existing) }; + return Layer { + layer, + pre_existing: true, + }; } + } - Layer::Allocated(layer) - }; + // If the layer was not `CAMetalLayer`, allocate a new one for the view. + let layer = unsafe { CAMetalLayer::new() }; + unsafe { view.setLayer(Some(&layer)) }; + view.setWantsLayer(true); + layer.setBounds(view.bounds()); - let _: *mut c_void = msg_send![view, retain]; - render_layer + if let Some(window) = view.window() { + let scale_factor = window.backingScaleFactor(); + layer.setContentsScale(scale_factor); + } + + Layer { + layer, + pre_existing: false, + } } diff --git a/src/lib.rs b/src/lib.rs index edbb519..559c2d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,55 @@ -#![cfg(any(target_os = "macos", target_os = "ios"))] -#![allow(clippy::missing_safety_doc, clippy::let_unit_value)] +#![cfg(target_vendor = "apple")] +#![allow(clippy::missing_safety_doc)] +#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc)))] +#![deny(unsafe_op_in_unsafe_fn)] -#[macro_use] -extern crate objc; - -use objc::runtime::Object; +use objc2::rc::Retained; +use objc2_quartz_core::CAMetalLayer; +use std::ffi::c_void; +#[cfg(any(target_os = "macos", doc))] pub mod appkit; + +#[cfg(any(not(target_os = "macos"), doc))] pub mod uikit; -pub type CAMetalLayer = *mut Object; +/// A wrapper around [`CAMetalLayer`]. +pub struct Layer { + layer: Retained, + pre_existing: bool, +} + +impl Layer { + /// Get a pointer to the underlying [`CAMetalLayer`]. The pointer is valid + /// for at least as long as the [`Layer`] is valid, but can be extended by + /// retaining it. + /// + /// + /// # Example + /// + /// ```no_run + /// use objc2::rc::Retained; + /// use objc2_quartz_core::CAMetalLayer; + /// use raw_window_metal::Layer; + /// + /// let layer: Layer; + /// # layer = unimplemented!(); + /// + /// let layer: *mut CAMetalLayer = layer.as_ptr().cast(); + /// // SAFETY: The pointer is a valid `CAMetalLayer`. + /// let layer = unsafe { Retained::retain(layer).unwrap() }; + /// + /// // Use the `CAMetalLayer` here. + /// ``` + #[inline] + pub fn as_ptr(&self) -> *mut c_void { + let ptr: *const CAMetalLayer = Retained::as_ptr(&self.layer); + ptr as *mut _ + } -pub enum Layer { - Existing(CAMetalLayer), - Allocated(CAMetalLayer), + /// Whether `raw-window-metal` created a new [`CAMetalLayer`] for you. + #[inline] + pub fn pre_existing(&self) -> bool { + self.pre_existing + } } diff --git a/src/uikit.rs b/src/uikit.rs index 64d4a9c..b48bd43 100644 --- a/src/uikit.rs +++ b/src/uikit.rs @@ -1,48 +1,63 @@ -use crate::{CAMetalLayer, Layer}; -use core_graphics::{base::CGFloat, geometry::CGRect}; -use objc::{ - msg_send, - runtime::{BOOL, YES}, -}; +use crate::Layer; +use objc2::rc::Retained; +use objc2_foundation::NSObjectProtocol; +use objc2_quartz_core::CAMetalLayer; use raw_window_handle::UiKitWindowHandle; use std::{ffi::c_void, ptr::NonNull}; +/// Get or create a new [`Layer`] associated with the given +/// [`UiKitWindowHandle`]. /// +/// # Safety +/// +/// The handle must be valid. pub unsafe fn metal_layer_from_handle(handle: UiKitWindowHandle) -> Layer { if let Some(_ui_view_controller) = handle.ui_view_controller { // TODO: ui_view_controller support } - metal_layer_from_ui_view(handle.ui_view) + unsafe { metal_layer_from_ui_view(handle.ui_view) } } +/// Get or create a new [`Layer`] associated with the given `UIView`. +/// +/// # Safety /// +/// The view must be a valid instance of `UIView`. pub unsafe fn metal_layer_from_ui_view(view: NonNull) -> Layer { - let view: cocoa::base::id = view.cast().as_ptr(); - let main_layer: CAMetalLayer = msg_send![view, layer]; + // SAFETY: Caller ensures that the view is a `UIView`. + let view = unsafe { view.cast::().as_ref() }; + + let main_layer = view.layer(); - let class = class!(CAMetalLayer); - let is_valid_layer: BOOL = msg_send![main_layer, isKindOfClass: class]; - let render_layer = if is_valid_layer == YES { - Layer::Existing(main_layer) + // Check if the view's layer is already a `CAMetalLayer`. + let render_layer = if main_layer.is_kind_of::() { + // SAFETY: Just checked that the layer is a `CAMetalLayer`. + let layer = unsafe { Retained::cast::(main_layer) }; + Layer { + layer, + pre_existing: true, + } } else { - // If the main layer is not a CAMetalLayer, we create a CAMetalLayer sublayer and use it instead. - // Unlike on macOS, we cannot replace the main view as UIView does not allow it (when NSView does). - let new_layer: CAMetalLayer = msg_send![class, new]; + // If the main layer is not a `CAMetalLayer`, we create a + // `CAMetalLayer` sublayer and use it instead. + // + // Unlike on macOS, we cannot replace the main view as `UIView` does + // not allow it (when `NSView` does). + let layer = unsafe { CAMetalLayer::new() }; - let bounds: CGRect = msg_send![main_layer, bounds]; - let () = msg_send![new_layer, setFrame: bounds]; + let bounds = main_layer.bounds(); + layer.setFrame(bounds); - let () = msg_send![main_layer, addSublayer: new_layer]; - Layer::Allocated(new_layer) - }; + main_layer.addSublayer(&layer); - let window: cocoa::base::id = msg_send![view, window]; - if !window.is_null() { - let screen: cocoa::base::id = msg_send![window, screen]; - assert!(!screen.is_null(), "window is not attached to a screen"); + Layer { + layer, + pre_existing: false, + } + }; - let scale_factor: CGFloat = msg_send![screen, nativeScale]; - let () = msg_send![view, setContentScaleFactor: scale_factor]; + if let Some(window) = view.window() { + view.setContentScaleFactor(window.screen().nativeScale()); } render_layer