Skip to content

Commit

Permalink
Use objc2 and its framework crates (#15)
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 authored Sep 4, 2024
1 parent b279c22 commit d3f10bd
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 81 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"]
93 changes: 53 additions & 40 deletions src/appkit.rs
Original file line number Diff line number Diff line change
@@ -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<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);
// Check if the view is a `CAMetalLayer`.
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];
// Check if the view contains a valid `CAMetalLayer`.
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
}
}
69 changes: 42 additions & 27 deletions src/uikit.rs
Original file line number Diff line number Diff line change
@@ -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<c_void>) -> 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::<objc2_ui_kit::UIView>().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::<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];
// 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
Expand Down

0 comments on commit d3f10bd

Please sign in to comment.