Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use objc2 and its framework crates #15

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() };
madsmtm marked this conversation as resolved.
Show resolved Hide resolved

madsmtm marked this conversation as resolved.
Show resolved Hide resolved
// 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());
madsmtm marked this conversation as resolved.
Show resolved Hide resolved

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() };
///
madsmtm marked this conversation as resolved.
Show resolved Hide resolved
/// // 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
}
}
madsmtm marked this conversation as resolved.
Show resolved Hide resolved
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() };

madsmtm marked this conversation as resolved.
Show resolved Hide resolved
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
Loading