From 4ecfbd09285f5ae2f73a55e5837f0cc4a3b53fa8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 2 Jan 2022 02:35:12 -0800 Subject: [PATCH] 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. --- Cargo.toml | 4 +- src/appkit/animation.rs | 73 +++++++ src/appkit/app/mod.rs | 4 +- src/appkit/mod.rs | 3 + src/appkit/toolbar/mod.rs | 4 +- src/appkit/window/controller/mod.rs | 9 +- src/appkit/window/mod.rs | 8 +- src/bundle.rs | 4 +- src/button/mod.rs | 39 +++- src/color/mod.rs | 4 + src/control/mod.rs | 56 +++++ src/filesystem/select.rs | 28 ++- src/foundation/data.rs | 15 ++ src/foundation/mod.rs | 4 + src/foundation/urls/bookmark_options.rs | 66 ++++++ src/foundation/urls/mod.rs | 181 ++++++++++++++++ src/foundation/urls/resource_keys.rs | 170 +++++++++++++++ src/image/icons.rs | 65 ++++-- src/image/image.rs | 49 ++++- src/image/mod.rs | 18 +- src/input/mod.rs | 50 ++++- src/keys.rs | 18 ++ src/layout/animator.rs | 27 +++ src/layout/constraint.rs | 10 +- src/layout/mod.rs | 21 +- src/layout/safe_guide.rs | 67 ++++++ src/layout/traits.rs | 48 +++-- src/lib.rs | 13 +- src/listview/appkit.rs | 31 ++- src/listview/mod.rs | 81 +++++-- src/listview/row/mod.rs | 44 +++- src/listview/traits.rs | 5 +- src/objc_access.rs | 23 ++ src/pasteboard/mod.rs | 39 +--- src/progress/mod.rs | 9 +- src/quicklook/config.rs | 2 +- src/quicklook/mod.rs | 10 +- src/scrollview/mod.rs | 10 +- src/select/mod.rs | 272 ++++++++++++++++++++++++ src/switch.rs | 11 +- src/text/attributed_string.rs | 13 ++ src/text/label/mod.rs | 25 ++- src/user_notifications/enums.rs | 1 + src/user_notifications/mod.rs | 3 +- src/user_notifications/notifications.rs | 1 + src/view/animator.rs | 26 +++ src/view/controller/mod.rs | 3 +- src/view/mod.rs | 33 ++- 48 files changed, 1516 insertions(+), 184 deletions(-) create mode 100644 src/appkit/animation.rs create mode 100644 src/control/mod.rs create mode 100644 src/foundation/urls/bookmark_options.rs create mode 100644 src/foundation/urls/mod.rs create mode 100644 src/foundation/urls/resource_keys.rs create mode 100644 src/keys.rs create mode 100644 src/layout/animator.rs create mode 100644 src/layout/safe_guide.rs create mode 100644 src/objc_access.rs create mode 100644 src/select/mod.rs create mode 100644 src/view/animator.rs diff --git a/Cargo.toml b/Cargo.toml index e4def77..270da49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,14 +23,14 @@ block = "0.1.6" core-foundation = { version = "0.9", features = ["with-chrono"] } core-graphics = "0.22" dispatch = "0.2.0" +infer = { version = "0.4", optional = true } lazy_static = "1.4.0" libc = "0.2" objc = "0.2.7" objc_id = "0.1.1" os_info = "3.0.1" -uuid = { version = "0.8", features = ["v4"], optional = true } url = "2.1.1" -infer = { version = "0.4", optional = true } +uuid = { version = "0.8", features = ["v4"], optional = true } [dev-dependencies] eval = "0.4" diff --git a/src/appkit/animation.rs b/src/appkit/animation.rs new file mode 100644 index 0000000..5bc312d --- /dev/null +++ b/src/appkit/animation.rs @@ -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(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(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]; + } + } +} diff --git a/src/appkit/app/mod.rs b/src/appkit/app/mod.rs index 437a68b..366ce08 100644 --- a/src/appkit/app/mod.rs +++ b/src/appkit/app/mod.rs @@ -49,6 +49,8 @@ use crate::appkit::menu::Menu; use crate::notification_center::Dispatcher; use crate::utils::activate_cocoa_multithreading; +//use crate::bundle::set_bundle_id; + mod class; use class::register_app_class; @@ -131,7 +133,7 @@ impl App where T: AppDelegate + 'static { /// policies), injects an `NSObject` delegate wrapper, and retains everything on the /// Objective-C side of things. pub fn new(_bundle_id: &str, delegate: T) -> Self { - // set_bundle_id(bundle_id); + //set_bundle_id(bundle_id); activate_cocoa_multithreading(); diff --git a/src/appkit/mod.rs b/src/appkit/mod.rs index f4ec45a..c96c132 100644 --- a/src/appkit/mod.rs +++ b/src/appkit/mod.rs @@ -8,6 +8,9 @@ mod alert; pub use alert::Alert; +mod animation; +pub use animation::AnimationContext; + mod app; pub use app::*; diff --git a/src/appkit/toolbar/mod.rs b/src/appkit/toolbar/mod.rs index 62d857a..c4b07f7 100644 --- a/src/appkit/toolbar/mod.rs +++ b/src/appkit/toolbar/mod.rs @@ -23,7 +23,7 @@ pub use traits::ToolbarDelegate; mod enums; 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 /// where our `NSToolbar` and associated delegate live. @@ -62,7 +62,7 @@ impl Toolbar where T: ToolbarDelegate + 'static { (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_delegate: objc_delegate.clone(), identifier: identifier.clone(), diff --git a/src/appkit/window/controller/mod.rs b/src/appkit/window/controller/mod.rs index 7771c29..25a35f4 100644 --- a/src/appkit/window/controller/mod.rs +++ b/src/appkit/window/controller/mod.rs @@ -54,7 +54,7 @@ impl WindowController where T: WindowDelegate + 'static { /// Allocates and configures an `NSWindowController` in the Objective-C/Cocoa runtime that maps over /// to your supplied delegate. 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 window_controller_class = register_window_controller_class::(); @@ -69,13 +69,6 @@ impl WindowController where T: WindowDelegate + 'static { Id::from_ptr(controller) }; - if let Some(delegate) = &mut window.delegate { - (*delegate).did_load(Window { - delegate: None, - objc: window.objc.clone() - }); - } - WindowController { objc, window } } diff --git a/src/appkit/window/mod.rs b/src/appkit/window/mod.rs index 61b5a04..161baad 100644 --- a/src/appkit/window/mod.rs +++ b/src/appkit/window/mod.rs @@ -17,10 +17,11 @@ use objc::{msg_send, sel, sel_impl, class}; use objc::runtime::Object; use objc_id::ShareId; +use crate::appkit::toolbar::{Toolbar, ToolbarDelegate}; use crate::color::Color; use crate::foundation::{id, nil, to_bool, YES, NO, NSString, NSInteger, NSUInteger}; -use crate::layout::traits::Layout; -use crate::appkit::toolbar::{Toolbar, ToolbarDelegate}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::{os, Controller}; mod class; @@ -289,7 +290,7 @@ impl Window { /// Given a view, sets it as the content view for this window. pub fn set_content_view(&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]; }); } @@ -446,6 +447,7 @@ impl Window { } } + /// Sets the separator style for this window. pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { unsafe { let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; diff --git a/src/bundle.rs b/src/bundle.rs index aa6ff2c..611ab9d 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -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 x: id = msg_send![url, absoluteString]; println!("Got here? {:?}", x); - unsafe { - NSString::alloc(nil).init_str("com.secretkeys.subatomic") - } + NSString::new("com.test.user_notifications").into() } else { msg_send![this, __bundleIdentifier] } diff --git a/src/button/mod.rs b/src/button/mod.rs index 2eb4502..7749bc2 100644 --- a/src/button/mod.rs +++ b/src/button/mod.rs @@ -29,10 +29,13 @@ use objc::runtime::{Class, Object, Sel}; use objc::{class, msg_send, sel, sel_impl}; use crate::color::Color; +use crate::control::Control; use crate::image::Image; use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger}; use crate::invoker::TargetActionHandler; +use crate::keys::Key; use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::text::{AttributedString, Font}; 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 /// button will fire. - pub fn set_key_equivalent(&self, key: &str) { - let key = NSString::new(key); + pub fn set_key_equivalent<'a, K>(&self, key: K) + where + K: Into> + { + let key: Key<'a> = key.into(); - self.objc.with_mut(|obj| unsafe { - let _: () = msg_send![obj, setKeyEquivalent:&*key]; + self.objc.with_mut(|obj| { + 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 { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for Button { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } -impl Layout for &Button { - fn with_backing_node(&self, handler: F) { +impl Layout for Button {} +impl Control for Button {} + +impl ObjcAccess for &Button { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for &Button {} +impl Control for &Button {} + impl Drop for Button { /// 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, diff --git a/src/color/mod.rs b/src/color/mod.rs index 7ed9528..be87818 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -217,6 +217,7 @@ pub enum Color { /// The default color to use for thin separators/lines that /// do not allow content underneath to be visible. /// This value automatically switches to the correct variant depending on light or dark mode. + #[cfg(feature = "uikit")] OpaqueSeparator, /// 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::SystemBackgroundTertiary => system_color_with_fallback!(color, tertiarySystemBackgroundColor, clearColor), Color::Separator => system_color_with_fallback!(color, separatorColor, lightGrayColor), + + #[cfg(feature = "uikit")] Color::OpaqueSeparator => system_color_with_fallback!(color, opaqueSeparatorColor, darkGrayColor), + Color::Link => system_color_with_fallback!(color, linkColor, blueColor), Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor), Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor), diff --git a/src/control/mod.rs b/src/control/mod.rs new file mode 100644 index 0000000..664f850 --- /dev/null +++ b/src/control/mod.rs @@ -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]; + }); + } +} diff --git a/src/filesystem/select.rs b/src/filesystem/select.rs index 4bc756a..f057991 100644 --- a/src/filesystem/select.rs +++ b/src/filesystem/select.rs @@ -10,7 +10,7 @@ use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; 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; #[cfg(feature = "appkit")] @@ -127,8 +127,7 @@ impl FileSelectPanel { self.allows_multiple_selection = allows; } - /// Shows the panel as a modal. Currently sheets are not supported, but you're free (and able - /// to) thread the Objective C calls yourself by using the panel field on this struct. + /// Shows the panel as a modal. /// /// 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 @@ -138,7 +137,7 @@ impl FileSelectPanel { /// script) or can't easily pass one to use as a sheet. pub fn show(&self, handler: F) where - F: Fn(Vec) + 'static + F: Fn(Vec) + 'static { let panel = self.panel.clone(); 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. /// If you're using a `Window` without a delegate, you may need to opt to use the `show()` /// method. @@ -164,7 +173,7 @@ impl FileSelectPanel { /// retain/ownership rules here. pub fn begin_sheet(&self, window: &Window, handler: F) where - F: Fn(Vec) + 'static + F: Fn(Vec) + 'static { let panel = self.panel.clone(); let completion = ConcreteBlock::new(move |result: NSInteger| { @@ -185,15 +194,16 @@ impl FileSelectPanel { /// 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 /// the world as it (ideally) shouldn't be called repeatedly in hot spots. -pub fn get_urls(panel: &Object) -> Vec { +/// +/// (We mostly do this to find the sweet spot between Rust constructs and necessary Foundation +/// interaction patterns) +fn get_urls(panel: &Object) -> Vec { unsafe { let urls: id = msg_send![&*panel, URLs]; let count: usize = msg_send![urls, count]; (0..count).map(|index| { - let url: id = msg_send![urls, objectAtIndex:index]; - let path = NSString::retain(msg_send![url, path]).to_string(); - path.into() + NSURL::retain(msg_send![urls, objectAtIndex:index]) }).collect() } } diff --git a/src/foundation/data.rs b/src/foundation/data.rs index 422d9c9..1135ccb 100644 --- a/src/foundation/data.rs +++ b/src/foundation/data.rs @@ -49,6 +49,21 @@ impl NSData { NSData(Id::from_ptr(obj)) } } + + /// 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. pub fn retain(data: id) -> Self { diff --git a/src/foundation/mod.rs b/src/foundation/mod.rs index 592c07a..d8bdfdc 100644 --- a/src/foundation/mod.rs +++ b/src/foundation/mod.rs @@ -42,6 +42,10 @@ pub use number::NSNumber; mod string; 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 /// against BOOL results throughout the framework, and this just simplifies some mismatches. #[inline(always)] diff --git a/src/foundation/urls/bookmark_options.rs b/src/foundation/urls/bookmark_options.rs new file mode 100644 index 0000000..4b98434 --- /dev/null +++ b/src/foundation/urls/bookmark_options.rs @@ -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 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 for NSUInteger { + fn from(flag: NSURLBookmarkResolutionOption) -> NSUInteger { + match flag { + NSURLBookmarkResolutionOption::WithoutUI => 1u64 << 8, + NSURLBookmarkResolutionOption::WithoutMounting => 1u64 << 9, + NSURLBookmarkResolutionOption::SecurityScoped => 1 << 10 + } + } +} diff --git a/src/foundation/urls/mod.rs b/src/foundation/urls/mod.rs new file mode 100644 index 0000000..e76c30e --- /dev/null +++ b/src/foundation/urls/mod.rs @@ -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, + 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 + ) -> Result> { + 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, + data_is_stale: bool + ) -> Result> { + 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> 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 + } +} diff --git a/src/foundation/urls/resource_keys.rs b/src/foundation/urls/resource_keys.rs new file mode 100644 index 0000000..3f14f88 --- /dev/null +++ b/src/foundation/urls/resource_keys.rs @@ -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 resource’s 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 +} diff --git a/src/image/icons.rs b/src/image/icons.rs index 5091ffb..7cd3cbd 100644 --- a/src/image/icons.rs +++ b/src/image/icons.rs @@ -1,3 +1,5 @@ +use crate::foundation::id; + /// 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 /// 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 /// 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")] impl MacSystemIcon { /// Maps system icons to their pre-11.0 framework identifiers. - pub fn to_str(&self) -> &'static str { - match self { - MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral", - MacSystemIcon::PreferencesAdvanced => "NSAdvanced", - MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts", - MacSystemIcon::Add => "NSImageNameAddTemplate" + pub fn to_id(&self) -> id { + unsafe { + match self { + MacSystemIcon::PreferencesGeneral => NSImageNamePreferencesGeneral, + MacSystemIcon::PreferencesAdvanced => NSImageNameAdvanced, + MacSystemIcon::PreferencesUserAccounts => NSImageNameUserAccounts, + MacSystemIcon::Add => NSImageNameAddTemplate, + MacSystemIcon::Remove => NSImageNameRemoveTemplate, + MacSystemIcon::Folder => NSImageNameFolder + } } } /// Maps system icons to their SFSymbols-counterparts for use on 11.0+. pub fn to_sfsymbol_str(&self) -> &'static str { match self { - MacSystemIcon::PreferencesGeneral => "gearshape", - MacSystemIcon::PreferencesAdvanced => "slider.vertical.3", - MacSystemIcon::PreferencesUserAccounts => "at", - MacSystemIcon::Add => "plus" + MacSystemIcon::PreferencesGeneral => SFSymbol::GearShape.to_str(), + MacSystemIcon::PreferencesAdvanced => SFSymbol::SliderVertical3.to_str(), + MacSystemIcon::PreferencesUserAccounts => SFSymbol::AtSymbol.to_str(), + MacSystemIcon::Add => SFSymbol::Plus.to_str(), + MacSystemIcon::Remove => SFSymbol::Minus.to_str(), + MacSystemIcon::Folder => SFSymbol::FolderFilled.to_str() } } } #[derive(Debug)] pub enum SFSymbol { + AtSymbol, + GearShape, + FolderFilled, PaperPlane, PaperPlaneFilled, + Plus, + Minus, + SliderVertical3, SquareAndArrowUpOnSquare, SquareAndArrowUpOnSquareFill, SquareAndArrowDownOnSquare, - SquareAndArrowDownOnSquareFill + SquareAndArrowDownOnSquareFill, + SquareDashed } impl SFSymbol { - pub fn to_str(&self) -> &str { + pub fn to_str(&self) -> &'static str { match self { + Self::AtSymbol => "at", + Self::GearShape => "gearshape", + Self::FolderFilled => "folder.fill", Self::PaperPlane => "paperplane", Self::PaperPlaneFilled => "paperplane.fill", + Self::Plus => "plus", + Self::Minus => "minus", + Self::SliderVertical3 => "slider.vertical.3", Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square", Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill", 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" } } } diff --git a/src/image/image.rs b/src/image/image.rs index d9e8d56..599ec4e 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -11,7 +11,7 @@ use core_graphics::{ }; 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 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 // let's keep the os flag here for now. /// Returns a stock system icon. These are guaranteed to exist across all versions of macOS /// supported. #[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 { ShareId::from_ptr(match os::is_minimum_version(11) { true => { @@ -147,8 +188,8 @@ impl Image { }, false => { - let icon = NSString::new(icon.to_str()); - msg_send![class!(NSImage), imageNamed:&*icon] + let icon = icon.to_id(); + msg_send![class!(NSImage), imageNamed:icon] } }) }) diff --git a/src/image/mod.rs b/src/image/mod.rs index b916bdf..8b393a4 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -5,6 +5,7 @@ use objc::{msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; #[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) { self.objc.with_mut(|obj| unsafe { 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 { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ImageView { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ImageView {} + impl Drop for ImageView { /// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if diff --git a/src/input/mod.rs b/src/input/mod.rs index c937f9d..bd99d3c 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -45,8 +45,10 @@ use objc::{msg_send, sel, sel_impl}; use objc_id::ShareId; use crate::color::Color; +use crate::control::Control; use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NO, YES}; use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::text::{Font, TextAlign}; use crate::utils::properties::ObjcProperty; @@ -309,6 +311,15 @@ impl TextField { }); } + /// 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. pub fn set_text_alignment(&self, alignment: TextAlign) { self.objc.with_mut(|obj| unsafe { @@ -317,6 +328,35 @@ impl TextField { }); } + /// 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. pub fn set_font>(&self, font: F) { let font = font.as_ref().clone(); @@ -327,16 +367,20 @@ impl TextField { } } -impl Layout for TextField { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for TextField { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for TextField {} + +impl Control for TextField {} + impl Drop for TextField { /// 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 diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..4dfc71f --- /dev/null +++ b/src/keys.rs @@ -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) + } +} diff --git a/src/layout/animator.rs b/src/layout/animator.rs new file mode 100644 index 0000000..5d1cc78 --- /dev/null +++ b/src/layout/animator.rs @@ -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); + +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]; + } + } +} diff --git a/src/layout/constraint.rs b/src/layout/constraint.rs index b5e0fdf..ae00671 100644 --- a/src/layout/constraint.rs +++ b/src/layout/constraint.rs @@ -10,6 +10,8 @@ use objc_id::ShareId; use crate::foundation::{id, YES, NO}; +use super::LayoutConstraintAnimatorProxy; + /// 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 /// height). @@ -27,16 +29,20 @@ pub struct LayoutConstraint { /// The priority used in computing this constraint. pub priority: f64, + + /// An animator proxy that can be used inside animation contexts. + pub animator: LayoutConstraintAnimatorProxy } impl LayoutConstraint { /// An internal method for wrapping existing constraints. pub(crate) fn new(object: id) -> Self { LayoutConstraint { + animator: LayoutConstraintAnimatorProxy::new(object), constraint: unsafe { ShareId::from_ptr(object) }, offset: 0.0, multiplier: 0.0, - priority: 0.0 + priority: 0.0, } } @@ -50,6 +56,7 @@ impl LayoutConstraint { LayoutConstraint { + animator: self.animator, constraint: self.constraint, offset: offset, multiplier: self.multiplier, @@ -57,6 +64,7 @@ impl LayoutConstraint { } } + /// Sets the offset of a borrowed constraint. pub fn set_offset>(&self, offset: F) { let offset: f64 = offset.into(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 1921461..082c222 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -3,35 +3,44 @@ //! `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. -pub mod traits; +mod traits; pub use traits::Layout; +mod animator; +pub use animator::LayoutConstraintAnimatorProxy; + #[cfg(feature = "autolayout")] -pub mod attributes; +mod attributes; #[cfg(feature = "autolayout")] pub use attributes::*; #[cfg(feature = "autolayout")] -pub mod constraint; +mod constraint; #[cfg(feature = "autolayout")] pub use constraint::LayoutConstraint; #[cfg(feature = "autolayout")] -pub mod dimension; +mod dimension; #[cfg(feature = "autolayout")] pub use dimension::LayoutAnchorDimension; #[cfg(feature = "autolayout")] -pub mod horizontal; +mod horizontal; #[cfg(feature = "autolayout")] pub use horizontal::LayoutAnchorX; #[cfg(feature = "autolayout")] -pub mod vertical; +mod vertical; #[cfg(feature = "autolayout")] pub use vertical::LayoutAnchorY; + +#[cfg(feature = "autolayout")] +mod safe_guide; + +#[cfg(feature = "autolayout")] +pub use safe_guide::SafeAreaLayoutGuide; diff --git a/src/layout/safe_guide.rs b/src/layout/safe_guide.rs new file mode 100644 index 0000000..a8315e8 --- /dev/null +++ b/src/layout/safe_guide.rs @@ -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) + } + } +} diff --git a/src/layout/traits.rs b/src/layout/traits.rs index 74976bc..16df281 100644 --- a/src/layout/traits.rs +++ b/src/layout/traits.rs @@ -1,6 +1,7 @@ //! Various traits related to controllers opting in to autolayout routines and support for view //! heirarchies. +use core_graphics::base::CGFloat; use core_graphics::geometry::{CGRect, CGPoint, CGSize}; 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::geometry::Rect; +use crate::objc_access::ObjcAccess; #[cfg(feature = "appkit")] use crate::pasteboard::PasteboardType; /// A trait that view wrappers must conform to. Enables managing the subview tree. #[allow(unused_variables)] -pub trait Layout { - /// Used for mutably interacting with the underlying Objective-C instance. - fn with_backing_node(&self, handler: F); - - /// Used for checking backing properties of the underlying Objective-C instance, without - /// needing a mutable borrow. - fn get_from_backing_node R, R>(&self, handler: F) -> R; - +pub trait Layout: ObjcAccess { /// 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 /// pass from the system will redraw it accordingly, and set the underlying value back to /// `false`. 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 { true => YES, false => NO @@ -39,8 +34,8 @@ pub trait Layout { /// Adds another Layout-backed control or view as a subview of this view. fn add_subview(&self, view: &V) { - self.with_backing_node(|backing_node| { - view.with_backing_node(|subview_node| unsafe { + self.with_backing_obj_mut(|backing_node| { + view.with_backing_obj_mut(|subview_node| unsafe { let _: () = msg_send![backing_node, addSubview:subview_node]; }); }); @@ -48,7 +43,7 @@ pub trait Layout { /// Removes a control or view from the superview. 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]; }); } @@ -61,7 +56,7 @@ pub trait Layout { fn set_frame>(&self, rect: R) { 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]; }); } @@ -73,7 +68,7 @@ pub trait Layout { /// then you should set this to `true` (or use an appropriate initializer that does it for you). #[cfg(feature = "autolayout")] 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 { true => YES, false => NO @@ -85,7 +80,7 @@ pub trait Layout { /// /// When hidden, widgets don't receive events and is not visible. 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 { true => YES, 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 /// that case, you may want `is_hidden_or_ancestor_is_hidden()`. fn is_hidden(&self) -> bool { - self.get_from_backing_node(|obj| { + self.get_from_backing_obj(|obj| { to_bool(unsafe { msg_send![obj, isHidden] }) @@ -108,7 +103,7 @@ pub trait Layout { /// Returns whether this is hidden, *or* whether an ancestor view is hidden. #[cfg(feature = "appkit")] fn is_hidden_or_ancestor_is_hidden(&self) -> bool { - self.get_from_backing_node(|obj| { + self.get_from_backing_obj(|obj| { to_bool(unsafe { msg_send![obj, isHiddenOrHasHiddenAncestor] }) @@ -126,7 +121,7 @@ pub trait Layout { x.into() }).collect::>().into(); - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, registerForDraggedTypes:&*types]; }); } @@ -137,7 +132,7 @@ pub trait Layout { /// currently to avoid compile issues. #[cfg(feature = "appkit")] fn unregister_dragged_types(&self) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, unregisterDraggedTypes]; }); } @@ -148,7 +143,7 @@ pub trait Layout { /// can be helpful - but always test! #[cfg(feature = "appkit")] 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 { true => YES, false => NO @@ -162,11 +157,22 @@ pub trait Layout { /// can be helpful - but always test! #[cfg(feature = "appkit")] 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 { true => YES, 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]; + }); + } } diff --git a/src/lib.rs b/src/lib.rs index e3a0f8d..1ea157c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,8 @@ pub use lazy_static; #[cfg_attr(docsrs, doc(cfg(feature = "appkit")))] pub mod appkit; +//pub mod bundle; + #[cfg(feature = "uikit")] #[cfg_attr(docsrs, doc(cfg(feature = "uikit")))] pub mod uikit; @@ -117,6 +119,9 @@ pub mod cloudkit; pub mod color; +#[cfg(feature = "appkit")] +pub mod control; + #[cfg(feature = "appkit")] pub mod dragdrop; @@ -138,14 +143,17 @@ pub mod image; #[cfg(feature = "appkit")] pub mod input; +pub(crate) mod invoker; + +pub mod keys; pub mod layer; -pub(crate) mod invoker; pub mod layout; #[cfg(feature = "appkit")] pub mod listview; pub mod networking; +pub(crate) mod objc_access; pub mod notification_center; #[cfg(feature = "appkit")] @@ -160,6 +168,9 @@ pub mod scrollview; #[cfg(feature = "appkit")] pub mod switch; +#[cfg(feature = "appkit")] +pub mod select; + #[cfg(feature = "appkit")] pub mod text; diff --git a/src/listview/appkit.rs b/src/listview/appkit.rs index b8ce823..8ed658e 100644 --- a/src/listview/appkit.rs +++ b/src/listview/appkit.rs @@ -37,9 +37,16 @@ extern fn view_for_column( this: &Object, _: Sel, _table_view: id, - _: id, + _table_column: id, item: NSInteger ) -> 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::(this, LISTVIEW_DELEGATE_PTR); let item = view.item_for(item as usize); @@ -79,7 +86,7 @@ extern fn menu_needs_update( 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. /// /// The other less obvious way is to subclass and override the `shouldSelectRow:` method; here, we @@ -94,6 +101,24 @@ extern fn select_row( let view = load::(this, LISTVIEW_DELEGATE_PTR); view.item_selected(item as usize); YES +}*/ + +extern fn selection_did_change( + this: &Object, + _: Sel, + notification: id +) { + let selected_row: NSInteger = unsafe { + let tableview: id = msg_send![notification, object]; + msg_send![tableview, selectedRow] + }; + + let view = load::(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( @@ -203,7 +228,7 @@ pub(crate) fn register_listview_class_with_delegate(instanc decl.add_method(sel!(numberOfRowsInTableView:), number_of_items:: as extern fn(&Object, _, id) -> NSInteger); decl.add_method(sel!(tableView:willDisplayCell:forTableColumn:row:), will_display_cell:: as extern fn(&Object, _, id, id, id, NSInteger)); decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column:: as extern fn(&Object, _, id, id, NSInteger) -> id); - decl.add_method(sel!(tableView:shouldSelectRow:), select_row:: as extern fn(&Object, _, id, NSInteger) -> BOOL); + decl.add_method(sel!(tableViewSelectionDidChange:), selection_did_change:: as extern fn(&Object, _, id)); decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row:: 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 diff --git a/src/listview/mod.rs b/src/listview/mod.rs index 6f20180..75b8b26 100644 --- a/src/listview/mod.rs +++ b/src/listview/mod.rs @@ -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::color::Color; - use crate::layout::Layout; #[cfg(feature = "autolayout")] use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::objc_access::ObjcAccess; use crate::scrollview::ScrollView; use crate::utils::{os, CellFactory, CGSize}; use crate::utils::properties::{ObjcProperty, PropertyNullable}; -use crate::view::ViewDelegate; +use crate::view::{ViewAnimatorProxy, ViewDelegate}; #[cfg(feature = "appkit")] 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. fn common_init(class: *const Class) -> id { 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]; - #[cfg(feature = "autolayout")] - let _: () = msg_send![tableview, setTranslatesAutoresizingMaskIntoConstraints:NO]; - // Let's... make NSTableView into UITableView-ish. #[cfg(feature = "appkit")] { @@ -144,6 +143,9 @@ pub struct ListView { /// A pointer to the Objective-C runtime view controller. 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 /// more old school like that... /// @@ -262,6 +264,11 @@ impl ListView { #[cfg(feature = "autolayout")] 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), scrollview @@ -310,6 +317,7 @@ impl ListView where T: ListViewDelegate + 'static { menu: PropertyNullable::default(), delegate: None, objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(anchor_view), #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(anchor_view), @@ -355,12 +363,13 @@ impl ListView { /// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to /// reference, customize and use the view but without the trickery of holding pieces of the /// delegate - the `View` is the only true holder of those. - pub(crate) fn clone_as_handle(&self) -> ListView { + pub fn clone_as_handle(&self) -> ListView { ListView { cell_factory: CellFactory::new(), menu: self.menu.clone(), delegate: None, objc: self.objc.clone(), + animator: self.animator.clone(), #[cfg(feature = "autolayout")] top: self.top.clone(), @@ -490,6 +499,34 @@ impl ListView { } } + /// 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(&self, handler: F) { + self.objc.get(handler); + } + /// 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 /// `ListView` for the handler is your `ListView`, and you can call `insert_rows`, @@ -511,14 +548,9 @@ impl ListView { let handle = self.clone_as_handle(); update(handle); - // This is cheating, but there's no good way around it at the moment. If we (mutably) lock in - // Rust here, firing this call will loop back around into `dequeue`, which will then - // hit a double lock. - // - // 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 { + // This is done for a very explicit reason; see the comments on the method itself for + // an explanation. + self.hack_avoid_dequeue_loop(|obj| unsafe { let _: () = msg_send![obj, endUpdates]; }); } @@ -545,7 +577,9 @@ impl ListView { // has also retained it. 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]; }); } @@ -647,6 +681,15 @@ impl ListView { }); } + /// 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 /// calls yourself, but often easier to implement. /// @@ -689,15 +732,15 @@ impl ListView { } } -impl Layout for ListView { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ListView { + fn with_backing_obj_mut(&self, handler: F) { // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // what to do normally. #[cfg(feature = "appkit")] self.scrollview.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // what to do normally. // @@ -708,6 +751,8 @@ impl Layout for ListView { } } +impl Layout for ListView {} + impl Drop for ListView { /// 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 diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index cdd567b..2cb8204 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -52,11 +52,12 @@ use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; use crate::layer::Layer; use crate::layout::Layout; -use crate::view::ViewDelegate; +use crate::objc_access::ObjcAccess; +use crate::view::{ViewAnimatorProxy, ViewDelegate}; use crate::utils::properties::ObjcProperty; #[cfg(feature = "autolayout")] -use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension, SafeAreaLayoutGuide}; #[cfg(feature = "appkit")] mod appkit; @@ -93,11 +94,18 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { /// side anyway. #[derive(Debug)] pub struct ListViewRow { + /// An object that supports limited animations. Can be cloned into animation closures. + pub animator: ViewAnimatorProxy, + /// A pointer to the Objective-C runtime view controller. pub objc: ObjcProperty, /// A pointer to the delegate for this view. pub delegate: Option>, + + /// A safe layout guide property. + #[cfg(feature = "autolayout")] + pub safe_layout_guide: SafeAreaLayoutGuide, /// A pointer to the Objective-C runtime top layout constraint. #[cfg(feature = "autolayout")] @@ -154,6 +162,10 @@ impl ListViewRow { ListViewRow { delegate: None, objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(view), @@ -211,6 +223,10 @@ impl ListViewRow where T: ViewDelegate + 'static { let view = ListViewRow { delegate: Some(delegate), objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(view), @@ -263,6 +279,10 @@ impl ListViewRow where T: ViewDelegate + 'static { let mut view = ListViewRow { delegate: None, objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(view), @@ -311,7 +331,11 @@ impl ListViewRow where T: ViewDelegate + 'static { ListViewRow { delegate: None, objc: self.objc.clone(), + animator: self.animator.clone(), + #[cfg(feature = "autolayout")] + safe_layout_guide: self.safe_layout_guide.clone(), + #[cfg(feature = "autolayout")] top: self.top.clone(), @@ -354,9 +378,13 @@ impl ListViewRow { crate::view::View { delegate: None, is_handle: true, - layer: Layer::new(), + layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row. objc: self.objc.clone(), - + animator: self.animator.clone(), + + #[cfg(feature = "autolayout")] + safe_layout_guide: self.safe_layout_guide.clone(), + #[cfg(feature = "autolayout")] top: self.top.clone(), @@ -408,16 +436,18 @@ impl ListViewRow { } } -impl Layout for ListViewRow { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ListViewRow { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ListViewRow {} + impl Drop for ListViewRow { fn drop(&mut self) { } diff --git a/src/listview/traits.rs b/src/listview/traits.rs index bc0c3b3..5ad01c1 100644 --- a/src/listview/traits.rs +++ b/src/listview/traits.rs @@ -36,8 +36,9 @@ pub trait ListViewDelegate { /// had time to sit down and figure them out properly yet. fn item_for(&self, row: usize) -> ListViewRow; - /// Called when an item has been selected (clicked/tapped on). - fn item_selected(&self, row: usize) {} + /// Called when an item has been selected (clicked/tapped on). If the selection was cleared, + /// then this will be called with `None`. + fn item_selected(&self, row: Option) {} /// 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 diff --git a/src/objc_access.rs b/src/objc_access.rs new file mode 100644 index 0000000..a24055e --- /dev/null +++ b/src/objc_access.rs @@ -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(&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 R, R>(&self, handler: F) -> R; +} diff --git a/src/pasteboard/mod.rs b/src/pasteboard/mod.rs index 3b6ffa9..56f87cb 100644 --- a/src/pasteboard/mod.rs +++ b/src/pasteboard/mod.rs @@ -20,7 +20,7 @@ use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; use url::Url; -use crate::foundation::{id, nil, NSString, NSArray}; +use crate::foundation::{id, nil, NSString, NSArray, NSURL}; use crate::error::Error; 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 /// Cocoa & co operate. This method may go away in the future if it's determined that people /// wind up just using `get_file_paths()`._ - pub fn get_file_urls(&self) -> Result, Box> { + pub fn get_file_urls(&self) -> Result, Box> { unsafe { let class: id = msg_send![class!(NSURL), class]; let classes = NSArray::new(&[class]); @@ -113,40 +113,7 @@ impl Pasteboard { } let urls = NSArray::retain(contents).map(|url| { - let path = NSString::retain(msg_send![url, path]); - 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, Box> { - 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) + NSURL::retain(url) }).into_iter().collect(); Ok(urls) diff --git a/src/progress/mod.rs b/src/progress/mod.rs index 9f93c99..9b76559 100644 --- a/src/progress/mod.rs +++ b/src/progress/mod.rs @@ -20,6 +20,7 @@ use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSUInteger}; use crate::color::Color; use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; #[cfg(feature = "autolayout")] @@ -201,16 +202,18 @@ impl ProgressIndicator { } } -impl Layout for ProgressIndicator { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ProgressIndicator { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ProgressIndicator {} + impl Drop for ProgressIndicator { /// A bit of extra cleanup for delegate callback pointers. /// If the originating `ProgressIndicator` is being diff --git a/src/quicklook/config.rs b/src/quicklook/config.rs index c4dc512..5d2577a 100644 --- a/src/quicklook/config.rs +++ b/src/quicklook/config.rs @@ -83,7 +83,7 @@ impl Default for ThumbnailConfig { // #TODO: Should query the current screen size maybe? 2x is fairly safe // for most moderns Macs right now. - scale: 2., + scale: 1., minimum_dimension: 0., diff --git a/src/quicklook/mod.rs b/src/quicklook/mod.rs index 2866d4f..049d96e 100644 --- a/src/quicklook/mod.rs +++ b/src/quicklook/mod.rs @@ -1,6 +1,6 @@ use std::path::Path; -use objc::runtime::{Object}; +use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; @@ -17,13 +17,19 @@ pub use config::{ThumbnailConfig, ThumbnailQuality}; pub struct ThumbnailGenerator(pub ShareId); impl ThumbnailGenerator { + /// Returns the global shared, wrapped, QLThumbnailGenerator. pub fn shared() -> Self { ThumbnailGenerator(unsafe { ShareId::from_ptr(msg_send![class!(QLThumbnailGenerator), sharedGenerator]) }) } - pub fn generate(&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(&self, path: &Path, config: ThumbnailConfig, callback: F) where F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static { diff --git a/src/scrollview/mod.rs b/src/scrollview/mod.rs index f2c99af..a66a977 100644 --- a/src/scrollview/mod.rs +++ b/src/scrollview/mod.rs @@ -47,8 +47,8 @@ use objc::{msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; - use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::pasteboard::PasteboardType; use crate::utils::properties::ObjcProperty; @@ -300,16 +300,18 @@ impl ScrollView { } } -impl Layout for ScrollView { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ScrollView { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ScrollView {} + impl Drop for ScrollView { /// 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 diff --git a/src/select/mod.rs b/src/select/mod.rs new file mode 100644 index 0000000..13ed692 --- /dev/null +++ b/src/select/mod.rs @@ -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 `