Add in .layer support for Views.

- Cleans up `View` implementation so there's less boilerplate all
  around.

- Adds in a `Layer` wrapper for `CALayer`s on widgets.
This commit is contained in:
Ryan McGrath 2021-03-26 16:25:57 -07:00
parent 2894699ace
commit 46ee9e2ea8
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
6 changed files with 122 additions and 51 deletions

View file

@ -40,6 +40,7 @@ impl WindowDelegate for AppWindow {
}); });
self.blue.set_background_color(Color::SystemBlue); self.blue.set_background_color(Color::SystemBlue);
self.blue.layer.set_corner_radius(16.);
self.content.add_subview(&self.blue); self.content.add_subview(&self.blue);
self.red.set_background_color(Color::SystemRed); self.red.set_background_color(Color::SystemRed);

View file

@ -212,10 +212,10 @@ impl UserDefaults {
"d" => Some(Value::Float(number.as_f64())), "d" => Some(Value::Float(number.as_f64())),
"q" => Some(Value::Integer(number.as_i64())), "q" => Some(Value::Integer(number.as_i64())),
x => { _x => {
// Debugging code that should be removed at some point. // Debugging code that should be removed at some point.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
println!("Unexpected code type found: {}", x); println!("Unexpected code type found: {}", _x);
None None
} }

62
src/layer/mod.rs Normal file
View file

@ -0,0 +1,62 @@
//! Wraps `CALayer` across all platforms.
//!
//! Each widget has an underlying `layer` field that you can access, which offers additional
//! rendering tools.
//!
//! ```rust,no_run
//! // Create a rounded red box
//! let view = View::default();
//! view.set_background_color(Color::SystemRed);
//! view.layer.set_corner_radius(4.0);
//! ```
use core_graphics::base::CGFloat;
use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::id;
use crate::utils::properties::ObjcProperty;
/// Represents a `CALayer`.
///
/// Each widget has an underlying `layer` field that you can access, which offers additional
/// rendering tools.
///
/// ```rust,no_run
/// // Create a rounded red box
/// let view = View::default();
/// view.set_background_color(Color::SystemRed);
/// view.layer.set_corner_radius(4.0);
/// ```
#[derive(Clone, Debug)]
pub struct Layer {
/// The underlying layer pointer.
pub objc: ObjcProperty
}
impl Layer {
/// Creates a new `CALayer` and retains it.
pub fn new() -> Self {
Layer {
objc: ObjcProperty::retain(unsafe {
msg_send![class!(CALayer), new]
})
}
}
/// Wraps an existing (already retained) `CALayer`.
pub fn wrap(layer: id) -> Self {
Layer {
objc: ObjcProperty::from_retained(layer)
}
}
/// Sets the corner radius (for all four corners).
///
/// Note that for performance sensitive contexts, you might want to apply a mask instead.
pub fn set_corner_radius(&self, radius: f64) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setCornerRadius:radius as CGFloat];
});
}
}

View file

@ -120,6 +120,7 @@ pub mod foundation;
pub mod geometry; pub mod geometry;
pub mod image; pub mod image;
pub mod input; pub mod input;
pub mod layer;
pub(crate) mod invoker; pub(crate) mod invoker;
pub mod layout; pub mod layout;
pub mod listview; pub mod listview;

View file

@ -46,10 +46,11 @@ use std::cell::RefCell;
use objc_id::{Id, ShareId}; use objc_id::{Id, ShareId};
use objc::runtime::{Class, Object}; use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layer::Layer;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::view::ViewDelegate; use crate::view::ViewDelegate;
@ -260,6 +261,8 @@ impl<T> ListViewRow<T> {
pub(crate) fn clone_as_handle(&self) -> crate::view::View { pub(crate) fn clone_as_handle(&self) -> crate::view::View {
crate::view::View { crate::view::View {
delegate: None, delegate: None,
is_handle: true,
layer: Layer::new(),
top: self.top.clone(), top: self.top.clone(),
leading: self.leading.clone(), leading: self.leading.clone(),
left: self.left.clone(), left: self.left.clone(),

View file

@ -7,7 +7,7 @@
//! Views implement Autolayout, which enable you to specify how things should appear on the screen. //! Views implement Autolayout, which enable you to specify how things should appear on the screen.
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use cacao::color::rgb; //! use cacao::color::Color;
//! use cacao::layout::{Layout, LayoutConstraint}; //! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::View; //! use cacao::view::View;
//! use cacao::window::{Window, WindowDelegate}; //! use cacao::window::{Window, WindowDelegate};
@ -24,7 +24,7 @@
//! window.set_minimum_content_size(300., 300.); //! window.set_minimum_content_size(300., 300.);
//! self.window = window; //! self.window = window;
//! //!
//! self.red.set_background_color(rgb(224, 82, 99)); //! self.red.set_background_color(Color::SystemRed);
//! self.content.add_subview(&self.red); //! self.content.add_subview(&self.red);
//! //!
//! self.window.set_content_view(&self.content); //! self.window.set_content_view(&self.content);
@ -41,12 +41,12 @@
//! //!
//! For more information on Autolayout, view the module or check out the examples folder. //! For more information on Autolayout, view the module or check out the examples folder.
use objc_id::{Id, ShareId};
use objc::runtime::{Class, Object}; use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layer::Layer;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
@ -75,27 +75,23 @@ pub use traits::ViewDelegate;
pub(crate) static BACKGROUND_COLOR: &str = "alchemyBackgroundColor"; pub(crate) static BACKGROUND_COLOR: &str = "alchemyBackgroundColor";
pub(crate) static VIEW_DELEGATE_PTR: &str = "rstViewDelegatePtr"; pub(crate) static VIEW_DELEGATE_PTR: &str = "rstViewDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn common_init(class: *const Class) -> id {
unsafe {
let view: id = msg_send![class, new];
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
let _: () = msg_send![view, setWantsLayer:YES];
view
}
}
/// A clone-able handler to a `ViewController` reference in the Objective C runtime. We use this /// A clone-able handler to a `ViewController` reference in the Objective C runtime. We use this
/// instead of a stock `View` for easier recordkeeping, since it'll need to hold the `View` on that /// instead of a stock `View` for easier recordkeeping, since it'll need to hold the `View` on that
/// side anyway. /// side anyway.
#[derive(Debug)] #[derive(Debug)]
pub struct View<T = ()> { pub struct View<T = ()> {
/// An internal flag for whether an instance of a View<T> is a handle. Typically, there's only
/// one instance that should have this set to `false` - if that one drops, we need to know to
/// do some extra cleanup.
pub is_handle: bool,
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ObjcProperty, pub objc: ObjcProperty,
/// References the underlying layer. This is consistent across macOS, iOS and tvOS - on macOS
/// we explicitly opt in to layer backed views.
pub layer: Layer,
/// A pointer to the delegate for this view. /// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>, pub delegate: Option<Box<T>>,
@ -131,17 +127,28 @@ pub struct View<T = ()> {
} }
impl Default for View { impl Default for View {
/// Returns a stock view, for... well, whatever you want.
fn default() -> Self { fn default() -> Self {
View::new() View::new()
} }
} }
impl View { impl View {
/// Returns a default `View`, suitable for /// An internal initializer method for very common things that we need to do, regardless of
pub fn new() -> Self { /// what type the end user is creating.
let view = common_init(register_view_class()); ///
/// This handles grabbing autolayout anchor pointers, as well as things related to layering and
/// so on. It returns a generic `View<T>`, which the caller can then customize as needed.
pub(crate) fn init<T>(view: id) -> View<T> {
unsafe {
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
let _: () = msg_send![view, setWantsLayer:YES];
}
View { View {
is_handle: false,
delegate: None, delegate: None,
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
left: LayoutAnchorX::left(view), left: LayoutAnchorX::left(view),
@ -153,9 +160,21 @@ impl View {
height: LayoutAnchorDimension::height(view), height: LayoutAnchorDimension::height(view),
center_x: LayoutAnchorX::center(view), center_x: LayoutAnchorX::center(view),
center_y: LayoutAnchorY::center(view), center_y: LayoutAnchorY::center(view),
layer: Layer::wrap(unsafe {
msg_send![view, layer]
}),
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
} }
} }
/// Returns a default `View`, suitable for customizing and displaying.
pub fn new() -> Self {
View::init(unsafe {
msg_send![register_view_class(), new]
})
}
} }
impl<T> View<T> where T: ViewDelegate + 'static { impl<T> View<T> where T: ViewDelegate + 'static {
@ -166,28 +185,14 @@ impl<T> View<T> where T: ViewDelegate + 'static {
let mut delegate = Box::new(delegate); let mut delegate = Box::new(delegate);
let view = unsafe { let view = unsafe {
let view: id = common_init(class); let view: id = msg_send![class, new];
let ptr = Box::into_raw(delegate); let ptr = Box::into_raw(delegate);
(&mut *view).set_ivar(VIEW_DELEGATE_PTR, ptr as usize); (&mut *view).set_ivar(VIEW_DELEGATE_PTR, ptr as usize);
delegate = Box::from_raw(ptr); delegate = Box::from_raw(ptr);
view view
}; };
let mut view = View { let mut view = View::init(view);
delegate: None,
top: LayoutAnchorY::top(view),
left: LayoutAnchorX::left(view),
leading: LayoutAnchorX::leading(view),
right: LayoutAnchorX::right(view),
trailing: LayoutAnchorX::trailing(view),
bottom: LayoutAnchorY::bottom(view),
width: LayoutAnchorDimension::width(view),
height: LayoutAnchorDimension::height(view),
center_x: LayoutAnchorX::center(view),
center_y: LayoutAnchorY::center(view),
objc: ObjcProperty::retain(view),
};
(&mut delegate).did_load(view.clone_as_handle()); (&mut delegate).did_load(view.clone_as_handle());
view.delegate = Some(delegate); view.delegate = Some(delegate);
view view
@ -202,6 +207,8 @@ impl<T> View<T> {
pub(crate) fn clone_as_handle(&self) -> View { pub(crate) fn clone_as_handle(&self) -> View {
View { View {
delegate: None, delegate: None,
is_handle: true,
layer: self.layer.clone(),
top: self.top.clone(), top: self.top.clone(),
leading: self.leading.clone(), leading: self.leading.clone(),
left: self.left.clone(), left: self.left.clone(),
@ -245,20 +252,17 @@ impl<T> Layout for View<T> {
} }
impl<T> Drop for View<T> { impl<T> Drop for View<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// If the instance being dropped is _not_ a handle, then we want to go ahead and explicitly
/// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if /// remove it from any super views.
/// this has a superview (i.e, it's in the heirarchy) on the AppKit side. If it does, we go
/// ahead and remove it - this is intended to match the semantics of how Rust handles things).
/// ///
/// There are, thankfully, no delegates we need to break here. /// Why do we do this? It's to try and match Rust's ownership model/semantics. If a Rust value
/// drops, it (theoretically) makes sense that the View would drop... and not be visible, etc.
///
/// If you're venturing into unsafe code for the sake of custom behavior via the Objective-C
/// runtime, you can consider flagging your instance as a handle - it will avoid the drop logic here.
fn drop(&mut self) { fn drop(&mut self) {
/*if self.delegate.is_some() { if !self.is_handle {
unsafe { self.remove_from_superview();
let superview: id = msg_send![&*self.objc, superview];
if superview != nil {
let _: () = msg_send![&*self.objc, removeFromSuperview];
} }
} }
}*/
}
} }