A large smattering of updates.

- Added basic animation support, via NSAnimationContext proxy objects.
  These can be used to animate layout constraints and alpha values,
  currently.

- Fixed a bug in ListView where the underlying NSTableView would not
  redraw the full correct virtual height in some conditions.

- Added safe layout guide support to some views.

- Added a new trait to buffer ObjC object access for view and control
  types. This is the supertrait of the Layout and Control traits.

- Added a Control trait, which implements various NSControl pieces.

- Added a Select control, which is a Select-ish HTML dropdown lookalike.

- Added NSURL support, which is one of the few types to expose here.

- Filesystem and pasteboard types now work with NSURLs. Users who need
  pathbufs can use the provided conversion method on NSURL.

- Fixed a bug where some Window and ViewController types could wind up
  in a double-init scenario.
This commit is contained in:
Ryan McGrath 2022-01-02 02:35:12 -08:00
parent 87533d576f
commit 4ecfbd0928
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
48 changed files with 1516 additions and 184 deletions

View file

@ -23,14 +23,14 @@ block = "0.1.6"
core-foundation = { version = "0.9", features = ["with-chrono"] } core-foundation = { version = "0.9", features = ["with-chrono"] }
core-graphics = "0.22" core-graphics = "0.22"
dispatch = "0.2.0" dispatch = "0.2.0"
infer = { version = "0.4", optional = true }
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2" libc = "0.2"
objc = "0.2.7" objc = "0.2.7"
objc_id = "0.1.1" objc_id = "0.1.1"
os_info = "3.0.1" os_info = "3.0.1"
uuid = { version = "0.8", features = ["v4"], optional = true }
url = "2.1.1" url = "2.1.1"
infer = { version = "0.4", optional = true } uuid = { version = "0.8", features = ["v4"], optional = true }
[dev-dependencies] [dev-dependencies]
eval = "0.4" eval = "0.4"

73
src/appkit/animation.rs Normal file
View file

@ -0,0 +1,73 @@
use block::ConcreteBlock;
use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::id;
/// A very, very basic wrapper around NSAnimationContext. 100% subject to change.
#[derive(Debug)]
pub struct AnimationContext(id);
impl AnimationContext {
/// Wraps an NSAnimationContext pointer.
pub fn new(ctx: id) -> Self {
Self(ctx)
}
/// Sets the animation duration.
pub fn set_duration(&mut self, duration: f64) {
unsafe {
let _: () = msg_send![self.0, setDuration:duration];
}
}
/// Pass it a block, and the changes in that block will be animated, provided they're
/// properties that support animation.
///
/// [https://developer.apple.com/documentation/appkit/nsanimationcontext?language=objc]
///
/// For more information, you should consult the documentation for NSAnimationContext, then skim
/// the supported methods here.
pub fn run<F>(animation: F)
where
F: Fn(&mut AnimationContext) + Send + Sync + 'static
{
let block = ConcreteBlock::new(move |ctx| {
let mut context = AnimationContext(ctx);
animation(&mut context);
});
let block = block.copy();
unsafe {
//let context: id = msg_send![class!(NSAnimationContext), currentContext];
let _: () = msg_send![class!(NSAnimationContext), runAnimationGroup:block];
}
}
/// Pass it a block, and the changes in that block will be animated, provided they're
/// properties that support animation.
///
/// [https://developer.apple.com/documentation/appkit/nsanimationcontext?language=objc]
///
/// For more information, you should consult the documentation for NSAnimationContext, then skim
/// the supported methods here.
pub fn run_with_completion_handler<F, C>(animation: F, completion_handler: C)
where
F: Fn(&mut AnimationContext) + Send + Sync + 'static,
C: Fn() + Send + Sync + 'static
{
let block = ConcreteBlock::new(move |ctx| {
let mut context = AnimationContext(ctx);
animation(&mut context);
});
let block = block.copy();
let completion_block = ConcreteBlock::new(completion_handler);
let completion_block = completion_block.copy();
unsafe {
//let context: id = msg_send![class!(NSAnimationContext), currentContext];
let _: () = msg_send![class!(NSAnimationContext), runAnimationGroup:block
completionHandler:completion_block];
}
}
}

View file

@ -49,6 +49,8 @@ use crate::appkit::menu::Menu;
use crate::notification_center::Dispatcher; use crate::notification_center::Dispatcher;
use crate::utils::activate_cocoa_multithreading; use crate::utils::activate_cocoa_multithreading;
//use crate::bundle::set_bundle_id;
mod class; mod class;
use class::register_app_class; use class::register_app_class;
@ -131,7 +133,7 @@ impl<T> App<T> where T: AppDelegate + 'static {
/// policies), injects an `NSObject` delegate wrapper, and retains everything on the /// policies), injects an `NSObject` delegate wrapper, and retains everything on the
/// Objective-C side of things. /// Objective-C side of things.
pub fn new(_bundle_id: &str, delegate: T) -> Self { pub fn new(_bundle_id: &str, delegate: T) -> Self {
// set_bundle_id(bundle_id); //set_bundle_id(bundle_id);
activate_cocoa_multithreading(); activate_cocoa_multithreading();

View file

@ -8,6 +8,9 @@
mod alert; mod alert;
pub use alert::Alert; pub use alert::Alert;
mod animation;
pub use animation::AnimationContext;
mod app; mod app;
pub use app::*; pub use app::*;

View file

@ -23,7 +23,7 @@ pub use traits::ToolbarDelegate;
mod enums; mod enums;
pub use enums::{ToolbarDisplayMode, ToolbarSizeMode, ItemIdentifier}; pub use enums::{ToolbarDisplayMode, ToolbarSizeMode, ItemIdentifier};
pub(crate) static TOOLBAR_PTR: &str = "rstToolbarPtr"; pub(crate) static TOOLBAR_PTR: &str = "cacaoToolbarPtr";
/// A wrapper for `NSToolbar`. Holds (retains) pointers for the Objective-C runtime /// A wrapper for `NSToolbar`. Holds (retains) pointers for the Objective-C runtime
/// where our `NSToolbar` and associated delegate live. /// where our `NSToolbar` and associated delegate live.
@ -62,7 +62,7 @@ impl<T> Toolbar<T> where T: ToolbarDelegate + 'static {
(ShareId::from_ptr(toolbar), ShareId::from_ptr(objc_delegate)) (ShareId::from_ptr(toolbar), ShareId::from_ptr(objc_delegate))
}; };
&mut delegate.did_load(Toolbar { let _ret = &mut delegate.did_load(Toolbar {
objc: objc.clone(), objc: objc.clone(),
objc_delegate: objc_delegate.clone(), objc_delegate: objc_delegate.clone(),
identifier: identifier.clone(), identifier: identifier.clone(),

View file

@ -54,7 +54,7 @@ impl<T> WindowController<T> where T: WindowDelegate + 'static {
/// Allocates and configures an `NSWindowController` in the Objective-C/Cocoa runtime that maps over /// Allocates and configures an `NSWindowController` in the Objective-C/Cocoa runtime that maps over
/// to your supplied delegate. /// to your supplied delegate.
pub fn with(config: WindowConfig, delegate: T) -> Self { pub fn with(config: WindowConfig, delegate: T) -> Self {
let mut window = Window::with(config, delegate); let window = Window::with(config, delegate);
let objc = unsafe { let objc = unsafe {
let window_controller_class = register_window_controller_class::<T>(); let window_controller_class = register_window_controller_class::<T>();
@ -69,13 +69,6 @@ impl<T> WindowController<T> where T: WindowDelegate + 'static {
Id::from_ptr(controller) Id::from_ptr(controller)
}; };
if let Some(delegate) = &mut window.delegate {
(*delegate).did_load(Window {
delegate: None,
objc: window.objc.clone()
});
}
WindowController { objc, window } WindowController { objc, window }
} }

View file

@ -17,10 +17,11 @@ use objc::{msg_send, sel, sel_impl, class};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::ShareId; use objc_id::ShareId;
use crate::appkit::toolbar::{Toolbar, ToolbarDelegate};
use crate::color::Color; use crate::color::Color;
use crate::foundation::{id, nil, to_bool, YES, NO, NSString, NSInteger, NSUInteger}; use crate::foundation::{id, nil, to_bool, YES, NO, NSString, NSInteger, NSUInteger};
use crate::layout::traits::Layout; use crate::layout::Layout;
use crate::appkit::toolbar::{Toolbar, ToolbarDelegate}; use crate::objc_access::ObjcAccess;
use crate::utils::{os, Controller}; use crate::utils::{os, Controller};
mod class; mod class;
@ -289,7 +290,7 @@ impl<T> Window<T> {
/// Given a view, sets it as the content view for this window. /// Given a view, sets it as the content view for this window.
pub fn set_content_view<L: Layout + 'static>(&self, view: &L) { pub fn set_content_view<L: Layout + 'static>(&self, view: &L) {
view.with_backing_node(|backing_node| unsafe { view.with_backing_obj_mut(|backing_node| unsafe {
let _: () = msg_send![&*self.objc, setContentView:&*backing_node]; let _: () = msg_send![&*self.objc, setContentView:&*backing_node];
}); });
} }
@ -446,6 +447,7 @@ impl<T> Window<T> {
} }
} }
/// Sets the separator style for this window.
pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) {
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style];

View file

@ -66,9 +66,7 @@ extern fn get_bundle_id(this: &Object, s: Sel, v: id) -> id {
let url: id = msg_send![main_bundle, bundleURL]; let url: id = msg_send![main_bundle, bundleURL];
let x: id = msg_send![url, absoluteString]; let x: id = msg_send![url, absoluteString];
println!("Got here? {:?}", x); println!("Got here? {:?}", x);
unsafe { NSString::new("com.test.user_notifications").into()
NSString::alloc(nil).init_str("com.secretkeys.subatomic")
}
} else { } else {
msg_send![this, __bundleIdentifier] msg_send![this, __bundleIdentifier]
} }

View file

@ -29,10 +29,13 @@ use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::color::Color; use crate::color::Color;
use crate::control::Control;
use crate::image::Image; use crate::image::Image;
use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger}; use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger};
use crate::invoker::TargetActionHandler; use crate::invoker::TargetActionHandler;
use crate::keys::Key;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::text::{AttributedString, Font}; use crate::text::{AttributedString, Font};
use crate::utils::{load, properties::ObjcProperty}; use crate::utils::{load, properties::ObjcProperty};
@ -213,11 +216,21 @@ impl Button {
/// Set a key to be bound to this button. When the key is pressed, the action coupled to this /// Set a key to be bound to this button. When the key is pressed, the action coupled to this
/// button will fire. /// button will fire.
pub fn set_key_equivalent(&self, key: &str) { pub fn set_key_equivalent<'a, K>(&self, key: K)
let key = NSString::new(key); where
K: Into<Key<'a>>
{
let key: Key<'a> = key.into();
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| {
let _: () = msg_send![obj, setKeyEquivalent:&*key]; let keychar = match key {
Key::Char(s) => NSString::new(s),
Key::Delete => NSString::new("\u{08}")
};
unsafe {
let _: () = msg_send![obj, setKeyEquivalent:&*keychar];
}
}); });
} }
@ -281,26 +294,32 @@ impl Button {
} }
} }
impl Layout for Button { impl ObjcAccess for Button {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl Layout for &Button { impl Layout for Button {}
fn with_backing_node<F: Fn(id)>(&self, handler: F) { impl Control for Button {}
impl ObjcAccess for &Button {
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl Layout for &Button {}
impl Control for &Button {}
impl Drop for Button { impl Drop for Button {
/// Nils out references on the Objective-C side and removes this from the backing view. /// Nils out references on the Objective-C side and removes this from the backing view.
// Just to be sure, let's... nil these out. They should be weak references, // Just to be sure, let's... nil these out. They should be weak references,

View file

@ -217,6 +217,7 @@ pub enum Color {
/// The default color to use for thin separators/lines that /// The default color to use for thin separators/lines that
/// do not allow content underneath to be visible. /// do not allow content underneath to be visible.
/// This value automatically switches to the correct variant depending on light or dark mode. /// This value automatically switches to the correct variant depending on light or dark mode.
#[cfg(feature = "uikit")]
OpaqueSeparator, OpaqueSeparator,
/// The default color to use for rendering links. /// The default color to use for rendering links.
@ -495,7 +496,10 @@ unsafe fn to_objc(obj: &Color) -> id {
Color::SystemBackgroundSecondary => system_color_with_fallback!(color, secondarySystemBackgroundColor, clearColor), Color::SystemBackgroundSecondary => system_color_with_fallback!(color, secondarySystemBackgroundColor, clearColor),
Color::SystemBackgroundTertiary => system_color_with_fallback!(color, tertiarySystemBackgroundColor, clearColor), Color::SystemBackgroundTertiary => system_color_with_fallback!(color, tertiarySystemBackgroundColor, clearColor),
Color::Separator => system_color_with_fallback!(color, separatorColor, lightGrayColor), Color::Separator => system_color_with_fallback!(color, separatorColor, lightGrayColor),
#[cfg(feature = "uikit")]
Color::OpaqueSeparator => system_color_with_fallback!(color, opaqueSeparatorColor, darkGrayColor), Color::OpaqueSeparator => system_color_with_fallback!(color, opaqueSeparatorColor, darkGrayColor),
Color::Link => system_color_with_fallback!(color, linkColor, blueColor), Color::Link => system_color_with_fallback!(color, linkColor, blueColor),
Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor), Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor),
Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor), Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor),

56
src/control/mod.rs Normal file
View file

@ -0,0 +1,56 @@
use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object;
use crate::foundation::{id, YES, NO, NSUInteger};
use crate::objc_access::ObjcAccess;
/// Use this enum for specifying NSControl size types.
#[derive(Copy, Clone, Debug)]
pub enum ControlSize {
/// The smallest control size.
Mini,
/// A smaller control size.
Small,
/// The default, regular, size.
Regular,
/// A large control. Only available on macOS 11.0+.
/// If you pass this to the `set_control_size` method on the `Control` trait, it will
/// transparently map to `Regular` on 10.15 and below.
Large
}
/// A trait that view wrappers must conform to. Enables managing the subview tree.
#[allow(unused_variables)]
pub trait Control: ObjcAccess {
/// Whether this control is enabled or not.
fn set_enabled(&self, is_enabled: bool) {
self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setEnabled:match is_enabled {
true => YES,
false => NO
}];
});
}
/// Sets the underlying control size.
fn set_control_size(&self, size: ControlSize) {
let control_size: NSUInteger = match size {
ControlSize::Mini => 2,
ControlSize::Small => 1,
ControlSize::Regular => 0,
ControlSize::Large => match crate::utils::os::is_minimum_version(11) {
true => 3,
false => 0
}
};
self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setControlSize:control_size];
});
}
}

View file

@ -10,7 +10,7 @@ use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::ShareId; use objc_id::ShareId;
use crate::foundation::{id, YES, NO, NSInteger, NSString}; use crate::foundation::{id, nil, YES, NO, NSInteger, NSString, NSURL};
use crate::filesystem::enums::ModalResponse; use crate::filesystem::enums::ModalResponse;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
@ -127,8 +127,7 @@ impl FileSelectPanel {
self.allows_multiple_selection = allows; self.allows_multiple_selection = allows;
} }
/// Shows the panel as a modal. Currently sheets are not supported, but you're free (and able /// Shows the panel as a modal.
/// to) thread the Objective C calls yourself by using the panel field on this struct.
/// ///
/// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as
/// the system runs and manages that in another process, and we're still abiding by the general /// the system runs and manages that in another process, and we're still abiding by the general
@ -138,7 +137,7 @@ impl FileSelectPanel {
/// script) or can't easily pass one to use as a sheet. /// script) or can't easily pass one to use as a sheet.
pub fn show<F>(&self, handler: F) pub fn show<F>(&self, handler: F)
where where
F: Fn(Vec<PathBuf>) + 'static F: Fn(Vec<NSURL>) + 'static
{ {
let panel = self.panel.clone(); let panel = self.panel.clone();
let completion = ConcreteBlock::new(move |result: NSInteger| { let completion = ConcreteBlock::new(move |result: NSInteger| {
@ -155,6 +154,16 @@ impl FileSelectPanel {
} }
} }
/// As panels descend behind the scenes from `NSWindow`, we can call through to close it.
///
/// You should really prefer to utilize sheets to display panels; this is offered as a
/// convenience for rare cases where you might need to retain a panel and close it later on.
pub fn close(&self) {
unsafe {
let _: () = msg_send![&*self.panel, close];
}
}
/// Shows the panel as a modal. Currently, this method accepts `Window`s which use a delegate. /// Shows the panel as a modal. Currently, this method accepts `Window`s which use a delegate.
/// If you're using a `Window` without a delegate, you may need to opt to use the `show()` /// If you're using a `Window` without a delegate, you may need to opt to use the `show()`
/// method. /// method.
@ -164,7 +173,7 @@ impl FileSelectPanel {
/// retain/ownership rules here. /// retain/ownership rules here.
pub fn begin_sheet<T, F>(&self, window: &Window<T>, handler: F) pub fn begin_sheet<T, F>(&self, window: &Window<T>, handler: F)
where where
F: Fn(Vec<PathBuf>) + 'static F: Fn(Vec<NSURL>) + 'static
{ {
let panel = self.panel.clone(); let panel = self.panel.clone();
let completion = ConcreteBlock::new(move |result: NSInteger| { let completion = ConcreteBlock::new(move |result: NSInteger| {
@ -185,15 +194,16 @@ impl FileSelectPanel {
/// Retrieves the selected URLs from the provided panel. /// Retrieves the selected URLs from the provided panel.
/// This is currently a bit ugly, but it's also not something that needs to be the best thing in /// This is currently a bit ugly, but it's also not something that needs to be the best thing in
/// the world as it (ideally) shouldn't be called repeatedly in hot spots. /// the world as it (ideally) shouldn't be called repeatedly in hot spots.
pub fn get_urls(panel: &Object) -> Vec<PathBuf> { ///
/// (We mostly do this to find the sweet spot between Rust constructs and necessary Foundation
/// interaction patterns)
fn get_urls(panel: &Object) -> Vec<NSURL> {
unsafe { unsafe {
let urls: id = msg_send![&*panel, URLs]; let urls: id = msg_send![&*panel, URLs];
let count: usize = msg_send![urls, count]; let count: usize = msg_send![urls, count];
(0..count).map(|index| { (0..count).map(|index| {
let url: id = msg_send![urls, objectAtIndex:index]; NSURL::retain(msg_send![urls, objectAtIndex:index])
let path = NSString::retain(msg_send![url, path]).to_string();
path.into()
}).collect() }).collect()
} }
} }

View file

@ -50,6 +50,21 @@ impl NSData {
} }
} }
/// Given a slice of bytes, creates, retains, and returns a wrapped `NSData`.
///
/// This method is borrowed straight out of [objc-foundation](objc-foundation) by the amazing
/// Steven Sheldon, and just tweaked slightly to fit the desired API semantics here.
///
/// [objc-foundation]: https://crates.io/crates/objc-foundation
pub fn with_slice(bytes: &[u8]) -> Self {
let bytes_ptr = bytes.as_ptr() as *mut c_void;
unsafe {
let obj: id = msg_send![class!(NSData), dataWithBytes:bytes_ptr length:bytes.len()];
NSData(Id::from_ptr(obj))
}
}
/// Given a (presumably) `NSData`, wraps and retains it. /// Given a (presumably) `NSData`, wraps and retains it.
pub fn retain(data: id) -> Self { pub fn retain(data: id) -> Self {
NSData(unsafe { NSData(unsafe {

View file

@ -42,6 +42,10 @@ pub use number::NSNumber;
mod string; mod string;
pub use string::NSString; pub use string::NSString;
// Separate named module to not conflict with the `url` crate. Go figure.
mod urls;
pub use urls::{NSURL, NSURLBookmarkCreationOption, NSURLBookmarkResolutionOption};
/// Bool mapping types differ between ARM and x64. There's a number of places that we need to check /// Bool mapping types differ between ARM and x64. There's a number of places that we need to check
/// against BOOL results throughout the framework, and this just simplifies some mismatches. /// against BOOL results throughout the framework, and this just simplifies some mismatches.
#[inline(always)] #[inline(always)]

View file

@ -0,0 +1,66 @@
use crate::foundation::NSUInteger;
/// Options used when creating bookmark data.
#[derive(Copy, Clone, Debug)]
pub enum NSURLBookmarkCreationOption {
/// Specifies that a bookmark created with this option should be created with minimal information.
Minimal,
/// Specifies that the bookmark data should include properties required to create Finder alias files.
SuitableForBookmarkFile,
/// Specifies that you want to create a security-scoped bookmark that, when resolved, provides a
/// security-scoped URL allowing read/write access to a file-system resource.
SecurityScoped,
/// When combined with the NSURLBookmarkCreationOptions::SecurityScoped option, specifies that you
/// want to create a security-scoped bookmark that, when resolved, provides a security-scoped URL allowing
/// read-only access to a file-system resource.
SecurityScopedReadOnly
}
impl From<NSURLBookmarkCreationOption> for NSUInteger {
fn from(flag: NSURLBookmarkCreationOption) -> NSUInteger {
match flag {
NSURLBookmarkCreationOption::Minimal => 1u64 << 9,
NSURLBookmarkCreationOption::SuitableForBookmarkFile => 1u64 << 10,
NSURLBookmarkCreationOption::SecurityScoped => 1 << 11,
NSURLBookmarkCreationOption::SecurityScopedReadOnly => 1 << 12
}
}
}
impl From<&NSURLBookmarkCreationOption> for NSUInteger {
fn from(flag: &NSURLBookmarkCreationOption) -> NSUInteger {
match flag {
NSURLBookmarkCreationOption::Minimal => 1u64 << 9,
NSURLBookmarkCreationOption::SuitableForBookmarkFile => 1u64 << 10,
NSURLBookmarkCreationOption::SecurityScoped => 1 << 11,
NSURLBookmarkCreationOption::SecurityScopedReadOnly => 1 << 12
}
}
}
/// Options used when resolving bookmark data.
#[derive(Debug)]
pub enum NSURLBookmarkResolutionOption {
/// Specifies that no UI feedback should accompany resolution of the bookmark data.
WithoutUI,
/// Specifies that no volume should be mounted during resolution of the bookmark data.
WithoutMounting,
/// Specifies that the security scope, applied to the bookmark when it was created, should
/// be used during resolution of the bookmark data.
SecurityScoped
}
impl From<NSURLBookmarkResolutionOption> for NSUInteger {
fn from(flag: NSURLBookmarkResolutionOption) -> NSUInteger {
match flag {
NSURLBookmarkResolutionOption::WithoutUI => 1u64 << 8,
NSURLBookmarkResolutionOption::WithoutMounting => 1u64 << 9,
NSURLBookmarkResolutionOption::SecurityScoped => 1 << 10
}
}
}

181
src/foundation/urls/mod.rs Normal file
View file

@ -0,0 +1,181 @@
use std::error::Error;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object;
use objc_id::ShareId;
use crate::foundation::{id, nil, NSData, NSString, NSUInteger};
mod bookmark_options;
pub use bookmark_options::{NSURLBookmarkCreationOption, NSURLBookmarkResolutionOption};
mod resource_keys;
pub use resource_keys::{NSURLResourceKey, NSURLFileResource, NSUbiquitousItemDownloadingStatus};
/// Wraps `NSURL` for use throughout the framework.
///
/// This type may also be returned to users in some callbacks (e.g, file manager/selectors) as it's
/// a core part of the macOS/iOS experience and bridging around it is arguably blocking people from
/// being able to actually build useful things.
///
/// For pure-Rust developers who have no interest in the Objective-C underpinnings, there's a
/// `pathbuf()` method that returns an `std::path::PathBuf` for working with. Note that this will
/// prove less useful in sandboxed applications, and if the underlying file that the PathBuf points
/// to moves, you'll be responsible for figuring out exactly what you do there.
///
/// Otherwise, this struct bridges enough of NSURL to be useful (loading, using, and bookmarks).
/// Pull requests for additional functionality are welcome.
#[derive(Clone, Debug)]
pub struct NSURL<'a> {
/// A reference to the backing `NSURL`.
pub objc: ShareId<Object>,
phantom: PhantomData<&'a ()>
}
impl<'a> NSURL<'a> {
/// In cases where we're vended an `NSURL` by the system, this can be used to wrap and
/// retain it.
pub fn retain(object: id) -> Self {
NSURL {
objc: unsafe { ShareId::from_ptr(object) },
phantom: PhantomData
}
}
/// In some cases, we want to wrap a system-provided NSURL without retaining it.
pub fn from_retained(object: id) -> Self {
NSURL {
objc: unsafe { ShareId::from_retained_ptr(object) },
phantom: PhantomData
}
}
/// Creates and returns a URL object by calling through to `[NSURL URLWithString]`.
pub fn with_str(url: &str) -> Self {
let url = NSString::new(url);
Self {
objc: unsafe {
ShareId::from_ptr(msg_send![class!(NSURL), URLWithString:&*url])
},
phantom: PhantomData
}
}
/// Returns the absolute string path that this URL points to.
///
/// Note that if the underlying file moved, this won't be accurate - you likely want to
/// research URL bookmarks.
pub fn absolute_string(&self) -> String {
let abs_str = NSString::retain(unsafe {
msg_send![&*self.objc, absoluteString]
});
abs_str.to_string()
}
/// Creates and returns a Rust `PathBuf`, for users who don't need the extra pieces of NSURL
/// and just want to write Rust code.
pub fn pathbuf(&self) -> PathBuf {
let path = NSString::retain(unsafe {
msg_send![&*self.objc, path]
});
path.to_str().into()
}
/// Returns bookmark data for this URL. Will error if the underlying API errors.
///
/// Bookmarks are useful for sandboxed applications, as well as situations where you might want
/// to later resolve the true location of a file (e.g, if the user moved it between when you
/// got the URL and when you need to use it).
pub fn bookmark_data(
&self,
options: &[NSURLBookmarkCreationOption],
resource_value_keys: &[NSURLResourceKey],
relative_to_url: Option<NSURL>
) -> Result<NSData, Box<dyn Error>> {
let mut opts: NSUInteger = 0;
for mask in options {
let i: NSUInteger = mask.into();
opts = opts | i;
}
// Build NSArray of resource keys
let resource_keys = nil;
// Mutability woes mean we just go through a match here to satisfy message passing needs.
let bookmark_data = NSData::retain(match relative_to_url {
Some(relative_url) => unsafe {
msg_send![&*self.objc, bookmarkDataWithOptions:opts
includingResourceValuesForKeys:resource_keys
relativeToURL:relative_url
error:nil
]
},
None => unsafe {
msg_send![&*self.objc, bookmarkDataWithOptions:opts
includingResourceValuesForKeys:resource_keys
relativeToURL:nil
error:nil
]
}
});
// Check for errors...
//Err("LOL".into())
Ok(bookmark_data)
}
/// Converts bookmark data into a URL.
pub fn from_bookmark_data(
data: NSData,
options: &[NSURLBookmarkResolutionOption],
relative_to_url: Option<NSURL>,
data_is_stale: bool
) -> Result<Self, Box<dyn Error>> {
Err("LOL".into())
}
/// In an app that has adopted App Sandbox, makes the resource pointed to by a security-scoped URL available to the app.
///
/// More information can be found at:
/// [https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc]
pub fn start_accessing_security_scoped_resource(&self) {
unsafe {
let _: () = msg_send![&*self.objc, startAccessingSecurityScopedResource];
}
}
/// In an app that adopts App Sandbox, revokes access to the resource pointed to by a security-scoped URL.
///
/// More information can be found at:
/// [https://developer.apple.com/documentation/foundation/nsurl/1413736-stopaccessingsecurityscopedresou?language=objc]
pub fn stop_accessing_security_scoped_resource(&self) {
unsafe {
let _: () = msg_send![&*self.objc, stopAccessingSecurityScopedResource];
}
}
}
/*impl From<NSString<'_>> for id {
/// Consumes and returns the pointer to the underlying NSString instance.
fn from(mut string: NSString) -> Self {
&mut *string.objc
}
}*/
impl Deref for NSURL<'_> {
type Target = Object;
/// Derefs to the underlying Objective-C Object.
fn deref(&self) -> &Object {
&*self.objc
}
}

View file

@ -0,0 +1,170 @@
use crate::foundation::id;
/// Possible values for the `NSURLResourceKey::FileResourceType` key.
#[derive(Debug)]
pub enum NSURLFileResource {
/// The resource is a named pipe.
NamedPipe,
/// The resource is a character special file.
CharacterSpecial,
/// The resource is a directory.
Directory,
/// The resource is a block special file.
BlockSpecial,
/// The resource is a regular file.
Regular,
/// The resource is a symbolic link.
SymbolicLink,
/// The resource is a socket.
Socket,
/// The resources type is unknown.
Unknown
}
/// Values that describe the iCloud storage state of a file.
#[derive(Debug)]
pub enum NSUbiquitousItemDownloadingStatus {
/// A local copy of this item exists and is the most up-to-date version known to the device.
Current,
/// A local copy of this item exists, but it is stale. The most recent version will be downloaded as soon as possible.
Downloaded,
/// This item has not been downloaded yet. Initiate a download.
NotDownloaded
}
#[derive(Debug)]
pub enum NSURLResourceKey {
IsApplication,
IsScriptable,
IsDirectory,
ParentDirectoryURL,
FileAllocatedSize,
FileProtection,
FileProtectionType,
FileResourceIdentifier,
FileResourceType(NSURLFileResource),
FileSecurity,
FileSize,
IsAliasFile,
IsPackage,
IsRegularFile,
PreferredIOBlockSize,
TotalFileAllocatedSize,
TotalFileSize,
VolumeAvailableCapacity,
VolumeAvailableCapacityForImportantUsage,
VolumeAvailableCapacityForOpportunisticUsage,
VolumeTotalCapacity,
VolumeIsAutomounted,
VolumeIsBrowsable,
VolumeIsEjectable,
VolumeIsEncrypted,
VolumeIsInternal,
VolumeIsJournaling,
VolumeIsLocal,
VolumeIsReadOnly,
VolumeIsRemovable,
VolumeIsRootFileSystem,
IsMountTrigger,
IsVolume,
VolumeCreationDate,
VolumeIdentifier,
VolumeLocalizedFormatDescription,
VolumeLocalizedName,
VolumeMaximumFileSize,
VolumeName,
VolumeResourceCount,
VolumeSupportsAccessPermissions,
VolumeSupportsAdvisoryFileLocking,
VolumeSupportsCasePreservedNames,
VolumeSupportsCaseSensitiveNames,
VolumeSupportsCompression,
VolumeSupportsExclusiveRenaming,
VolumeSupportsExtendedSecurity,
VolumeSupportsFileCloning,
VolumeSupportsHardLinks,
VolumeSupportsImmutableFiles,
VolumeSupportsJournaling,
VolumeSupportsPersistentIDs,
VolumeSupportsRenaming,
VolumeSupportsRootDirectoryDates,
VolumeSupportsSparseFiles,
VolumeSupportsSwapRenaming,
VolumeSupportsSymbolicLinks,
VolumeSupportsVolumeSizes,
VolumeSupportsZeroRuns,
VolumeURLForRemounting,
VolumeURL,
VolumeUUIDString,
IsUbiquitousItem,
UbiquitousSharedItemMostRecentEditorNameComponents,
UbiquitousItemDownloadRequested,
UbiquitousItemIsDownloading,
UbiquitousItemDownloadingError,
UbiquitousItemDownloadingStatus(NSUbiquitousItemDownloadingStatus),
UbiquitousItemIsUploaded,
UbiquitousItemIsUploading,
UbiquitousItemUploadingError,
UbiquitousItemHasUnresolvedConflicts,
UbiquitousItemContainerDisplayName,
UbiquitousSharedItemOwnerNameComponents,
UbiquitousSharedItemCurrentUserPermissions,
UbiquitousSharedItemCurrentUserRole,
UbiquitousItemIsShared,
UbiquitousSharedItemRole,
UbiquitousSharedItemPermissions,
ThumbnailDictionaryItem,
KeysOfUnsetValues,
QuarantineProperties,
AddedToDirectoryDate,
AttributeModificationDate,
ContentAccessDate,
ContentModificationDate,
CreationDate,
CustomIcon,
DocumentIdentifier,
EffectiveIcon,
GenerationIdentifier,
HasHiddenExtension,
IsExcludedFromBackup,
IsExecutable,
IsHidden,
IsReadable,
IsSymbolicLink,
IsSystemImmutable,
IsUserImmutable,
IsWritable,
LabelColor,
LabelNumber,
LinkCount,
LocalizedLabel,
LocalizedName,
LocalizedTypeDescription,
Name,
Path,
CanonicalPath,
TagNames,
ContentType,
FileContentIdentifier,
IsPurgeable,
IsSparse,
MayHaveExtendedAttributes,
MayShareFileContent,
UbiquitousItemIsExcludedFromSync,
VolumeSupportsFileProtection
}

View file

@ -1,3 +1,5 @@
use crate::foundation::id;
/// These icons are system-provided icons that are guaranteed to exist in all versions of macOS /// These icons are system-provided icons that are guaranteed to exist in all versions of macOS
/// that Cacao supports. These will use SFSymbols on Big Sur and onwards (11.0+), and the correct /// that Cacao supports. These will use SFSymbols on Big Sur and onwards (11.0+), and the correct
/// controls for prior macOS versions. /// controls for prior macOS versions.
@ -22,51 +24,86 @@ pub enum MacSystemIcon {
/// Returns a stock "+" icon that's common to the system. Use this for buttons that need the /// Returns a stock "+" icon that's common to the system. Use this for buttons that need the
/// symbol. /// symbol.
Add Add,
/// A stock "-" icon that's common to the system. Use this for buttons that need the symbol.
Remove,
/// Returns a Folder icon.
Folder
}
extern "C" {
static NSImageNamePreferencesGeneral: id;
static NSImageNameAdvanced: id;
static NSImageNameUserAccounts: id;
static NSImageNameAddTemplate: id;
static NSImageNameFolder: id;
static NSImageNameRemoveTemplate: id;
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
impl MacSystemIcon { impl MacSystemIcon {
/// Maps system icons to their pre-11.0 framework identifiers. /// Maps system icons to their pre-11.0 framework identifiers.
pub fn to_str(&self) -> &'static str { pub fn to_id(&self) -> id {
match self { unsafe {
MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral", match self {
MacSystemIcon::PreferencesAdvanced => "NSAdvanced", MacSystemIcon::PreferencesGeneral => NSImageNamePreferencesGeneral,
MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts", MacSystemIcon::PreferencesAdvanced => NSImageNameAdvanced,
MacSystemIcon::Add => "NSImageNameAddTemplate" MacSystemIcon::PreferencesUserAccounts => NSImageNameUserAccounts,
MacSystemIcon::Add => NSImageNameAddTemplate,
MacSystemIcon::Remove => NSImageNameRemoveTemplate,
MacSystemIcon::Folder => NSImageNameFolder
}
} }
} }
/// Maps system icons to their SFSymbols-counterparts for use on 11.0+. /// Maps system icons to their SFSymbols-counterparts for use on 11.0+.
pub fn to_sfsymbol_str(&self) -> &'static str { pub fn to_sfsymbol_str(&self) -> &'static str {
match self { match self {
MacSystemIcon::PreferencesGeneral => "gearshape", MacSystemIcon::PreferencesGeneral => SFSymbol::GearShape.to_str(),
MacSystemIcon::PreferencesAdvanced => "slider.vertical.3", MacSystemIcon::PreferencesAdvanced => SFSymbol::SliderVertical3.to_str(),
MacSystemIcon::PreferencesUserAccounts => "at", MacSystemIcon::PreferencesUserAccounts => SFSymbol::AtSymbol.to_str(),
MacSystemIcon::Add => "plus" MacSystemIcon::Add => SFSymbol::Plus.to_str(),
MacSystemIcon::Remove => SFSymbol::Minus.to_str(),
MacSystemIcon::Folder => SFSymbol::FolderFilled.to_str()
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum SFSymbol { pub enum SFSymbol {
AtSymbol,
GearShape,
FolderFilled,
PaperPlane, PaperPlane,
PaperPlaneFilled, PaperPlaneFilled,
Plus,
Minus,
SliderVertical3,
SquareAndArrowUpOnSquare, SquareAndArrowUpOnSquare,
SquareAndArrowUpOnSquareFill, SquareAndArrowUpOnSquareFill,
SquareAndArrowDownOnSquare, SquareAndArrowDownOnSquare,
SquareAndArrowDownOnSquareFill SquareAndArrowDownOnSquareFill,
SquareDashed
} }
impl SFSymbol { impl SFSymbol {
pub fn to_str(&self) -> &str { pub fn to_str(&self) -> &'static str {
match self { match self {
Self::AtSymbol => "at",
Self::GearShape => "gearshape",
Self::FolderFilled => "folder.fill",
Self::PaperPlane => "paperplane", Self::PaperPlane => "paperplane",
Self::PaperPlaneFilled => "paperplane.fill", Self::PaperPlaneFilled => "paperplane.fill",
Self::Plus => "plus",
Self::Minus => "minus",
Self::SliderVertical3 => "slider.vertical.3",
Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square", Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square",
Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill", Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill",
Self::SquareAndArrowDownOnSquare => "square.and.arrow.down.on.square", Self::SquareAndArrowDownOnSquare => "square.and.arrow.down.on.square",
Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill" Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill",
Self::SquareDashed => "square.dashed"
} }
} }
} }

View file

@ -11,7 +11,7 @@ use core_graphics::{
}; };
use core_graphics::context::{CGContext, CGContextRef}; use core_graphics::context::{CGContext, CGContextRef};
use crate::foundation::{id, YES, NO, NSString}; use crate::foundation::{id, YES, NO, NSString, NSData};
use crate::utils::os; use crate::utils::os;
use super::icons::*; use super::icons::*;
@ -131,12 +131,53 @@ impl Image {
}) })
} }
/// Loads an image from the specified path.
pub fn with_contents_of_file(path: &str) -> Self {
let file_path = NSString::new(path);
Image(unsafe {
let alloc: id = msg_send![class!(NSImage), alloc];
ShareId::from_ptr(msg_send![alloc, initWithContentsOfFile:file_path])
})
}
/// Given a Vec of data, will transform it into an Image by passing it through NSData.
/// This can be useful for when you need to include_bytes!() something into your binary.
pub fn with_data(data: &[u8]) -> Self {
let data = NSData::with_slice(data);
Image(unsafe {
let alloc: id = msg_send![class!(NSImage), alloc];
ShareId::from_ptr(msg_send![alloc, initWithData:data])
})
}
// @TODO: for Airyx, unsure if this is supported - and it's somewhat modern macOS-specific, so // @TODO: for Airyx, unsure if this is supported - and it's somewhat modern macOS-specific, so
// let's keep the os flag here for now. // let's keep the os flag here for now.
/// Returns a stock system icon. These are guaranteed to exist across all versions of macOS /// Returns a stock system icon. These are guaranteed to exist across all versions of macOS
/// supported. /// supported.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn system_icon(icon: MacSystemIcon, accessibility_description: &str) -> Self { pub fn system_icon(icon: MacSystemIcon) -> Self {
Image(unsafe {
ShareId::from_ptr({
let icon = icon.to_id();
msg_send![class!(NSImage), imageNamed:icon]
})
})
}
// @TODO: for Airyx, unsure if this is supported - and it's somewhat modern macOS-specific, so
// let's keep the os flag here for now.
/// The name here can be confusing, I know.
///
/// A system symbol will swap an SFSymbol in for macOS 11.0+, but return the correct
/// MacSystemIcon image type for versions prior to that. This is mostly helpful in situations
/// like Preferences windows, where you want to have the correct modern styling for newer OS
/// versions.
///
/// However, if you need the correct "folder" icon for instance, you probably want `system_icon`.
#[cfg(target_os = "macos")]
pub fn system_symbol(icon: MacSystemIcon, accessibility_description: &str) -> Self {
Image(unsafe { Image(unsafe {
ShareId::from_ptr(match os::is_minimum_version(11) { ShareId::from_ptr(match os::is_minimum_version(11) {
true => { true => {
@ -147,8 +188,8 @@ impl Image {
}, },
false => { false => {
let icon = NSString::new(icon.to_str()); let icon = icon.to_id();
msg_send![class!(NSImage), imageNamed:&*icon] msg_send![class!(NSImage), imageNamed:icon]
} }
}) })
}) })

View file

@ -5,6 +5,7 @@ use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -147,23 +148,34 @@ impl ImageView {
}); });
} }
/// Given an image reference, sets it on the image view. You're currently responsible for
/// retaining this yourself.
pub fn set_image(&self, image: &Image) { pub fn set_image(&self, image: &Image) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setImage:&*image.0]; let _: () = msg_send![obj, setImage:&*image.0];
}); });
} }
/*pub fn set_image_scaling(&self, scaling_type: ImageScale) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setImageScaling:
});
}*/
} }
impl Layout for ImageView { impl ObjcAccess for ImageView {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl Layout for ImageView {}
impl Drop for ImageView { impl Drop for ImageView {
/// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// 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 /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if

View file

@ -45,8 +45,10 @@ use objc::{msg_send, sel, sel_impl};
use objc_id::ShareId; use objc_id::ShareId;
use crate::color::Color; use crate::color::Color;
use crate::control::Control;
use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NO, YES}; use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NO, YES};
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::text::{Font, TextAlign}; use crate::text::{Font, TextAlign};
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
@ -309,6 +311,15 @@ impl<T> TextField<T> {
}); });
} }
/// Call this to set the text for the label.
pub fn set_placeholder_text(&self, text: &str) {
let s = NSString::new(text);
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setPlaceholderString:&*s];
});
}
/// The the text alignment style for this control. /// The the text alignment style for this control.
pub fn set_text_alignment(&self, alignment: TextAlign) { pub fn set_text_alignment(&self, alignment: TextAlign) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
@ -317,6 +328,35 @@ impl<T> TextField<T> {
}); });
} }
/// Set whether this field operates in single-line mode.
pub fn set_uses_single_line(&self, uses_single_line: bool) {
self.objc.with_mut(|obj| unsafe {
let cell: id = msg_send![obj, cell];
let _: () = msg_send![cell, setUsesSingleLineMode:match uses_single_line {
true => YES,
false => NO
}];
});
}
/// Set whether this field operates in single-line mode.
pub fn set_wraps(&self, uses_single_line: bool) {
self.objc.with_mut(|obj| unsafe {
let cell: id = msg_send![obj, cell];
let _: () = msg_send![cell, setWraps:match uses_single_line {
true => YES,
false => NO
}];
});
}
/// Sets the maximum number of lines.
pub fn set_max_number_of_lines(&self, num: NSInteger) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setMaximumNumberOfLines:num];
});
}
/// Sets the font for this input. /// Sets the font for this input.
pub fn set_font<F: AsRef<Font>>(&self, font: F) { pub fn set_font<F: AsRef<Font>>(&self, font: F) {
let font = font.as_ref().clone(); let font = font.as_ref().clone();
@ -327,16 +367,20 @@ impl<T> TextField<T> {
} }
} }
impl<T> Layout for TextField<T> { impl<T> ObjcAccess for TextField<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl<T> Layout for TextField<T> {}
impl<T> Control for TextField<T> {}
impl<T> Drop for TextField<T> { impl<T> Drop for TextField<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `TextField` is being /// 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 /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if

18
src/keys.rs Normal file
View file

@ -0,0 +1,18 @@
//! This provides some basic mapping for providing Key characters to controls. It's mostly meant as
//! a wrapper to stop magic symbols all over the place.
/// Represents a Key character.
#[derive(Debug)]
pub enum Key<'a> {
/// Behind the scenes, this translates to NSDeleteCharacter (for AppKit).
Delete,
/// Whatever character you want.
Char(&'a str)
}
impl<'a> From<&'a str> for Key<'a> {
fn from(s: &'a str) -> Self {
Key::Char(s)
}
}

27
src/layout/animator.rs Normal file
View file

@ -0,0 +1,27 @@
use core_graphics::base::CGFloat;
use objc::{msg_send, sel, sel_impl};
use objc::runtime::{Class, Object};
use objc_id::ShareId;
use crate::foundation::id;
/// A wrapper for an animation proxy object in Cocoa that supports basic animations.
#[derive(Clone, Debug)]
pub struct LayoutConstraintAnimatorProxy(pub ShareId<Object>);
impl LayoutConstraintAnimatorProxy {
/// Wraps and returns a proxy for animation of layout constraint values.
pub fn new(proxy: id) -> Self {
Self(unsafe {
ShareId::from_ptr(msg_send![proxy, animator])
})
}
/// Sets the constant (usually referred to as `offset` in Cacao) value for the constraint being animated.
pub fn set_offset(&self, value: CGFloat) {
unsafe {
let _: () = msg_send![&*self.0, setConstant:value];
}
}
}

View file

@ -10,6 +10,8 @@ use objc_id::ShareId;
use crate::foundation::{id, YES, NO}; use crate::foundation::{id, YES, NO};
use super::LayoutConstraintAnimatorProxy;
/// A wrapper for `NSLayoutConstraint`. This both acts as a central path through which to activate /// A wrapper for `NSLayoutConstraint`. This both acts as a central path through which to activate
/// constraints, as well as a wrapper for layout constraints that are not axis bound (e.g, width or /// constraints, as well as a wrapper for layout constraints that are not axis bound (e.g, width or
/// height). /// height).
@ -27,16 +29,20 @@ pub struct LayoutConstraint {
/// The priority used in computing this constraint. /// The priority used in computing this constraint.
pub priority: f64, pub priority: f64,
/// An animator proxy that can be used inside animation contexts.
pub animator: LayoutConstraintAnimatorProxy
} }
impl LayoutConstraint { impl LayoutConstraint {
/// An internal method for wrapping existing constraints. /// An internal method for wrapping existing constraints.
pub(crate) fn new(object: id) -> Self { pub(crate) fn new(object: id) -> Self {
LayoutConstraint { LayoutConstraint {
animator: LayoutConstraintAnimatorProxy::new(object),
constraint: unsafe { ShareId::from_ptr(object) }, constraint: unsafe { ShareId::from_ptr(object) },
offset: 0.0, offset: 0.0,
multiplier: 0.0, multiplier: 0.0,
priority: 0.0 priority: 0.0,
} }
} }
@ -50,6 +56,7 @@ impl LayoutConstraint {
LayoutConstraint { LayoutConstraint {
animator: self.animator,
constraint: self.constraint, constraint: self.constraint,
offset: offset, offset: offset,
multiplier: self.multiplier, multiplier: self.multiplier,
@ -57,6 +64,7 @@ impl LayoutConstraint {
} }
} }
/// Sets the offset of a borrowed constraint.
pub fn set_offset<F: Into<f64>>(&self, offset: F) { pub fn set_offset<F: Into<f64>>(&self, offset: F) {
let offset: f64 = offset.into(); let offset: f64 = offset.into();

View file

@ -3,35 +3,44 @@
//! `AutoLayout` feature, each widget will default to using AutoLayout, which can be beneficial in //! `AutoLayout` feature, each widget will default to using AutoLayout, which can be beneficial in
//! more complicated views that need to deal with differing screen sizes. //! more complicated views that need to deal with differing screen sizes.
pub mod traits; mod traits;
pub use traits::Layout; pub use traits::Layout;
mod animator;
pub use animator::LayoutConstraintAnimatorProxy;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub mod attributes; mod attributes;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub use attributes::*; pub use attributes::*;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub mod constraint; mod constraint;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub use constraint::LayoutConstraint; pub use constraint::LayoutConstraint;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub mod dimension; mod dimension;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub use dimension::LayoutAnchorDimension; pub use dimension::LayoutAnchorDimension;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub mod horizontal; mod horizontal;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub use horizontal::LayoutAnchorX; pub use horizontal::LayoutAnchorX;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub mod vertical; mod vertical;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub use vertical::LayoutAnchorY; pub use vertical::LayoutAnchorY;
#[cfg(feature = "autolayout")]
mod safe_guide;
#[cfg(feature = "autolayout")]
pub use safe_guide::SafeAreaLayoutGuide;

67
src/layout/safe_guide.rs Normal file
View file

@ -0,0 +1,67 @@
use objc::{msg_send, sel, sel_impl};
use crate::foundation::id;
use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::utils::os;
/// A SafeAreaLayoutGuide should exist on all view types, and ensures that there are anchor points
/// that work within the system constraints. On macOS 11+, this will ensure you work around system
/// padding transprently - on macOS 10.15 and under, this will transparently map to the normal
/// edges, as the underlying properties were not supported there.
#[derive(Clone, Debug)]
pub struct SafeAreaLayoutGuide {
/// 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 left layout constraint.
pub left: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime right layout constraint.
pub right: 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 SafeAreaLayoutGuide {
/// Given a view pointer, will extract the safe area layout guide properties and return a
/// `SafeAreaLayoutGuide` composed of them.
pub fn new(view: id) -> Self {
// For versions prior to Big Sur, we'll just use the default view anchors in place.
let guide: id = match os::is_minimum_version(11) {
true => unsafe { msg_send![view, layoutMarginsGuide] },
false => view
};
Self {
top: LayoutAnchorY::top(guide),
left: LayoutAnchorX::left(guide),
leading: LayoutAnchorX::leading(guide),
right: LayoutAnchorX::right(guide),
trailing: LayoutAnchorX::trailing(guide),
bottom: LayoutAnchorY::bottom(guide),
width: LayoutAnchorDimension::width(guide),
height: LayoutAnchorDimension::height(guide),
center_x: LayoutAnchorX::center(guide),
center_y: LayoutAnchorY::center(guide)
}
}
}

View file

@ -1,6 +1,7 @@
//! Various traits related to controllers opting in to autolayout routines and support for view //! Various traits related to controllers opting in to autolayout routines and support for view
//! heirarchies. //! heirarchies.
use core_graphics::base::CGFloat;
use core_graphics::geometry::{CGRect, CGPoint, CGSize}; use core_graphics::geometry::{CGRect, CGPoint, CGSize};
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
@ -9,27 +10,21 @@ use objc_id::ShareId;
use crate::foundation::{id, nil, to_bool, YES, NO, NSArray, NSString}; use crate::foundation::{id, nil, to_bool, YES, NO, NSArray, NSString};
use crate::geometry::Rect; use crate::geometry::Rect;
use crate::objc_access::ObjcAccess;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
/// A trait that view wrappers must conform to. Enables managing the subview tree. /// A trait that view wrappers must conform to. Enables managing the subview tree.
#[allow(unused_variables)] #[allow(unused_variables)]
pub trait Layout { pub trait Layout: ObjcAccess {
/// Used for mutably interacting with the underlying Objective-C instance.
fn with_backing_node<F: Fn(id)>(&self, handler: F);
/// Used for checking backing properties of the underlying Objective-C instance, without
/// needing a mutable borrow.
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R;
/// Sets whether this needs to be redrawn before being displayed. /// Sets whether this needs to be redrawn before being displayed.
/// ///
/// If you're updating data that dynamically impacts this view, mark this as true - the next /// If you're updating data that dynamically impacts this view, mark this as true - the next
/// pass from the system will redraw it accordingly, and set the underlying value back to /// pass from the system will redraw it accordingly, and set the underlying value back to
/// `false`. /// `false`.
fn set_needs_display(&self, needs_display: bool) { fn set_needs_display(&self, needs_display: bool) {
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setNeedsDisplay:match needs_display { let _: () = msg_send![obj, setNeedsDisplay:match needs_display {
true => YES, true => YES,
false => NO false => NO
@ -39,8 +34,8 @@ pub trait Layout {
/// Adds another Layout-backed control or view as a subview of this view. /// Adds another Layout-backed control or view as a subview of this view.
fn add_subview<V: Layout>(&self, view: &V) { fn add_subview<V: Layout>(&self, view: &V) {
self.with_backing_node(|backing_node| { self.with_backing_obj_mut(|backing_node| {
view.with_backing_node(|subview_node| unsafe { view.with_backing_obj_mut(|subview_node| unsafe {
let _: () = msg_send![backing_node, addSubview:subview_node]; let _: () = msg_send![backing_node, addSubview:subview_node];
}); });
}); });
@ -48,7 +43,7 @@ pub trait Layout {
/// Removes a control or view from the superview. /// Removes a control or view from the superview.
fn remove_from_superview(&self) { fn remove_from_superview(&self) {
self.with_backing_node(|backing_node| unsafe { self.with_backing_obj_mut(|backing_node| unsafe {
let _: () = msg_send![backing_node, removeFromSuperview]; let _: () = msg_send![backing_node, removeFromSuperview];
}); });
} }
@ -61,7 +56,7 @@ pub trait Layout {
fn set_frame<R: Into<CGRect>>(&self, rect: R) { fn set_frame<R: Into<CGRect>>(&self, rect: R) {
let frame: CGRect = rect.into(); let frame: CGRect = rect.into();
self.with_backing_node(move |backing_node| unsafe { self.with_backing_obj_mut(move |backing_node| unsafe {
let _: () = msg_send![backing_node, setFrame:frame]; let _: () = msg_send![backing_node, setFrame:frame];
}); });
} }
@ -73,7 +68,7 @@ pub trait Layout {
/// then you should set this to `true` (or use an appropriate initializer that does it for you). /// then you should set this to `true` (or use an appropriate initializer that does it for you).
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
fn set_translates_autoresizing_mask_into_constraints(&self, translates: bool) { fn set_translates_autoresizing_mask_into_constraints(&self, translates: bool) {
self.with_backing_node(|backing_node| unsafe { self.with_backing_obj_mut(|backing_node| unsafe {
let _: () = msg_send![backing_node, setTranslatesAutoresizingMaskIntoConstraints:match translates { let _: () = msg_send![backing_node, setTranslatesAutoresizingMaskIntoConstraints:match translates {
true => YES, true => YES,
false => NO false => NO
@ -85,7 +80,7 @@ pub trait Layout {
/// ///
/// When hidden, widgets don't receive events and is not visible. /// When hidden, widgets don't receive events and is not visible.
fn set_hidden(&self, hide: bool) { fn set_hidden(&self, hide: bool) {
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setHidden:match hide { let _: () = msg_send![obj, setHidden:match hide {
true => YES, true => YES,
false => NO false => NO
@ -98,7 +93,7 @@ pub trait Layout {
/// Note that this can report `false` if an ancestor widget is hidden, thus hiding this - to check in /// Note that this can report `false` if an ancestor widget is hidden, thus hiding this - to check in
/// that case, you may want `is_hidden_or_ancestor_is_hidden()`. /// that case, you may want `is_hidden_or_ancestor_is_hidden()`.
fn is_hidden(&self) -> bool { fn is_hidden(&self) -> bool {
self.get_from_backing_node(|obj| { self.get_from_backing_obj(|obj| {
to_bool(unsafe { to_bool(unsafe {
msg_send![obj, isHidden] msg_send![obj, isHidden]
}) })
@ -108,7 +103,7 @@ pub trait Layout {
/// Returns whether this is hidden, *or* whether an ancestor view is hidden. /// Returns whether this is hidden, *or* whether an ancestor view is hidden.
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
fn is_hidden_or_ancestor_is_hidden(&self) -> bool { fn is_hidden_or_ancestor_is_hidden(&self) -> bool {
self.get_from_backing_node(|obj| { self.get_from_backing_obj(|obj| {
to_bool(unsafe { to_bool(unsafe {
msg_send![obj, isHiddenOrHasHiddenAncestor] msg_send![obj, isHiddenOrHasHiddenAncestor]
}) })
@ -126,7 +121,7 @@ pub trait Layout {
x.into() x.into()
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, registerForDraggedTypes:&*types]; let _: () = msg_send![obj, registerForDraggedTypes:&*types];
}); });
} }
@ -137,7 +132,7 @@ pub trait Layout {
/// currently to avoid compile issues. /// currently to avoid compile issues.
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
fn unregister_dragged_types(&self) { fn unregister_dragged_types(&self) {
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, unregisterDraggedTypes]; let _: () = msg_send![obj, unregisterDraggedTypes];
}); });
} }
@ -148,7 +143,7 @@ pub trait Layout {
/// can be helpful - but always test! /// can be helpful - but always test!
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
fn set_posts_frame_change_notifications(&self, posts: bool) { fn set_posts_frame_change_notifications(&self, posts: bool) {
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setPostsFrameChangedNotifications:match posts { let _: () = msg_send![obj, setPostsFrameChangedNotifications:match posts {
true => YES, true => YES,
false => NO false => NO
@ -162,11 +157,22 @@ pub trait Layout {
/// can be helpful - but always test! /// can be helpful - but always test!
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
fn set_posts_bounds_change_notifications(&self, posts: bool) { fn set_posts_bounds_change_notifications(&self, posts: bool) {
self.with_backing_node(|obj| unsafe { self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setPostsBoundsChangedNotifications:match posts { let _: () = msg_send![obj, setPostsBoundsChangedNotifications:match posts {
true => YES, true => YES,
false => NO false => NO
}]; }];
}); });
} }
/// Theoretically this belongs elsewhere, but we want to enable this on all view layers, since
/// it's common enough anyway.
#[cfg(feature = "appkit")]
fn set_alpha(&self, value: f64) {
let value: CGFloat = value.into();
self.with_backing_obj_mut(|obj| unsafe {
let _: () = msg_send![obj, setAlphaValue:value];
});
}
} }

View file

@ -104,6 +104,8 @@ pub use lazy_static;
#[cfg_attr(docsrs, doc(cfg(feature = "appkit")))] #[cfg_attr(docsrs, doc(cfg(feature = "appkit")))]
pub mod appkit; pub mod appkit;
//pub mod bundle;
#[cfg(feature = "uikit")] #[cfg(feature = "uikit")]
#[cfg_attr(docsrs, doc(cfg(feature = "uikit")))] #[cfg_attr(docsrs, doc(cfg(feature = "uikit")))]
pub mod uikit; pub mod uikit;
@ -117,6 +119,9 @@ pub mod cloudkit;
pub mod color; pub mod color;
#[cfg(feature = "appkit")]
pub mod control;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod dragdrop; pub mod dragdrop;
@ -138,14 +143,17 @@ pub mod image;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod input; pub mod input;
pub(crate) mod invoker;
pub mod keys;
pub mod layer; pub mod layer;
pub(crate) mod invoker;
pub mod layout; pub mod layout;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod listview; pub mod listview;
pub mod networking; pub mod networking;
pub(crate) mod objc_access;
pub mod notification_center; pub mod notification_center;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
@ -160,6 +168,9 @@ pub mod scrollview;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod switch; pub mod switch;
#[cfg(feature = "appkit")]
pub mod select;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod text; pub mod text;

View file

@ -37,9 +37,16 @@ extern fn view_for_column<T: ListViewDelegate>(
this: &Object, this: &Object,
_: Sel, _: Sel,
_table_view: id, _table_view: id,
_: id, _table_column: id,
item: NSInteger item: NSInteger
) -> id { ) -> id {
/*use core_graphics::geometry::CGRect;
unsafe {
//let superview: id = msg_send![table_view, superview];
let frame: CGRect = msg_send![table_view, frame];
let _: () = msg_send![table_column, setWidth:frame.size.width];
}*/
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR); let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
let item = view.item_for(item as usize); let item = view.item_for(item as usize);
@ -79,7 +86,7 @@ extern fn menu_needs_update<T: ListViewDelegate>(
let _ = Menu::append(menu, items); let _ = Menu::append(menu, items);
} }
/// NSTableView requires listening to an observer to detect row selection changes, but that is... /*/// NSTableView requires listening to an observer to detect row selection changes, but that is...
/// even clunkier than what we do in this framework. /// even clunkier than what we do in this framework.
/// ///
/// The other less obvious way is to subclass and override the `shouldSelectRow:` method; here, we /// The other less obvious way is to subclass and override the `shouldSelectRow:` method; here, we
@ -94,6 +101,24 @@ extern fn select_row<T: ListViewDelegate>(
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR); let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.item_selected(item as usize); view.item_selected(item as usize);
YES YES
}*/
extern fn selection_did_change<T: ListViewDelegate>(
this: &Object,
_: Sel,
notification: id
) {
let selected_row: NSInteger = unsafe {
let tableview: id = msg_send![notification, object];
msg_send![tableview, selectedRow]
};
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
if selected_row == -1 {
view.item_selected(None);
} else {
view.item_selected(Some(selected_row as usize));
}
} }
extern fn row_actions_for_row<T: ListViewDelegate>( extern fn row_actions_for_row<T: ListViewDelegate>(
@ -203,7 +228,7 @@ pub(crate) fn register_listview_class_with_delegate<T: ListViewDelegate>(instanc
decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger); decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger);
decl.add_method(sel!(tableView:willDisplayCell:forTableColumn:row:), will_display_cell::<T> as extern fn(&Object, _, id, id, id, NSInteger)); decl.add_method(sel!(tableView:willDisplayCell:forTableColumn:row:), will_display_cell::<T> as extern fn(&Object, _, id, id, id, NSInteger));
decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id); decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id);
decl.add_method(sel!(tableView:shouldSelectRow:), select_row::<T> as extern fn(&Object, _, id, NSInteger) -> BOOL); decl.add_method(sel!(tableViewSelectionDidChange:), selection_did_change::<T> as extern fn(&Object, _, id));
decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row::<T> as extern fn(&Object, _, id, NSInteger, NSInteger) -> id); decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row::<T> as extern fn(&Object, _, id, NSInteger, NSInteger) -> id);
// A slot for some menu handling; we just let it be done here for now rather than do the // A slot for some menu handling; we just let it be done here for now rather than do the

View file

@ -50,16 +50,16 @@ use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSInteger, NSUInteger}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSInteger, NSUInteger};
use crate::color::Color; use crate::color::Color;
use crate::layout::Layout; use crate::layout::Layout;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::objc_access::ObjcAccess;
use crate::scrollview::ScrollView; use crate::scrollview::ScrollView;
use crate::utils::{os, CellFactory, CGSize}; use crate::utils::{os, CellFactory, CGSize};
use crate::utils::properties::{ObjcProperty, PropertyNullable}; use crate::utils::properties::{ObjcProperty, PropertyNullable};
use crate::view::ViewDelegate; use crate::view::{ViewAnimatorProxy, ViewDelegate};
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use crate::appkit::menu::MenuItem; use crate::appkit::menu::MenuItem;
@ -99,11 +99,10 @@ use std::cell::RefCell;
/// A helper method for instantiating view classes and applying default settings to them. /// A helper method for instantiating view classes and applying default settings to them.
fn common_init(class: *const Class) -> id { fn common_init(class: *const Class) -> id {
unsafe { unsafe {
// Note: we do *not* enable AutoLayout here as we're by default placing this in a scroll
// view, and we want it to just do its thing.
let tableview: id = msg_send![class, new]; let tableview: id = msg_send![class, new];
#[cfg(feature = "autolayout")]
let _: () = msg_send![tableview, setTranslatesAutoresizingMaskIntoConstraints:NO];
// Let's... make NSTableView into UITableView-ish. // Let's... make NSTableView into UITableView-ish.
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
{ {
@ -144,6 +143,9 @@ pub struct ListView<T = ()> {
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ObjcProperty, pub objc: ObjcProperty,
/// An object that supports limited animations. Can be cloned into animation closures.
pub animator: ViewAnimatorProxy,
/// In AppKit, we need to manage the NSScrollView ourselves. It's a bit /// In AppKit, we need to manage the NSScrollView ourselves. It's a bit
/// more old school like that... /// more old school like that...
/// ///
@ -262,6 +264,11 @@ impl ListView {
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(anchor_view), center_y: LayoutAnchorY::center(anchor_view),
// Note that AppKit needs this to be the ScrollView!
// @TODO: Figure out if there's a use case for exposing the inner tableview animator
// property...
animator: ViewAnimatorProxy::new(anchor_view),
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
scrollview scrollview
@ -310,6 +317,7 @@ impl<T> ListView<T> where T: ListViewDelegate + 'static {
menu: PropertyNullable::default(), menu: PropertyNullable::default(),
delegate: None, delegate: None,
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
animator: ViewAnimatorProxy::new(anchor_view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(anchor_view), top: LayoutAnchorY::top(anchor_view),
@ -355,12 +363,13 @@ impl<T> ListView<T> {
/// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to /// 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 /// 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. /// delegate - the `View` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> ListView { pub fn clone_as_handle(&self) -> ListView {
ListView { ListView {
cell_factory: CellFactory::new(), cell_factory: CellFactory::new(),
menu: self.menu.clone(), menu: self.menu.clone(),
delegate: None, delegate: None,
objc: self.objc.clone(), objc: self.objc.clone(),
animator: self.animator.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: self.top.clone(), top: self.top.clone(),
@ -490,6 +499,34 @@ impl<T> ListView<T> {
} }
} }
/// This hack exists to avoid a bug with how Rust's model isn't really friendly with more
/// old-school GUI models. The tl;dr is that we unfortunately have to cheat a bit to gracefully
/// handle two conditions.
///
/// The gist of it is that there are two situations (`perform_batch_updates` and `insert_rows`)
/// where we call over to the list view to, well, perform updates. This causes the internal
/// machinery of AppKit to call to the delegate, and the delegate then - rightfully - calls to
/// dequeue a cell.
///
/// The problem is then that dequeue'ing a cell requires borrowing the underlying cell handler,
/// per Rust's model. We haven't been able to drop our existing lock though! Thus it winds up
/// panic'ing and all hell breaks loose.
///
/// For now, we just drop to Objective-C and message pass directly to avoid a
/// double-locking-attempt on the Rust side of things. This is explicitly not ideal, and if
/// you're reading this and rightfully going "WTF?", I encourage you to contribute a solution
/// if you can come up with one.
///
/// In practice, this hack isn't that bad - at least, no worse than existing Objective-C code.
/// The behavior is relatively well understood and documented in the above paragraph, so I'm
/// comfortable with the hack for now.
///
/// To be ultra-clear: the hack is that we don't `borrow_mut` before sending a message. It just
/// feels dirty, hence the novel. ;P
fn hack_avoid_dequeue_loop<F: Fn(&Object)>(&self, handler: F) {
self.objc.get(handler);
}
/// This method should be used when inserting or removing multiple rows at once. Under the /// This method should be used when inserting or removing multiple rows at once. Under the
/// hood, it batches the changes and tries to ensure things are done properly. The provided /// hood, it batches the changes and tries to ensure things are done properly. The provided
/// `ListView` for the handler is your `ListView`, and you can call `insert_rows`, /// `ListView` for the handler is your `ListView`, and you can call `insert_rows`,
@ -511,14 +548,9 @@ impl<T> ListView<T> {
let handle = self.clone_as_handle(); let handle = self.clone_as_handle();
update(handle); update(handle);
// This is cheating, but there's no good way around it at the moment. If we (mutably) lock in // This is done for a very explicit reason; see the comments on the method itself for
// Rust here, firing this call will loop back around into `dequeue`, which will then // an explanation.
// hit a double lock. self.hack_avoid_dequeue_loop(|obj| unsafe {
//
// Personally, I can live with this - `endUpdates` is effectively just flushing the
// already added updates, so with this small hack here we're able to keep the mutable
// borrow structure everywhere else, which feels "correct".
self.objc.get(|obj| unsafe {
let _: () = msg_send![obj, endUpdates]; let _: () = msg_send![obj, endUpdates];
}); });
} }
@ -545,7 +577,9 @@ impl<T> ListView<T> {
// has also retained it. // has also retained it.
let x = ShareId::from_ptr(index_set); let x = ShareId::from_ptr(index_set);
self.objc.with_mut(|obj| { // This is done for a very explicit reason; see the comments on the method itself for
// an explanation.
self.hack_avoid_dequeue_loop(|obj| {
let _: () = msg_send![obj, insertRowsAtIndexes:&*x withAnimation:animation_options]; let _: () = msg_send![obj, insertRowsAtIndexes:&*x withAnimation:animation_options];
}); });
} }
@ -647,6 +681,15 @@ impl<T> ListView<T> {
}); });
} }
/// Makes this table view the first responder.
#[cfg(feature = "appkit")]
pub fn make_first_responder(&self) {
self.objc.with_mut(|obj| unsafe {
let window: id = msg_send![&*obj, window];
let _: () = msg_send![window, makeFirstResponder:&*obj];
});
}
/// Reloads the underlying ListView. This is more expensive than handling insert/reload/remove /// Reloads the underlying ListView. This is more expensive than handling insert/reload/remove
/// calls yourself, but often easier to implement. /// calls yourself, but often easier to implement.
/// ///
@ -689,15 +732,15 @@ impl<T> ListView<T> {
} }
} }
impl<T> Layout for ListView<T> { impl<T> ObjcAccess for ListView<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
// In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know
// what to do normally. // what to do normally.
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
self.scrollview.objc.with_mut(handler); self.scrollview.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
// In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know
// what to do normally. // what to do normally.
// //
@ -708,6 +751,8 @@ impl<T> Layout for ListView<T> {
} }
} }
impl<T> Layout for ListView<T> {}
impl<T> Drop for ListView<T> { impl<T> Drop for ListView<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// 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 /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if

View file

@ -52,11 +52,12 @@ use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layer::Layer; use crate::layer::Layer;
use crate::layout::Layout; use crate::layout::Layout;
use crate::view::ViewDelegate; use crate::objc_access::ObjcAccess;
use crate::view::{ViewAnimatorProxy, ViewDelegate};
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension, SafeAreaLayoutGuide};
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
mod appkit; mod appkit;
@ -93,12 +94,19 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id {
/// side anyway. /// side anyway.
#[derive(Debug)] #[derive(Debug)]
pub struct ListViewRow<T = ()> { pub struct ListViewRow<T = ()> {
/// An object that supports limited animations. Can be cloned into animation closures.
pub animator: ViewAnimatorProxy,
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ObjcProperty, pub objc: ObjcProperty,
/// A pointer to the delegate for this view. /// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>, pub delegate: Option<Box<T>>,
/// A safe layout guide property.
#[cfg(feature = "autolayout")]
pub safe_layout_guide: SafeAreaLayoutGuide,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub top: LayoutAnchorY, pub top: LayoutAnchorY,
@ -154,6 +162,10 @@ impl ListViewRow {
ListViewRow { ListViewRow {
delegate: None, delegate: None,
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")]
safe_layout_guide: SafeAreaLayoutGuide::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
@ -211,6 +223,10 @@ impl<T> ListViewRow<T> where T: ViewDelegate + 'static {
let view = ListViewRow { let view = ListViewRow {
delegate: Some(delegate), delegate: Some(delegate),
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")]
safe_layout_guide: SafeAreaLayoutGuide::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
@ -263,6 +279,10 @@ impl<T> ListViewRow<T> where T: ViewDelegate + 'static {
let mut view = ListViewRow { let mut view = ListViewRow {
delegate: None, delegate: None,
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")]
safe_layout_guide: SafeAreaLayoutGuide::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
@ -311,6 +331,10 @@ impl<T> ListViewRow<T> where T: ViewDelegate + 'static {
ListViewRow { ListViewRow {
delegate: None, delegate: None,
objc: self.objc.clone(), objc: self.objc.clone(),
animator: self.animator.clone(),
#[cfg(feature = "autolayout")]
safe_layout_guide: self.safe_layout_guide.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: self.top.clone(), top: self.top.clone(),
@ -354,8 +378,12 @@ impl<T> ListViewRow<T> {
crate::view::View { crate::view::View {
delegate: None, delegate: None,
is_handle: true, is_handle: true,
layer: Layer::new(), layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row.
objc: self.objc.clone(), objc: self.objc.clone(),
animator: self.animator.clone(),
#[cfg(feature = "autolayout")]
safe_layout_guide: self.safe_layout_guide.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: self.top.clone(), top: self.top.clone(),
@ -408,16 +436,18 @@ impl<T> ListViewRow<T> {
} }
} }
impl<T> Layout for ListViewRow<T> { impl<T> ObjcAccess for ListViewRow<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl<T> Layout for ListViewRow<T> {}
impl<T> Drop for ListViewRow<T> { impl<T> Drop for ListViewRow<T> {
fn drop(&mut self) { fn drop(&mut self) {
} }

View file

@ -36,8 +36,9 @@ pub trait ListViewDelegate {
/// had time to sit down and figure them out properly yet. /// had time to sit down and figure them out properly yet.
fn item_for(&self, row: usize) -> ListViewRow; fn item_for(&self, row: usize) -> ListViewRow;
/// Called when an item has been selected (clicked/tapped on). /// Called when an item has been selected (clicked/tapped on). If the selection was cleared,
fn item_selected(&self, row: usize) {} /// then this will be called with `None`.
fn item_selected(&self, row: Option<usize>) {}
/// Called when the menu for the tableview is about to be shown. You can update the menu here /// Called when the menu for the tableview is about to be shown. You can update the menu here
/// depending on, say, what the user has context-clicked on. You should avoid any expensive /// depending on, say, what the user has context-clicked on. You should avoid any expensive

23
src/objc_access.rs Normal file
View file

@ -0,0 +1,23 @@
//! Implements a parent trait for the various sub-traits we use throughout Cacao. The methods
//! defined on here provide access handlers for common properties that the sub-traits need to
//! enable modifying.
use objc::runtime::Object;
use crate::foundation::id;
/// Types that implement this should provide access to their underlying root node type (e.g, the
/// view or control). Traits that have this as their super-trait can rely on this to ensure access
/// without needing to derive or do extra work elsewhere.
#[allow(unused_variables)]
pub trait ObjcAccess {
/// Used for mutably interacting with the underlying Objective-C instance.
/// Setters should use this.
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F);
/// Used for checking backing properties of the underlying Objective-C instance, without
/// needing a mutable borrow.
///
/// Getters should use this.
fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R;
}

View file

@ -20,7 +20,7 @@ use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId; use objc_id::ShareId;
use url::Url; use url::Url;
use crate::foundation::{id, nil, NSString, NSArray}; use crate::foundation::{id, nil, NSString, NSArray, NSURL};
use crate::error::Error; use crate::error::Error;
mod types; mod types;
@ -93,7 +93,7 @@ impl Pasteboard {
/// _Note that this method returns a list of `Url` entities, in an attempt to be closer to how /// _Note that this method returns a list of `Url` entities, in an attempt to be closer to how
/// Cocoa & co operate. This method may go away in the future if it's determined that people /// Cocoa & co operate. This method may go away in the future if it's determined that people
/// wind up just using `get_file_paths()`._ /// wind up just using `get_file_paths()`._
pub fn get_file_urls(&self) -> Result<Vec<Url>, Box<dyn std::error::Error>> { pub fn get_file_urls(&self) -> Result<Vec<NSURL>, Box<dyn std::error::Error>> {
unsafe { unsafe {
let class: id = msg_send![class!(NSURL), class]; let class: id = msg_send![class!(NSURL), class];
let classes = NSArray::new(&[class]); let classes = NSArray::new(&[class]);
@ -113,40 +113,7 @@ impl Pasteboard {
} }
let urls = NSArray::retain(contents).map(|url| { let urls = NSArray::retain(contents).map(|url| {
let path = NSString::retain(msg_send![url, path]); NSURL::retain(url)
Url::parse(&format!("file://{}", path.to_str()))
}).into_iter().filter_map(|r| r.ok()).collect();
Ok(urls)
}
}
/// Looks inside the pasteboard contents and extracts what FileURLs are there, if any.
///
/// Note that this method operates on file paths, as opposed to URLs, and returns a list of
/// results in a format more Rust-y.
pub fn get_file_paths(&self) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
unsafe {
let class: id = msg_send![class!(NSURL), class];
let classes = NSArray::new(&[class]);
let contents: id = msg_send![&*self.0, readObjectsForClasses:classes options:nil];
// This can happen if the Pasteboard server has an error in returning items.
// In our case, we'll bubble up an error by checking the pasteboard.
if contents == nil {
// This error is not necessarily "correct", but in the event of an error in
// Pasteboard server retrieval I'm not sure where to check... and this stuff is
// kinda ancient and has conflicting docs in places. ;P
return Err(Box::new(Error {
code: 666,
domain: "com.cacao-rs.pasteboard".to_string(),
description: "Pasteboard server returned no data.".to_string()
}));
}
let urls = NSArray::retain(contents).map(|url| {
let path = NSString::retain(msg_send![url, path]).to_str().to_string();
PathBuf::from(path)
}).into_iter().collect(); }).into_iter().collect();
Ok(urls) Ok(urls)

View file

@ -20,6 +20,7 @@ use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSUInteger}; use crate::foundation::{id, nil, YES, NO, NSUInteger};
use crate::color::Color; use crate::color::Color;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -201,16 +202,18 @@ impl ProgressIndicator {
} }
} }
impl Layout for ProgressIndicator { impl ObjcAccess for ProgressIndicator {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl Layout for ProgressIndicator {}
impl Drop for ProgressIndicator { impl Drop for ProgressIndicator {
/// A bit of extra cleanup for delegate callback pointers. /// A bit of extra cleanup for delegate callback pointers.
/// If the originating `ProgressIndicator` is being /// If the originating `ProgressIndicator` is being

View file

@ -83,7 +83,7 @@ impl Default for ThumbnailConfig {
// #TODO: Should query the current screen size maybe? 2x is fairly safe // #TODO: Should query the current screen size maybe? 2x is fairly safe
// for most moderns Macs right now. // for most moderns Macs right now.
scale: 2., scale: 1.,
minimum_dimension: 0., minimum_dimension: 0.,

View file

@ -1,6 +1,6 @@
use std::path::Path; use std::path::Path;
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::ShareId; use objc_id::ShareId;
@ -17,13 +17,19 @@ pub use config::{ThumbnailConfig, ThumbnailQuality};
pub struct ThumbnailGenerator(pub ShareId<Object>); pub struct ThumbnailGenerator(pub ShareId<Object>);
impl ThumbnailGenerator { impl ThumbnailGenerator {
/// Returns the global shared, wrapped, QLThumbnailGenerator.
pub fn shared() -> Self { pub fn shared() -> Self {
ThumbnailGenerator(unsafe { ThumbnailGenerator(unsafe {
ShareId::from_ptr(msg_send![class!(QLThumbnailGenerator), sharedGenerator]) ShareId::from_ptr(msg_send![class!(QLThumbnailGenerator), sharedGenerator])
}) })
} }
pub fn generate<F>(&self, path: &Path, config: ThumbnailConfig, callback: F) /// Given a path and config, will generate a preview image, calling back on the provided
/// callback closure.
///
/// Note that this callback can come back on a separate thread, so react accordingly to get to
/// the main thread if you need to.
pub fn generate_from_path<F>(&self, path: &Path, config: ThumbnailConfig, callback: F)
where where
F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static
{ {

View file

@ -47,8 +47,8 @@ use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
@ -300,16 +300,18 @@ impl<T> ScrollView<T> {
} }
} }
impl<T> Layout for ScrollView<T> { impl<T> ObjcAccess for ScrollView<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl<T> Layout for ScrollView<T> {}
impl<T> Drop for ScrollView<T> { impl<T> Drop for ScrollView<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// 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 /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if

272
src/select/mod.rs Normal file
View file

@ -0,0 +1,272 @@
//! Implements a Select-style dropdown. By default this uses NSPopupSelect on macOS.
use std::sync::Once;
use core_graphics::geometry::CGRect;
use objc_id::ShareId;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use crate::control::Control;
use crate::foundation::{id, nil, YES, NO, NSString, NSInteger};
use crate::invoker::TargetActionHandler;
use crate::geometry::Rect;
use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
/// Wraps `NSPopUpSelect` on AppKit. Not currently implemented for iOS.
///
/// Acts like a `<select>` dropdown, if you're familiar with HTML. Use for dropdown option
/// selecting.
///
/// Some properties are platform-specific; see the documentation for further information.
///
/// ```rust,no_run
/// let mut dropdown = Select::new();
///
/// // Make sure you don't let your Select drop for as long as you need it.
/// my_view.add_subview(&dropdown);
/// ```
#[derive(Debug)]
pub struct Select {
/// A handle for the underlying Objective-C object.
pub objc: ObjcProperty,
handler: Option<TargetActionHandler>,
/// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")]
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
#[cfg(feature = "autolayout")]
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime left layout constraint.
#[cfg(feature = "autolayout")]
pub left: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
#[cfg(feature = "autolayout")]
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime right layout constraint.
#[cfg(feature = "autolayout")]
pub right: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
#[cfg(feature = "autolayout")]
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
#[cfg(feature = "autolayout")]
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
#[cfg(feature = "autolayout")]
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
#[cfg(feature = "autolayout")]
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
#[cfg(feature = "autolayout")]
pub center_y: LayoutAnchorY
}
impl Select {
/// Creates a new `Select` instance, configures it appropriately,
/// and retains the necessary Objective-C runtime pointer.
pub fn new() -> Self {
let zero: CGRect = Rect::zero().into();
let view: id = unsafe {
let alloc: id = msg_send![register_class(), alloc];
let select: id = msg_send![alloc, initWithFrame:zero pullsDown:NO];
#[cfg(feature = "autolayout")]
let _: () = msg_send![select, setTranslatesAutoresizingMaskIntoConstraints:NO];
select
};
Select {
handler: None,
#[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view),
#[cfg(feature = "autolayout")]
left: LayoutAnchorX::left(view),
#[cfg(feature = "autolayout")]
leading: LayoutAnchorX::leading(view),
#[cfg(feature = "autolayout")]
right: LayoutAnchorX::right(view),
#[cfg(feature = "autolayout")]
trailing: LayoutAnchorX::trailing(view),
#[cfg(feature = "autolayout")]
bottom: LayoutAnchorY::bottom(view),
#[cfg(feature = "autolayout")]
width: LayoutAnchorDimension::width(view),
#[cfg(feature = "autolayout")]
height: LayoutAnchorDimension::height(view),
#[cfg(feature = "autolayout")]
center_x: LayoutAnchorX::center(view),
#[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(view),
objc: ObjcProperty::retain(view),
}
}
/// Attaches a callback for selection events.
/// Much like `Button`, this really needs to be revisited.
///
/// Really, this is not ideal.
///
/// I cannot stress this enough.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) {
// @TODO: This probably isn't ideal but gets the job done for now; needs revisiting.
let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) });
let handler = TargetActionHandler::new(&*this, action);
self.handler = Some(handler);
}
/// Sets whether this pulls down (dropdown) or pops up.
pub fn set_pulls_down(&self, pulls_down: bool) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setPullsDown:match pulls_down {
true => YES,
false => NO
}];
});
}
/// Adds an item to the dropdown list.
pub fn add_item(&self, title: &str) {
self.objc.with_mut(|obj| unsafe {
let s = NSString::new(title);
let _: () = msg_send![obj, addItemWithTitle:s];
});
}
/// Removes all items from the dropdown list.
pub fn remove_all_items(&self) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, removeAllItems];
});
}
/// Remove the item at the specified index.
pub fn remove_item_at_index(&self, index: usize) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, removeItemAtIndex:index];
});
}
pub fn set_selected_index(&self, index: NSInteger) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, selectItemAtIndex:index];
});
}
/// Gets the selected index.
pub fn get_selected_index(&self) -> usize {
self.objc.get(|obj| unsafe {
let index: NSInteger = msg_send![obj, indexOfSelectedItem];
index as usize
})
}
/// Returns the number of items in the dropdown.
pub fn len(&self) -> usize {
self.objc.get(|obj| unsafe {
let index: NSInteger = msg_send![obj, numberOfItems];
index as usize
})
}
}
impl ObjcAccess for Select {
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler);
}
fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler)
}
}
impl Layout for Select {
fn add_subview<V: Layout>(&self, _view: &V) {
panic!(r#"
Tried to add a subview to a Select. This is not allowed in Cacao. If you think this should be supported,
open a discussion on the GitHub repo.
"#);
}
}
impl Control for Select {}
impl ObjcAccess for &Select {
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler);
}
fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler)
}
}
impl Layout for &Select {
fn add_subview<V: Layout>(&self, _view: &V) {
panic!(r#"
Tried to add a subview to a Select. This is not allowed in Cacao. If you think this should be supported,
open a discussion on the GitHub repo.
"#);
}
}
impl Control for &Select {}
impl Drop for Select {
/// Nils out references on the Objective-C side and removes this from the backing view.
// 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) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setTarget:nil];
let _: () = msg_send![obj, setAction:nil];
});
}
}
/// Registers an `NSSelect` subclass, and configures it to hold some ivars
/// for various things we need to store.
fn register_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!(NSPopUpButton);
let decl = ClassDecl::new("CacaoSelect", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -12,6 +12,7 @@ use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, nil, BOOL, YES, NO, NSString}; use crate::foundation::{id, nil, BOOL, YES, NO, NSString};
use crate::invoker::TargetActionHandler; use crate::invoker::TargetActionHandler;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::utils::{load, properties::ObjcProperty}; use crate::utils::{load, properties::ObjcProperty};
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -143,15 +144,17 @@ impl Switch {
} }
} }
impl Layout for Switch { impl ObjcAccess for Switch {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
}
impl Layout for Switch {
fn add_subview<V: Layout>(&self, _view: &V) { fn add_subview<V: Layout>(&self, _view: &V) {
panic!(r#" panic!(r#"
Tried to add a subview to a Switch. This is not allowed in Cacao. If you think this should be supported, Tried to add a subview to a Switch. This is not allowed in Cacao. If you think this should be supported,

View file

@ -12,8 +12,11 @@ use objc_id::Id;
use crate::color::Color; use crate::color::Color;
use crate::foundation::{id, to_bool, BOOL, YES, NO, NSString}; use crate::foundation::{id, to_bool, BOOL, YES, NO, NSString};
use super::Font;
extern "C" { extern "C" {
static NSForegroundColorAttributeName: id; static NSForegroundColorAttributeName: id;
static NSFontAttributeName: id;
} }
/// A wrapper around `NSMutableAttributedString`, which can be used for more complex text /// A wrapper around `NSMutableAttributedString`, which can be used for more complex text
@ -54,6 +57,16 @@ impl AttributedString {
]; ];
} }
} }
/// Set the font for the specified range.
pub fn set_font(&mut self, font: Font, range: Range<isize>) {
unsafe {
let _: () = msg_send![&*self.0, addAttribute:NSFontAttributeName
value:&*font
range:range
];
}
}
} }
impl fmt::Display for AttributedString { impl fmt::Display for AttributedString {

View file

@ -47,7 +47,8 @@ use objc::{msg_send, sel, sel_impl};
use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSUInteger, NSString}; use crate::foundation::{id, nil, YES, NO, NSArray, NSInteger, NSUInteger, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layout::Layout; use crate::layout::Layout;
use crate::text::{Font, TextAlign, LineBreakMode}; use crate::objc_access::ObjcAccess;
use crate::text::{AttributedString, Font, TextAlign, LineBreakMode};
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -77,7 +78,12 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id {
let view: id = { let view: id = {
// This sucks, but for now, sure. // This sucks, but for now, sure.
let blank = NSString::no_copy(""); let blank = NSString::no_copy("");
msg_send![registration_fn(), labelWithString:&*blank] let label: id = msg_send![registration_fn(), wrappingLabelWithString:&*blank];
// We sub this in to get the general expected behavior for 202*.
let _: () = msg_send![label, setSelectable:NO];
label
}; };
#[cfg(feature = "uikit")] #[cfg(feature = "uikit")]
@ -361,6 +367,13 @@ impl<T> Label<T> {
}); });
} }
/// Sets the attributed string to be the attributed string value on this label.
pub fn set_attributed_text(&self, text: AttributedString) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setAttributedStringValue:&*text];
});
}
/// Retrieve the text currently held in the label. /// Retrieve the text currently held in the label.
pub fn get_text(&self) -> String { pub fn get_text(&self) -> String {
self.objc.get(|obj| unsafe { self.objc.get(|obj| unsafe {
@ -416,16 +429,18 @@ impl<T> Label<T> {
} }
} }
impl<T> Layout for Label<T> { impl<T> ObjcAccess for Label<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl<T> Layout for Label<T> {}
impl<T> Drop for Label<T> { impl<T> Drop for Label<T> {
/// A bit of extra cleanup for delegate callback pointers. If the originating `Label` is being /// 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 /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if

View file

@ -2,6 +2,7 @@
use crate::foundation::NSUInteger; use crate::foundation::NSUInteger;
#[derive(Debug)]
pub enum NotificationAuthOption { pub enum NotificationAuthOption {
Badge, Badge,
Sound, Sound,

View file

@ -18,6 +18,7 @@ pub mod notifications;
pub use notifications::Notification; pub use notifications::Notification;
/// Acts as a central interface to the Notification Center on macOS. /// Acts as a central interface to the Notification Center on macOS.
#[derive(Debug)]
pub struct NotificationCenter; pub struct NotificationCenter;
impl NotificationCenter { impl NotificationCenter {
@ -26,7 +27,7 @@ impl NotificationCenter {
unsafe { unsafe {
// @TODO: Revisit. // @TODO: Revisit.
let block = ConcreteBlock::new(|_: id, error: id| { let block = ConcreteBlock::new(|_: id, error: id| {
let localized_description = NSString::wrap(msg_send![error, localizedDescription]); let localized_description = NSString::new(msg_send![error, localizedDescription]);
let e = localized_description.to_str(); let e = localized_description.to_str();
if e != "" { if e != "" {
println!("{:?}", e); println!("{:?}", e);

View file

@ -9,6 +9,7 @@ use crate::foundation::{id, NSString};
/// A wrapper for `UNMutableNotificationContent`. Retains the pointer from the Objective C side, /// A wrapper for `UNMutableNotificationContent`. Retains the pointer from the Objective C side,
/// and is ultimately dropped upon sending. /// and is ultimately dropped upon sending.
#[derive(Debug)]
pub struct Notification(pub Id<Object>); pub struct Notification(pub Id<Object>);
impl Notification { impl Notification {

26
src/view/animator.rs Normal file
View file

@ -0,0 +1,26 @@
use core_graphics::base::CGFloat;
use objc::{msg_send, sel, sel_impl};
use objc::runtime::{Class, Object};
use objc_id::ShareId;
use crate::foundation::id;
/// A wrapper for an animation proxy object in Cocoa that supports basic animations.
#[derive(Clone, Debug)]
pub struct ViewAnimatorProxy(pub ShareId<Object>);
impl ViewAnimatorProxy {
pub fn new(proxy: id) -> Self {
Self(unsafe {
ShareId::from_ptr(msg_send![proxy, animator])
})
}
/// Sets the alpha value for the view being animated.
pub fn set_alpha(&self, value: CGFloat) {
unsafe {
let _: () = msg_send![&*self.0, setAlphaValue:value];
}
}
}

View file

@ -4,6 +4,7 @@ use objc::{msg_send, sel, sel_impl};
use crate::foundation::id; use crate::foundation::id;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::view::{VIEW_DELEGATE_PTR, View, ViewDelegate}; use crate::view::{VIEW_DELEGATE_PTR, View, ViewDelegate};
use crate::utils::Controller; use crate::utils::Controller;
@ -55,7 +56,7 @@ where
(&mut *vc).set_ivar(VIEW_DELEGATE_PTR, ptr as usize); (&mut *vc).set_ivar(VIEW_DELEGATE_PTR, ptr as usize);
} }
view.with_backing_node(|backing_node| { view.with_backing_obj_mut(|backing_node| {
let _: () = msg_send![vc, setView:backing_node]; let _: () = msg_send![vc, setView:backing_node];
}); });

View file

@ -48,14 +48,18 @@ use crate::foundation::{id, nil, YES, NO, NSArray, NSString};
use crate::color::Color; use crate::color::Color;
use crate::layer::Layer; use crate::layer::Layer;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension, SafeAreaLayoutGuide};
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
mod animator;
pub use animator::ViewAnimatorProxy;
#[cfg_attr(feature = "appkit", path = "appkit.rs")] #[cfg_attr(feature = "appkit", path = "appkit.rs")]
#[cfg_attr(feature = "uikit", path = "uikit.rs")] #[cfg_attr(feature = "uikit", path = "uikit.rs")]
mod native_interface; mod native_interface;
@ -88,6 +92,9 @@ pub struct View<T = ()> {
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ObjcProperty, pub objc: ObjcProperty,
/// An object that supports limited animations. Can be cloned into animation closures.
pub animator: ViewAnimatorProxy,
/// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit /// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit
/// we explicitly opt in to layer backed views. /// we explicitly opt in to layer backed views.
pub layer: Layer, pub layer: Layer,
@ -95,6 +102,10 @@ pub struct View<T = ()> {
/// A pointer to the delegate for this view. /// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>, pub delegate: Option<Box<T>>,
/// A property containing safe layout guides.
#[cfg(feature = "autolayout")]
pub safe_layout_guide: SafeAreaLayoutGuide,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub top: LayoutAnchorY, pub top: LayoutAnchorY,
@ -162,6 +173,9 @@ impl View {
is_handle: false, is_handle: false,
delegate: None, delegate: None,
#[cfg(feature = "autolayout")]
safe_layout_guide: SafeAreaLayoutGuide::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
@ -196,6 +210,7 @@ impl View {
msg_send![view, layer] msg_send![view, layer]
}), }),
animator: ViewAnimatorProxy::new(view),
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
} }
} }
@ -231,16 +246,20 @@ impl<T> View<T> where T: ViewDelegate + 'static {
} }
impl<T> View<T> { impl<T> View<T> {
/// An internal method that returns a clone of this object, sans references to the delegate or /// 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 /// 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 /// 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. /// delegate - the `View` is the only true holder of those.
pub(crate) fn clone_as_handle(&self) -> View { pub fn clone_as_handle(&self) -> View {
View { View {
delegate: None, delegate: None,
is_handle: true, is_handle: true,
layer: self.layer.clone(), layer: self.layer.clone(),
objc: self.objc.clone(), objc: self.objc.clone(),
animator: self.animator.clone(),
#[cfg(feature = "autolayout")]
safe_layout_guide: self.safe_layout_guide.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: self.top.clone(), top: self.top.clone(),
@ -291,16 +310,18 @@ impl<T> View<T> {
} }
impl<T> Layout for View<T> { impl<T> ObjcAccess for View<T> {
fn with_backing_node<F: Fn(id)>(&self, handler: F) { fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler); self.objc.with_mut(handler);
} }
fn get_from_backing_node<F: Fn(&Object) -> R, R>(&self, handler: F) -> R { fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler) self.objc.get(handler)
} }
} }
impl<T> Layout for View<T> {}
impl<T> Drop for View<T> { impl<T> Drop for View<T> {
/// If the instance being dropped is _not_ a handle, then we want to go ahead and explicitly /// If the instance being dropped is _not_ a handle, then we want to go ahead and explicitly
/// remove it from any super views. /// remove it from any super views.