From 9c7166492a1599e74f47244af97443047cb33144 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 5 Sep 2023 15:27:14 +0200 Subject: [PATCH 1/3] Use objc2 and its framework crates 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 | 80 +++++++++++++++++++++++++++------------------------ src/lib.rs | 58 ++++++++++++++++++++++++++++++------- src/uikit.rs | 66 +++++++++++++++++++++++------------------- 5 files changed, 161 insertions(+), 82 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 844e310..cfbfde6 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..cc73eb8 100644 --- a/src/appkit.rs +++ b/src/appkit.rs @@ -1,57 +1,61 @@ -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; + /// 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) } } /// 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); + 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]; + 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..c1ccdf6 100644 --- a/src/uikit.rs +++ b/src/uikit.rs @@ -1,9 +1,7 @@ -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}; @@ -12,37 +10,45 @@ 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) } } /// 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]; - - 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) + // SAFETY: Caller ensures that the view is a UIView + let view = unsafe { view.cast::().as_ref() }; + + let main_layer = view.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]; - - let bounds: CGRect = msg_send![main_layer, bounds]; - let () = msg_send![new_layer, setFrame: bounds]; - - let () = msg_send![main_layer, addSublayer: new_layer]; - Layer::Allocated(new_layer) + // 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 = main_layer.bounds(); + layer.setFrame(bounds); + + main_layer.addSublayer(&layer); + + Layer { + layer, + pre_existing: false, + } }; - 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"); - - 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 From 883d5c537a1e4195d4cab0ea0c1fba880185a2c4 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 28 Jun 2024 23:30:43 +0200 Subject: [PATCH 2/3] Fix comment formatting --- src/appkit.rs | 8 ++++---- src/uikit.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/appkit.rs b/src/appkit.rs index cc73eb8..89429d9 100644 --- a/src/appkit.rs +++ b/src/appkit.rs @@ -18,7 +18,7 @@ pub unsafe fn metal_layer_from_ns_view(view: NonNull) -> Layer { // SAFETY: Caller ensures that the view is valid. let obj = unsafe { view.cast::().as_ref() }; - // Check if the view is a CAMetalLayer + // 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() }; @@ -27,10 +27,10 @@ pub unsafe fn metal_layer_from_ns_view(view: NonNull) -> Layer { pre_existing: true, }; } - // Otherwise assume the view is `NSView` + // Otherwise assume the view is `NSView`. let view = unsafe { view.cast::().as_ref() }; - // Check if the view contains a valid CAMetalLayer + // Check if the view contains a valid `CAMetalLayer`. let existing = unsafe { view.layer() }; if let Some(existing) = existing { if existing.is_kind_of::() { @@ -43,7 +43,7 @@ pub unsafe fn metal_layer_from_ns_view(view: NonNull) -> Layer { } } - // If the layer was not `CAMetalLayer`, allocate a new one for the view + // 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); diff --git a/src/uikit.rs b/src/uikit.rs index c1ccdf6..2bc4bf8 100644 --- a/src/uikit.rs +++ b/src/uikit.rs @@ -15,12 +15,12 @@ pub unsafe fn metal_layer_from_handle(handle: UiKitWindowHandle) -> Layer { /// pub unsafe fn metal_layer_from_ui_view(view: NonNull) -> Layer { - // SAFETY: Caller ensures that the view is a UIView + // SAFETY: Caller ensures that the view is a `UIView`. let view = unsafe { view.cast::().as_ref() }; let main_layer = view.layer(); - // Check if the view's layer is already a CAMetalLayer + // 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) }; @@ -29,11 +29,11 @@ pub unsafe fn metal_layer_from_ui_view(view: NonNull) -> Layer { pre_existing: true, } } else { - // If the main layer is not a CAMetalLayer, we create a CAMetalLayer - // sublayer and use it instead. + // 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). + // 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 = main_layer.bounds(); From 4bfd0c28038ea382c446c373d6698f621b1674a8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 28 Jun 2024 23:33:03 +0200 Subject: [PATCH 3/3] Safety docs --- src/appkit.rs | 9 +++++++++ src/uikit.rs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/appkit.rs b/src/appkit.rs index 89429d9..eeae971 100644 --- a/src/appkit.rs +++ b/src/appkit.rs @@ -8,12 +8,21 @@ 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 { 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 { // SAFETY: Caller ensures that the view is valid. let obj = unsafe { view.cast::().as_ref() }; diff --git a/src/uikit.rs b/src/uikit.rs index 2bc4bf8..b48bd43 100644 --- a/src/uikit.rs +++ b/src/uikit.rs @@ -5,7 +5,12 @@ 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 @@ -13,7 +18,11 @@ pub unsafe fn metal_layer_from_handle(handle: UiKitWindowHandle) -> Layer { 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 { // SAFETY: Caller ensures that the view is a `UIView`. let view = unsafe { view.cast::().as_ref() };