From 46ee9e2ea883c5ee372819c8fc38828eeebebf43 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 26 Mar 2021 16:25:57 -0700 Subject: [PATCH] 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. --- examples/autolayout.rs | 1 + src/defaults/mod.rs | 4 +- src/layer/mod.rs | 62 +++++++++++++++++++++++++ src/lib.rs | 1 + src/listview/row/mod.rs | 5 +- src/view/mod.rs | 100 +++++++++++++++++++++------------------- 6 files changed, 122 insertions(+), 51 deletions(-) create mode 100644 src/layer/mod.rs diff --git a/examples/autolayout.rs b/examples/autolayout.rs index 10b2164..fb564c4 100644 --- a/examples/autolayout.rs +++ b/examples/autolayout.rs @@ -40,6 +40,7 @@ impl WindowDelegate for AppWindow { }); self.blue.set_background_color(Color::SystemBlue); + self.blue.layer.set_corner_radius(16.); self.content.add_subview(&self.blue); self.red.set_background_color(Color::SystemRed); diff --git a/src/defaults/mod.rs b/src/defaults/mod.rs index e3a6e04..581193a 100644 --- a/src/defaults/mod.rs +++ b/src/defaults/mod.rs @@ -212,10 +212,10 @@ impl UserDefaults { "d" => Some(Value::Float(number.as_f64())), "q" => Some(Value::Integer(number.as_i64())), - x => { + _x => { // Debugging code that should be removed at some point. #[cfg(debug_assertions)] - println!("Unexpected code type found: {}", x); + println!("Unexpected code type found: {}", _x); None } diff --git a/src/layer/mod.rs b/src/layer/mod.rs new file mode 100644 index 0000000..c185f5b --- /dev/null +++ b/src/layer/mod.rs @@ -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]; + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 46b0086..86d1756 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,7 @@ pub mod foundation; pub mod geometry; pub mod image; pub mod input; +pub mod layer; pub(crate) mod invoker; pub mod layout; pub mod listview; diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index 10f491c..0f0f0eb 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -46,10 +46,11 @@ use std::cell::RefCell; use objc_id::{Id, ShareId}; 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::color::Color; +use crate::layer::Layer; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::pasteboard::PasteboardType; use crate::view::ViewDelegate; @@ -260,6 +261,8 @@ impl ListViewRow { pub(crate) fn clone_as_handle(&self) -> crate::view::View { crate::view::View { delegate: None, + is_handle: true, + layer: Layer::new(), top: self.top.clone(), leading: self.leading.clone(), left: self.left.clone(), diff --git a/src/view/mod.rs b/src/view/mod.rs index 55976b3..2812035 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -7,7 +7,7 @@ //! Views implement Autolayout, which enable you to specify how things should appear on the screen. //! //! ```rust,no_run -//! use cacao::color::rgb; +//! use cacao::color::Color; //! use cacao::layout::{Layout, LayoutConstraint}; //! use cacao::view::View; //! use cacao::window::{Window, WindowDelegate}; @@ -24,7 +24,7 @@ //! window.set_minimum_content_size(300., 300.); //! 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.window.set_content_view(&self.content); @@ -41,12 +41,12 @@ //! //! 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::{msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; +use crate::layer::Layer; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::pasteboard::PasteboardType; use crate::utils::properties::ObjcProperty; @@ -75,27 +75,23 @@ pub use traits::ViewDelegate; pub(crate) static BACKGROUND_COLOR: &str = "alchemyBackgroundColor"; 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 /// instead of a stock `View` for easier recordkeeping, since it'll need to hold the `View` on that /// side anyway. #[derive(Debug)] pub struct View { + /// An internal flag for whether an instance of a View 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. 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. pub delegate: Option>, @@ -131,17 +127,28 @@ pub struct View { } impl Default for View { + /// Returns a stock view, for... well, whatever you want. fn default() -> Self { View::new() } } impl View { - /// Returns a default `View`, suitable for - pub fn new() -> Self { - let view = common_init(register_view_class()); + /// An internal initializer method for very common things that we need to do, regardless of + /// what type the end user is creating. + /// + /// This handles grabbing autolayout anchor pointers, as well as things related to layering and + /// so on. It returns a generic `View`, which the caller can then customize as needed. + pub(crate) fn init(view: id) -> View { + unsafe { + let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; + + #[cfg(target_os = "macos")] + let _: () = msg_send![view, setWantsLayer:YES]; + } View { + is_handle: false, delegate: None, top: LayoutAnchorY::top(view), left: LayoutAnchorX::left(view), @@ -153,9 +160,21 @@ impl View { height: LayoutAnchorDimension::height(view), center_x: LayoutAnchorX::center(view), center_y: LayoutAnchorY::center(view), + + layer: Layer::wrap(unsafe { + msg_send![view, layer] + }), + 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 View where T: ViewDelegate + 'static { @@ -166,28 +185,14 @@ impl View where T: ViewDelegate + 'static { let mut delegate = Box::new(delegate); let view = unsafe { - let view: id = common_init(class); + let view: id = msg_send![class, new]; let ptr = Box::into_raw(delegate); (&mut *view).set_ivar(VIEW_DELEGATE_PTR, ptr as usize); delegate = Box::from_raw(ptr); view }; - let mut view = 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), - }; - + let mut view = View::init(view); (&mut delegate).did_load(view.clone_as_handle()); view.delegate = Some(delegate); view @@ -202,6 +207,8 @@ impl View { pub(crate) fn clone_as_handle(&self) -> View { View { delegate: None, + is_handle: true, + layer: self.layer.clone(), top: self.top.clone(), leading: self.leading.clone(), left: self.left.clone(), @@ -245,20 +252,17 @@ impl Layout for View { } impl Drop for View { - /// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being - /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if - /// 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). + /// If the instance being dropped is _not_ a handle, then we want to go ahead and explicitly + /// remove it from any super views. /// - /// 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) { - /*if self.delegate.is_some() { - unsafe { - let superview: id = msg_send![&*self.objc, superview]; - if superview != nil { - let _: () = msg_send![&*self.objc, removeFromSuperview]; - } - } - }*/ + if !self.is_handle { + self.remove_from_superview(); + } } }