Skip to content

Commit

Permalink
Use objc2 and its framework crates
Browse files Browse the repository at this point in the history
This makes the memory management very clear, and uses a type-safe API to
access everything.
  • Loading branch information
madsmtm committed Jun 28, 2024
1 parent 13c4e59 commit 9c71664
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 82 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
36 changes: 32 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
80 changes: 42 additions & 38 deletions src/appkit.rs
Original file line number Diff line number Diff line change
@@ -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;

///

Check warning on line 11 in src/appkit.rs

View workflow job for this annotation

GitHub Actions / Build - MacOS (macOS-latest)

empty doc comment
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) }
}

///

Check warning on line 16 in src/appkit.rs

View workflow job for this annotation

GitHub Actions / Build - MacOS (macOS-latest)

empty doc comment
pub unsafe fn metal_layer_from_ns_view(view: NonNull<c_void>) -> Layer {
let view: cocoa::base::id = view.cast().as_ptr();
// SAFETY: Caller ensures that the view is valid.
let obj = unsafe { view.cast::<NSObject>().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::<CAMetalLayer>() {
// SAFETY: Just checked that the view is a `CAMetalLayer`.
let layer = unsafe { view.cast::<CAMetalLayer>().as_ref() };
return Layer {
layer: layer.retain(),
pre_existing: true,
};
}
// Otherwise assume the view is `NSView`
let view = unsafe { view.cast::<objc2_app_kit::NSView>().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::<CAMetalLayer>() {
// SAFETY: Just checked that the layer is a `CAMetalLayer`.
let layer = unsafe { Retained::cast::<CAMetalLayer>(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,
}
}
58 changes: 48 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<CAMetalLayer>,
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
}
}
66 changes: 36 additions & 30 deletions src/uikit.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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<c_void>) -> 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::<objc2_ui_kit::UIView>().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::<CAMetalLayer>() {
// SAFETY: Just checked that the layer is a `CAMetalLayer`.
let layer = unsafe { Retained::cast::<CAMetalLayer>(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
Expand Down

0 comments on commit 9c71664

Please sign in to comment.