Skip to content

Commit 4a3978b

Browse files
committed
Use objc2 and its framework crates
This makes the memory management very clear, and uses a type-safe API to access everything.
1 parent 13c4e59 commit 4a3978b

File tree

5 files changed

+159
-86
lines changed

5 files changed

+159
-86
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22
- Bump Rust Edition from 2018 to 2021.
3+
- Make `Layer`'s implementation details private; it is now a struct with `as_ptr` and `is_existing` accessor methods.
4+
- Add support for tvOS, watchOS and visionOS.
5+
- Use `objc2` internally.
36

47
# 0.4.0 (2023-10-31)
58
- Update `raw-window-handle` dep to `0.6.0`.

Cargo.toml

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,36 @@ exclude = [".github/*"]
1414

1515
[dependencies]
1616
raw-window-handle = "0.6.0"
17-
objc = "0.2"
1817

19-
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
20-
cocoa = "0.25"
21-
core-graphics = "0.23"
18+
[target.'cfg(target_vendor = "apple")'.dependencies]
19+
objc2 = "0.5.2"
20+
objc2-foundation = { version = "0.2.2", features = [
21+
"NSObjCRuntime",
22+
"NSGeometry",
23+
] }
24+
objc2-quartz-core = { version = "0.2.2", features = [
25+
"CALayer",
26+
"CAMetalLayer",
27+
"objc2-metal",
28+
] }
29+
30+
[target.'cfg(target_os = "macos")'.dependencies]
31+
objc2-app-kit = { version = "0.2.2", features = [
32+
"NSResponder",
33+
"NSView",
34+
"NSWindow",
35+
"objc2-quartz-core",
36+
] }
37+
38+
[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies]
39+
objc2-ui-kit = { version = "0.2.2", features = [
40+
"UIResponder",
41+
"UIView",
42+
"UIWindow",
43+
"UIScreen",
44+
"objc2-quartz-core",
45+
] }
2246

2347
[package.metadata.docs.rs]
24-
targets = [
25-
"x86_64-apple-darwin",
26-
"aarch64-apple-ios",
27-
]
48+
targets = ["x86_64-apple-darwin", "aarch64-apple-ios"]
49+
rustdoc-args = ["--cfg", "docsrs"]

src/appkit.rs

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,61 @@
1-
use crate::{CAMetalLayer, Layer};
21
use core::ffi::c_void;
3-
use core_graphics::{base::CGFloat, geometry::CGRect};
4-
use objc::{
5-
msg_send,
6-
runtime::{BOOL, YES},
7-
};
2+
use objc2::rc::Retained;
3+
use objc2::ClassType;
4+
use objc2_foundation::{NSObject, NSObjectProtocol};
5+
use objc2_quartz_core::CAMetalLayer;
86
use raw_window_handle::AppKitWindowHandle;
97
use std::ptr::NonNull;
108

9+
use crate::Layer;
10+
1111
///
1212
pub unsafe fn metal_layer_from_handle(handle: AppKitWindowHandle) -> Layer {
13-
metal_layer_from_ns_view(handle.ns_view)
13+
unsafe { metal_layer_from_ns_view(handle.ns_view) }
1414
}
1515

1616
///
1717
pub unsafe fn metal_layer_from_ns_view(view: NonNull<c_void>) -> Layer {
18-
let view: cocoa::base::id = view.cast().as_ptr();
18+
// SAFETY: Caller ensures that the view is valid.
19+
let obj = unsafe { view.cast::<NSObject>().as_ref() };
1920

2021
// Check if the view is a CAMetalLayer
21-
let class = class!(CAMetalLayer);
22-
let is_actually_layer: BOOL = msg_send![view, isKindOfClass: class];
23-
if is_actually_layer == YES {
24-
return Layer::Existing(view);
22+
if obj.is_kind_of::<CAMetalLayer>() {
23+
// SAFETY: Just checked that the view is a `CAMetalLayer`.
24+
let layer = unsafe { view.cast::<CAMetalLayer>().as_ref() };
25+
return Layer {
26+
layer: layer.retain(),
27+
pre_existing: true,
28+
};
2529
}
30+
// Otherwise assume the view is `NSView`
31+
let view = unsafe { view.cast::<objc2_app_kit::NSView>().as_ref() };
2632

2733
// Check if the view contains a valid CAMetalLayer
28-
let existing: CAMetalLayer = msg_send![view, layer];
29-
let use_current = if existing.is_null() {
30-
false
31-
} else {
32-
let result: BOOL = msg_send![existing, isKindOfClass: class];
33-
result == YES
34-
};
35-
36-
let render_layer = if use_current {
37-
Layer::Existing(existing)
38-
} else {
39-
// Allocate a new CAMetalLayer for the current view
40-
let layer: CAMetalLayer = msg_send![class, new];
41-
let () = msg_send![view, setLayer: layer];
42-
let () = msg_send![view, setWantsLayer: YES];
43-
let bounds: CGRect = msg_send![view, bounds];
44-
let () = msg_send![layer, setBounds: bounds];
45-
46-
let window: cocoa::base::id = msg_send![view, window];
47-
if !window.is_null() {
48-
let scale_factor: CGFloat = msg_send![window, backingScaleFactor];
49-
let () = msg_send![layer, setContentsScale: scale_factor];
34+
let existing = unsafe { view.layer() };
35+
if let Some(existing) = existing {
36+
if existing.is_kind_of::<CAMetalLayer>() {
37+
// SAFETY: Just checked that the layer is a `CAMetalLayer`.
38+
let layer = unsafe { Retained::cast::<CAMetalLayer>(existing) };
39+
return Layer {
40+
layer,
41+
pre_existing: true,
42+
};
5043
}
44+
}
5145

52-
Layer::Allocated(layer)
53-
};
46+
// If the layer was not `CAMetalLayer`, allocate a new one for the view
47+
let layer = unsafe { CAMetalLayer::new() };
48+
unsafe { view.setLayer(Some(&layer)) };
49+
view.setWantsLayer(true);
50+
layer.setBounds(view.bounds());
5451

55-
let _: *mut c_void = msg_send![view, retain];
56-
render_layer
52+
if let Some(window) = view.window() {
53+
let scale_factor = window.backingScaleFactor();
54+
layer.setContentsScale(scale_factor);
55+
}
56+
57+
Layer {
58+
layer,
59+
pre_existing: false,
60+
}
5761
}

src/lib.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,55 @@
1-
#![cfg(any(target_os = "macos", target_os = "ios"))]
2-
#![allow(clippy::missing_safety_doc, clippy::let_unit_value)]
1+
#![cfg(target_vendor = "apple")]
2+
#![allow(clippy::missing_safety_doc)]
3+
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc)))]
4+
#![deny(unsafe_op_in_unsafe_fn)]
35

4-
#[macro_use]
5-
extern crate objc;
6-
7-
use objc::runtime::Object;
6+
use objc2::rc::Retained;
7+
use objc2_quartz_core::CAMetalLayer;
8+
use std::ffi::c_void;
89

10+
#[cfg(any(target_os = "macos", doc))]
911
pub mod appkit;
12+
13+
#[cfg(any(not(target_os = "macos"), doc))]
1014
pub mod uikit;
1115

12-
pub type CAMetalLayer = *mut Object;
16+
/// A wrapper around [`CAMetalLayer`].
17+
pub struct Layer {
18+
layer: Retained<CAMetalLayer>,
19+
pre_existing: bool,
20+
}
21+
22+
impl Layer {
23+
/// Get a pointer to the underlying [`CAMetalLayer`]. The pointer is valid
24+
/// for at least as long as the [`Layer`] is valid, but can be extended by
25+
/// retaining it.
26+
///
27+
///
28+
/// # Example
29+
///
30+
/// ```no_run
31+
/// use objc2::rc::Retained;
32+
/// use objc2_quartz_core::CAMetalLayer;
33+
/// use raw_window_metal::Layer;
34+
///
35+
/// let layer: Layer;
36+
/// # layer = unimplemented!();
37+
///
38+
/// let layer: *mut CAMetalLayer = layer.as_ptr().cast();
39+
/// // SAFETY: The pointer is a valid `CAMetalLayer`.
40+
/// let layer = unsafe { Retained::retain(layer).unwrap() };
41+
///
42+
/// // Use the `CAMetalLayer` here.
43+
/// ```
44+
#[inline]
45+
pub fn as_ptr(&self) -> *mut c_void {
46+
let ptr: *const CAMetalLayer = Retained::as_ptr(&self.layer);
47+
ptr as *mut _
48+
}
1349

14-
pub enum Layer {
15-
Existing(CAMetalLayer),
16-
Allocated(CAMetalLayer),
50+
/// Whether `raw-window-metal` created a new [`CAMetalLayer`] for you.
51+
#[inline]
52+
pub fn pre_existing(&self) -> bool {
53+
self.pre_existing
54+
}
1755
}

src/uikit.rs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
use crate::{CAMetalLayer, Layer};
2-
use core_graphics::{base::CGFloat, geometry::CGRect};
3-
use objc::{
4-
msg_send,
5-
runtime::{BOOL, YES},
6-
};
1+
use crate::Layer;
2+
use objc2::rc::Retained;
3+
use objc2_foundation::NSObjectProtocol;
4+
use objc2_quartz_core::CAMetalLayer;
75
use raw_window_handle::UiKitWindowHandle;
86
use std::{ffi::c_void, ptr::NonNull};
97

@@ -12,37 +10,45 @@ pub unsafe fn metal_layer_from_handle(handle: UiKitWindowHandle) -> Layer {
1210
if let Some(_ui_view_controller) = handle.ui_view_controller {
1311
// TODO: ui_view_controller support
1412
}
15-
metal_layer_from_ui_view(handle.ui_view)
13+
unsafe { metal_layer_from_ui_view(handle.ui_view) }
1614
}
1715

1816
///
1917
pub unsafe fn metal_layer_from_ui_view(view: NonNull<c_void>) -> Layer {
20-
let view: cocoa::base::id = view.cast().as_ptr();
21-
let main_layer: CAMetalLayer = msg_send![view, layer];
22-
23-
let class = class!(CAMetalLayer);
24-
let is_valid_layer: BOOL = msg_send![main_layer, isKindOfClass: class];
25-
let render_layer = if is_valid_layer == YES {
26-
Layer::Existing(main_layer)
18+
// SAFETY: Caller ensures that the view is a UIView
19+
let view = unsafe { view.cast::<objc2_ui_kit::UIView>().as_ref() };
20+
21+
let main_layer = view.layer();
22+
23+
// Check if the view's layer is already a CAMetalLayer
24+
let render_layer = if main_layer.is_kind_of::<CAMetalLayer>() {
25+
// SAFETY: Just checked that the layer is a `CAMetalLayer`.
26+
let layer = unsafe { Retained::cast::<CAMetalLayer>(main_layer) };
27+
Layer {
28+
layer,
29+
pre_existing: true,
30+
}
2731
} else {
28-
// If the main layer is not a CAMetalLayer, we create a CAMetalLayer sublayer and use it instead.
29-
// Unlike on macOS, we cannot replace the main view as UIView does not allow it (when NSView does).
30-
let new_layer: CAMetalLayer = msg_send![class, new];
31-
32-
let bounds: CGRect = msg_send![main_layer, bounds];
33-
let () = msg_send![new_layer, setFrame: bounds];
34-
35-
let () = msg_send![main_layer, addSublayer: new_layer];
36-
Layer::Allocated(new_layer)
32+
// If the main layer is not a CAMetalLayer, we create a CAMetalLayer
33+
// sublayer and use it instead.
34+
//
35+
// Unlike on macOS, we cannot replace the main view as UIView does not
36+
// allow it (when NSView does).
37+
let layer = unsafe { CAMetalLayer::new() };
38+
39+
let bounds = main_layer.bounds();
40+
layer.setFrame(bounds);
41+
42+
main_layer.addSublayer(&layer);
43+
44+
Layer {
45+
layer,
46+
pre_existing: false,
47+
}
3748
};
3849

39-
let window: cocoa::base::id = msg_send![view, window];
40-
if !window.is_null() {
41-
let screen: cocoa::base::id = msg_send![window, screen];
42-
assert!(!screen.is_null(), "window is not attached to a screen");
43-
44-
let scale_factor: CGFloat = msg_send![screen, nativeScale];
45-
let () = msg_send![view, setContentScaleFactor: scale_factor];
50+
if let Some(window) = view.window() {
51+
view.setContentScaleFactor(window.screen().nativeScale());
4652
}
4753

4854
render_layer

0 commit comments

Comments
 (0)