Some updates. API still in flux, but you can build

- Added support for Image
- Added a QuickLook feature, to enable thumbnail generation.
- Added support for NSButton.
- Fixed a bug where App activation under Big Sur would leave menus
  without the ability to be used.
- Added the ability for Buttons and ToolbarItems to execute callbacks.
- Added support for Labels and TextFields.
- Added support for MenuItems to have callbacks as well.
- Preliminary ListView support; you have to cache your ListViewRow items
  yourself for the time being, but it works.
- Animation support for ListView operations.
- Support for ScrollViews.
- Helpers for dispatching actions to the main thread (for UI work).
- Updated the Dispatcher trait to make thread handling simpler.
- Basic font support.
This commit is contained in:
Ryan McGrath 2021-01-16 17:11:04 -08:00
parent 784727748c
commit 121a2f938e
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
46 changed files with 3183 additions and 74 deletions

View file

@ -1,14 +1,13 @@
//! Specifies various frameworks to link against. Note that this is something where you probably //! Emits linker flags depending on platforms and features.
//! only want to be compiling this project on macOS. ;P
//! //!
//! (it checks to see if it's macOS before emitting anything, but still) //! (iOS/macOS only right now... maybe tvOS one day?)
fn main() { fn main() {
let target = std::env::var("TARGET").unwrap(); let target = std::env::var("TARGET").unwrap();
println!("cargo:rustc-link-lib=framework=Foundation"); println!("cargo:rustc-link-lib=framework=Foundation");
if std::env::var("TARGET").unwrap().contains("-ios") { if target.contains("-ios") {
println!("cargo:rustc-link-lib=framework=UIKit"); println!("cargo:rustc-link-lib=framework=UIKit");
} else { } else {
println!("cargo:rustc-link-lib=framework=AppKit"); println!("cargo:rustc-link-lib=framework=AppKit");
@ -16,7 +15,6 @@ fn main() {
println!("cargo:rustc-link-lib=framework=CoreGraphics"); println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=QuartzCore"); println!("cargo:rustc-link-lib=framework=QuartzCore");
println!("cargo:rustc-link-lib=framework=Security"); println!("cargo:rustc-link-lib=framework=Security");
#[cfg(feature = "webview")] #[cfg(feature = "webview")]
@ -27,4 +25,7 @@ fn main() {
#[cfg(feature = "user-notifications")] #[cfg(feature = "user-notifications")]
println!("cargo:rustc-link-lib=framework=UserNotifications"); println!("cargo:rustc-link-lib=framework=UserNotifications");
#[cfg(feature = "quicklook")]
println!("cargo:rustc-link-lib=framework=QuickLook");
} }

View file

@ -1,19 +1,49 @@
//! A wrapper for NSButton. Currently the epitome of jank - if you're poking around here, expect //! A wrapper for NSButton. Currently the epitome of jank - if you're poking around here, expect
//! that this will change at some point. //! that this will change at some point.
use std::fmt;
use std::sync::Once; use std::sync::Once;
use objc_id::Id; use objc_id::ShareId;
use objc::declare::ClassDecl; use objc::declare::ClassDecl;
use objc::runtime::{Class, Object}; use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{nil, NSString}; use crate::foundation::{id, nil, BOOL, YES, NO, NSString};
use crate::invoker::TargetActionHandler;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::utils::load;
/// A wrapper for `NSButton`. Holds (retains) pointers for the Objective-C runtime /// A wrapper for `NSButton`. Holds (retains) pointers for the Objective-C runtime
/// where our `NSButton` lives. /// where our `NSButton` lives.
#[derive(Debug)]
pub struct Button { pub struct Button {
pub objc: Id<Object> pub objc: ShareId<Object>,
handler: Option<TargetActionHandler>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
} }
impl Button { impl Button {
@ -21,12 +51,24 @@ impl Button {
/// and retains the necessary Objective-C runtime pointer. /// and retains the necessary Objective-C runtime pointer.
pub fn new(text: &str) -> Self { pub fn new(text: &str) -> Self {
let title = NSString::new(text); let title = NSString::new(text);
let objc = unsafe {
Id::from_ptr(msg_send![register_class(), buttonWithTitle:title target:nil action:nil]) let view: id = unsafe {
let button: id = msg_send![register_class(), buttonWithTitle:title target:nil action:nil];
let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO];
button
}; };
Button { Button {
objc: objc handler: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
} }
} }
@ -36,10 +78,43 @@ impl Button {
let _: () = msg_send![&*self.objc, setBezelStyle:bezel_style]; let _: () = msg_send![&*self.objc, setBezelStyle:bezel_style];
} }
} }
/// Attaches a callback for button press events. Don't get too creative now...
/// best just to message pass or something.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) {
let handler = TargetActionHandler::new(&*self.objc, action);
self.handler = Some(handler);
}
} }
/// Registers an `NSButton` subclass, and configures it to hold some ivars for various things we need impl Layout for Button {
/// to store. fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
/*let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}*/
}
}
impl Drop for Button {
// Just to be sure, let's... nil these out. They should be weak references,
// but I'd rather be paranoid and remove them later.
fn drop(&mut self) {
unsafe {
let _: () = msg_send![&*self.objc, setTarget:nil];
let _: () = msg_send![&*self.objc, setAction:nil];
}
}
}
/// Registers an `NSButton` subclass, and configures it to hold some ivars
/// for various things we need to store.
fn register_class() -> *const Class { fn register_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class; static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new(); static INIT: Once = Once::new();

View file

@ -4,7 +4,7 @@
use objc::runtime::Object; use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use objc_id::Id; use objc_id::ShareId;
use crate::foundation::NSUInteger; use crate::foundation::NSUInteger;
use crate::pasteboard::Pasteboard; use crate::pasteboard::Pasteboard;
@ -54,7 +54,7 @@ impl From<DragOperation> for NSUInteger {
/// A wrapper for `NSDraggingInfo`. As this is a protocol/type you should never create yourself, /// A wrapper for `NSDraggingInfo`. As this is a protocol/type you should never create yourself,
/// this only provides getters - merely a Rust-y way to grab what you need. /// this only provides getters - merely a Rust-y way to grab what you need.
pub struct DragInfo { pub struct DragInfo {
pub info: Id<Object> pub info: ShareId<Object>
} }
impl DragInfo { impl DragInfo {

View file

@ -49,7 +49,7 @@ impl NSArray {
/// This handles that edge case. /// This handles that edge case.
pub fn wrap(array: id) -> Self { pub fn wrap(array: id) -> Self {
NSArray(unsafe { NSArray(unsafe {
Id::from_retained_ptr(array) Id::from_ptr(array)
}) })
} }

54
src/image/handle.rs Normal file
View file

@ -0,0 +1,54 @@
use objc_id::ShareId;
use objc::runtime::Object;
/// Views get passed these, and can
#[derive(Debug)]
pub struct ViewHandle<T> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
_t: std::marker::PhantomData<T>
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl<T> TextControl for ViewHandle<T>
where
T:
impl<T> Layout for ViewHandle<T> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}

16
src/image/image.rs Normal file
View file

@ -0,0 +1,16 @@
use objc_id::ShareId;
use objc::runtime::Object;
use crate::foundation::{id};
#[derive(Clone, Debug)]
pub struct Image(pub ShareId<Object>);
impl Image {
pub fn with(image: id) -> Self {
Image(unsafe {
ShareId::from_ptr(image)
})
}
}

45
src/image/ios.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate};
use crate::utils::load;
/// Injects an `NSView` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: ViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTViewWithDelegate", superclass).unwrap();
decl.add_ivar::<usize>(VIEW_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

39
src/image/macos.rs Normal file
View file

@ -0,0 +1,39 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate};
use crate::utils::load;
/// Injects an `NSView` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_image_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSImageView);
let mut decl = ClassDecl::new("RSTImageView", superclass).unwrap();
//decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

143
src/image/mod.rs Normal file
View file

@ -0,0 +1,143 @@
use objc_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::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::register_image_view_class;
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::register_image_view_class;
mod image;
pub use image::Image;
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
let view: id = msg_send![registration_fn(), 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 ImageView {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for ImageView {
fn default() -> Self {
ImageView::new()
}
}
impl ImageView {
/// Returns a default `View`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_image_view_class);
ImageView {
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
pub fn set_image(&self, image: &Image) {
unsafe {
let _: () = msg_send![&*self.objc, setImage:&*image.0];
}
}
}
impl Layout for ImageView {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl Drop for ImageView {
/// 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).
///
/// There are, thankfully, no delegates we need to break 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];
}
}
}*/
}
}

39
src/image/traits.rs Normal file
View file

@ -0,0 +1,39 @@
//! Various traits used for Views.
use crate::dragdrop::{DragInfo, DragOperation};
use crate::view::View;
pub trait ViewDelegate {
/// Called when the View is ready to work with. You're passed a `View` - this is safe to
/// store and use repeatedly, but it's not thread safe - any UI calls must be made from the
/// main thread!
fn did_load(&mut self, _view: View) {}
/// Called when this is about to be added to the view heirarchy.
fn will_appear(&self, _animated: bool) {}
/// Called after this has been added to the view heirarchy.
fn did_appear(&self, _animated: bool) {}
/// Called when this is about to be removed from the view heirarchy.
fn will_disappear(&self, _animated: bool) {}
/// Called when this has been removed from the view heirarchy.
fn did_disappear(&self, _animated: bool) {}
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None }
/// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation.
fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data.
fn perform_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up.
fn conclude_drag_operation(&self, _info: DragInfo) {}
/// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame
/// rectangle (in the case of a window object).
fn dragging_exited(&self, _info: DragInfo) {}
}

58
src/input/macos.rs Normal file
View file

@ -0,0 +1,58 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::input::{TEXTFIELD_DELEGATE_PTR, TextFieldDelegate};
use crate::utils::load;
/// Injects an `NSTextField` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSTextField);
let mut decl = ClassDecl::new("RSTTextInputField", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSTextField` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: TextFieldDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSView);
let mut decl = ClassDecl::new("RSTTextInputFieldWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(TEXTFIELD_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

274
src/input/mod.rs Normal file
View file

@ -0,0 +1,274 @@
//! Wraps `NSTextField` and `UITextField` across platforms, explicitly as a TextField.
//! In AppKit, `NSTextField` does double duty, and for clarity we just double
//! the implementation.
//!
//! TextFields implement Autolayout, which enable you to specify how things should appear on the screen.
//!
//! ```rust,no_run
//! use cacao::color::rgb;
//! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::TextField;
//! use cacao::window::{Window, WindowDelegate};
//!
//! #[derive(Default)]
//! struct AppWindow {
//! content: TextField,
//! label: TextField,
//! window: Window
//! }
//!
//! impl WindowDelegate for AppWindow {
//! fn did_load(&mut self, window: Window) {
//! window.set_minimum_content_size(300., 300.);
//! self.window = window;
//!
//! self.label.set_background_color(rgb(224, 82, 99));
//! self.label.set_text("LOL");
//! self.content.add_subview(&self.red);
//!
//! self.window.set_content_view(&self.content);
//!
//! LayoutConstraint::activate(&[
//! self.red.top.constraint_equal_to(&self.content.top).offset(16.),
//! self.red.leading.constraint_equal_to(&self.content.leading).offset(16.),
//! self.red.trailing.constraint_equal_to(&self.content.trailing).offset(-16.),
//! self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
//! ]);
//! }
//! }
//! ```
//!
//! For more information on Autolayout, view the module or check out the examples folder.
use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSString};
use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::text::{Font, TextAlign};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::{register_view_class, register_view_class_with_delegate};
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate};
//mod controller;
//pub use controller::TextFieldController;
mod traits;
pub use traits::TextFieldDelegate;
pub(crate) static TEXTFIELD_DELEGATE_PTR: &str = "rstTextFieldDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
let view: id = msg_send![registration_fn(), new];
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
let _: () = msg_send![view, setWantsLayer:YES];
view
}
}
/// A clone-able handler to an `NSTextField/UITextField` reference in the
/// Objective-C runtime.
#[derive(Debug)]
pub struct TextField<T = ()> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for TextField {
fn default() -> Self {
TextField::new()
}
}
impl TextField {
/// Returns a default `TextField`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_view_class);
TextField {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
}
impl<T> TextField<T> where T: TextFieldDelegate + 'static {
/// Initializes a new TextField with a given `TextFieldDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> TextField<T> {
let delegate = Box::new(delegate);
let label = allocate_view(register_view_class_with_delegate::<T>);
unsafe {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let ptr: *const T = &*delegate;
(&mut *label).set_ivar(TEXTFIELD_DELEGATE_PTR, ptr as usize);
};
let mut label = TextField {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![label, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![label, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![label, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![label, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![label, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![label, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![label, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![label, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(label) },
};
//(&mut delegate).did_load(label.clone_as_handle());
label.delegate = Some(delegate);
label
}
}
impl<T> TextField<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to
/// reference, customize and use the view but without the trickery of holding pieces of the
/// delegate - the `TextField` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> TextField {
TextField {
delegate: None,
top: self.top.clone(),
leading: self.leading.clone(),
trailing: self.trailing.clone(),
bottom: self.bottom.clone(),
width: self.width.clone(),
height: self.height.clone(),
center_x: self.center_x.clone(),
center_y: self.center_y.clone(),
objc: self.objc.clone()
}
}
/// Grabs the value from the textfield and returns it as an owned String.
pub fn get_value(&self) -> String {
let value = NSString::wrap(unsafe {
msg_send![&*self.objc, stringValue]
});
value.to_str().to_string()
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
/// Call this to set the text for the label.
pub fn set_text(&self, text: &str) {
let s = NSString::new(text);
unsafe {
let _: () = msg_send![&*self.objc, setStringValue:s.into_inner()];
}
}
pub fn set_text_alignment(&self, alignment: TextAlign) {
unsafe {
let alignment: NSInteger = alignment.into();
let _: () = msg_send![&*self.objc, setAlignment:alignment];
}
}
pub fn set_font(&self, font: &Font) {
unsafe {
let _: () = msg_send![&*self.objc, setFont:&*font.objc];
}
}
}
impl<T> Layout for TextField<T> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl<T> Drop for TextField<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `TextField` 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).
///
/// There are, thankfully, no delegates we need to break 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];
}
}
}
}
}

4
src/input/traits.rs Normal file
View file

@ -0,0 +1,4 @@
//! Various traits used for Labels.
pub trait TextFieldDelegate {
}

113
src/invoker.rs Normal file
View file

@ -0,0 +1,113 @@
//! This module contains an NSObject subclass that can act as a generic target
//! for action dispatch - e.g, for buttons, toolbars, etc. It loops back around
//! to a Rust callback; you won't be able to necessarily use it like you would
//! elsewhere, but you can message pass to achieve what you need.
//!
//! Note that this is explicitly intended to be 1:1 with a widget; it is not
//! something you should ever attempt to clone or really bother interacting with.
//! It is imperative that this drop whenever a corresponding control/widget
//! is going away.
use std::fmt;
use std::sync::{Arc, Mutex, Once};
use objc_id::ShareId;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use block::{Block, ConcreteBlock, RcBlock};
use crate::foundation::{id, nil, NSString};
use crate::utils::load;
pub static ACTION_CALLBACK_PTR: &str = "rstTargetActionPtr";
/// An Action is just an indirection layer to get around Rust and optimizing
/// zero-sum types; without this, pointers to callbacks will end up being
/// 0x1, and all point to whatever is there first (unsure if this is due to
/// Rust or Cocoa or what).
///
/// Point is, Button aren't created that much in the grand scheme of things,
/// and the heap isn't our enemy in a GUI framework anyway. If someone knows
/// a better way to do this that doesn't require double-boxing, I'm all ears.
pub struct Action(Box<Fn() + Send + Sync + 'static>);
impl fmt::Debug for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Action")
.finish()
}
}
/// A handler that contains the class for callback storage and invocation on
/// the Objective-C side.
///
/// This effectively wraps the target:action selector usage on NSControl and
/// associated widgets.
///
/// Widgets that use this should keep it around; on drop,
/// it _will_ remove your events somewhat transparently per Cocoa rules.
#[derive(Debug)]
pub struct TargetActionHandler {
action: Box<Action>,
invoker: ShareId<Object>
}
impl TargetActionHandler {
/// Returns a new TargetEventHandler.
pub fn new<F: Fn() + Send + Sync + 'static>(control: &Object, action: F) -> Self {
let block = Box::new(Action(Box::new(action)));
let ptr = Box::into_raw(block);
let invoker = unsafe {
ShareId::from_ptr({
let invoker: id = msg_send![register_invoker_class::<F>(), alloc];
let invoker: id = msg_send![invoker, init];
(&mut *invoker).set_ivar(ACTION_CALLBACK_PTR, ptr as usize);
let _: () = msg_send![control, setAction:sel!(perform:)];
let _: () = msg_send![control, setTarget:invoker];
invoker
})
};
TargetActionHandler {
invoker: invoker,
action: unsafe { Box::from_raw(ptr) }
}
}
}
/// This will fire for an NSButton callback.
extern fn perform<F: Fn() + 'static>(this: &mut Object, _: Sel, _sender: id) {
let action = load::<Action>(this, ACTION_CALLBACK_PTR);
(action.0)();
}
/// Due to the way that Rust and Objective-C live... very different lifestyles,
/// we need to find a way to make events work without _needing_ the whole
/// target/action setup you'd use in a standard Cocoa/AppKit/UIKit app.
///
/// Here, we inject a subclass that can store a pointer for a callback. We use
/// this as our target/action combo, which allows passing a
/// generic block over. It's still Rust, so you can't do crazy callbacks, but
/// you can at least fire an event off and do something.
///
/// The `NSButton` owns this object on instantiation, and will release it
/// on drop. We handle the heap copy on the Rust side, so setting the block
/// is just an ivar.
pub(crate) fn register_invoker_class<F: Fn() + 'static>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSObject);
let mut decl = ClassDecl::new("RSTTargetActionHandler", superclass).unwrap();
decl.add_ivar::<usize>(ACTION_CALLBACK_PTR);
decl.add_method(sel!(perform:), perform::<F> as extern fn (&mut Object, _, id));
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -72,6 +72,12 @@ pub use core_foundation;
pub use core_graphics; pub use core_graphics;
pub use objc; pub use objc;
pub use url; pub use url;
pub use lazy_static;
/// Until we figure out a better way to handle reusable views (i.e, the
/// "correct" way for a list view to work), just let the delegates pass
/// back the pointer and handle keeping the pools for themselves.
pub type Node = objc_id::ShareId<objc::runtime::Object>;
#[cfg(feature = "macos")] #[cfg(feature = "macos")]
pub mod macos; pub mod macos;
@ -92,10 +98,19 @@ pub mod defaults;
pub mod filesystem; pub mod filesystem;
pub mod foundation; pub mod foundation;
pub mod geometry; pub mod geometry;
pub mod image;
pub mod input;
pub(crate) mod invoker;
pub mod layout; pub mod layout;
pub mod listview;
pub mod networking; pub mod networking;
pub mod notification_center; pub mod notification_center;
pub mod pasteboard; pub mod pasteboard;
pub mod scrollview;
pub mod text;
#[cfg(feature = "quicklook")]
pub mod quicklook;
#[cfg(feature = "user-notifications")] #[cfg(feature = "user-notifications")]
pub mod user_notifications; pub mod user_notifications;

42
src/listview/enums.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::foundation::NSUInteger;
/// This enum represents the different stock animations possible
/// for ListView row operations. You can pass it to `insert_rows`
/// and `remove_rows` - reloads don't get animations.
pub enum ListViewAnimation {
/// No animation.
None,
/// Fades rows in and out.
Fade,
/// Creates a gap - this one is mostly useful during
/// drag and drop operations.
Gap,
/// Animates in or out by sliding upwards.
SlideUp,
/// Animates in or out by sliding down.
SlideDown,
/// Animates in or out by sliding left.
SlideLeft,
/// Animates in or out by sliding right.
SlideRight
}
impl Into<NSUInteger> for ListViewAnimation {
fn into(self) -> NSUInteger {
match self {
ListViewAnimation::None => 0x0,
ListViewAnimation::Fade => 0x1,
ListViewAnimation::Gap => 0x2,
ListViewAnimation::SlideUp => 0x10,
ListViewAnimation::SlideDown => 0x20,
ListViewAnimation::SlideLeft => 0x30,
ListViewAnimation::SlideRight => 0x40
}
}
}

163
src/listview/macos.rs Normal file
View file

@ -0,0 +1,163 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl, msg_send};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSInteger, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::listview::{LISTVIEW_DELEGATE_PTR, ListViewDelegate};
use crate::utils::load;
/// Determines the number of items by way of the backing data source (the Rust struct).
extern fn number_of_items<T: ListViewDelegate>(
this: &Object,
_: Sel,
_: id
) -> NSInteger {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.number_of_items() as NSInteger
}
extern fn view_for_column<T: ListViewDelegate>(
this: &Object,
_: Sel,
table_view: id,
_: id,
item: NSInteger
) -> id {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
let item = view.item(item as usize);
// A hacky method of returning the underlying pointer
// without Rust annoying us.
//
// @TODO: probably find a better way to do this. It's theoretically fine,
// as we *know* the underlying view will be retained by the NSTableView, so
// passing over one more won't really screw up retain counts.
unsafe {
msg_send![&*item, self]
}
}
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_entered<T: ListViewDelegate>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.dragging_entered(DragInfo {
info: unsafe { Id::from_ptr(info) }
}).into()
}
/// Called when a drag/drop operation has entered this view.
extern fn prepare_for_drag_operation<T: ListViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
match view.prepare_for_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn perform_drag_operation<T: ListViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
match view.perform_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn conclude_drag_operation<T: ListViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.conclude_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_exited<T: ListViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.dragging_exited(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Injects an `NSTableView` subclass, with some callback and pointer ivars for what we
/// need to do. Note that we treat and constrain this as a one-column "list" view to match
/// `UITableView` semantics; if `NSTableView`'s multi column behavior is needed, then it can
/// be added in.
pub(crate) fn register_listview_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSTableView);
let mut decl = ClassDecl::new("RSTListView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}
/// Injects an `NSTableView` subclass, with some callback and pointer ivars for what we
/// need to do. Note that we treat and constrain this as a one-column "list" view to match
/// `UITableView` semantics; if `NSTableView`'s multi column behavior is needed, then it can
/// be added in.
pub(crate) fn register_listview_class_with_delegate<T: ListViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSTableView);
let mut decl = ClassDecl::new("RSTListViewWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(LISTVIEW_DELEGATE_PTR);
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Tableview-specific
decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger);
decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id);
// Drag and drop operations (e.g, accepting files)
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(performDragOperation:), perform_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation::<T> as extern fn (&mut Object, _, _));
decl.add_method(sel!(draggingExited:), dragging_exited::<T> as extern fn (&mut Object, _, _));
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

454
src/listview/mod.rs Normal file
View file

@ -0,0 +1,454 @@
//! Wraps `NSView` and `UIView` across platforms.
//!
//! This implementation errs towards the `UIView` side of things, and mostly acts as a wrapper to
//! bring `NSView` to the modern era. It does this by flipping the coordinate system to be what
//! people expect in 2020, and layer-backing all views by default.
//!
//! Views implement Autolayout, which enable you to specify how things should appear on the screen.
//!
//! ```rust,no_run
//! use cacao::color::rgb;
//! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::View;
//! use cacao::window::{Window, WindowDelegate};
//!
//! #[derive(Default)]
//! struct AppWindow {
//! content: View,
//! red: View,
//! window: Window
//! }
//!
//! impl WindowDelegate for AppWindow {
//! fn did_load(&mut self, window: Window) {
//! window.set_minimum_content_size(300., 300.);
//! self.window = window;
//!
//! self.red.set_background_color(rgb(224, 82, 99));
//! self.content.add_subview(&self.red);
//!
//! self.window.set_content_view(&self.content);
//!
//! LayoutConstraint::activate(&[
//! self.red.top.constraint_equal_to(&self.content.top).offset(16.),
//! self.red.leading.constraint_equal_to(&self.content.leading).offset(16.),
//! self.red.trailing.constraint_equal_to(&self.content.trailing).offset(-16.),
//! self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
//! ]);
//! }
//! }
//! ```
//!
//! For more information on Autolayout, view the module or check out the examples folder.
use core_graphics::base::CGFloat;
use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSUInteger};
use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType;
use crate::scrollview::ScrollView;
use crate::utils::CGSize;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::{register_listview_class, register_listview_class_with_delegate};
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate};
mod enums;
pub use enums::ListViewAnimation;
mod traits;
pub use traits::ListViewDelegate;
mod row;
pub use row::ListViewRow;
pub(crate) static LISTVIEW_DELEGATE_PTR: &str = "rstListViewDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
let tableview: id = msg_send![registration_fn(), new];
let _: () = msg_send![tableview, setTranslatesAutoresizingMaskIntoConstraints:NO];
// Let's... make NSTableView into UITableView-ish.
#[cfg(target_os = "macos")]
{
let _: () = msg_send![tableview, setWantsLayer:YES];
let _: () = msg_send![tableview, setUsesAutomaticRowHeights:YES];
let _: () = msg_send![tableview, setFloatsGroupRows:YES];
let _: () = msg_send![tableview, setIntercellSpacing:CGSize::new(0., 0.)];
let _: () = msg_send![tableview, setColumnAutoresizingStyle:1];
//msg_send![tableview, setSelectionHighlightStyle:-1];
let _: () = msg_send![tableview, setAllowsEmptySelection:YES];
let _: () = msg_send![tableview, setAllowsMultipleSelection:NO];
let _: () = msg_send![tableview, setHeaderView:nil];
// NSTableView requires at least one column to be manually added if doing so by code.
// A relic of a bygone era, indeed.
let identifier = NSString::new("CacaoListViewColumn");
let default_column_alloc: id = msg_send![class!(NSTableColumn), new];
let default_column: id = msg_send![default_column_alloc, initWithIdentifier:identifier.into_inner()];
let _: () = msg_send![default_column, setResizingMask:(1<<0)];
let _: () = msg_send![tableview, addTableColumn:default_column];
}
tableview
}
}
#[derive(Debug)]
pub struct ListView<T = ()> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// On macOS, we need to manage the NSScrollView ourselves. It's a bit
/// more old school like that...
#[cfg(target_os = "macos")]
pub scrollview: ScrollView,
/// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for ListView {
fn default() -> Self {
ListView::new()
}
}
impl ListView {
/// Returns a default `View`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_listview_class);
#[cfg(target_os = "macos")]
let scrollview = {
let sview = ScrollView::new();
unsafe {
let _: () = msg_send![&*sview.objc, setDocumentView:view];
}
sview
};
// For macOS, we need to use the NSScrollView anchor points, not the NSTableView.
#[cfg(target_os = "macos")]
let anchor_view = &*scrollview.objc;
#[cfg(target_os = "ios")]
let anchor_view = view;
ListView {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![anchor_view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![anchor_view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![anchor_view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![anchor_view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![anchor_view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![anchor_view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
#[cfg(target_os = "macos")]
scrollview: scrollview
}
}
}
impl<T> ListView<T> where T: ListViewDelegate + 'static {
/// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> ListView<T> {
let mut delegate = Box::new(delegate);
let view = allocate_view(register_listview_class_with_delegate::<T>);
unsafe {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let ptr: *const T = &*delegate;
(&mut *view).set_ivar(LISTVIEW_DELEGATE_PTR, ptr as usize);
let _: () = msg_send![view, setDelegate:view];
let _: () = msg_send![view, setDataSource:view];
};
#[cfg(target_os = "macos")]
let scrollview = {
let sview = ScrollView::new();
unsafe {
let _: () = msg_send![&*sview.objc, setDocumentView:view];
}
sview
};
// For macOS, we need to use the NSScrollView anchor points, not the NSTableView.
#[cfg(target_os = "macos")]
let anchor_view = &*scrollview.objc;
#[cfg(target_os = "ios")]
let anchor_view = view;
let mut view = ListView {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![anchor_view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![anchor_view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![anchor_view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![anchor_view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![anchor_view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![anchor_view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
#[cfg(target_os = "macos")]
scrollview: scrollview
};
(&mut delegate).did_load(view.clone_as_handle());
view.delegate = Some(delegate);
view
}
}
impl<T> ListView<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to
/// reference, customize and use the view but without the trickery of holding pieces of the
/// delegate - the `View` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> ListView {
ListView {
delegate: None,
top: self.top.clone(),
leading: self.leading.clone(),
trailing: self.trailing.clone(),
bottom: self.bottom.clone(),
width: self.width.clone(),
height: self.height.clone(),
center_x: self.center_x.clone(),
center_y: self.center_y.clone(),
objc: self.objc.clone(),
#[cfg(target_os = "macos")]
scrollview: self.scrollview.clone_as_handle()
}
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
pub fn perform_batch_updates<F: Fn(ListView)>(&self, update: F) {
#[cfg(target_os = "macos")]
unsafe {
let _: () = msg_send![&*self.objc, beginUpdates];
let handle = self.clone_as_handle();
update(handle);
let _: () = msg_send![&*self.objc, endUpdates];
}
}
pub fn insert_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: ListViewAnimation) {
#[cfg(target_os = "macos")]
unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new];
for index in indexes {
let x: NSUInteger = index as NSUInteger;
let _: () = msg_send![index_set, addIndex:x];
}
let animation_options: NSUInteger = animations.into();
// We need to temporarily retain this; it can drop after the underlying NSTableView
// has also retained it.
let x = ShareId::from_ptr(index_set);
let _: () = msg_send![&*self.objc, insertRowsAtIndexes:&*x withAnimation:20];
}
}
pub fn reload_rows(&self, indexes: &[usize]) {
#[cfg(target_os = "macos")]
unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new];
for index in indexes {
let x: NSUInteger = *index as NSUInteger;
let _: () = msg_send![index_set, addIndex:x];
}
let x = ShareId::from_ptr(index_set);
let ye: id = msg_send![class!(NSIndexSet), indexSetWithIndex:0];
let y = ShareId::from_ptr(ye);
let _: () = msg_send![&*self.objc, reloadDataForRowIndexes:&*x columnIndexes:&*y];
}
}
pub fn remove_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: ListViewAnimation) {
#[cfg(target_os = "macos")]
unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new];
for index in indexes {
let x: NSUInteger = index as NSUInteger;
let _: () = msg_send![index_set, addIndex:x];
}
let animation_options: NSUInteger = animations.into();
// We need to temporarily retain this; it can drop after the underlying NSTableView
// has also retained it.
let x = ShareId::from_ptr(index_set);
let _: () = msg_send![&*self.objc, removeRowsAtIndexes:&*x withAnimation:20];
}
}
/// Sets an enforced row-height; if you need dynamic rows, you'll want to
/// look at ListViewDelegate methods, or use AutoLayout.
pub fn set_row_height(&self, height: CGFloat) {
unsafe {
let _: () = msg_send![&*self.objc, setRowHeight:height];
}
}
/// This defaults to true. If you're using manual heights, you may want to set this to `false`,
/// as it will tell AppKit internally to just use the number instead of trying to judge
/// heights.
///
/// It can make some scrolling situations much smoother.
pub fn set_uses_automatic_row_heights(&self, uses: bool) {
#[cfg(target_os = "macos")]
unsafe {
let _: () = msg_send![&*self.objc, setUsesAutomaticRowHeights:match uses {
true => YES,
false => NO
}];
}
}
/// On macOS, this will instruct the underlying NSTableView to alternate
/// background colors automatically. If you set this, you possibly want
/// to hard-set a row height as well.
pub fn set_uses_alternating_backgrounds(&self, uses: bool) {
#[cfg(target_os = "macos")]
unsafe {
let _: () = msg_send![&*self.objc, setUsesAlternatingRowBackgroundColors:match uses {
true => YES,
false => NO
}];
}
}
/// Register this view for drag and drop operations.
pub fn register_for_dragged_types(&self, types: &[PasteboardType]) {
unsafe {
let types: NSArray = types.into_iter().map(|t| {
// This clone probably doesn't need to be here, but it should also be cheap as
// this is just an enum... and this is not an oft called method.
let x: NSString = t.clone().into();
x.into_inner()
}).collect::<Vec<id>>().into();
let _: () = msg_send![&*self.objc, registerForDraggedTypes:types.into_inner()];
}
}
pub fn reload(&self) {
unsafe {
let _: () = msg_send![&*self.objc, reloadData];
}
}
}
impl<T> Layout for ListView<T> {
/// On macOS, this returns the NSScrollView, not the NSTableView.
fn get_backing_node(&self) -> ShareId<Object> {
#[cfg(target_os = "macos")]
let val = self.scrollview.objc.clone();
#[cfg(target_os = "ios")]
let val = self.objc.clone();
val
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
#[cfg(target_os = "macos")]
let _: () = msg_send![&*self.scrollview.objc, addSubview:backing_node];
#[cfg(target_os = "ios")]
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl<T> Drop for ListView<T> {
/// 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).
///
/// There are, thankfully, no delegates we need to break 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];
}
}
}
}
}

45
src/listview/row/ios.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate};
use crate::utils::load;
/// Injects an `NSView` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: ViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTViewWithDelegate", superclass).unwrap();
decl.add_ivar::<usize>(VIEW_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

125
src/listview/row/macos.rs Normal file
View file

@ -0,0 +1,125 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::listview::row::{LISTVIEW_ROW_DELEGATE_PTR, ViewDelegate};
use crate::utils::load;
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_entered<T: ViewDelegate>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
let view = load::<T>(this, LISTVIEW_ROW_DELEGATE_PTR);
view.dragging_entered(DragInfo {
info: unsafe { Id::from_ptr(info) }
}).into()
}
/// Called when a drag/drop operation has entered this view.
extern fn prepare_for_drag_operation<T: ViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, LISTVIEW_ROW_DELEGATE_PTR);
match view.prepare_for_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn perform_drag_operation<T: ViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, LISTVIEW_ROW_DELEGATE_PTR);
match view.perform_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn conclude_drag_operation<T: ViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, LISTVIEW_ROW_DELEGATE_PTR);
view.conclude_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_exited<T: ViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, LISTVIEW_ROW_DELEGATE_PTR);
view.dragging_exited(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Injects an `NSView` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_listview_row_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSView);
let mut decl = ClassDecl::new("RSTTableViewRow", superclass).unwrap();
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_listview_row_class_with_delegate<T: ViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSView);
let mut decl = ClassDecl::new("RSTableViewRowWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(LISTVIEW_ROW_DELEGATE_PTR);
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Drag and drop operations (e.g, accepting files)
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(performDragOperation:), perform_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation::<T> as extern fn (&mut Object, _, _));
decl.add_method(sel!(draggingExited:), dragging_exited::<T> as extern fn (&mut Object, _, _));
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

245
src/listview/row/mod.rs Normal file
View file

@ -0,0 +1,245 @@
//! Wraps `NSView` and `UIView` across platforms.
//!
//! This implementation errs towards the `UIView` side of things, and mostly acts as a wrapper to
//! bring `NSView` to the modern era. It does this by flipping the coordinate system to be what
//! people expect in 2020, and layer-backing all views by default.
//!
//! Views implement Autolayout, which enable you to specify how things should appear on the screen.
//!
//! ```rust,no_run
//! use cacao::color::rgb;
//! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::View;
//! use cacao::window::{Window, WindowDelegate};
//!
//! #[derive(Default)]
//! struct AppWindow {
//! content: View,
//! red: View,
//! window: Window
//! }
//!
//! impl WindowDelegate for AppWindow {
//! fn did_load(&mut self, window: Window) {
//! window.set_minimum_content_size(300., 300.);
//! self.window = window;
//!
//! self.red.set_background_color(rgb(224, 82, 99));
//! self.content.add_subview(&self.red);
//!
//! self.window.set_content_view(&self.content);
//!
//! LayoutConstraint::activate(&[
//! self.red.top.constraint_equal_to(&self.content.top).offset(16.),
//! self.red.leading.constraint_equal_to(&self.content.leading).offset(16.),
//! self.red.trailing.constraint_equal_to(&self.content.trailing).offset(-16.),
//! self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
//! ]);
//! }
//! }
//! ```
//!
//! For more information on Autolayout, view the module or check out the examples folder.
use objc_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::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType;
use crate::view::ViewDelegate;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::{register_listview_row_class, register_listview_row_class_with_delegate};
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::{register_listview_row_view_class, register_listview_row_class_with_delegate};
pub(crate) static LISTVIEW_ROW_DELEGATE_PTR: &str = "rstListViewRowDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
let view: id = msg_send![registration_fn(), 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 ListViewRow<T = ()> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for ListViewRow {
fn default() -> Self {
ListViewRow::new()
}
}
impl ListViewRow {
/// Returns a default `View`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_listview_row_class);
ListViewRow {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
}
impl<T> ListViewRow<T> where T: ViewDelegate + 'static {
/// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> ListViewRow<T> {
let mut delegate = Box::new(delegate);
let view = allocate_view(register_listview_row_class_with_delegate::<T>);
unsafe {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let ptr: *const T = &*delegate;
(&mut *view).set_ivar(LISTVIEW_ROW_DELEGATE_PTR, ptr as usize);
};
let mut view = ListViewRow {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
};
(&mut delegate).did_load(view.clone_as_handle());
view.delegate = Some(delegate);
view
}
}
impl<T> From<&ListViewRow<T>> for ShareId<Object> {
fn from(row: &ListViewRow<T>) -> ShareId<Object> {
row.objc.clone()
}
}
impl<T> ListViewRow<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to
/// reference, customize and use the view but without the trickery of holding pieces of the
/// delegate - the `View` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> crate::view::View {
crate::view::View {
delegate: None,
top: self.top.clone(),
leading: self.leading.clone(),
trailing: self.trailing.clone(),
bottom: self.bottom.clone(),
width: self.width.clone(),
height: self.height.clone(),
center_x: self.center_x.clone(),
center_y: self.center_y.clone(),
objc: self.objc.clone()
}
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
/// Register this view for drag and drop operations.
pub fn register_for_dragged_types(&self, types: &[PasteboardType]) {
unsafe {
let types: NSArray = types.into_iter().map(|t| {
// This clone probably doesn't need to be here, but it should also be cheap as
// this is just an enum... and this is not an oft called method.
let x: NSString = t.clone().into();
x.into_inner()
}).collect::<Vec<id>>().into();
let _: () = msg_send![&*self.objc, registerForDraggedTypes:types.into_inner()];
}
}
}
impl<T> Layout for ListViewRow<T> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl<T> Drop for ListViewRow<T> {
fn drop(&mut self) {
}
}

51
src/listview/traits.rs Normal file
View file

@ -0,0 +1,51 @@
//! Various traits used for Views.
use crate::Node;
use crate::dragdrop::{DragInfo, DragOperation};
use crate::listview::{ListView, ListViewRow};
use crate::layout::Layout;
use crate::view::View;
pub trait ListViewDelegate {
/// Called when the View is ready to work with. You're passed a `View` - this is safe to
/// store and use repeatedly, but it's not thread safe - any UI calls must be made from the
/// main thread!
fn did_load(&mut self, _view: ListView) {}
/// Returns the number of items in the list view.
fn number_of_items(&self) -> usize;
/// This is temporary and you should not rely on this signature if you
/// choose to try and work with this. NSTableView & such associated delegate patterns
/// are tricky to support in Rust, and while I have a few ideas about them, I haven't
/// had time to sit down and figure them out properly yet.
fn item(&self, _row: usize) -> Node;
/// Called when this is about to be added to the view heirarchy.
fn will_appear(&self, _animated: bool) {}
/// Called after this has been added to the view heirarchy.
fn did_appear(&self, _animated: bool) {}
/// Called when this is about to be removed from the view heirarchy.
fn will_disappear(&self, _animated: bool) {}
/// Called when this has been removed from the view heirarchy.
fn did_disappear(&self, _animated: bool) {}
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None }
/// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation.
fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data.
fn perform_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up.
fn conclude_drag_operation(&self, _info: DragInfo) {}
/// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame
/// rectangle (in the case of a window object).
fn dragging_exited(&self, _info: DragInfo) {}
}

View file

@ -34,11 +34,15 @@
//! Certain lifecycle events are specific to certain platforms. Where this is the case, the //! Certain lifecycle events are specific to certain platforms. Where this is the case, the
//! documentation makes every effort to note. //! documentation makes every effort to note.
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use objc_id::Id; use objc_id::Id;
use objc::runtime::Object; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSUInteger, AutoReleasePool}; use crate::foundation::{id, nil, YES, NO, NSUInteger, AutoReleasePool};
use crate::invoker::TargetActionHandler;
use crate::macos::menu::Menu; use crate::macos::menu::Menu;
use crate::notification_center::Dispatcher; use crate::notification_center::Dispatcher;
use crate::utils::activate_cocoa_multithreading; use crate::utils::activate_cocoa_multithreading;
@ -57,6 +61,31 @@ pub use traits::AppDelegate;
pub(crate) static APP_PTR: &str = "rstAppPtr"; pub(crate) static APP_PTR: &str = "rstAppPtr";
// Alright, so this... sucks.
//
// But let me explain.
//
// macOS only has one top level menu, and it's probably one of the old(est|er)
// parts of the system - there's still Carbon there, if you know where to look.
// We store our event handlers on the Rust side, and in most cases this works fine -
// we can enforce that the programmer should be retaining ownership and dropping
// when ready.
//
// We can't enforce this with NSMenu, as it takes ownership of the items and then
// lives in its own world. We want to mirror the same contract, somehow... so, again,
// we go back to the "one top level menu" bit.
//
// Yes, all this to say, this is a singleton that caches TargetActionHandler entries
// when menus are constructed. It's mostly 1:1 with the NSMenu, I think - and probably
// the only true singleton I want to see in this framework.
//
// Calls to App::set_menu() will reconfigure the contents of this Vec, so that's also
// the easiest way to remove things as necessary - just rebuild your menu. Trying to debug
// and/or predict NSMenu is a whole task that I'm not even sure is worth trying to do.
lazy_static! {
static ref MENU_ITEMS_HANDLER_CACHE: Arc<Mutex<Vec<TargetActionHandler>>> = Arc::new(Mutex::new(Vec::new()));
}
/// A handler to make some boilerplate less annoying. /// A handler to make some boilerplate less annoying.
#[inline] #[inline]
fn shared_application<F: Fn(id)>(handler: F) { fn shared_application<F: Fn(id)>(handler: F) {
@ -91,7 +120,6 @@ impl<T> App<T> {
pub fn run(&self) { pub fn run(&self) {
unsafe { unsafe {
let current_app: id = msg_send![class!(NSRunningApplication), currentApplication]; let current_app: id = msg_send![class!(NSRunningApplication), currentApplication];
let _: () = msg_send![current_app, activateWithOptions:1<<1];
let shared_app: id = msg_send![class!(RSTApplication), sharedApplication]; let shared_app: id = msg_send![class!(RSTApplication), sharedApplication];
let _: () = msg_send![shared_app, run]; let _: () = msg_send![shared_app, run];
self.pool.drain(); self.pool.drain();
@ -112,7 +140,6 @@ impl<T> App<T> where T: AppDelegate + 'static {
let inner = unsafe { let inner = unsafe {
let app: id = msg_send![register_app_class(), sharedApplication]; let app: id = msg_send![register_app_class(), sharedApplication];
let _: () = msg_send![app, setActivationPolicy:0];
Id::from_ptr(app) Id::from_ptr(app)
}; };
@ -137,11 +164,25 @@ impl<T> App<T> where T: AppDelegate + 'static {
} }
} }
// This is a hack and should be replaced with an actual messaging pipeline at some point. :) // This is a very basic "dispatch" mechanism. In macOS, it's critical that UI work happen on the
// UI ("main") thread. We can hook into the standard mechanism for this by dispatching on
// queues; in our case, we'll just offer two points - one for the background queue(s), and one
// for the main queue. They automatically forward through to our registered `AppDelegate`.
//
// One thing I don't like about GCD is that detecting incorrect thread usage has historically been
// a bit... annoying. Here, the `Dispatcher` trait explicitly requires implementing two methods -
// one for UI messages, and one for background messages. I think that this helps separate intent
// on the implementation side, and makes it a bit easier to detect when a message has come in on
// the wrong side.
//
// This is definitely, absolutely, 100% not a performant way to do things - but at the same time,
// ObjC and such is fast enough that for a large class of applications this is workable.
//
// tl;dr: This is all a bit of a hack, and should go away eventually. :)
impl<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate + Dispatcher<Message = M> { impl<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate + Dispatcher<Message = M> {
/// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate, /// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate,
/// and passing back through there. All messages are currently dispatched on the main thread. /// and passing back through there.
pub fn dispatch(message: M) { pub fn dispatch_main(message: M) {
let queue = dispatch::Queue::main(); let queue = dispatch::Queue::main();
queue.exec_async(move || unsafe { queue.exec_async(move || unsafe {
@ -149,7 +190,21 @@ impl<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate + Dispatcher
let app_delegate: id = msg_send![app, delegate]; let app_delegate: id = msg_send![app, delegate];
let delegate_ptr: usize = *(*app_delegate).get_ivar(APP_PTR); let delegate_ptr: usize = *(*app_delegate).get_ivar(APP_PTR);
let delegate = delegate_ptr as *const T; let delegate = delegate_ptr as *const T;
(&*delegate).on_message(message); (&*delegate).on_ui_message(message);
});
}
/// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate,
/// and passing back through there.
pub fn dispatch_background(message: M) {
let queue = dispatch::Queue::main();
queue.exec_async(move || unsafe {
let app: id = msg_send![register_app_class(), sharedApplication];
let app_delegate: id = msg_send![app, delegate];
let delegate_ptr: usize = *(*app_delegate).get_ivar(APP_PTR);
let delegate = delegate_ptr as *const T;
(&*delegate).on_background_message(message);
}); });
} }
} }
@ -202,22 +257,54 @@ impl App {
/// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind /// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind
/// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and /// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and
/// you shouldn't bother to either. /// you shouldn't bother to either.
pub fn set_menu(menus: Vec<Menu>) { ///
shared_application(|app| unsafe { /// The one note here: we internally cache actions to avoid them dropping without the
/// Objective-C side knowing. The the comments on the
pub fn set_menu(mut menus: Vec<Menu>) {
let mut handlers = vec![];
let main_menu = unsafe {
let menu_cls = class!(NSMenu); let menu_cls = class!(NSMenu);
let item_cls = class!(NSMenuItem);
let main_menu: id = msg_send![menu_cls, new]; let main_menu: id = msg_send![menu_cls, new];
let item_cls = class!(NSMenuItem); for menu in menus.iter_mut() {
for menu in menus.iter() { handlers.append(&mut menu.actions);
let item: id = msg_send![item_cls, new]; let item: id = msg_send![item_cls, new];
let _: () = msg_send![item, setSubmenu:&*menu.inner]; let _: () = msg_send![item, setSubmenu:&*menu.inner];
let _: () = msg_send![main_menu, addItem:item]; let _: () = msg_send![main_menu, addItem:item];
} }
main_menu
};
// Cache our menu handlers, whatever they may be - since we're replacing the
// existing menu, and macOS only has one menu on screen at a time, we can go
// ahead and blow away the old ones.
{
let mut cache = MENU_ITEMS_HANDLER_CACHE.lock().unwrap();
*cache = handlers;
}
shared_application(move |app| unsafe {
let _: () = msg_send![app, setMainMenu:main_menu]; let _: () = msg_send![app, setMainMenu:main_menu];
}); });
} }
/// For nib-less applications (which, if you're here, this is) need to call the activation
/// routines after the NSMenu has been set, otherwise it won't be interact-able without
/// switching away from the app and then coming back.
///
/// @TODO: Accept an ActivationPolicy enum or something.
pub fn activate() {
shared_application(|app| unsafe {
let _: () = msg_send![app, setActivationPolicy:0];
let current_app: id = msg_send![class!(NSRunningApplication), currentApplication];
let _: () = msg_send![current_app, activateWithOptions:1<<1];
});
}
/// Terminates the application, firing the requisite cleanup delegate methods in the process. /// Terminates the application, firing the requisite cleanup delegate methods in the process.
/// ///
/// This is typically called when the user chooses to quit via the App menu. /// This is typically called when the user chooses to quit via the App menu.

View file

@ -5,9 +5,11 @@
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::{Object, Sel}; use objc::runtime::{Object, Sel};
use objc_id::ShareId; use objc_id::ShareId;
use std::sync::Once;
use crate::foundation::{id, nil, NSString, NSUInteger}; use crate::foundation::{id, nil, NSString, NSUInteger};
use crate::events::EventModifierFlag; use crate::events::EventModifierFlag;
use crate::invoker::TargetActionHandler;
/// Internal method (shorthand) for generating `NSMenuItem` holders. /// Internal method (shorthand) for generating `NSMenuItem` holders.
fn make_menu_item( fn make_menu_item(
@ -17,8 +19,6 @@ fn make_menu_item(
modifiers: Option<&[EventModifierFlag]> modifiers: Option<&[EventModifierFlag]>
) -> MenuItem { ) -> MenuItem {
unsafe { unsafe {
let cls = class!(NSMenuItem);
let alloc: id = msg_send![cls, alloc];
let title = NSString::new(title); let title = NSString::new(title);
// Note that AppKit requires a blank string if nil, not nil. // Note that AppKit requires a blank string if nil, not nil.
@ -27,6 +27,7 @@ fn make_menu_item(
None => "" None => ""
}); });
let alloc: id = msg_send![class!(NSMenuItem), alloc];
let item = ShareId::from_ptr(match action { let item = ShareId::from_ptr(match action {
Some(a) => msg_send![alloc, initWithTitle:title action:a keyEquivalent:key], Some(a) => msg_send![alloc, initWithTitle:title action:a keyEquivalent:key],
None => msg_send![alloc, initWithTitle:title action:nil keyEquivalent:key] None => msg_send![alloc, initWithTitle:title action:nil keyEquivalent:key]
@ -43,7 +44,7 @@ fn make_menu_item(
let _: () = msg_send![&*item, setKeyEquivalentModifierMask:key_mask]; let _: () = msg_send![&*item, setKeyEquivalentModifierMask:key_mask];
} }
MenuItem::Action(item) MenuItem::Entry((item, None))
} }
} }
@ -52,7 +53,7 @@ fn make_menu_item(
pub enum MenuItem { pub enum MenuItem {
/// Represents a Menu item that's not a separator - for all intents and purposes, you can consider /// Represents a Menu item that's not a separator - for all intents and purposes, you can consider
/// this the real `NSMenuItem`. /// this the real `NSMenuItem`.
Action(ShareId<Object>), Entry((ShareId<Object>, Option<TargetActionHandler>)),
/// Represents a Separator. You can't do anything with this, but it's useful nonetheless for /// Represents a Separator. You can't do anything with this, but it's useful nonetheless for
/// separating out pieces of the `NSMenu` structure. /// separating out pieces of the `NSMenu` structure.
@ -60,8 +61,8 @@ pub enum MenuItem {
} }
impl MenuItem { impl MenuItem {
/// Creates and returns a `MenuItem::Action` with the specified title. /// Creates and returns a `MenuItem::Entry` with the specified title.
pub fn action(title: &str) -> Self { pub fn entry(title: &str) -> Self {
make_menu_item(title, None, None, None) make_menu_item(title, None, None, None)
} }
@ -70,13 +71,25 @@ impl MenuItem {
match self { match self {
MenuItem::Separator => MenuItem::Separator, MenuItem::Separator => MenuItem::Separator,
MenuItem::Action(item) => { MenuItem::Entry((item, action)) => {
unsafe { unsafe {
let key = NSString::new(key); let key = NSString::new(key);
let _: () = msg_send![&*item, setKeyEquivalent:key]; let _: () = msg_send![&*item, setKeyEquivalent:key];
} }
MenuItem::Action(item) MenuItem::Entry((item, action))
}
}
}
/// Attaches a target/action handler to dispatch events.
pub fn action<F: Fn() + Send + Sync + 'static>(self, action: F) -> Self {
match self {
MenuItem::Separator => MenuItem::Separator,
MenuItem::Entry((item, old_action)) => {
let action = TargetActionHandler::new(&*item, action);
MenuItem::Entry((item, Some(action)))
} }
} }
} }
@ -97,14 +110,14 @@ impl MenuItem {
pub fn services() -> Self { pub fn services() -> Self {
match make_menu_item("Services", None, None, None) { match make_menu_item("Services", None, None, None) {
// Link in the services menu, which is part of NSApp // Link in the services menu, which is part of NSApp
MenuItem::Action(item) => { MenuItem::Entry((item, action)) => {
unsafe { unsafe {
let app: id = msg_send![class!(RSTApplication), sharedApplication]; let app: id = msg_send![class!(RSTApplication), sharedApplication];
let services: id = msg_send![app, servicesMenu]; let services: id = msg_send![app, servicesMenu];
let _: () = msg_send![&*item, setSubmenu:services]; let _: () = msg_send![&*item, setSubmenu:services];
} }
MenuItem::Action(item) MenuItem::Entry((item, action))
}, },
// Should never be hit // Should never be hit

View file

@ -1,22 +1,33 @@
//! Wraps NSMenu and handles instrumenting necessary delegate pieces. //! Wraps NSMenu and handles instrumenting necessary delegate pieces.
use objc_id::Id; use std::sync::{Arc, Mutex};
use objc_id::{Id, ShareId};
use objc::runtime::Object; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, NSString}; use crate::foundation::{id, NSString};
use crate::macos::menu::item::MenuItem; use crate::macos::menu::item::MenuItem;
use crate::invoker::TargetActionHandler;
/// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting /// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting
/// them throughout the application lifecycle. /// them throughout the application lifecycle.
#[derive(Debug)] #[derive(Debug)]
pub struct Menu { pub struct Menu {
pub inner: Id<Object>, pub inner: Id<Object>,
pub items: Vec<MenuItem> pub actions: Vec<TargetActionHandler>
} }
impl Menu { impl Menu {
/// Creates a new `Menu` with the given title, and uses the passed items as submenu items. /// Creates a new `Menu` with the given title, and uses the passed items as submenu items.
///
/// This method effectively does three things:
///
/// - Consumes the MenuItem Vec, and pulls out handlers we need to cache
/// - Configures the menu items appropriately, and wires them up
/// - Drops the values we no longer need, and returns only what's necessary
/// to get the menu functioning.
///
pub fn new(title: &str, items: Vec<MenuItem>) -> Self { pub fn new(title: &str, items: Vec<MenuItem>) -> Self {
let inner = unsafe { let inner = unsafe {
let cls = class!(NSMenu); let cls = class!(NSMenu);
@ -26,11 +37,17 @@ impl Menu {
Id::from_ptr(inner) Id::from_ptr(inner)
}; };
for item in items.iter() { let mut actions = vec![];
for item in items {
match item { match item {
MenuItem::Action(item) => { MenuItem::Entry((item, action)) => {
unsafe { unsafe {
let _: () = msg_send![&*inner, addItem:item.clone()]; let _: () = msg_send![&*inner, addItem:item];
}
if action.is_some() {
actions.push(action.unwrap());
} }
}, },
@ -46,7 +63,7 @@ impl Menu {
Menu { Menu {
inner: inner, inner: inner,
items: items actions: actions
} }
} }
} }

View file

@ -4,7 +4,7 @@ use std::sync::Once;
use objc::declare::ClassDecl; use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel}; use objc::runtime::{Class, Object, Sel};
use objc::{class, sel, sel_impl}; use objc::{class, sel, sel_impl, msg_send};
use crate::foundation::{id, NSArray, NSString}; use crate::foundation::{id, NSArray, NSString};
use crate::macos::toolbar::{TOOLBAR_PTR, ToolbarDelegate}; use crate::macos::toolbar::{TOOLBAR_PTR, ToolbarDelegate};
@ -38,8 +38,11 @@ extern fn item_for_identifier<T: ToolbarDelegate>(this: &Object, _: Sel, _: id,
let toolbar = load::<T>(this, TOOLBAR_PTR); let toolbar = load::<T>(this, TOOLBAR_PTR);
let identifier = NSString::wrap(identifier); let identifier = NSString::wrap(identifier);
let mut item = toolbar.item_for(identifier.to_str()); let item = toolbar.item_for(identifier.to_str());
&mut *item.objc unsafe {
msg_send![&*item.objc, self]
}
//&mut *item.objc
} }
/// Registers a `NSToolbar` subclass, and configures it to hold some ivars for various things we need /// Registers a `NSToolbar` subclass, and configures it to hold some ivars for various things we need

View file

@ -3,20 +3,24 @@
//! //!
//! UNFORTUNATELY, this is a very old and janky API. So... yeah. //! UNFORTUNATELY, this is a very old and janky API. So... yeah.
use std::fmt;
use core_graphics::geometry::CGSize; use core_graphics::geometry::CGSize;
use objc_id::Id; use objc_id::{Id, ShareId};
use objc::runtime::Object; use objc::runtime::{Object};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, NSString}; use crate::foundation::{id, NSString};
use crate::invoker::TargetActionHandler;
use crate::button::Button; use crate::button::Button;
/// Wraps `NSToolbarItem`. Enables configuring things like size, view, and so on. /// Wraps `NSToolbarItem`. Enables configuring things like size, view, and so on.
#[derive(Debug)]
pub struct ToolbarItem { pub struct ToolbarItem {
pub identifier: String, pub identifier: String,
pub objc: Id<Object>, pub objc: Id<Object>,
pub button: Option<Button> pub button: Option<Button>,
handler: Option<TargetActionHandler>
} }
impl ToolbarItem { impl ToolbarItem {
@ -35,15 +39,17 @@ impl ToolbarItem {
ToolbarItem { ToolbarItem {
identifier: identifier, identifier: identifier,
objc: objc, objc: objc,
button: None button: None,
handler: None
} }
} }
/// Sets the title for this item. /// Sets the title for this item.
pub fn set_title(&mut self, title: &str) { pub fn set_title(&mut self, title: &str) {
unsafe { unsafe {
let title = NSString::new(title); let title = NSString::new(title).into_inner();
let _: () = msg_send![&*self.objc, setTitle:title]; let _: () = msg_send![&*self.objc, setLabel:&*title];
let _: () = msg_send![&*self.objc, setTitle:&*title];
} }
} }
@ -73,4 +79,9 @@ impl ToolbarItem {
let _: () = msg_send![&*self.objc, setMaxSize:size]; let _: () = msg_send![&*self.objc, setMaxSize:size];
} }
} }
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) {
let handler = TargetActionHandler::new(&*self.objc, action);
self.handler = Some(handler);
}
} }

View file

@ -18,5 +18,5 @@ pub trait ToolbarDelegate {
fn default_item_identifiers(&self) -> Vec<&'static str>; fn default_item_identifiers(&self) -> Vec<&'static str>;
/// For a given `identifier`, return the `ToolbarItem` that should be displayed. /// For a given `identifier`, return the `ToolbarItem` that should be displayed.
fn item_for(&self, _identifier: &str) -> ToolbarItem; fn item_for(&self, _identifier: &str) -> &ToolbarItem;
} }

View file

@ -137,6 +137,8 @@ impl<T> Window<T> where T: WindowDelegate + 'static {
// We set the window to be its own delegate - this is cleaned up inside `Drop`. // We set the window to be its own delegate - this is cleaned up inside `Drop`.
let _: () = msg_send![window, setDelegate:window]; let _: () = msg_send![window, setDelegate:window];
let _: () = msg_send![window, setRestorable:NO];
ShareId::from_ptr(window) ShareId::from_ptr(window)
}; };

View file

@ -1,8 +1,7 @@
/// Controllers interested in processing messages can implement this to respond to messages as
/// they're dispatched. All messages come in on the main thread.
pub trait Dispatcher { pub trait Dispatcher {
type Message: Send + Sync; type Message: Send + Sync;
fn on_message(&self, _message: Self::Message) {} fn on_ui_message(&self, _message: Self::Message) {}
fn on_background_message(&self, _message: Self::Message) {}
} }

View file

@ -4,7 +4,7 @@
use objc::runtime::Object; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id; use objc_id::ShareId;
use url::Url; use url::Url;
use crate::foundation::{id, nil, NSString, NSArray}; use crate::foundation::{id, nil, NSString, NSArray};
@ -14,12 +14,12 @@ mod types;
pub use types::{PasteboardName, PasteboardType}; pub use types::{PasteboardName, PasteboardType};
/// Represents an `NSPasteboard`, enabling you to handle copy/paste/drag and drop. /// Represents an `NSPasteboard`, enabling you to handle copy/paste/drag and drop.
pub struct Pasteboard(pub Id<Object>); pub struct Pasteboard(pub ShareId<Object>);
impl Default for Pasteboard { impl Default for Pasteboard {
fn default() -> Self { fn default() -> Self {
Pasteboard(unsafe { Pasteboard(unsafe {
Id::from_retained_ptr(msg_send![class!(NSPasteboard), generalPasteboard]) ShareId::from_ptr(msg_send![class!(NSPasteboard), generalPasteboard])
}) })
} }
} }
@ -28,7 +28,7 @@ impl Pasteboard {
/// Used internally for wrapping a Pasteboard returned from operations (say, drag and drop). /// Used internally for wrapping a Pasteboard returned from operations (say, drag and drop).
pub(crate) fn with(existing: id) -> Self { pub(crate) fn with(existing: id) -> Self {
Pasteboard(unsafe { Pasteboard(unsafe {
Id::from_retained_ptr(existing) ShareId::from_ptr(existing)
}) })
} }
@ -36,7 +36,7 @@ impl Pasteboard {
pub fn named(name: PasteboardName) -> Self { pub fn named(name: PasteboardName) -> Self {
Pasteboard(unsafe { Pasteboard(unsafe {
let name: NSString = name.into(); let name: NSString = name.into();
Id::from_retained_ptr(msg_send![class!(NSPasteboard), pasteboardWithName:&*name.0]) ShareId::from_ptr(msg_send![class!(NSPasteboard), pasteboardWithName:&*name.0])
}) })
} }
@ -44,7 +44,7 @@ impl Pasteboard {
/// respect to other pasteboards in the system. /// respect to other pasteboards in the system.
pub fn unique() -> Self { pub fn unique() -> Self {
Pasteboard(unsafe { Pasteboard(unsafe {
Id::from_ptr(msg_send![class!(NSPasteboard), pasteboardWithUniqueName]) ShareId::from_ptr(msg_send![class!(NSPasteboard), pasteboardWithUniqueName])
}) })
} }

102
src/quicklook/config.rs Normal file
View file

@ -0,0 +1,102 @@
use core_graphics::base::CGFloat;
use objc::runtime::{Object};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId;
use url::Url;
use crate::foundation::{id, YES, NSString, NSUInteger};
use crate::utils::CGSize;
/// Describes the quality of the thumbnail you expect back from the
/// generator service.
#[derive(Debug)]
pub enum ThumbnailQuality {
/// Appropriate for a file icon.
Icon,
/// Low-ish quality, but fast.
Low,
/// Higher quality, but potentially slower.
High,
/// Ask for them all, and pick which one you
/// use via your provided callback.
All
}
impl From<&ThumbnailQuality> for NSUInteger {
fn from(quality: &ThumbnailQuality) -> Self {
match quality {
ThumbnailQuality::Icon => 1 << 0,
ThumbnailQuality::Low => 1 << 1,
ThumbnailQuality::High => 1 << 2,
ThumbnailQuality::All => NSUInteger::MAX
}
}
}
#[derive(Clone, Debug)]
pub struct ThumbnailConfig {
pub size: (CGFloat, CGFloat),
pub scale: CGFloat,
pub minimum_dimension: CGFloat,
pub icon_mode: bool,
pub types: &'static [ThumbnailQuality]
}
impl Default for ThumbnailConfig {
fn default() -> Self {
ThumbnailConfig {
size: (44., 44.),
// #TODO: Should query the current screen size maybe? 2x is fairly safe
// for most moderns Macs right now.
scale: 2.,
minimum_dimension: 0.,
icon_mode: false,
types: &[ThumbnailQuality::All]
}
}
}
impl ThumbnailConfig {
/// Consumes the request and returns a native representation
/// (`QLThumbnailGenerationRequest`).
pub fn to_request(self, url: &Url) -> id {
let file = NSString::new(url.as_str());
let mut types: NSUInteger = 0;
for mask in self.types {
let i: NSUInteger = mask.into();
types = types | i;
}
unsafe {
let size = CGSize::new(self.size.0, self.size.1);
let from_url: id = msg_send![class!(NSURL), URLWithString:file.into_inner()];
let request: id = msg_send![class!(QLThumbnailGenerationRequest), alloc];
let request: id = msg_send![request, initWithFileAtURL:from_url
size:size
scale:self.scale
representationTypes:types];
if self.icon_mode {
let _: () = msg_send![request, setIconMode:YES];
}
if self.minimum_dimension != 0. {
let _: () = msg_send![request, setMinimumDimension:self.minimum_dimension];
}
request
}
}
}

46
src/quicklook/mod.rs Normal file
View file

@ -0,0 +1,46 @@
use objc::runtime::{Object};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId;
use block::ConcreteBlock;
use url::Url;
use crate::error::Error;
use crate::foundation::{id, NSUInteger};
use crate::image::Image;
mod config;
pub use config::{ThumbnailConfig, ThumbnailQuality};
#[derive(Debug)]
pub struct ThumbnailGenerator(pub ShareId<Object>);
impl ThumbnailGenerator {
pub fn shared() -> Self {
ThumbnailGenerator(unsafe {
ShareId::from_ptr(msg_send![class!(QLThumbnailGenerator), sharedGenerator])
})
}
pub fn generate<F>(&self, url: &Url, config: ThumbnailConfig, callback: F)
where
//F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static
F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static
{
let block = ConcreteBlock::new(move |thumbnail: id, thumbnail_type: NSUInteger, error: id| {
unsafe {
let image = Image::with(msg_send![thumbnail, NSImage]);
callback(Ok((image, ThumbnailQuality::Low)));
}
});
let block = block.copy();
let request = config.to_request(url);
unsafe {
let _: () = msg_send![&*self.0, generateRepresentationsForRequest:request
updateHandler:block];
}
}
}

120
src/scrollview/macos.rs Normal file
View file

@ -0,0 +1,120 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::scrollview::{SCROLLVIEW_DELEGATE_PTR, ScrollViewDelegate};
use crate::utils::load;
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_entered<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.dragging_entered(DragInfo {
info: unsafe { Id::from_ptr(info) }
}).into()
}
/// Called when a drag/drop operation has entered this view.
extern fn prepare_for_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
match view.prepare_for_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn perform_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
match view.perform_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern fn conclude_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.conclude_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Called when a drag/drop operation has entered this view.
extern fn dragging_exited<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.dragging_exited(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Injects an `NSScrollView` subclass.
pub(crate) fn register_scrollview_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSScrollView);
let mut decl = ClassDecl::new("RSTScrollView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_scrollview_class_with_delegate<T: ScrollViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSScrollView);
let mut decl = ClassDecl::new("RSTScrollViewWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(SCROLLVIEW_DELEGATE_PTR);
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Drag and drop operations (e.g, accepting files)
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(performDragOperation:), perform_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);
decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation::<T> as extern fn (&mut Object, _, _));
decl.add_method(sel!(draggingExited:), dragging_exited::<T> as extern fn (&mut Object, _, _));
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

245
src/scrollview/mod.rs Normal file
View file

@ -0,0 +1,245 @@
//! Wraps `NSView` and `UIView` across platforms.
//!
//! This implementation errs towards the `UIView` side of things, and mostly acts as a wrapper to
//! bring `NSView` to the modern era. It does this by flipping the coordinate system to be what
//! people expect in 2020, and layer-backing all views by default.
//!
//! Views implement Autolayout, which enable you to specify how things should appear on the screen.
//!
//! ```rust,no_run
//! use cacao::color::rgb;
//! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::View;
//! use cacao::window::{Window, WindowDelegate};
//!
//! #[derive(Default)]
//! struct AppWindow {
//! content: View,
//! red: View,
//! window: Window
//! }
//!
//! impl WindowDelegate for AppWindow {
//! fn did_load(&mut self, window: Window) {
//! window.set_minimum_content_size(300., 300.);
//! self.window = window;
//!
//! self.red.set_background_color(rgb(224, 82, 99));
//! self.content.add_subview(&self.red);
//!
//! self.window.set_content_view(&self.content);
//!
//! LayoutConstraint::activate(&[
//! self.red.top.constraint_equal_to(&self.content.top).offset(16.),
//! self.red.leading.constraint_equal_to(&self.content.leading).offset(16.),
//! self.red.trailing.constraint_equal_to(&self.content.trailing).offset(-16.),
//! self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
//! ]);
//! }
//! }
//! ```
//!
//! For more information on Autolayout, view the module or check out the examples folder.
use objc_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::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::{register_scrollview_class, register_scrollview_class_with_delegate};
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate};
mod traits;
pub use traits::ScrollViewDelegate;
pub(crate) static SCROLLVIEW_DELEGATE_PTR: &str = "rstScrollViewDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
let view: id = msg_send![registration_fn(), new];
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
{
let _: () = msg_send![view, setDrawsBackground:NO];
let _: () = msg_send![view, setWantsLayer:YES];
let _: () = msg_send![view, setBorderType:0];
let _: () = msg_send![view, setHorizontalScrollElasticity:1];
let _: () = msg_send![view, setHasVerticalScroller:YES];
}
view
}
}
/// A clone-able handler to a `NS/UIScrollView` reference in the Objective C runtime.
#[derive(Debug)]
pub struct ScrollView<T = ()> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for ScrollView {
fn default() -> Self {
ScrollView::new()
}
}
impl ScrollView {
/// Returns a default `View`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_scrollview_class);
ScrollView {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
}
impl<T> ScrollView<T> where T: ScrollViewDelegate + 'static {
/// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> ScrollView<T> {
let mut delegate = Box::new(delegate);
let view = allocate_view(register_scrollview_class_with_delegate::<T>);
unsafe {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let ptr: *const T = &*delegate;
(&mut *view).set_ivar(SCROLLVIEW_DELEGATE_PTR, ptr as usize);
};
let mut view = ScrollView {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
};
(&mut delegate).did_load(view.clone_as_handle());
view.delegate = Some(delegate);
view
}
}
impl<T> ScrollView<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to
/// reference, customize and use the view but without the trickery of holding pieces of the
/// delegate - the `View` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> ScrollView {
ScrollView {
delegate: None,
top: self.top.clone(),
leading: self.leading.clone(),
trailing: self.trailing.clone(),
bottom: self.bottom.clone(),
width: self.width.clone(),
height: self.height.clone(),
center_x: self.center_x.clone(),
center_y: self.center_y.clone(),
objc: self.objc.clone()
}
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
}
impl<T> Layout for ScrollView<T> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl<T> Drop for ScrollView<T> {
/// 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).
///
/// There are, thankfully, no delegates we need to break 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];
}
}
}
}
}

37
src/scrollview/traits.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::dragdrop::{DragInfo, DragOperation};
use crate::scrollview::ScrollView;
pub trait ScrollViewDelegate {
/// Called when the View is ready to work with. You're passed a `View` - this is safe to
/// store and use repeatedly, but it's not thread safe - any UI calls must be made from the
/// main thread!
fn did_load(&mut self, _view: ScrollView) {}
/// Called when this is about to be added to the view heirarchy.
fn will_appear(&self, _animated: bool) {}
/// Called after this has been added to the view heirarchy.
fn did_appear(&self, _animated: bool) {}
/// Called when this is about to be removed from the view heirarchy.
fn will_disappear(&self, _animated: bool) {}
/// Called when this has been removed from the view heirarchy.
fn did_disappear(&self, _animated: bool) {}
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None }
/// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation.
fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data.
fn perform_drag_operation(&self, _info: DragInfo) -> bool { false }
/// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up.
fn conclude_drag_operation(&self, _info: DragInfo) {}
/// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame
/// rectangle (in the case of a window object).
fn dragging_exited(&self, _info: DragInfo) {}
}

22
src/text/enums.rs Normal file
View file

@ -0,0 +1,22 @@
use crate::foundation::{NSInteger, NSUInteger};
pub enum TextAlign {
Left,
Right,
Center,
Justified,
Natural
}
impl From<TextAlign> for NSInteger {
fn from(alignment: TextAlign) -> Self {
match alignment {
TextAlign::Left => 0,
TextAlign::Center => 1,
TextAlign::Right => 2,
TextAlign::Justified => 3,
TextAlign::Natural => 4
}
}
}

36
src/text/font.rs Normal file
View file

@ -0,0 +1,36 @@
//! Implements `Font`, a wrapper around `NSFont` on macOS and `UIFont` on iOS.
use core_graphics::base::CGFloat;
use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
#[derive(Debug)]
pub struct Font {
pub objc: ShareId<Object>
}
impl Default for Font {
fn default() -> Self {
Font {
objc: unsafe {
let cls = class!(NSFont);
let default_size: id = msg_send![cls, labelFontSize];
msg_send![cls, labelFontOfSize:default_size]
}
}
}
}
impl Font {
pub fn system(size: CGFloat) -> Self {
Font {
objc: unsafe {
msg_send![class!(NSFont), systemFontOfSize:size]
}
}
}
}

58
src/text/label/macos.rs Normal file
View file

@ -0,0 +1,58 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::dragdrop::DragInfo;
use crate::text::label::{LABEL_DELEGATE_PTR, LabelDelegate};
use crate::utils::load;
/// Injects an `NSTextField` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSTextField);
let mut decl = ClassDecl::new("RSTTextField", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSTextField` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: LabelDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSView);
let mut decl = ClassDecl::new("RSTTextFieldWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(LABEL_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe {
VIEW_CLASS
}
}

273
src/text/label/mod.rs Normal file
View file

@ -0,0 +1,273 @@
//! Wraps `NSTextField` and `UILabel` across platforms, explicitly as a Label.
//! In AppKit, `NSTextField` does double duty, and for clarity we just double
//! the implementation.
//!
//! Labels implement Autolayout, which enable you to specify how things should appear on the screen.
//!
//! ```rust,no_run
//! use cacao::color::rgb;
//! use cacao::layout::{Layout, LayoutConstraint};
//! use cacao::view::Label;
//! use cacao::window::{Window, WindowDelegate};
//!
//! #[derive(Default)]
//! struct AppWindow {
//! content: Label,
//! label: Label,
//! window: Window
//! }
//!
//! impl WindowDelegate for AppWindow {
//! fn did_load(&mut self, window: Window) {
//! window.set_minimum_content_size(300., 300.);
//! self.window = window;
//!
//! self.label.set_background_color(rgb(224, 82, 99));
//! self.label.set_text("LOL");
//! self.content.add_subview(&self.red);
//!
//! self.window.set_content_view(&self.content);
//!
//! LayoutConstraint::activate(&[
//! self.red.top.constraint_equal_to(&self.content.top).offset(16.),
//! self.red.leading.constraint_equal_to(&self.content.leading).offset(16.),
//! self.red.trailing.constraint_equal_to(&self.content.trailing).offset(-16.),
//! self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
//! ]);
//! }
//! }
//! ```
//!
//! For more information on Autolayout, view the module or check out the examples folder.
use objc_id::ShareId;
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSString};
use crate::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::text::{Font, TextAlign};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::{register_view_class, register_view_class_with_delegate};
#[cfg(target_os = "ios")]
mod ios;
#[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate};
//mod controller;
//pub use controller::LabelController;
mod traits;
pub use traits::LabelDelegate;
pub(crate) static LABEL_DELEGATE_PTR: &str = "rstLabelDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them.
fn allocate_view(registration_fn: fn() -> *const Class) -> id {
unsafe {
#[cfg(target_os = "macos")]
let view: id = {
// This sucks, but for now, sure.
let blank = NSString::new("");
msg_send![registration_fn(), labelWithString:blank.into_inner()]
};
#[cfg(target_os = "ios")]
let view: id = msg_send![registration_fn(), new];
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
#[cfg(target_os = "macos")]
let _: () = msg_send![view, setWantsLayer:YES];
view
}
}
/// A clone-able handler to an `NSTextField/UILabel` reference in the
/// Objective-C runtime.
#[derive(Debug)]
pub struct Label<T = ()> {
/// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>,
/// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>,
/// A pointer to the Objective-C runtime top layout constraint.
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
pub center_y: LayoutAnchorY
}
impl Default for Label {
fn default() -> Self {
Label::new()
}
}
impl Label {
/// Returns a default `Label`, suitable for
pub fn new() -> Self {
let view = allocate_view(register_view_class);
Label {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![view, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![view, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![view, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![view, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![view, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(view) },
}
}
}
impl<T> Label<T> where T: LabelDelegate + 'static {
/// Initializes a new Label with a given `LabelDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> Label<T> {
let delegate = Box::new(delegate);
let label = allocate_view(register_view_class_with_delegate::<T>);
unsafe {
//let view: id = msg_send![register_view_class_with_delegate::<T>(), new];
//let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
let ptr: *const T = &*delegate;
(&mut *label).set_ivar(LABEL_DELEGATE_PTR, ptr as usize);
};
let mut label = Label {
delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![label, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![label, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![label, trailingAnchor] }),
bottom: LayoutAnchorY::new(unsafe { msg_send![label, bottomAnchor] }),
width: LayoutAnchorDimension::new(unsafe { msg_send![label, widthAnchor] }),
height: LayoutAnchorDimension::new(unsafe { msg_send![label, heightAnchor] }),
center_x: LayoutAnchorX::new(unsafe { msg_send![label, centerXAnchor] }),
center_y: LayoutAnchorY::new(unsafe { msg_send![label, centerYAnchor] }),
objc: unsafe { ShareId::from_ptr(label) },
};
//(&mut delegate).did_load(label.clone_as_handle());
label.delegate = Some(delegate);
label
}
}
impl<T> Label<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to
/// reference, customize and use the view but without the trickery of holding pieces of the
/// delegate - the `Label` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> Label {
Label {
delegate: None,
top: self.top.clone(),
leading: self.leading.clone(),
trailing: self.trailing.clone(),
bottom: self.bottom.clone(),
width: self.width.clone(),
height: self.height.clone(),
center_x: self.center_x.clone(),
center_y: self.center_y.clone(),
objc: self.objc.clone()
}
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color();
unsafe {
let cg: id = msg_send![bg, CGColor];
let layer: id = msg_send![&*self.objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
}
}
/// Call this to set the text for the label.
pub fn set_text(&self, text: &str) {
let s = NSString::new(text);
unsafe {
let _: () = msg_send![&*self.objc, setStringValue:s.into_inner()];
}
}
pub fn set_text_alignment(&self, alignment: TextAlign) {
unsafe {
let alignment: NSInteger = alignment.into();
let _: () = msg_send![&*self.objc, setAlignment:alignment];
}
}
pub fn set_font(&self, font: &Font) {
unsafe {
let _: () = msg_send![&*self.objc, setFont:&*font.objc];
}
}
}
impl<T> Layout for Label<T> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
fn add_subview<V: Layout>(&self, view: &V) {
let backing_node = view.get_backing_node();
unsafe {
let _: () = msg_send![&*self.objc, addSubview:backing_node];
}
}
}
impl<T> Drop for Label<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `Label` 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).
///
/// There are, thankfully, no delegates we need to break 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];
}
}
}
}
}

4
src/text/label/traits.rs Normal file
View file

@ -0,0 +1,4 @@
//! Various traits used for Labels.
pub trait LabelDelegate {
}

11
src/text/mod.rs Normal file
View file

@ -0,0 +1,11 @@
//! The `text` module encompasses various widgets for rendering and interacting
//! with text.
pub mod label;
pub use label::Label;
pub mod enums;
pub use enums::TextAlign;
pub mod font;
pub use font::Font;

View file

@ -42,6 +42,22 @@ pub fn load<'a, T>(this: &'a Object, ptr_name: &str) -> &'a T {
} }
} }
pub fn async_main_thread<F>(method: F)
where
F: Fn() + Send + 'static
{
let queue = dispatch::Queue::main();
queue.exec_async(method);
}
pub fn sync_main_thread<F>(method: F)
where
F: Fn() + Send + 'static
{
let queue = dispatch::Queue::main();
queue.exec_sync(method);
}
/// Upstream core graphics does not implement Encode for certain things, so we wrap them here - /// Upstream core graphics does not implement Encode for certain things, so we wrap them here -
/// these are only used in reading certain types passed to us from some delegate methods. /// these are only used in reading certain types passed to us from some delegate methods.
#[repr(C)] #[repr(C)]
@ -51,6 +67,12 @@ pub struct CGSize {
pub height: CGFloat, pub height: CGFloat,
} }
impl CGSize {
pub fn new(width: CGFloat, height: CGFloat) -> Self {
CGSize { width, height }
}
}
unsafe impl Encode for CGSize { unsafe impl Encode for CGSize {
fn encode() -> Encoding { fn encode() -> Encoding {
let encoding = format!("{{CGSize={}{}}}", let encoding = format!("{{CGSize={}{}}}",

View file

@ -19,7 +19,7 @@ mod ios;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use ios::register_view_controller_class; use ios::register_view_controller_class;
//#[derive(Debug)] #[derive(Debug)]
pub struct ViewController<T> { pub struct ViewController<T> {
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
pub view: View<T> pub view: View<T>
@ -27,7 +27,7 @@ pub struct ViewController<T> {
impl<T> ViewController<T> where T: ViewDelegate + 'static { impl<T> ViewController<T> where T: ViewDelegate + 'static {
pub fn new(delegate: T) -> Self { pub fn new(delegate: T) -> Self {
let view = View::with(delegate); let mut view = View::with(delegate);
let objc = unsafe { let objc = unsafe {
let vc: id = msg_send![register_view_controller_class::<T>(), new]; let vc: id = msg_send![register_view_controller_class::<T>(), new];
@ -42,10 +42,10 @@ impl<T> ViewController<T> where T: ViewDelegate + 'static {
ShareId::from_ptr(vc) ShareId::from_ptr(vc)
}; };
let handle = view.clone_as_handle(); //let handle = view.clone_as_handle();
if let Some(view_delegate) = &view.delegate { //if let Some(view_delegate) = &mut view.delegate {
view_delegate.did_load(handle); // view_delegate.did_load(handle);
} //}
ViewController { ViewController {
objc: objc, objc: objc,

View file

@ -149,7 +149,7 @@ impl<T> View<T> where T: ViewDelegate + 'static {
/// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events /// Initializes a new View with a given `ViewDelegate`. This enables you to respond to events
/// and customize the view as a module, similar to class-based systems. /// and customize the view as a module, similar to class-based systems.
pub fn with(delegate: T) -> View<T> { pub fn with(delegate: T) -> View<T> {
let delegate = Box::new(delegate); let mut delegate = Box::new(delegate);
let view = allocate_view(register_view_class_with_delegate::<T>); let view = allocate_view(register_view_class_with_delegate::<T>);
unsafe { unsafe {
@ -172,7 +172,7 @@ impl<T> View<T> where T: ViewDelegate + 'static {
objc: unsafe { ShareId::from_ptr(view) }, objc: unsafe { ShareId::from_ptr(view) },
}; };
&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
} }

View file

@ -4,10 +4,10 @@ use crate::dragdrop::{DragInfo, DragOperation};
use crate::view::View; use crate::view::View;
pub trait ViewDelegate { pub trait ViewDelegate {
/// Called when the View is ready to work with. You're passed a `ViewHandle` - this is safe to /// Called when the View is ready to work with. You're passed a `View` - this is safe to
/// store and use repeatedly, but it's not thread safe - any UI calls must be made from the /// store and use repeatedly, but it's not thread safe - any UI calls must be made from the
/// main thread! /// main thread!
fn did_load(&self, _view: View) {} fn did_load(&mut self, _view: View) {}
/// Called when this is about to be added to the view heirarchy. /// Called when this is about to be added to the view heirarchy.
fn will_appear(&self, _animated: bool) {} fn will_appear(&self, _animated: bool) {}