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:
parent
784727748c
commit
121a2f938e
11
build.rs
11
build.rs
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
54
src/image/handle.rs
Normal 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
16
src/image/image.rs
Normal 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
45
src/image/ios.rs
Normal 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
39
src/image/macos.rs
Normal 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
143
src/image/mod.rs
Normal 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
39
src/image/traits.rs
Normal 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 destination’s 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
58
src/input/macos.rs
Normal 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
274
src/input/mod.rs
Normal 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
4
src/input/traits.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//! Various traits used for Labels.
|
||||||
|
|
||||||
|
pub trait TextFieldDelegate {
|
||||||
|
}
|
113
src/invoker.rs
Normal file
113
src/invoker.rs
Normal 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 }
|
||||||
|
}
|
15
src/lib.rs
15
src/lib.rs
|
@ -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
42
src/listview/enums.rs
Normal 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
163
src/listview/macos.rs
Normal 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
454
src/listview/mod.rs
Normal 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
45
src/listview/row/ios.rs
Normal 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
125
src/listview/row/macos.rs
Normal 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
245
src/listview/row/mod.rs
Normal 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
51
src/listview/traits.rs
Normal 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 destination’s 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) {}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
102
src/quicklook/config.rs
Normal 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
46
src/quicklook/mod.rs
Normal 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
120
src/scrollview/macos.rs
Normal 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
245
src/scrollview/mod.rs
Normal 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
37
src/scrollview/traits.rs
Normal 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 destination’s 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
22
src/text/enums.rs
Normal 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
36
src/text/font.rs
Normal 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
58
src/text/label/macos.rs
Normal 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
273
src/text/label/mod.rs
Normal 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
4
src/text/label/traits.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//! Various traits used for Labels.
|
||||||
|
|
||||||
|
pub trait LabelDelegate {
|
||||||
|
}
|
11
src/text/mod.rs
Normal file
11
src/text/mod.rs
Normal 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;
|
22
src/utils.rs
22
src/utils.rs
|
@ -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={}{}}}",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
Loading…
Reference in a new issue