From 10c513edada5b6c10808927c829209d959d009fa Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 4 Mar 2021 17:24:39 -0800 Subject: [PATCH] A rather large and not very clean update. - Adds support for NSSplitViewController. - Reworks NSMenu support to be cleaner with enum variants. - Reworks the Foundation underpinnings to be a bit safer and more clear in how they're used and passed around. - Changes to docs structure for push towards v0.1. - Examples updated to account for changes. --- Cargo.toml | 4 +- examples/autolayout.rs | 6 +- examples/calculator/calculator.rs | 2 - examples/calculator/content_view.rs | 2 +- examples/calculator/main.rs | 10 +- examples/todos_list/menu.rs | 40 +- examples/todos_list/preferences/toolbar.rs | 14 +- examples/todos_list/todos/list/mod.rs | 2 +- examples/todos_list/todos/toolbar.rs | 13 +- src/button.rs | 17 +- src/color/macos_dynamic_color.rs | 4 +- src/color/mod.rs | 8 +- src/defaults/mod.rs | 26 +- src/defaults/value.rs | 16 +- src/error.rs | 10 +- src/filesystem/manager.rs | 6 +- src/filesystem/save.rs | 2 +- src/filesystem/select.rs | 58 ++- src/foundation/array.rs | 125 +++--- src/foundation/autoreleasepool.rs | 31 +- src/foundation/class.rs | 18 +- src/foundation/data.rs | 45 ++- src/foundation/dictionary.rs | 35 +- src/foundation/mod.rs | 2 +- src/foundation/number.rs | 28 +- src/foundation/string.rs | 97 ++++- src/image/icons.rs | 34 +- src/image/image.rs | 32 +- src/image/mod.rs | 9 + src/input/mod.rs | 11 +- src/ios/app/mod.rs | 8 +- src/ios/scene/enums.rs | 10 +- src/layout/constraint.rs | 36 +- src/layout/dimension.rs | 30 +- src/lib.rs | 55 ++- src/listview/actions.rs | 11 +- src/listview/macos.rs | 62 ++- src/listview/mod.rs | 139 ++++++- src/listview/row/macos.rs | 19 +- src/listview/row/mod.rs | 18 +- src/listview/traits.rs | 13 +- src/macos/alert.rs | 5 +- src/macos/app/delegate.rs | 25 +- src/macos/app/mod.rs | 21 +- src/macos/event/mod.rs | 4 +- src/macos/menu/item.rs | 417 +++++++++++++-------- src/macos/menu/menu.rs | 66 ++-- src/macos/mod.rs | 22 +- src/macos/toolbar/class.rs | 56 ++- src/macos/toolbar/enums.rs | 82 +++- src/macos/toolbar/item.rs | 30 +- src/macos/toolbar/mod.rs | 12 +- src/macos/toolbar/traits.rs | 8 +- src/macos/window/config.rs | 2 +- src/macos/window/mod.rs | 22 ++ src/networking/mod.rs | 2 +- src/notification_center/name.rs | 4 +- src/pasteboard/mod.rs | 20 +- src/pasteboard/types.rs | 4 +- src/quicklook/config.rs | 2 +- src/text/font.rs | 12 +- src/text/label/mod.rs | 24 +- src/view/mod.rs | 20 +- src/view/splitviewcontroller/ios.rs | 72 ++++ src/view/splitviewcontroller/macos.rs | 63 ++++ src/view/splitviewcontroller/mod.rs | 123 ++++++ 66 files changed, 1634 insertions(+), 592 deletions(-) create mode 100644 src/view/splitviewcontroller/ios.rs create mode 100644 src/view/splitviewcontroller/macos.rs create mode 100644 src/view/splitviewcontroller/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 7efba77..3baa2ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ categories = ["gui", "os::macos-apis", "os::ios-apis"] keywords = ["gui", "macos", "ios", "appkit", "uikit"] [package.metadata.docs.rs] +all-features = true default-target = "x86_64-apple-darwin" +rustdoc-args = ["--cfg", "docsrs"] [dependencies] block = "0.1.6" @@ -37,4 +39,4 @@ color_fallbacks = [] quicklook = [] user-notifications = ["uuid"] webview = [] -webview-downloading = [] +webview-downloading-macos = [] diff --git a/examples/autolayout.rs b/examples/autolayout.rs index 889cfa6..10b2164 100644 --- a/examples/autolayout.rs +++ b/examples/autolayout.rs @@ -51,16 +51,16 @@ impl WindowDelegate for AppWindow { window.set_content_view(&self.content); LayoutConstraint::activate(&[ - self.blue.top.constraint_equal_to(&self.content.top).offset(16.), + self.blue.top.constraint_equal_to(&self.content.top).offset(46.), self.blue.leading.constraint_equal_to(&self.content.leading).offset(16.), self.blue.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), self.blue.width.constraint_equal_to_constant(100.), - self.red.top.constraint_equal_to(&self.content.top).offset(16.), + self.red.top.constraint_equal_to(&self.content.top).offset(46.), self.red.leading.constraint_equal_to(&self.blue.trailing).offset(16.), self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), - self.green.top.constraint_equal_to(&self.content.top).offset(16.), + self.green.top.constraint_equal_to(&self.content.top).offset(46.), self.green.leading.constraint_equal_to(&self.red.trailing).offset(16.), self.green.trailing.constraint_equal_to(&self.content.trailing).offset(-16.), self.green.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), diff --git a/examples/calculator/calculator.rs b/examples/calculator/calculator.rs index cd7bfe7..407a9c5 100644 --- a/examples/calculator/calculator.rs +++ b/examples/calculator/calculator.rs @@ -2,7 +2,6 @@ use std::sync::{Arc, RwLock}; use cacao::lazy_static::lazy_static; use cacao::macos::App; -use cacao::notification_center::Dispatcher; use crate::CalculatorApp; @@ -24,7 +23,6 @@ pub enum Msg { /// on the main thread. pub fn dispatch(msg: Msg) { println!("Dispatching UI message: {:?}", msg); - //App::::dispatch_main(msg) CALCULATOR.run(msg) } diff --git a/examples/calculator/content_view.rs b/examples/calculator/content_view.rs index 4004fc1..ca55804 100644 --- a/examples/calculator/content_view.rs +++ b/examples/calculator/content_view.rs @@ -107,7 +107,7 @@ impl ViewDelegate for CalculatorView { self.results_wrapper.top.constraint_equal_to(&view.top), self.results_wrapper.leading.constraint_equal_to(&view.leading), self.results_wrapper.trailing.constraint_equal_to(&view.trailing), - self.results_wrapper.height.constraint_equal_to_constant(52.), + self.results_wrapper.height.constraint_equal_to_constant(80.), self.label.leading.constraint_equal_to(&self.results_wrapper.leading).offset(22.), self.label.trailing.constraint_equal_to(&self.results_wrapper.trailing).offset(-16.), diff --git a/examples/calculator/main.rs b/examples/calculator/main.rs index 4ef4d23..d965556 100644 --- a/examples/calculator/main.rs +++ b/examples/calculator/main.rs @@ -16,11 +16,10 @@ use cacao::macos::window::{Window, WindowConfig, TitleVisibility}; use cacao::macos::{Event, EventMask, EventMonitor}; use cacao::color::Color; use cacao::notification_center::Dispatcher; -use cacao::view::{View, ViewDelegate}; +use cacao::view::View; mod button_row; mod calculator; -use calculator::{dispatch, Msg}; mod content_view; use content_view::CalculatorView; @@ -38,7 +37,7 @@ impl AppDelegate for CalculatorApp { // Event Monitors need to be started after the App has been activated. // We use an RwLock here, but it's possible this entire method can be // &mut self and you wouldn't need these kinds of shenanigans. - //self.start_monitoring(); + self.start_monitoring(); self.window.set_title("Calculator"); self.window.set_background_color(Color::rgb(49,49,49)); @@ -70,7 +69,8 @@ impl CalculatorApp { let characters = evt.characters(); println!("{}", characters); - match characters.as_ref() { + //use calculator::{dispatch, Msg}; + /*match characters.as_ref() { "0" => dispatch(Msg::Push(0)), "1" => dispatch(Msg::Push(1)), "2" => dispatch(Msg::Push(2)), @@ -90,7 +90,7 @@ impl CalculatorApp { "c" => dispatch(Msg::Clear), "." => dispatch(Msg::Decimal), _ => {} - } + }*/ None })); diff --git a/examples/todos_list/menu.rs b/examples/todos_list/menu.rs index 2a9ef51..9e9ddd6 100644 --- a/examples/todos_list/menu.rs +++ b/examples/todos_list/menu.rs @@ -12,58 +12,58 @@ use crate::storage::{dispatch_ui, Message}; pub fn menu() -> Vec { vec![ Menu::new("", vec![ - MenuItem::about("Todos"), + MenuItem::About("Todos".to_string()), MenuItem::Separator, - MenuItem::entry("Preferences").key(",").action(|| { + MenuItem::new("Preferences").key(",").action(|| { dispatch_ui(Message::OpenPreferencesWindow); }), MenuItem::Separator, - MenuItem::services(), + MenuItem::Services, MenuItem::Separator, - MenuItem::hide(), - MenuItem::hide_others(), - MenuItem::show_all(), + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, MenuItem::Separator, - MenuItem::quit() + MenuItem::Quit ]), Menu::new("File", vec![ - MenuItem::entry("Open/Show Window").key("n").action(|| { + MenuItem::new("Open/Show Window").key("n").action(|| { dispatch_ui(Message::OpenMainWindow); }), MenuItem::Separator, - MenuItem::entry("Add Todo").key("+").action(|| { + MenuItem::new("Add Todo").key("+").action(|| { dispatch_ui(Message::OpenNewTodoSheet); }), MenuItem::Separator, - MenuItem::close_window(), + MenuItem::CloseWindow ]), Menu::new("Edit", vec![ - MenuItem::undo(), - MenuItem::redo(), + MenuItem::Undo, + MenuItem::Redo, MenuItem::Separator, - MenuItem::cut(), - MenuItem::copy(), - MenuItem::paste(), + MenuItem::Cut, + MenuItem::Copy, + MenuItem::Paste, MenuItem::Separator, - MenuItem::select_all() + MenuItem::SelectAll ]), Menu::new("View", vec![ - MenuItem::enter_full_screen() + MenuItem::EnterFullScreen ]), Menu::new("Window", vec![ - MenuItem::minimize(), - MenuItem::zoom(), + MenuItem::Minimize, + MenuItem::Zoom, MenuItem::Separator, - MenuItem::entry("Bring All to Front") + MenuItem::new("Bring All to Front") ]), Menu::new("Help", vec![]) diff --git a/examples/todos_list/preferences/toolbar.rs b/examples/todos_list/preferences/toolbar.rs index 113f08b..ee47414 100644 --- a/examples/todos_list/preferences/toolbar.rs +++ b/examples/todos_list/preferences/toolbar.rs @@ -1,7 +1,7 @@ //! Implements an example toolbar for a Preferences app. Could be cleaner, probably worth cleaning //! up at some point. -use cacao::macos::toolbar::{Toolbar, ToolbarDelegate, ToolbarItem}; +use cacao::macos::toolbar::{Toolbar, ToolbarDelegate, ToolbarItem, ItemIdentifier}; use cacao::image::{Image, MacSystemIcon}; use crate::storage::{dispatch_ui, Message}; @@ -46,16 +46,16 @@ impl ToolbarDelegate for PreferencesToolbar { toolbar.set_selected("general"); } - fn allowed_item_identifiers(&self) -> Vec<&'static str> { - vec!["general", "advanced"] + fn allowed_item_identifiers(&self) -> Vec { + vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")] } - fn default_item_identifiers(&self) -> Vec<&'static str> { - vec!["general", "advanced"] + fn default_item_identifiers(&self) -> Vec { + vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")] } - fn selectable_item_identifiers(&self) -> Vec<&'static str> { - vec!["general", "advanced"] + fn selectable_item_identifiers(&self) -> Vec { + vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")] } fn item_for(&self, identifier: &str) -> &ToolbarItem { diff --git a/examples/todos_list/todos/list/mod.rs b/examples/todos_list/todos/list/mod.rs index 7dd5513..e54925b 100644 --- a/examples/todos_list/todos/list/mod.rs +++ b/examples/todos_list/todos/list/mod.rs @@ -48,7 +48,7 @@ impl TodosListView { self.view.as_ref().unwrap().perform_batch_updates(|listview| { // We know we always insert at the 0 index, so this is a simple calculation. // You'd need to diff yourself for anything more complicated. - listview.insert_rows(0..1, RowAnimation::SlideDown); + listview.insert_rows(&[0], RowAnimation::SlideDown); }); }, diff --git a/examples/todos_list/todos/toolbar.rs b/examples/todos_list/todos/toolbar.rs index da1e356..ec2edeb 100644 --- a/examples/todos_list/todos/toolbar.rs +++ b/examples/todos_list/todos/toolbar.rs @@ -1,7 +1,10 @@ //! The main Todos window toolbar. Contains a button to enable adding a new task. use cacao::button::Button; -use cacao::macos::toolbar::{Toolbar, ToolbarDelegate, ToolbarItem, ToolbarDisplayMode}; +use cacao::macos::toolbar::{ + Toolbar, ToolbarDelegate, ToolbarItem, + ToolbarDisplayMode, ItemIdentifier +}; use crate::storage::{dispatch_ui, Message}; @@ -31,12 +34,12 @@ impl ToolbarDelegate for TodosToolbar { toolbar.set_display_mode(ToolbarDisplayMode::IconOnly); } - fn allowed_item_identifiers(&self) -> Vec<&'static str> { - vec!["AddTodoButton"] + fn allowed_item_identifiers(&self) -> Vec { + vec![ItemIdentifier::Custom("AddTodoButton")] } - fn default_item_identifiers(&self) -> Vec<&'static str> { - vec!["AddTodoButton"] + fn default_item_identifiers(&self) -> Vec { + vec![ItemIdentifier::Custom("AddTodoButton")] } // We only have one item, so we don't care about the identifier. diff --git a/src/button.rs b/src/button.rs index 681d9f2..9b67854 100644 --- a/src/button.rs +++ b/src/button.rs @@ -10,6 +10,7 @@ use objc::runtime::{Class, Object, Sel}; use objc::{class, msg_send, sel, sel_impl}; use crate::color::Color; +use crate::image::Image; use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger}; use crate::invoker::TargetActionHandler; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; @@ -28,6 +29,7 @@ extern "C" { #[derive(Debug)] pub struct Button { pub objc: ShareId, + pub image: Option, handler: Option, /// A pointer to the Objective-C runtime top layout constraint. @@ -62,7 +64,7 @@ impl Button { let title = NSString::new(text); let view: id = unsafe { - let button: id = msg_send![register_class(), buttonWithTitle:title target:nil action:nil]; + let button: id = msg_send![register_class(), buttonWithTitle:&*title target:nil action:nil]; let _: () = msg_send![button, setWantsLayer:YES]; let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO]; button @@ -70,6 +72,7 @@ impl Button { Button { handler: None, + image: None, top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }), trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }), @@ -82,6 +85,14 @@ impl Button { } } + pub fn set_image(&mut self, image: Image) { + unsafe { + let _: () = msg_send![&*self.objc, setImage:&*image.0]; + } + + self.image = Some(image); + } + /// Sets the bezel style for this button. #[cfg(feature = "macos")] pub fn set_bezel_style(&self, bezel_style: BezelStyle) { @@ -115,10 +126,10 @@ impl Button { } pub fn set_key_equivalent(&self, key: &str) { - let key = NSString::new(key).into_inner(); + let key = NSString::new(key); unsafe { - let _: () = msg_send![&*self.objc, setKeyEquivalent:key]; + let _: () = msg_send![&*self.objc, setKeyEquivalent:&*key]; } } diff --git a/src/color/macos_dynamic_color.rs b/src/color/macos_dynamic_color.rs index beb3a0f..10d7db6 100644 --- a/src/color/macos_dynamic_color.rs +++ b/src/color/macos_dynamic_color.rs @@ -10,6 +10,7 @@ //! that enables this functionality, we want to be able to provide this with some level of //! backwards compatibility for Mojave, as that's still a supported OS. +use std::os::raw::c_void; use std::sync::Once; use core_graphics::base::CGFloat; @@ -26,7 +27,6 @@ pub(crate) const AQUA_LIGHT_COLOR_HIGH_CONTRAST: &'static str = "AQUA_LIGHT_COLO pub(crate) const AQUA_DARK_COLOR_NORMAL_CONTRAST: &'static str = "AQUA_DARK_COLOR_NORMAL_CONTRAST"; pub(crate) const AQUA_DARK_COLOR_HIGH_CONTRAST: &'static str = "AQUA_DARK_COLOR_HIGH_CONTRAST"; -use std::os::raw::c_void; extern "C" { static NSAppearanceNameAqua: id; @@ -56,7 +56,7 @@ fn get_effective_color(this: &Object) -> id { NSAppearanceNameAccessibilityHighContrastDarkAqua ]); - let style: id = msg_send![appearance, bestMatchFromAppearancesWithNames:names.into_inner()]; + let style: id = msg_send![appearance, bestMatchFromAppearancesWithNames:&*names]; if style == NSAppearanceNameDarkAqua { return *this.get_ivar(AQUA_DARK_COLOR_NORMAL_CONTRAST); diff --git a/src/color/mod.rs b/src/color/mod.rs index f0625d7..60bdbe1 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -242,7 +242,11 @@ pub enum Color { DarkText, /// The un-adaptable color for text on a dark background. - LightText + LightText, + + WindowBackgroundColor, + + UnderPageBackgroundColor } impl Color { @@ -484,5 +488,7 @@ unsafe fn to_objc(obj: &Color) -> id { 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), + Color::WindowBackgroundColor => system_color_with_fallback!(color, windowBackgroundColor, clearColor), + Color::UnderPageBackgroundColor => system_color_with_fallback!(color, underPageBackgroundColor, clearColor), } } diff --git a/src/defaults/mod.rs b/src/defaults/mod.rs index 73b284c..e3a6e04 100644 --- a/src/defaults/mod.rs +++ b/src/defaults/mod.rs @@ -15,7 +15,7 @@ //! a loop. //! //! ## Example -//! ```rust +//! ```rust,no_run //! use std::collections::HashMap; //! use cacao::defaults::{UserDefaults, Value}; //! @@ -27,7 +27,6 @@ //! map //! }); //! -//! // Ignore the unwrap() calls, it's a demo ;P //! let value = defaults.get("test").unwrap().as_str().unwrap(); //! assert_eq!(value, "value"); //! ``` @@ -38,7 +37,10 @@ use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::Id; -use crate::foundation::{id, nil, to_bool, YES, NO, BOOL, NSData, NSString, NSDictionary, NSNumber}; +use crate::foundation::{ + id, nil, to_bool, YES, NO, BOOL, + NSData, NSString, NSMutableDictionary, NSNumber +}; mod value; pub use value::Value; @@ -92,7 +94,7 @@ impl UserDefaults { UserDefaults(unsafe { let alloc: id = msg_send![class!(NSUserDefaults), alloc]; - Id::from_ptr(msg_send![alloc, initWithSuiteName:name.into_inner()]) + Id::from_ptr(msg_send![alloc, initWithSuiteName:&*name]) }) } @@ -114,10 +116,10 @@ impl UserDefaults { /// }); /// ``` pub fn register>(&mut self, values: HashMap) { - let dictionary = NSDictionary::from(values); + let dictionary = NSMutableDictionary::from(values); unsafe { - let _: () = msg_send![&*self.0, registerDefaults:dictionary.into_inner()]; + let _: () = msg_send![&*self.0, registerDefaults:&*dictionary]; } } @@ -151,7 +153,7 @@ impl UserDefaults { let key = NSString::new(key.as_ref()); unsafe { - let _: () = msg_send![&*self.0, removeObjectForKey:key.into_inner()]; + let _: () = msg_send![&*self.0, removeObjectForKey:&*key]; } } @@ -176,7 +178,7 @@ impl UserDefaults { let key = NSString::new(key.as_ref()); let result: id = unsafe { - msg_send![&*self.0, objectForKey:key.into_inner()] + msg_send![&*self.0, objectForKey:&*key] }; if result == nil { @@ -184,12 +186,12 @@ impl UserDefaults { } if NSData::is(result) { - let data = NSData::wrap(result); + let data = NSData::retain(result); return Some(Value::Data(data.into_vec())); } if NSString::is(result) { - let s = NSString::wrap(result).to_str().to_string(); + let s = NSString::retain(result).to_string(); return Some(Value::String(s)); } @@ -213,7 +215,7 @@ impl UserDefaults { x => { // Debugging code that should be removed at some point. #[cfg(debug_assertions)] - println!("Code: {}", x); + println!("Unexpected code type found: {}", x); None } @@ -241,7 +243,7 @@ impl UserDefaults { pub fn is_forced_for_key>(&self, key: K) -> bool { let result: BOOL = unsafe { let key = NSString::new(key.as_ref()); - msg_send![&*self.0, objectIsForcedForKey:key.into_inner()] + msg_send![&*self.0, objectIsForcedForKey:&*key] }; to_bool(result) diff --git a/src/defaults/value.rs b/src/defaults/value.rs index 6ee0f4b..b210634 100644 --- a/src/defaults/value.rs +++ b/src/defaults/value.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::foundation::{id, NSData, NSDictionary, NSString, NSNumber}; +use crate::foundation::{id, NSData, NSMutableDictionary, NSString, NSNumber}; /// Represents a Value that can be stored or queried with `UserDefaults`. /// @@ -136,22 +136,22 @@ impl From for id { // period. fn from(value: Value) -> Self { match value { - Value::Bool(b) => NSNumber::bool(b).into_inner(), - Value::String(s) => NSString::new(&s).into_inner(), - Value::Float(f) => NSNumber::float(f).into_inner(), - Value::Integer(i) => NSNumber::integer(i).into_inner(), - Value::Data(data) => NSData::new(data).into_inner() + Value::Bool(b) => NSNumber::bool(b).into(), + Value::String(s) => NSString::new(&s).into(), + Value::Float(f) => NSNumber::float(f).into(), + Value::Integer(i) => NSNumber::integer(i).into(), + Value::Data(data) => NSData::new(data).into() } } } -impl From> for NSDictionary +impl From> for NSMutableDictionary where K: AsRef { /// Translates a `HashMap` of `Value`s into an `NSDictionary`. fn from(map: HashMap) -> Self { - let mut dictionary = NSDictionary::new(); + let mut dictionary = NSMutableDictionary::new(); for (key, value) in map.into_iter() { let k = NSString::new(key.as_ref()); diff --git a/src/error.rs b/src/error.rs index 78500bc..0620fd2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -33,16 +33,16 @@ impl Error { pub fn new(error: id) -> Self { let (code, domain, description) = unsafe { let code: usize = msg_send![error, code]; - let domain = NSString::wrap(msg_send![error, domain]); - let description = NSString::wrap(msg_send![error, localizedDescription]); + let domain = NSString::retain(msg_send![error, domain]); + let description = NSString::retain(msg_send![error, localizedDescription]); (code, domain, description) }; Error { - code: code, - domain: domain.to_str().to_string(), - description: description.to_str().to_string() + code, + domain: domain.to_string(), + description: description.to_string() } } diff --git a/src/filesystem/manager.rs b/src/filesystem/manager.rs index 633f027..0b14904 100644 --- a/src/filesystem/manager.rs +++ b/src/filesystem/manager.rs @@ -56,7 +56,7 @@ impl FileManager { create:NO error:nil]; - NSString::wrap(msg_send![dir, absoluteString]) + NSString::retain(msg_send![dir, absoluteString]) }; Url::parse(directory.to_str()).map_err(|e| e.into()) @@ -70,8 +70,8 @@ impl FileManager { let to = NSString::new(to.as_str()); unsafe { - let from_url: id = msg_send![class!(NSURL), URLWithString:from.into_inner()]; - let to_url: id = msg_send![class!(NSURL), URLWithString:to.into_inner()]; + let from_url: id = msg_send![class!(NSURL), URLWithString:&*from]; + let to_url: id = msg_send![class!(NSURL), URLWithString:&*to]; // This should potentially be write(), but the backing class handles this logic // already, so... going to leave it as read. diff --git a/src/filesystem/save.rs b/src/filesystem/save.rs index e7f9e01..601f7fc 100644 --- a/src/filesystem/save.rs +++ b/src/filesystem/save.rs @@ -102,7 +102,7 @@ pub fn get_url(panel: &Object) -> Option { None } else { let path: id = msg_send![url, path]; - Some(NSString::wrap(path).to_str().to_string()) + Some(NSString::retain(path).to_string()) } } } diff --git a/src/filesystem/select.rs b/src/filesystem/select.rs index 932c941..66dd9e6 100644 --- a/src/filesystem/select.rs +++ b/src/filesystem/select.rs @@ -13,6 +13,8 @@ use objc_id::ShareId; use crate::foundation::{id, YES, NO, NSInteger, NSString}; use crate::filesystem::enums::ModalResponse; +use crate::macos::window::{Window, WindowDelegate}; + #[derive(Debug)] pub struct FileSelectPanel { /// The internal Objective C `NSOpenPanel` instance. @@ -122,7 +124,13 @@ impl FileSelectPanel { /// 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 /// retain/ownership rules here. - pub fn show) + 'static>(&self, handler: F) { + /// + /// This is offered for scenarios where you don't necessarily have a Window (e.g, a shell + /// script) or can't easily pass one to use as a sheet. + pub fn show(&self, handler: F) + where + F: Fn(Vec) + 'static + { let panel = self.panel.clone(); let completion = ConcreteBlock::new(move |result: NSInteger| { let response: ModalResponse = result.into(); @@ -137,30 +145,46 @@ impl FileSelectPanel { let _: () = msg_send![&*self.panel, beginWithCompletionHandler:completion.copy()]; } } + + /// 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. + /// + /// 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 + /// retain/ownership rules here. + pub fn begin_sheet(&self, window: &Window, handler: F) + where + F: Fn(Vec) + 'static + { + let panel = self.panel.clone(); + let completion = ConcreteBlock::new(move |result: NSInteger| { + let response: ModalResponse = result.into(); + + handler(match response { + ModalResponse::Ok => get_urls(&panel), + _ => Vec::new() + }); + }); + + unsafe { + let _: () = msg_send![&*self.panel, beginSheetModalForWindow:&*window.objc completionHandler:completion.copy()]; + } + } } /// 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 { - let mut paths: Vec = vec![]; - unsafe { let urls: id = msg_send![&*panel, URLs]; - let mut count: usize = msg_send![urls, count]; + let count: usize = msg_send![urls, count]; - loop { - if count == 0 { - break; - } - - let url: id = msg_send![urls, objectAtIndex:count-1]; - let path = NSString::wrap(msg_send![url, path]).to_str().to_string(); - paths.push(path.into()); - count -= 1; - } + (0..count).map(|index| { + let url: id = msg_send![urls, objectAtIndex:index]; + let path = NSString::retain(msg_send![url, path]).to_string(); + path.into() + }).collect() } - - paths.reverse(); - paths } diff --git a/src/foundation/array.rs b/src/foundation/array.rs index 1eda6c0..0a4ddd9 100644 --- a/src/foundation/array.rs +++ b/src/foundation/array.rs @@ -1,12 +1,4 @@ -//! A wrapper type for `NSArray`. -//! -//! This is abstracted out as we need to use `NSArray` in a ton of -//! instances in this framework, and down the road I'd like to investigate using `CFArray` instead -//! of `NSArray` (i.e, if the ObjC runtime is ever pulled or something - perhaps those types would -//! stick around). -//! -//! Essentially, consider this some sanity/cleanliness/future-proofing. End users should never need -//! to touch this. +use std::ops::{Deref, DerefMut}; use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; @@ -15,15 +7,68 @@ use objc_id::Id; use crate::foundation::id; /// A wrapper for `NSArray` that makes common operations in our framework a bit easier to handle -/// and reason about. +/// and reason about. This also provides a central place to look at replacing with `CFArray` if +/// ever deemed necessary (unlikely, given how much Apple has optimized the Foundation classes, but +/// still...). #[derive(Debug)] pub struct NSArray(pub Id); +impl NSArray { + /// Given a set of `Object`s, creates and retains an `NSArray` that holds them. + pub fn new(objects: &[id]) -> Self { + NSArray(unsafe { + Id::from_ptr(msg_send![class!(NSArray), + arrayWithObjects:objects.as_ptr() + count:objects.len() + ]) + }) + } + + /// In some cases, we're vended an `NSArray` by the system that we need to call retain on. + /// This handles that case. + pub fn retain(array: id) -> Self { + NSArray(unsafe { + Id::from_ptr(array) + }) + } + + /// In some cases, we're vended an `NSArray` by the system, and it's ideal to not retain that. + /// This handles that edge case. + pub fn from_retained(array: id) -> Self { + NSArray(unsafe { + Id::from_retained_ptr(array) + }) + } + + /// Returns the `count` (`len()` equivalent) for the backing `NSArray`. + pub fn count(&self) -> usize { + unsafe { msg_send![&*self.0, count] } + } + + /// A helper method for mapping over the backing `NSArray` items and producing a Rust `Vec`. + /// Often times we need to map in this framework to convert between Rust types, so isolating + /// this out makes life much easier. + pub fn map T>(&self, transform: F) -> Vec { + let count = self.count(); + let objc = &*self.0; + + // I don't know if it's worth trying to get in with NSFastEnumeration here. I'm content to + // just rely on Rust, but someone is free to profile it if they want. + (0..count).map(|index| { + let item: id = unsafe { msg_send![objc, objectAtIndex:index] }; + transform(item) + }).collect() + } +} + impl From> for NSArray { /// Given a set of `Object`s, creates an `NSArray` that holds them. fn from(objects: Vec<&Object>) -> Self { NSArray(unsafe { - Id::from_ptr(msg_send![class!(NSArray), arrayWithObjects:objects.as_ptr() count:objects.len()]) + Id::from_ptr(msg_send![class!(NSArray), + arrayWithObjects:objects.as_ptr() + count:objects.len() + ]) }) } } @@ -32,53 +77,33 @@ impl From> for NSArray { /// Given a set of `*mut Object`s, creates an `NSArray` that holds them. fn from(objects: Vec) -> Self { NSArray(unsafe { - Id::from_ptr(msg_send![class!(NSArray), arrayWithObjects:objects.as_ptr() count:objects.len()]) + Id::from_ptr(msg_send![class!(NSArray), + arrayWithObjects:objects.as_ptr() + count:objects.len() + ]) }) } } -impl NSArray { - /// Given a set of `Object`s, creates an `NSArray` that holds them. - pub fn new(objects: &[id]) -> Self { - NSArray(unsafe { - Id::from_ptr(msg_send![class!(NSArray), arrayWithObjects:objects.as_ptr() count:objects.len()]) - }) +impl From for id { + /// Consumes and returns the pointer to the underlying NSArray. + fn from(mut array: NSArray) -> Self { + &mut *array } +} - /// In some cases, we're vended an `NSArray` by the system, and it's ideal to not retain that. - /// This handles that edge case. - pub fn wrap(array: id) -> Self { - NSArray(unsafe { - Id::from_ptr(array) - }) +impl Deref for NSArray { + type Target = Object; + + /// Derefs to the underlying Objective-C Object. + fn deref(&self) -> &Object { + &*self.0 } +} - /// Consumes and returns the underlying Objective-C value. - pub fn into_inner(mut self) -> id { +impl DerefMut for NSArray { + /// Derefs to the underlying Objective-C Object. + fn deref_mut(&mut self) -> &mut Object { &mut *self.0 } - - /// Returns the `count` (`len()` equivalent) for the backing `NSArray`. - pub fn count(&self) -> usize { - unsafe { msg_send![self.0, count] } - } - - /// A helper method for mapping over the backing `NSArray` items. - /// Often times we need to map in this framework to convert between Rust types, so isolating - /// this out makes life much easier. - pub fn map T>(&self, transform: F) -> Vec { - let count = self.count(); - let mut ret: Vec = Vec::with_capacity(count); - let mut index = 0; - - loop { - let item: id = unsafe { msg_send![&*self.0, objectAtIndex:index] }; - ret.push(transform(item)); - - index += 1; - if index == count { break } - } - - ret - } } diff --git a/src/foundation/autoreleasepool.rs b/src/foundation/autoreleasepool.rs index e623063..8715718 100644 --- a/src/foundation/autoreleasepool.rs +++ b/src/foundation/autoreleasepool.rs @@ -1,19 +1,46 @@ -//! A lightweight wrapper around `NSAutoreleasePool`. - use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::Id; +/// A wrapper around `NSAutoReleasePool`. The core `App` structures create and manage one of these, +/// but it's conceivable that users might need to create their own. +/// +/// When this is dropped, we automatically send a `drain` message to the underlying pool. You can +/// also call `drain()` yourself if you need to drain for whatever reason. pub struct AutoReleasePool(pub Id); impl AutoReleasePool { + /// Creates and returns a new `AutoReleasePool`. You need to take care to keep this alive for + /// as long as you need it. pub fn new() -> Self { AutoReleasePool(unsafe { Id::from_retained_ptr(msg_send![class!(NSAutoreleasePool), new]) }) } + /// Drains the underlying AutoreleasePool. pub fn drain(&self) { let _: () = unsafe { msg_send![&*self.0, drain] }; } + + /// Run a function with a one-off AutoReleasePool. + /// + /// This will create a custom NSAutoreleasePool, run your handler, and automatically drain + /// when done. This is (roughly, ish) equivalent to `@autoreleasepool {}` under ARC. If you + /// need to perform Cocoa calls on a different thread, it's important to ensure they're backed + /// with an autorelease pool - otherwise your memory footprint will continue to grow. + pub fn run(handler: F) + where + F: Fn() + 'static + { + let _pool = AutoReleasePool::new(); + handler(); + } +} + +impl Drop for AutoReleasePool { + /// Drains the underlying NSAutoreleasePool. + fn drop(&mut self) { + let _: () = unsafe { msg_send![&*self.0, drain] }; + } } diff --git a/src/foundation/class.rs b/src/foundation/class.rs index db692ae..a179bc1 100644 --- a/src/foundation/class.rs +++ b/src/foundation/class.rs @@ -15,7 +15,7 @@ lazy_static! { /// constantly calling into the runtime, we store pointers to Class types here after first lookup /// and/or creation. The general store format is (roughly speaking) as follows: /// -/// ``` +/// ```no_run /// { /// "subclass_type": { /// "superclass_type": *const Class as usize @@ -39,8 +39,8 @@ impl ClassMap { let mut map = HashMap::new(); // Top-level classes, like `NSView`, we cache here. The reasoning is that if a subclass - // is being created, we can avoid querying the runtime for the superclass - i.e, many subclasses - // will have `NSView` as their superclass. + // is being created, we can avoid querying the runtime for the superclass - i.e, many + // subclasses will have `NSView` as their superclass. map.insert("_supers", HashMap::new()); map @@ -48,7 +48,11 @@ impl ClassMap { } /// Attempts to load a previously registered subclass. - pub fn load_subclass(&self, subclass_name: &'static str, superclass_name: &'static str) -> Option<*const Class> { + pub fn load_subclass( + &self, + subclass_name: &'static str, + superclass_name: &'static str + ) -> Option<*const Class> { let reader = self.0.read().unwrap(); if let Some(inner) = (*reader).get(subclass_name) { @@ -152,7 +156,11 @@ where }, None => { - panic!("Subclass of type {}_{} could not be allocated.", subclass_name, superclass_name); + panic!( + "Subclass of type {}_{} could not be allocated.", + subclass_name, + superclass_name + ); } } } diff --git a/src/foundation/data.rs b/src/foundation/data.rs index 2c85c8f..422d9c9 100644 --- a/src/foundation/data.rs +++ b/src/foundation/data.rs @@ -1,13 +1,5 @@ -//! A wrapper for `NSData`. -//! -//! This more or less exists to try and wrap the specific APIs we use to interface with Cocoa. Note -//! that in general this is only concerned with bridging for arguments - i.e, we need an `NSData` -//! from a `Vec` to satisfy the Cocoa side of things. It's expected that all processing of data -//! happens solely on the Rust side of things before coming through here. -//! -//! tl;dr this is an intentionally limited API. - use std::mem; +use std::ops::{Deref, DerefMut}; use std::os::raw::c_void; use std::slice; @@ -23,6 +15,8 @@ use crate::foundation::{id, to_bool, BOOL, YES, NO, NSUInteger}; /// /// Supports constructing a new `NSData` from a `Vec`, wrapping and retaining an existing /// pointer from the Objective-C side, and turning an `NSData` into a `Vec`. +/// +/// This is an intentionally limited API. #[derive(Debug)] pub struct NSData(pub Id); @@ -56,14 +50,21 @@ impl NSData { } } - /// If we're vended an NSData from a method (e.g, a push notification token) we might want to - /// wrap it while we figure out what to do with it. This does that. - pub fn wrap(data: id) -> Self { + /// Given a (presumably) `NSData`, wraps and retains it. + pub fn retain(data: id) -> Self { NSData(unsafe { Id::from_ptr(data) }) } + /// If we're vended an NSData from a method (e.g, a push notification token) we might want to + /// wrap it while we figure out what to do with it. This does that. + pub fn from_retained(data: id) -> Self { + NSData(unsafe { + Id::from_retained_ptr(data) + }) + } + /// A helper method for determining if a given `NSObject` is an `NSData`. pub fn is(obj: id) -> bool { let result: BOOL = unsafe { @@ -117,9 +118,27 @@ impl NSData { data } +} +impl From for id { /// Consumes and returns the underlying `NSData`. - pub fn into_inner(mut self) -> id { + fn from(mut data: NSData) -> Self { + &mut *data.0 + } +} + +impl Deref for NSData { + type Target = Object; + + /// Derefs to the underlying Objective-C Object. + fn deref(&self) -> &Object { + &*self.0 + } +} + +impl DerefMut for NSData { + /// Derefs to the underlying Objective-C Object. + fn deref_mut(&mut self) -> &mut Object { &mut *self.0 } } diff --git a/src/foundation/dictionary.rs b/src/foundation/dictionary.rs index b3466ae..61fd058 100644 --- a/src/foundation/dictionary.rs +++ b/src/foundation/dictionary.rs @@ -1,21 +1,24 @@ +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; + use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::Id; use crate::foundation::{id, NSString}; -/// A wrapper for `NSDictionary`. Behind the scenes we actually wrap `NSMutableDictionary`, and -/// rely on Rust doing the usual borrow-checking guards that it does so well. +/// A wrapper for `NSMutableDictionary`. #[derive(Debug)] -pub struct NSDictionary(pub Id); +pub struct NSMutableDictionary(pub Id); -impl Default for NSDictionary { +impl Default for NSMutableDictionary { + /// Returns a blank NSMutableDictionary. fn default() -> Self { - NSDictionary::new() + NSMutableDictionary::new() } } -impl NSDictionary { +impl NSMutableDictionary { /// Constructs an `NSMutableDictionary` and retains it. /// /// Why mutable? It's just easier for working with it, as they're (mostly) interchangeable when @@ -23,7 +26,7 @@ impl NSDictionary { /// object model. You can, of course, bypass it and `msg_send![]` yourself, but it'd require an /// `unsafe {}` block... so you'll know you're in special territory then. pub fn new() -> Self { - NSDictionary(unsafe { + NSMutableDictionary(unsafe { Id::from_ptr(msg_send![class!(NSMutableDictionary), new]) }) } @@ -33,7 +36,7 @@ impl NSDictionary { /// This intentionally requires `NSString` be allocated ahead of time. pub fn insert(&mut self, key: NSString, object: id) { unsafe { - let _: () = msg_send![&*self.0, setObject:object forKey:key.into_inner()]; + let _: () = msg_send![&*self.0, setObject:object forKey:&*key]; } } @@ -42,3 +45,19 @@ impl NSDictionary { &mut *self.0 } } + +impl Deref for NSMutableDictionary { + type Target = Object; + + /// Derefs to the underlying Objective-C Object. + fn deref(&self) -> &Object { + &*self.0 + } +} + +impl DerefMut for NSMutableDictionary { + /// Derefs to the underlying Objective-C Object. + fn deref_mut(&mut self) -> &mut Object { + &mut *self.0 + } +} diff --git a/src/foundation/mod.rs b/src/foundation/mod.rs index 0dc3b7c..592c07a 100644 --- a/src/foundation/mod.rs +++ b/src/foundation/mod.rs @@ -34,7 +34,7 @@ mod data; pub use data::NSData; mod dictionary; -pub use dictionary::NSDictionary; +pub use dictionary::NSMutableDictionary; mod number; pub use number::NSNumber; diff --git a/src/foundation/number.rs b/src/foundation/number.rs index c7bc3bd..f205dc2 100644 --- a/src/foundation/number.rs +++ b/src/foundation/number.rs @@ -7,7 +7,7 @@ use objc_id::Id; use crate::foundation::{id, to_bool, BOOL, YES, NO, NSInteger}; -/// Wrapper for a retained `NSNumber` object. +/// Wrapper for a `NSNumber` object. /// /// In general we strive to avoid using this in the codebase, but it's a requirement for moving /// objects in and out of certain situations (e.g, `UserDefaults`). @@ -15,18 +15,26 @@ use crate::foundation::{id, to_bool, BOOL, YES, NO, NSInteger}; pub struct NSNumber(pub Id); impl NSNumber { + /// If we're vended an NSNumber from a method (e.g, `NSUserDefaults` querying) we might want to + /// wrap (and retain) it while we figure out what to do with it. This does that. + pub fn retain(data: id) -> Self { + NSNumber(unsafe { + Id::from_ptr(data) + }) + } + /// If we're vended an NSNumber from a method (e.g, `NSUserDefaults` querying) we might want to /// wrap it while we figure out what to do with it. This does that. pub fn wrap(data: id) -> Self { NSNumber(unsafe { - Id::from_ptr(data) + Id::from_retained_ptr(data) }) } /// Constructs a `numberWithBool` instance of `NSNumber` and retains it. pub fn bool(value: bool) -> Self { NSNumber(unsafe { - Id::from_ptr(msg_send![class!(NSNumber), numberWithBool:match value { + Id::from_retained_ptr(msg_send![class!(NSNumber), numberWithBool:match value { true => YES, false => NO }]) @@ -36,14 +44,14 @@ impl NSNumber { /// Constructs a `numberWithInteger` instance of `NSNumber` and retains it. pub fn integer(value: i64) -> Self { NSNumber(unsafe { - Id::from_ptr(msg_send![class!(NSNumber), numberWithInteger:value as NSInteger]) + Id::from_retained_ptr(msg_send![class!(NSNumber), numberWithInteger:value as NSInteger]) }) } /// Constructs a `numberWithDouble` instance of `NSNumber` and retains it. pub fn float(value: f64) -> Self { NSNumber(unsafe { - Id::from_ptr(msg_send![class!(NSNumber), numberWithDouble:value]) + Id::from_retained_ptr(msg_send![class!(NSNumber), numberWithDouble:value]) }) } @@ -51,7 +59,7 @@ impl NSNumber { /// to inform you how you should pull the underlying data out of the `NSNumber`. /// /// For more information: - /// [https://nshipster.com/type-encodings/](https://nshipster.com/type-encodings/) + /// pub fn objc_type(&self) -> &str { unsafe { let t: *const c_char = msg_send![&*self.0, objCType]; @@ -101,9 +109,11 @@ impl NSNumber { to_bool(result) } - +} + +impl From for id { /// Consumes and returns the underlying `NSNumber`. - pub fn into_inner(mut self) -> id { - &mut *self.0 + fn from(mut number: NSNumber) -> Self { + &mut *number.0 } } diff --git a/src/foundation/string.rs b/src/foundation/string.rs index af04c19..101e43c 100644 --- a/src/foundation/string.rs +++ b/src/foundation/string.rs @@ -1,4 +1,6 @@ -use std::{slice, str}; +use std::{fmt, slice, str}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; use std::os::raw::c_char; use objc::{class, msg_send, sel, sel_impl}; @@ -14,25 +16,58 @@ const UTF8_ENCODING: usize = 4; /// We can make a few safety guarantees in this module as the UTF8 code on the Foundation /// side is fairly battle tested. #[derive(Debug)] -pub struct NSString(pub Id); +pub struct NSString<'a> { + pub objc: Id, + phantom: PhantomData<&'a ()> +} -impl NSString { - /// Creates a new `NSString`. Note that `NSString` lives on the heap, so this allocates - /// accordingly. +impl<'a> NSString<'a> { + /// Creates a new `NSString`. pub fn new(s: &str) -> Self { - NSString(unsafe { - let nsstring: *mut Object = msg_send![class!(NSString), alloc]; - //msg_send![nsstring, initWithBytesNoCopy:s.as_ptr() length:s.len() encoding:4 freeWhenDone:NO] - Id::from_ptr(msg_send![nsstring, initWithBytes:s.as_ptr() length:s.len() encoding:UTF8_ENCODING]) - }) + NSString { + objc: unsafe { + let nsstring: *mut Object = msg_send![class!(NSString), alloc]; + Id::from_ptr(msg_send![nsstring, initWithBytes:s.as_ptr() + length:s.len() + encoding:UTF8_ENCODING + ]) + }, + + phantom: PhantomData + } + } + + /// Creates a new `NSString` without copying the bytes for the passed-in string. + pub fn no_copy(s: &'a str) -> Self { + NSString { + objc: unsafe { + let nsstring: id = msg_send![class!(NSString), alloc]; + Id::from_ptr(msg_send![nsstring, initWithBytesNoCopy:s.as_ptr() + length:s.len() + encoding:UTF8_ENCODING + freeWhenDone:NO + ]) + }, + + phantom: PhantomData + } } /// In cases where we're vended an `NSString` by the system, this can be used to wrap and /// retain it. - pub fn wrap(object: id) -> Self { - NSString(unsafe { - Id::from_ptr(object) - }) + pub fn retain(object: id) -> Self { + NSString { + objc: unsafe { Id::from_ptr(object) }, + phantom: PhantomData + } + } + + /// In some cases, we want to wrap a system-provided NSString without retaining it. + pub fn from_retained(object: id) -> Self { + NSString { + objc: unsafe { Id::from_retained_ptr(object) }, + phantom: PhantomData + } } /// Utility method for checking whether an `NSObject` is an `NSString`. @@ -44,7 +79,7 @@ impl NSString { /// Helper method for returning the UTF8 bytes for this `NSString`. fn bytes(&self) -> *const u8 { unsafe { - let bytes: *const c_char = msg_send![&*self.0, UTF8String]; + let bytes: *const c_char = msg_send![&*self.objc, UTF8String]; bytes as *const u8 } } @@ -52,7 +87,7 @@ impl NSString { /// Helper method for grabbing the proper byte length for this `NSString` (the UTF8 variant). fn bytes_len(&self) -> usize { unsafe { - msg_send![&*self.0, lengthOfBytesUsingEncoding:UTF8_ENCODING] + msg_send![&*self.objc, lengthOfBytesUsingEncoding:UTF8_ENCODING] } } @@ -71,9 +106,33 @@ impl NSString { pub fn to_string(&self) -> String { self.to_str().to_string() } +} - /// Consumes and returns the underlying `NSString` instance. - pub fn into_inner(mut self) -> id { - &mut *self.0 +impl fmt::Display for NSString<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +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 NSString<'_> { + type Target = Object; + + /// Derefs to the underlying Objective-C Object. + fn deref(&self) -> &Object { + &*self.objc + } +} + +impl DerefMut for NSString<'_> { + /// Derefs to the underlying Objective-C Object. + fn deref_mut(&mut self) -> &mut Object { + &mut *self.objc } } diff --git a/src/image/icons.rs b/src/image/icons.rs index 61ea2d1..3287fc9 100644 --- a/src/image/icons.rs +++ b/src/image/icons.rs @@ -19,9 +19,14 @@ pub enum MacSystemIcon { PreferencesAdvanced, /// A standard "Accounts" preferences icon. This is intended for usage in Preferences toolbars. - PreferencesUserAccounts + PreferencesUserAccounts, + + /// Returns a stock "+" icon that's common to the system. Use this for buttons that need the + /// symbol. + Add } +#[cfg(feature = "macos")] impl MacSystemIcon { /// Maps system icons to their pre-11.0 framework identifiers. pub fn to_str(&self) -> &'static str { @@ -29,6 +34,7 @@ impl MacSystemIcon { MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral", MacSystemIcon::PreferencesAdvanced => "NSAdvanced", MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts", + MacSystemIcon::Add => "NSImageNameAddTemplate" } } @@ -37,7 +43,31 @@ impl MacSystemIcon { match self { MacSystemIcon::PreferencesGeneral => "gearshape", MacSystemIcon::PreferencesAdvanced => "slider.vertical.3", - MacSystemIcon::PreferencesUserAccounts => "person.crop.circle" + MacSystemIcon::PreferencesUserAccounts => "at", + MacSystemIcon::Add => "plus" } } } + +#[derive(Debug)] +pub enum SFSymbol { + PaperPlane, + PaperPlaneFilled, + SquareAndArrowUpOnSquare, + SquareAndArrowUpOnSquareFill, + SquareAndArrowDownOnSquare, + SquareAndArrowDownOnSquareFill +} + +impl SFSymbol { + pub fn to_str(&self) -> &str { + match self { + Self::PaperPlane => "paperplane", + Self::PaperPlaneFilled => "paperplane.fill", + 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" + } + } +} diff --git a/src/image/image.rs b/src/image/image.rs index f37b8b0..5cc2933 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -119,14 +119,36 @@ impl Image { Image(unsafe { ShareId::from_ptr(match os::is_minimum_version(11) { true => { - let icon = NSString::new(icon.to_sfsymbol_str()).into_inner(); - let desc = NSString::new(accessibility_description).into_inner(); - msg_send![class!(NSImage), imageWithSystemSymbolName:icon accessibilityDescription:desc] + let icon = NSString::new(icon.to_sfsymbol_str()); + let desc = NSString::new(accessibility_description); + msg_send![class!(NSImage), imageWithSystemSymbolName:&*icon + accessibilityDescription:&*desc] }, false => { - let icon = NSString::new(icon.to_str()).into_inner(); - msg_send![class!(NSImage), imageNamed:icon] + let icon = NSString::new(icon.to_str()); + msg_send![class!(NSImage), imageNamed:&*icon] + } + }) + }) + } + + /// Creates and returns an Image with the specified `SFSymbol`. Note that `SFSymbol` is + /// supported on 11.0+; as such, this will panic if called on a lower system. Take care to + /// provide a fallback image or user experience if you need to support an older OS. + pub fn symbol(symbol: SFSymbol, accessibility_description: &str) -> Self { + Image(unsafe { + ShareId::from_ptr(match os::is_minimum_version(11) { + true => { + let icon = NSString::new(symbol.to_str()); + let desc = NSString::new(accessibility_description); + msg_send![class!(NSImage), imageWithSystemSymbolName:&*icon + accessibilityDescription:&*desc] + }, + + false => { + #[cfg(feature = "macos")] + panic!("SFSymbols are only supported on macOS 11.0 and up."); } }) }) diff --git a/src/image/mod.rs b/src/image/mod.rs index 794e6f2..fe0ab9d 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -110,6 +110,15 @@ impl ImageView { let _: () = msg_send![&*self.objc, setImage:&*image.0]; } } + + pub fn set_hidden(&self, hidden: bool) { + unsafe { + let _: () = msg_send![&*self.objc, setHidden:match hidden { + true => YES, + false => NO + }]; + } + } } impl Layout for ImageView { diff --git a/src/input/mod.rs b/src/input/mod.rs index 21a58fd..9db00ad 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -61,9 +61,6 @@ mod ios; #[cfg(target_os = "ios")] use ios::{register_view_class, register_view_class_with_delegate}; -//mod controller; -//pub use controller::TextFieldController; - mod traits; pub use traits::TextFieldDelegate; @@ -199,16 +196,16 @@ impl TextField { /// Grabs the value from the textfield and returns it as an owned String. pub fn get_value(&self) -> String { - let value = NSString::wrap(unsafe { + let value = NSString::retain(unsafe { msg_send![&*self.objc, stringValue] }); - value.to_str().to_string() + value.to_string() } /// Call this to set the background color for the backing layer. pub fn set_background_color(&self, color: Color) { - let bg = color.into_platform_specific_color(); + let bg = color.to_objc(); unsafe { let cg: id = msg_send![bg, CGColor]; @@ -222,7 +219,7 @@ impl TextField { let s = NSString::new(text); unsafe { - let _: () = msg_send![&*self.objc, setStringValue:s.into_inner()]; + let _: () = msg_send![&*self.objc, setStringValue:&*s]; } } diff --git a/src/ios/app/mod.rs b/src/ios/app/mod.rs index f2d1f73..854d263 100644 --- a/src/ios/app/mod.rs +++ b/src/ios/app/mod.rs @@ -121,9 +121,9 @@ where let dl = register_app_delegate_class::(); let w = register_window_scene_delegate_class::(); - // This probably needs to be Arc>'d at some point, but this is still exploratory. let app_delegate = Box::new(delegate); let vendor = Box::new(scene_delegate_vendor); + unsafe { let delegate_ptr: *const T = &*app_delegate; APP_DELEGATE = delegate_ptr as usize; @@ -148,11 +148,11 @@ impl App { let args = std::env::args().map(|arg| CString::new(arg).unwrap() ).collect::>(); let c_args = args.iter().map(|arg| arg.as_ptr()).collect::>(); - let s = NSString::new("RSTApplication"); - let s2 = NSString::new("RSTAppDelegate"); + let s = NSString::no_copy("RSTApplication"); + let s2 = NSString::no_copy("RSTAppDelegate"); unsafe { - UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), s.into_inner(), s2.into_inner()); + UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), &*s, &*s2); } self.pool.drain(); diff --git a/src/ios/scene/enums.rs b/src/ios/scene/enums.rs index 6a6d91e..5aee1d8 100644 --- a/src/ios/scene/enums.rs +++ b/src/ios/scene/enums.rs @@ -13,18 +13,18 @@ pub enum SessionRole { //CarPlayApplication } -impl From for NSString { +impl From for NSString<'_> { fn from(role: SessionRole) -> Self { NSString::new(match role { - SessionRole::Application => "UIWindowSceneSessionRoleApplication", - SessionRole::ExternalDisplay => "UIWindowSceneSessionRoleExternalDisplay", + SessionRole::Application => NSString::no_copy("UIWindowSceneSessionRoleApplication"), + SessionRole::ExternalDisplay => NSString::no_copy("UIWindowSceneSessionRoleExternalDisplay"), //SessionRole::CarPlayApplication => "" }) } } -impl From for SessionRole { - fn from(value: NSString) -> Self { +impl From> for SessionRole { + fn from(value: NSString<'_>) -> Self { match value.to_str() { "UIWindowSceneSessionRoleApplication" => SessionRole::Application, "UIWindowSceneSessionRoleExternalDisplay" => SessionRole::ExternalDisplay, diff --git a/src/layout/constraint.rs b/src/layout/constraint.rs index 13de91a..b5e0fdf 100644 --- a/src/layout/constraint.rs +++ b/src/layout/constraint.rs @@ -8,7 +8,7 @@ use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::ShareId; -use crate::foundation::id; +use crate::foundation::{id, YES, NO}; /// 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 @@ -43,12 +43,12 @@ impl LayoutConstraint { /// Sets the offset for this constraint. pub fn offset>(self, offset: F) -> Self { let offset: f64 = offset.into(); - unsafe { let o = offset as CGFloat; let _: () = msg_send![&*self.constraint, setConstant:o]; } + LayoutConstraint { constraint: self.constraint, offset: offset, @@ -57,6 +57,27 @@ impl LayoutConstraint { } } + pub fn set_offset>(&self, offset: F) { + let offset: f64 = offset.into(); + + unsafe { + let o = offset as CGFloat; + let _: () = msg_send![&*self.constraint, setConstant:o]; + } + } + + /// Set whether this constraint is active or not. If you're doing this across a batch of + /// constraints, it's often more performant to batch-deactivate with + /// `LayoutConstraint::deactivate()`. + pub fn set_active(&self, active: bool) { + unsafe { + let _: () = msg_send![&*self.constraint, setActive:match active { + true => YES, + false => NO + }]; + } + } + /// Call this with your batch of constraints to activate them. // If you're astute, you'll note that, yes... this is kind of hacking around some // borrowing rules with how objc_id::Id/objc_id::ShareId works. In this case, to @@ -75,4 +96,15 @@ impl LayoutConstraint { let _: () = msg_send![class!(NSLayoutConstraint), activateConstraints:constraints]; } } + + pub fn deactivate(constraints: &[LayoutConstraint]) { + unsafe { + let ids: Vec<&Object> = constraints.into_iter().map(|constraint| { + &*constraint.constraint + }).collect(); + + let constraints: id = msg_send![class!(NSArray), arrayWithObjects:ids.as_ptr() count:ids.len()]; + let _: () = msg_send![class!(NSLayoutConstraint), deactivateConstraints:constraints]; + } + } } diff --git a/src/layout/dimension.rs b/src/layout/dimension.rs index 2ef6757..e23f6d4 100644 --- a/src/layout/dimension.rs +++ b/src/layout/dimension.rs @@ -3,13 +3,15 @@ use core_graphics::base::CGFloat; -use objc::{msg_send, sel, sel_impl}; +use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::ShareId; -use crate::foundation::id; +use crate::foundation::{id, nil, NSInteger}; use crate::layout::constraint::LayoutConstraint; +use super::attributes::{LayoutAttribute, LayoutRelation}; + /// A wrapper for `NSLayoutAnchor`. You should never be creating this yourself - it's more of a /// factory/helper for creating `LayoutConstraint` objects based on your views. #[derive(Clone, Debug, Default)] @@ -57,6 +59,18 @@ impl LayoutAnchorDimension { } } + /// Return a constraint greater than or equal to a constant value. + pub fn constraint_greater_than_or_equal_to_constant(&self, constant: f64) -> LayoutConstraint { + match &self.0 { + Some(from) => LayoutConstraint::new(unsafe { + let value = constant as CGFloat; + msg_send![*from, constraintGreaterThanOrEqualToConstant:value] + }), + + _ => { panic!("Attempted to create constraints with an uninitialized anchor!"); } + } + } + /// Return a constraint less than or equal to another dimension anchor. pub fn constraint_less_than_or_equal_to(&self, anchor_to: &LayoutAnchorDimension) -> LayoutConstraint { match (&self.0, &anchor_to.0) { @@ -67,4 +81,16 @@ impl LayoutAnchorDimension { _ => { panic!("Attempted to create constraints with an uninitialized anchor!"); } } } + + /// Return a constraint greater than or equal to a constant value. + pub fn constraint_less_than_or_equal_to_constant(&self, constant: f64) -> LayoutConstraint { + match &self.0 { + Some(from) => LayoutConstraint::new(unsafe { + let value = constant as CGFloat; + msg_send![*from, constraintLessThanOrEqualToConstant:value] + }), + + _ => { panic!("Attempted to create constraints with an uninitialized anchor!"); } + } + } } diff --git a/src/lib.rs b/src/lib.rs index 56a08b0..6f866f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ //#![deny(missing_docs)] //#![deny(missing_debug_implementations)] #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] +#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))] +#![cfg_attr(docsrs, feature(doc_cfg))] // Copyright 2019+, the Cacao developers. // See the COPYRIGHT file at the top-level directory of this distribution. @@ -8,8 +10,8 @@ //! # Cacao //! -//! This library provides safe Rust bindings for `AppKit` on macOS and (eventually) `UIKit` on iOS. It -//! tries to do so in a way that, if you've done programming for the framework before (in Swift or +//! This library provides safe Rust bindings for `AppKit` on macOS and (eventually) `UIKit` on iOS and tvOS. +//! It tries to do so in a way that, if you've done programming for the framework before (in Swift or //! Objective-C), will feel familiar. This is tricky in Rust due to the ownership model, but some //! creative coding and assumptions can get us pretty far. //! @@ -18,14 +20,14 @@ //! already fine for some apps. //! //! _Note that this crate relies on the Objective-C runtime. Interfacing with the runtime *requires* -//! unsafe blocks; this crate handles those unsafe interactions for you and provides a safe wrapper, +//! unsafe blocks; this crate handles those unsafe interactions for you and provides a mostly safe wrapper, //! but by using this crate you understand that usage of `unsafe` is a given and will be somewhat //! rampant for wrapped controls. This does _not_ mean you can't assess, review, or question unsafe //! usage - just know it's happening, and in large part it's not going away._ //! //! # Hello World //! -//! ```rust +//! ```rust,no_run //! use cacao::macos::app::{App, AppDelegate}; //! use cacao::macos::window::Window; //! @@ -48,23 +50,41 @@ //! ``` //! //! ## Initialization +//! //! Due to the way that AppKit and UIKit programs typically work, you're encouraged to do the bulk //! of your work starting from the `did_finish_launching()` method of your `AppDelegate`. This //! ensures the application has had time to initialize and do any housekeeping necessary behind the //! scenes. //! +//! Note that, in order for this framework to be useful, you must always elect one of the following +//! features: +//! +//! - `macos`: Implements macOS-specific APIs (menus, toolbars, windowing, and so on). +//! - `ios`: Implements iOS-specific APIs (scenes, navigation controllers, and so on). +//! - `tvos`: Implements tvOS-specific APIs. Currently barely implemented. +//! +//! The rest of the features in this framework attempt to expose a common API across the three +//! aforementioned feature platforms; if you need something else, you can often implement it +//! yourself by accessing the underlying `objc` property of a control and sending messages to it +//! directly. +//! //! ## Optional Features //! //! The following are a list of [Cargo features][cargo-features] that can be enabled or disabled. //! -//! - **cloudkit**: Links `CloudKit.framework` and provides some wrappers around CloudKit +//! - `cloudkit`: Links `CloudKit.framework` and provides some wrappers around CloudKit //! functionality. Currently not feature complete. -//! - **user-notifications**: Links `UserNotifications.framework` and provides functionality for +//! - `quicklook`: Links `QuickLook.framework` and offers methods for generating preview images for +//! files. +//! - `user-notifications`: Links `UserNotifications.framework` and provides functionality for //! emitting notifications on macOS and iOS. Note that this _requires_ your application be //! code-signed, and will not work without it. -//! - **webview**: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. -//! - **webview-downloading**: Enables downloading files from the `WebView` via a private -//! interface. This is not an App-Store-safe feature, so be aware of that before enabling. +//! - `webview`: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. +//! This feature is not supported on tvOS, as the platform has no webview control. +//! - `webview-downloading-macos`: Enables downloading files from the `WebView` via a private +//! interface. This is not an App-Store-safe feature, so be aware of that before enabling. This +//! feature is not supported on iOS (a user would handle downloads very differently) or tvOS +//! (there's no web browser there at all). //! //! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section @@ -74,20 +94,18 @@ pub use objc; pub use url; pub use lazy_static; -/// Until we figure out a better way to handle reusable views (i.e, the -/// "correct" way for a list view to work), just let the delegates pass -/// back the pointer and handle keeping the pools for themselves. -pub type Node = objc_id::ShareId; - #[cfg(feature = "macos")] +#[cfg_attr(docsrs, doc(cfg(feature = "macos")))] pub mod macos; #[cfg(feature = "ios")] +#[cfg_attr(docsrs, doc(cfg(feature = "ios")))] pub mod ios; pub mod button; -#[cfg(feature = "cloudkit")] +#[cfg(any(feature = "cloudkit", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudkit")))] pub mod cloudkit; pub mod color; @@ -112,15 +130,18 @@ pub mod switch; pub mod text; #[cfg(feature = "quicklook")] +#[cfg_attr(docsrs, doc(cfg(feature = "quicklook")))] pub mod quicklook; #[cfg(feature = "user-notifications")] +#[cfg_attr(docsrs, doc(cfg(feature = "user-notifications")))] pub mod user_notifications; pub mod user_activity; -pub(crate) mod utils; +pub mod utils; pub mod view; -#[cfg(feature = "webview")] +#[cfg(any(feature = "webview", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "webview")))] pub mod webview; diff --git a/src/listview/actions.rs b/src/listview/actions.rs index 0990cd7..4ed6063 100644 --- a/src/listview/actions.rs +++ b/src/listview/actions.rs @@ -47,9 +47,12 @@ impl RowAction { /// on your ListViewRow. /// /// Additional configuration can be done after initialization, if need be. + /// + /// These run on the main thread, as they're UI handlers - so we can avoid Send + Sync on + /// our definitions. pub fn new(title: &str, style: RowActionStyle, handler: F) -> Self where - F: Fn(RowAction, usize) + Send + Sync + 'static + F: Fn(RowAction, usize) + 'static { let title = NSString::new(title); let block = ConcreteBlock::new(move |action: id, row: NSUInteger| { @@ -65,7 +68,7 @@ impl RowAction { RowAction(unsafe { let cls = class!(NSTableViewRowAction); Id::from_ptr(msg_send![cls, rowActionWithStyle:style - title:title.into_inner() + title:&*title handler:block ]) }) @@ -76,13 +79,13 @@ impl RowAction { let title = NSString::new(title); unsafe { - let _: () = msg_send![&*self.0, setTitle:title.into_inner()]; + let _: () = msg_send![&*self.0, setTitle:&*title]; } } /// Sets the background color of this action. pub fn set_background_color(&mut self, color: Color) { - let color = color.into_platform_specific_color(); + let color = color.to_objc(); unsafe { let _: () = msg_send![&*self.0, setBackgroundColor:color]; diff --git a/src/listview/macos.rs b/src/listview/macos.rs index 4be6371..a5f1674 100644 --- a/src/listview/macos.rs +++ b/src/listview/macos.rs @@ -14,7 +14,8 @@ use objc::runtime::{Class, Object, Sel, BOOL}; use objc::{class, sel, sel_impl, msg_send}; use objc_id::Id; -use crate::foundation::{load_or_register_class, id, YES, NO, NSArray, NSInteger, NSUInteger}; +use crate::macos::menu::{Menu, MenuItem}; +use crate::foundation::{load_or_register_class, id, nil, YES, NO, NSArray, NSInteger, NSUInteger}; use crate::dragdrop::DragInfo; use crate::listview::{ LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR, @@ -57,6 +58,45 @@ extern fn view_for_column( } } +extern fn will_display_cell( + this: &Object, + _: Sel, + _table_view: id, + _cell: id, + _column: id, + item: NSInteger +) { + let view = load::(this, LISTVIEW_DELEGATE_PTR); + view.will_display_item(item as usize); +} + +extern fn menu_needs_update( + this: &Object, + _: Sel, + menu: id +) { + let view = load::(this, LISTVIEW_DELEGATE_PTR); + let items = view.context_menu(); + let _ = Menu::append(menu, items); +} + +/// 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 +/// simply assume things are selectable and call our delegate as if things were selected. This may +/// need to change in the future, but it works well enough for now. +extern fn select_row( + this: &Object, + _: Sel, + _table_view: id, + item: NSInteger +) -> BOOL { + let view = load::(this, LISTVIEW_DELEGATE_PTR); + view.item_selected(item as usize); + YES +} + extern fn row_actions_for_row( this: &Object, _: Sel, @@ -67,14 +107,13 @@ extern fn row_actions_for_row( let edge: RowEdge = edge.into(); let view = load::(this, LISTVIEW_DELEGATE_PTR); - let actions = view.actions_for(row as usize, edge); + let mut ids: NSArray = view.actions_for(row as usize, edge) + .iter_mut() + .map(|action| &*action.0) + .collect::>() + .into(); - //if actions.len() > 0 { - let ids: Vec<&Object> = actions.iter().map(|action| &*action.0).collect(); - NSArray::from(ids).into_inner() - //} else { - // NSArray::new(&[]).into_inner() - //} + &mut *ids } /// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. @@ -164,9 +203,16 @@ pub(crate) fn register_listview_class_with_delegate(instanc // Tableview-specific 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!(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 + // whole delegate run, since things are fast enough nowadays to just replace the entire + // menu. + decl.add_method(sel!(menuNeedsUpdate:), menu_needs_update:: as extern fn(&Object, _, id)); + // Drag and drop operations (e.g, accepting files) decl.add_method(sel!(draggingEntered:), dragging_entered:: as extern fn (&mut Object, _, _) -> NSUInteger); decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation:: as extern fn (&mut Object, _, _) -> BOOL); diff --git a/src/listview/mod.rs b/src/listview/mod.rs index df81b91..f7aa70f 100644 --- a/src/listview/mod.rs +++ b/src/listview/mod.rs @@ -48,13 +48,16 @@ use objc_id::ShareId; use objc::runtime::{Class, Object}; use objc::{class, msg_send, sel, sel_impl}; -use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSUInteger}; +use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSInteger, NSUInteger}; use crate::color::Color; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::pasteboard::PasteboardType; use crate::scrollview::ScrollView; use crate::utils::CGSize; +#[cfg(target_os = "macos")] +use crate::macos::menu::MenuItem; + #[cfg(target_os = "macos")] mod macos; @@ -148,21 +151,24 @@ fn common_init(class: *const Class) -> id { // Let's... make NSTableView into UITableView-ish. #[cfg(target_os = "macos")] { + // @TODO: Clean this up in a dealloc method. + let menu: id = msg_send![class!(NSMenu), new]; + let _: () = msg_send![menu, setDelegate:tableview]; + let _: () = msg_send![tableview, setMenu:menu]; + let _: () = msg_send![tableview, setWantsLayer:YES]; let _: () = msg_send![tableview, setUsesAutomaticRowHeights:YES]; let _: () = msg_send![tableview, setFloatsGroupRows:YES]; - let _: () = msg_send![tableview, setIntercellSpacing:CGSize::new(0., 0.)]; + //let _: () = msg_send![tableview, setIntercellSpacing:CGSize::new(0., 0.)]; let _: () = msg_send![tableview, setColumnAutoresizingStyle:1]; //msg_send![tableview, setSelectionHighlightStyle:-1]; - let _: () = msg_send![tableview, setAllowsEmptySelection:YES]; - let _: () = msg_send![tableview, setAllowsMultipleSelection:NO]; + //let _: () = msg_send![tableview, setAllowsMultipleSelection:NO]; let _: () = msg_send![tableview, setHeaderView:nil]; // NSTableView requires at least one column to be manually added if doing so by code. - // A relic of a bygone era, indeed. - let identifier = NSString::new("CacaoListViewColumn"); + let identifier = NSString::no_copy("CacaoListViewColumn"); let default_column_alloc: id = msg_send![class!(NSTableColumn), new]; - let default_column: id = msg_send![default_column_alloc, initWithIdentifier:identifier.into_inner()]; + let default_column: id = msg_send![default_column_alloc, initWithIdentifier:&*identifier]; let _: () = msg_send![default_column, setResizingMask:(1<<0)]; let _: () = msg_send![tableview, addTableColumn:default_column]; } @@ -171,12 +177,63 @@ fn common_init(class: *const Class) -> id { } } +use objc_id::Id; + +#[derive(Clone, Debug)] +pub struct ObjcProperty(Rc>>); + +impl ObjcProperty { + pub fn new(obj: id) -> Self { + Self(Rc::new(RefCell::new(unsafe { + Id::from_ptr(obj) + }))) + } + + pub fn with(&self, handler: F) + where + F: Fn(&Object) + { + let borrow = self.0.borrow(); + handler(&borrow); + } +} + +#[derive(Debug, Default)] +pub struct PropertyNullable(Rc>>); + +impl PropertyNullable { + pub fn new(obj: T) -> Self { + Self(Rc::new(RefCell::new(Some(obj)))) + } + + pub fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } + + pub fn with(&self, handler: F) + where + F: Fn(&T) + { + let borrow = self.0.borrow(); + if let Some(s) = &*borrow { + handler(s); + } + } + + pub fn set(&self, obj: T) { + let mut borrow = self.0.borrow_mut(); + *borrow = Some(obj); + } +} + #[derive(Debug)] pub struct ListView { /// Internal map of cell identifers/vendors. These are used for handling dynamic cell /// allocation and reuse, which is necessary for an "infinite" listview. cell_factory: CellFactory, + pub menu: PropertyNullable>, + /// A pointer to the Objective-C runtime view controller. pub objc: ShareId, @@ -245,6 +302,7 @@ impl ListView { ListView { cell_factory: CellFactory::new(), + menu: PropertyNullable::default(), delegate: None, top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), @@ -302,6 +360,7 @@ impl ListView where T: ListViewDelegate + 'static { let mut view = ListView { cell_factory: cell, + menu: PropertyNullable::default(), delegate: None, top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), @@ -331,6 +390,7 @@ impl ListView { pub(crate) fn clone_as_handle(&self) -> ListView { ListView { cell_factory: CellFactory::new(), + menu: self.menu.clone(), delegate: None, top: self.top.clone(), leading: self.leading.clone(), @@ -361,8 +421,8 @@ impl ListView { pub fn dequeue(&self, identifier: &'static str) -> ListViewRow { #[cfg(target_os = "macos")] unsafe { - let key = NSString::new(identifier).into_inner(); - let cell: id = msg_send![&*self.objc, makeViewWithIdentifier:key owner:nil]; + let key = NSString::new(identifier); + let cell: id = msg_send![&*self.objc, makeViewWithIdentifier:&*key owner:nil]; if cell != nil { ListViewRow::from_cached(cell) @@ -386,9 +446,46 @@ impl ListView { } } + /// Style + pub fn set_style(&self, style: crate::foundation::NSInteger) { + unsafe { + let _: () = msg_send![&*self.objc, setStyle:style]; + } + } + + pub fn set_allows_empty_selection(&self, allows: bool) { + unsafe { + let _: () = msg_send![&*self.objc, setAllowsEmptySelection:match allows { + true => YES, + false => NO + }]; + } + } + + pub fn set_selection_highlight_style(&self, style: crate::foundation::NSInteger) { + unsafe { + let _: () = msg_send![&*self.objc, setSelectionHighlightStyle:style]; + } + } + + pub fn select_row_indexes(&self, indexes: &[usize], extends_existing: bool) { + unsafe { + let index_set: id = msg_send![class!(NSMutableIndexSet), new]; + + for index in indexes { + let _: () = msg_send![index_set, addIndex:index]; + } + + let _: () = msg_send![&*self.objc, selectRowIndexes:index_set byExtendingSelection:match extends_existing { + true => YES, + false => NO + }]; + } + } + pub fn perform_batch_updates(&self, update: F) { #[cfg(target_os = "macos")] - unsafe { + unsafe { let _: () = msg_send![&*self.objc, beginUpdates]; let handle = self.clone_as_handle(); @@ -398,13 +495,13 @@ impl ListView { } } - pub fn insert_rows>(&self, indexes: I, animation: RowAnimation) { + pub fn insert_rows(&self, indexes: &[usize], animation: RowAnimation) { #[cfg(target_os = "macos")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; for index in indexes { - let x: NSUInteger = index as NSUInteger; + let x: NSUInteger = *index as NSUInteger; let _: () = msg_send![index_set, addIndex:x]; } @@ -435,13 +532,13 @@ impl ListView { } } - pub fn remove_rows>(&self, indexes: I, animations: RowAnimation) { + pub fn remove_rows(&self, indexes: &[usize], animations: RowAnimation) { #[cfg(target_os = "macos")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; for index in indexes { - let x: NSUInteger = index as NSUInteger; + let x: NSUInteger = *index as NSUInteger; let _: () = msg_send![index_set, addIndex:x]; } @@ -507,11 +604,11 @@ impl ListView { let types: NSArray = types.into_iter().map(|t| { // This clone probably doesn't need to be here, but it should also be cheap as // this is just an enum... and this is not an oft called method. - let x: NSString = t.clone().into(); - x.into_inner() + let x: NSString = (*t).into(); + x.into() }).collect::>().into(); - let _: () = msg_send![&*self.objc, registerForDraggedTypes:types.into_inner()]; + let _: () = msg_send![&*self.objc, registerForDraggedTypes:&*types]; } } @@ -520,6 +617,14 @@ impl ListView { let _: () = msg_send![&*self.objc, reloadData]; } } + + pub fn get_selected_row_index(&self) -> NSInteger { + unsafe { msg_send![&*self.objc, selectedRow] } + } + + pub fn get_clicked_row_index(&self) -> NSInteger { + unsafe { msg_send![&*self.objc, clickedRow] } + } } impl Layout for ListView { diff --git a/src/listview/row/macos.rs b/src/listview/row/macos.rs index d908b6a..2e936fa 100644 --- a/src/listview/row/macos.rs +++ b/src/listview/row/macos.rs @@ -14,9 +14,9 @@ use objc::runtime::{Class, Object, Sel, BOOL}; use objc::{class, msg_send, sel, sel_impl}; use objc_id::Id; -use crate::foundation::{id, YES, NO, NSUInteger}; +use crate::foundation::{id, nil, YES, NO, NSUInteger}; use crate::dragdrop::DragInfo; -use crate::listview::row::{LISTVIEW_ROW_DELEGATE_PTR, ViewDelegate}; +use crate::listview::row::{LISTVIEW_ROW_DELEGATE_PTR, BACKGROUND_COLOR, ViewDelegate}; use crate::utils::load; /// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. @@ -74,6 +74,19 @@ extern fn dragging_exited(this: &mut Object, _: Sel, info: id) }); } +/// Called for layer updates. +extern fn update_layer(this: &Object, _: Sel) { + unsafe { + let background_color: id = *this.get_ivar(BACKGROUND_COLOR); + + if background_color != nil { + let layer: id = msg_send![this, layer]; + let cg: id = msg_send![background_color, CGColor]; + let _: () = msg_send![layer, setBackgroundColor:cg]; + } + } +} + /// Normally, you might not want to do a custom dealloc override. However, reusable cells are /// tricky - since we "forget" them when we give them to the system, we need to make sure to do /// proper cleanup then the backing (cached) version is deallocated on the Objective-C side. Since @@ -121,8 +134,10 @@ pub(crate) fn register_listview_row_class_with_delegate() -> *c // A pointer to the "view controller" on the Rust side. It's expected that this doesn't // move. decl.add_ivar::(LISTVIEW_ROW_DELEGATE_PTR); + decl.add_ivar::(BACKGROUND_COLOR); decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL); + decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _)); // Drag and drop operations (e.g, accepting files) decl.add_method(sel!(draggingEntered:), dragging_entered:: as extern fn (&mut Object, _, _) -> NSUInteger); diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index 22f6fee..1221ee9 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -66,6 +66,7 @@ mod ios; #[cfg(target_os = "ios")] use ios::{register_listview_row_view_class, register_listview_row_class_with_delegate}; +pub(crate) static BACKGROUND_COLOR: &str = "alchemyBackgroundColor"; pub(crate) static LISTVIEW_ROW_DELEGATE_PTR: &str = "rstListViewRowDelegatePtr"; /// A helper method for instantiating view classes and applying default settings to them. @@ -264,23 +265,20 @@ impl ListViewRow { /// Sets the identifier, which enables cells to be reused and dequeued properly. pub fn set_identifier(&self, identifier: &'static str) { - let identifier = NSString::new(identifier).into_inner(); + let identifier = NSString::new(identifier); let objc = self.objc.borrow(); unsafe { - let _: () = msg_send![&**objc, setIdentifier:identifier]; + let _: () = msg_send![&**objc, setIdentifier:&*identifier]; } } /// Call this to set the background color for the backing layer. pub fn set_background_color(&self, color: Color) { - let bg = color.into_platform_specific_color(); + let mut objc = self.objc.borrow_mut(); - let objc = self.objc.borrow(); unsafe { - let cg: id = msg_send![bg, CGColor]; - let layer: id = msg_send![&**objc, layer]; - let _: () = msg_send![layer, setBackgroundColor:cg]; + (&mut **objc).set_ivar(BACKGROUND_COLOR, color.as_ref().to_objc()); } } @@ -290,12 +288,12 @@ impl ListViewRow { let types: NSArray = types.into_iter().map(|t| { // This clone probably doesn't need to be here, but it should also be cheap as // this is just an enum... and this is not an oft called method. - let x: NSString = t.clone().into(); - x.into_inner() + let x: NSString = (*t).into(); + x.into() }).collect::>().into(); let objc = self.objc.borrow(); - let _: () = msg_send![&**objc, registerForDraggedTypes:types.into_inner()]; + let _: () = msg_send![&**objc, registerForDraggedTypes:&*types]; } } } diff --git a/src/listview/traits.rs b/src/listview/traits.rs index dbe8ed0..1590ffa 100644 --- a/src/listview/traits.rs +++ b/src/listview/traits.rs @@ -1,6 +1,6 @@ //! Various traits used for Views. -use crate::Node; +use crate::macos::menu::MenuItem; use crate::dragdrop::{DragInfo, DragOperation}; use crate::listview::{ListView, ListViewRow, RowAction, RowEdge}; use crate::layout::Layout; @@ -27,11 +27,22 @@ pub trait ListViewDelegate { /// Returns the number of items in the list view. fn number_of_items(&self) -> usize; + /// Called when an item will be displayed. + fn will_display_item(&self, row: usize) {} + /// This is temporary and you should not rely on this signature if you /// choose to try and work with this. NSTableView & such associated delegate patterns /// are tricky to support in Rust, and while I have a few ideas about them, I haven't /// had time to sit down and figure them out properly yet. fn item_for(&self, row: usize) -> ListViewRow; + + /// Called when an item has been selected (clicked/tapped on). + fn item_selected(&self, row: usize) {} + + /// 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 + /// work in here and return the menu as fast as possible. + fn context_menu(&self) -> Vec { vec![] } /// An optional delegate method; implement this if you'd like swipe-to-reveal to be /// supported for a given row by returning a vector of actions to show. diff --git a/src/macos/alert.rs b/src/macos/alert.rs index f481a37..ea12ec9 100644 --- a/src/macos/alert.rs +++ b/src/macos/alert.rs @@ -32,6 +32,7 @@ use crate::foundation::{id, NSString}; /// Represents an `NSAlert`. Has no information other than the retained pointer to the Objective C /// side, so... don't bother inspecting this. +#[derive(Debug)] pub struct Alert(Id); impl Alert { @@ -40,13 +41,13 @@ impl Alert { pub fn new(title: &str, message: &str) -> Self { let title = NSString::new(title); let message = NSString::new(message); - let x = NSString::new("OK"); + let ok = NSString::new("OK"); Alert(unsafe { let alert: id = msg_send![class!(NSAlert), new]; let _: () = msg_send![alert, setMessageText:title]; let _: () = msg_send![alert, setInformativeText:message]; - let _: () = msg_send![alert, addButtonWithTitle:x]; + let _: () = msg_send![alert, addButtonWithTitle:ok]; Id::from_ptr(alert) }) } diff --git a/src/macos/app/delegate.rs b/src/macos/app/delegate.rs index 01b66b8..7e39a17 100644 --- a/src/macos/app/delegate.rs +++ b/src/macos/app/delegate.rs @@ -112,9 +112,10 @@ extern fn should_handle_reopen(this: &Object, _: Sel, _: id, has } /// Fires when the application delegate receives a `applicationDockMenu:` request. +// @TODO: Make this return Vec. extern fn dock_menu(this: &Object, _: Sel, _: id) -> id { match app::(this).dock_menu() { - Some(mut menu) => &mut *menu.inner, + Some(mut menu) => &mut *menu.0, None => nil } } @@ -133,7 +134,7 @@ extern fn did_change_screen_parameters(this: &Object, _: Sel, _: /// Fires when the application receives a `application:willContinueUserActivityWithType:` /// notification. extern fn will_continue_user_activity_with_type(this: &Object, _: Sel, _: id, activity_type: id) -> BOOL { - let activity = NSString::wrap(activity_type); + let activity = NSString::retain(activity_type); match app::(this).will_continue_user_activity(activity.to_str()) { true => YES, @@ -144,7 +145,7 @@ extern fn will_continue_user_activity_with_type(this: &Object, _ /// Fires when the application receives a `application:continueUserActivity:restorationHandler:` notification. extern fn continue_user_activity(this: &Object, _: Sel, _: id, activity: id, handler: id) -> BOOL { // @TODO: This needs to support restorable objects, but it involves a larger question about how - // much `NSObject` wrapping we want to do here. For now, pass the handler for whenever it's + // much `NSObject` retainping we want to do here. For now, pass the handler for whenever it's // useful. let activity = UserActivity::with_inner(activity); @@ -161,7 +162,7 @@ extern fn continue_user_activity(this: &Object, _: Sel, _: id, a /// `application:didFailToContinueUserActivityWithType:error:` message. extern fn failed_to_continue_user_activity(this: &Object, _: Sel, _: id, activity_type: id, error: id) { app::(this).failed_to_continue_user_activity( - NSString::wrap(activity_type).to_str(), + NSString::retain(activity_type).to_str(), Error::new(error) ); } @@ -197,8 +198,8 @@ extern fn accepted_cloudkit_share(this: &Object, _: Sel, _: id, /// Fires when the application receives an `application:openURLs` message. extern fn open_urls(this: &Object, _: Sel, _: id, file_urls: id) { - let urls = NSArray::wrap(file_urls).map(|url| { - let uri = NSString::wrap(unsafe { + let urls = NSArray::retain(file_urls).map(|url| { + let uri = NSString::retain(unsafe { msg_send![url, absoluteString] }); @@ -210,7 +211,7 @@ extern fn open_urls(this: &Object, _: Sel, _: id, file_urls: id) /// Fires when the application receives an `application:openFileWithoutUI:` message. extern fn open_file_without_ui(this: &Object, _: Sel, _: id, file: id) -> BOOL { - let filename = NSString::wrap(file); + let filename = NSString::retain(file); match app::(this).open_file_without_ui(filename.to_str()) { true => YES, @@ -236,7 +237,7 @@ extern fn open_untitled_file(this: &Object, _: Sel, _: id) -> BO /// Fired when the application receives an `application:openTempFile:` message. extern fn open_temp_file(this: &Object, _: Sel, _: id, filename: id) -> BOOL { - let filename = NSString::wrap(filename); + let filename = NSString::retain(filename); match app::(this).open_temp_file(filename.to_str()) { true => YES, @@ -246,7 +247,7 @@ extern fn open_temp_file(this: &Object, _: Sel, _: id, filename: /// Fired when the application receives an `application:printFile:` message. extern fn print_file(this: &Object, _: Sel, _: id, file: id) -> BOOL { - let filename = NSString::wrap(file); + let filename = NSString::retain(file); match app::(this).print_file(filename.to_str()) { true => YES, @@ -257,8 +258,8 @@ extern fn print_file(this: &Object, _: Sel, _: id, file: id) -> /// Fired when the application receives an `application:printFiles:withSettings:showPrintPanels:` /// message. extern fn print_files(this: &Object, _: Sel, _: id, files: id, settings: id, show_print_panels: BOOL) -> NSUInteger { - let files = NSArray::wrap(files).map(|file| { - NSString::wrap(file).to_str().to_string() + let files = NSArray::retain(files).map(|file| { + NSString::retain(file).to_str().to_string() }); let settings = PrintSettings::with_inner(settings); @@ -275,7 +276,7 @@ extern fn did_change_occlusion_state(this: &Object, _: Sel, _: i /// Note: this may not fire in sandboxed applications. Apple's documentation is unclear on the /// matter. extern fn delegate_handles_key(this: &Object, _: Sel, _: id, key: id) -> BOOL { - let key = NSString::wrap(key); + let key = NSString::retain(key); match app::(this).delegate_handles_key(key.to_str()) { true => YES, diff --git a/src/macos/app/mod.rs b/src/macos/app/mod.rs index 938afe4..784af61 100644 --- a/src/macos/app/mod.rs +++ b/src/macos/app/mod.rs @@ -82,9 +82,9 @@ pub(crate) static APP_PTR: &str = "rstAppPtr"; // Calls to App::set_menu() will reconfigure the contents of this Vec, so that's also // the easiest way to remove things as necessary - just rebuild your menu. Trying to debug // and/or predict NSMenu is a whole task that I'm not even sure is worth trying to do. -lazy_static! { +/*lazy_static! { static ref MENU_ITEMS_HANDLER_CACHE: Arc>> = Arc::new(Mutex::new(Vec::new())); -} +}*/ /// A handler to make some boilerplate less annoying. #[inline] @@ -257,36 +257,21 @@ impl App { /// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind /// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and /// you shouldn't bother to either. - /// - /// The one note here: we internally cache actions to avoid them dropping without the - /// Objective-C side knowing. The the comments on the pub fn set_menu(mut menus: Vec) { - let mut handlers = vec![]; - let main_menu = unsafe { let menu_cls = class!(NSMenu); let item_cls = class!(NSMenuItem); let main_menu: id = msg_send![menu_cls, new]; for menu in menus.iter_mut() { - handlers.append(&mut menu.actions); - let item: id = msg_send![item_cls, new]; - let _: () = msg_send![item, setSubmenu:&*menu.inner]; + let _: () = msg_send![item, setSubmenu:&*menu.0]; let _: () = msg_send![main_menu, addItem:item]; } main_menu }; - // Cache our menu handlers, whatever they may be - since we're replacing the - // existing menu, and macOS only has one menu on screen at a time, we can go - // ahead and blow away the old ones. - { - let mut cache = MENU_ITEMS_HANDLER_CACHE.lock().unwrap(); - *cache = handlers; - } - shared_application(move |app| unsafe { let _: () = msg_send![app, setMainMenu:main_menu]; }); diff --git a/src/macos/event/mod.rs b/src/macos/event/mod.rs index 14bb7f2..3ec77f7 100644 --- a/src/macos/event/mod.rs +++ b/src/macos/event/mod.rs @@ -32,11 +32,11 @@ impl Event { // @TODO: Check here if key event, invalid otherwise. // @TODO: Figure out if we can just return &str here, since the Objective-C side // should... make it work, I think. - let characters = NSString::wrap(unsafe { + let characters = NSString::from_retained(unsafe { msg_send![&*self.0, characters] }); - characters.to_str().to_string() + characters.to_string() } /*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool { diff --git a/src/macos/menu/item.rs b/src/macos/menu/item.rs index a623e6d..8633040 100644 --- a/src/macos/menu/item.rs +++ b/src/macos/menu/item.rs @@ -2,24 +2,38 @@ //! one level deep; this could change in the future but is fine for //! now. -use objc::{class, msg_send, sel, sel_impl}; -use objc::runtime::{Object, Sel}; -use objc_id::ShareId; use std::sync::Once; +use block::ConcreteBlock; +use objc::{class, msg_send, sel, sel_impl}; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc_id::Id; + use crate::foundation::{id, nil, NSString, NSUInteger}; use crate::events::EventModifierFlag; -use crate::invoker::TargetActionHandler; + +static BLOCK_PTR: &'static str = "cacaoMenuItemBlockPtr"; + +/// An Action is just an indirection layer to get around Rust and optimizing +/// zero-sum types; without this, pointers to callbacks will end up being +/// 0x1, and all point to whatever is there first (unsure if this is due to +/// Rust or Cocoa or what). +/// +/// Point is, Button aren't created that much in the grand scheme of things, +/// and the heap isn't our enemy in a GUI framework anyway. If someone knows +/// a better way to do this that doesn't require double-boxing, I'm all ears. +pub struct Action(Box); /// Internal method (shorthand) for generating `NSMenuItem` holders. -fn make_menu_item( - title: &str, +fn make_menu_item>( + title: S, key: Option<&str>, action: Option, modifiers: Option<&[EventModifierFlag]> -) -> MenuItem { +) -> Id { unsafe { - let title = NSString::new(title); + let title = NSString::new(title.as_ref()); // Note that AppKit requires a blank string if nil, not nil. let key = NSString::new(match key { @@ -27,10 +41,16 @@ fn make_menu_item( None => "" }); - let alloc: id = msg_send![class!(NSMenuItem), alloc]; - let item = ShareId::from_ptr(match action { - Some(a) => msg_send![alloc, initWithTitle:title action:a keyEquivalent:key], - None => msg_send![alloc, initWithTitle:title action:nil keyEquivalent:key] + // Stock menu items that use selectors targeted at system pieces are just standard + // `NSMenuItem`s. If there's no custom ones, we use our subclass that has a slot to store a + // handler pointer. + let alloc: id = msg_send![register_menu_item_class(), alloc]; + let item = Id::from_retained_ptr(match action { + Some(a) => msg_send![alloc, initWithTitle:&*title action:a keyEquivalent:&*key], + + None => msg_send![alloc, initWithTitle:&*title + action:sel!(fireBlockAction:) + keyEquivalent:&*key] }); if let Some(modifiers) = modifiers { @@ -44,169 +64,262 @@ fn make_menu_item( let _: () = msg_send![&*item, setKeyEquivalentModifierMask:key_mask]; } - MenuItem::Entry((item, None)) + item } } -/// Represents varying `NSMenuItem` types - e.g, a separator vs an action. +/// Represents varying `NSMenuItem` types - e.g, a separator vs an action. If you need something +/// outside of the stock item types, you can create a `Custom` variant that supports dispatching a +/// callback on the Rust side of things. #[derive(Debug)] pub enum MenuItem { - /// Represents a Menu item that's not a separator - for all intents and purposes, you can consider - /// this the real `NSMenuItem`. - Entry((ShareId, Option)), + /// A custom MenuItem. This type functions as a builder, so you can customize it easier. + /// You can (and should) create this variant via the `new(title)` method, but if you need to do + /// something crazier, then wrap it in this and you can hook into the Cacao menu system + /// accordingly. + Custom(Id), - /// Represents a Separator. You can't do anything with this, but it's useful nonetheless for + /// Shows a standard "About" item, which will bring up the necessary window when clicked + /// (include a `credits.html` in your App to make use of here). The argument baked in here + /// should be your app name. + About(String), + + /// A standard "hide the app" menu item. + Hide, + + /// A standard "Services" menu item. + Services, + + /// A "hide all other windows" menu item. + HideOthers, + + /// A menu item to show all the windows for this app. + ShowAll, + + /// Close the current window. + CloseWindow, + + /// A "quit this app" menu icon. + Quit, + + /// A menu item for enabling copying (often text) from responders. + Copy, + + /// A menu item for enabling cutting (often text) from responders. + Cut, + + /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle + /// of events. + Undo, + + /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle + /// of events. + Redo, + + /// A menu item for selecting all (often text) from responders. + SelectAll, + + /// A menu item for pasting (often text) into responders. + Paste, + + /// A standard "enter full screen" item. + EnterFullScreen, + + /// An item for minimizing the window with the standard system controls. + Minimize, + + /// An item for instructing the app to zoom. Your app must react to this with necessary window + /// lifecycle events. + Zoom, + + /// An item for automatically telling a SplitViewController to hide or show the sidebar. This + /// only works on macOS 11.0+. + ToggleSidebar, + + /// Represents a Separator. It's useful nonetheless for /// separating out pieces of the `NSMenu` structure. Separator } impl MenuItem { - /// Creates and returns a `MenuItem::Entry` with the specified title. - pub fn entry(title: &str) -> Self { - make_menu_item(title, None, None, None) - } - - /// Configures the menu item, if it's not a separator, to support a key equivalent. - pub fn key(self, key: &str) -> Self { + /// Consumes and returns a handle for the underlying MenuItem. This is internal as we make a few assumptions + /// for how it interacts with our `Menu` setup, but this could be made public in the future. + pub(crate) unsafe fn to_objc(self) -> Id { match self { - MenuItem::Separator => MenuItem::Separator, - - MenuItem::Entry((item, action)) => { - unsafe { - let key = NSString::new(key); - let _: () = msg_send![&*item, setKeyEquivalent:key]; - } - - MenuItem::Entry((item, action)) - } - } - } - - /// Attaches a target/action handler to dispatch events. - pub fn action(self, action: F) -> Self { - match self { - MenuItem::Separator => MenuItem::Separator, - - MenuItem::Entry((item, _)) => { - let action = TargetActionHandler::new(&*item, action); - MenuItem::Entry((item, Some(action))) - } - } - } - - /// Returns a standard "About" item. - pub fn about(name: &str) -> Self { - let title = format!("About {}", name); - make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None) - } - - /// Returns a standard "Hide" item. - pub fn hide() -> Self { - make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None) - } - - /// Returns the standard "Services" item. This one does some extra work to link in the default - /// Services submenu. - pub fn services() -> Self { - match make_menu_item("Services", None, None, None) { - // Link in the services menu, which is part of NSApp - MenuItem::Entry((item, action)) => { - unsafe { - let app: id = msg_send![class!(RSTApplication), sharedApplication]; - let services: id = msg_send![app, servicesMenu]; - let _: () = msg_send![&*item, setSubmenu:services]; - } - - MenuItem::Entry((item, action)) + Self::Custom(objc) => objc, + + Self::About(app_name) => { + let title = format!("About {}", app_name); + make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None) }, - // Should never be hit - MenuItem::Separator => MenuItem::Separator + Self::Hide => make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None), + + // This one is a bit tricky to do right, as we need to expose a submenu, which isn't + // supported by MenuItem yet. + Self::Services => { + let item = make_menu_item("Services", None, None, None); + let app: id = msg_send![class!(RSTApplication), sharedApplication]; + let services: id = msg_send![app, servicesMenu]; + let _: () = msg_send![&*item, setSubmenu:services]; + item + }, + + Self::HideOthers => make_menu_item( + "Hide Others", + Some("h"), + Some(sel!(hide:)), + Some(&[EventModifierFlag::Command, EventModifierFlag::Option]) + ), + + Self::ShowAll => make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None), + Self::CloseWindow => make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None), + Self::Quit => make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None), + Self::Copy => make_menu_item("Copy", Some("c"), Some(sel!(copy:)), None), + Self::Cut => make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None), + Self::Undo => make_menu_item("Undo", Some("z"), Some(sel!(undo:)), None), + Self::Redo => make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None), + Self::SelectAll => make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None), + Self::Paste => make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None), + + Self::EnterFullScreen => make_menu_item( + "Enter Full Screen", + Some("f"), + Some(sel!(toggleFullScreen:)), + Some(&[EventModifierFlag::Command, EventModifierFlag::Control]) + ), + + Self::Minimize => make_menu_item("Minimize", Some("m"), Some(sel!(performMiniaturize:)), None), + Self::Zoom => make_menu_item("Zoom", None, Some(sel!(performZoom:)), None), + + Self::ToggleSidebar => make_menu_item( + "Toggle Sidebar", + Some("s"), + Some(sel!(toggleSidebar:)), + Some(&[EventModifierFlag::Command, EventModifierFlag::Option]) + ), + + Self::Separator => { + let cls = class!(NSMenuItem); + let separator: id = msg_send![cls, separatorItem]; + Id::from_ptr(separator) + } } } - - /// Returns a standard "Hide" item. - pub fn hide_others() -> Self { - make_menu_item( - "Hide Others", - Some("h"), - Some(sel!(hide:)), - Some(&[EventModifierFlag::Command, EventModifierFlag::Option]) - ) + + /// Returns a `Custom` menu item, with the given title. You can configure this further with the + /// builder methods on this object. + pub fn new>(title: S) -> Self { + MenuItem::Custom(make_menu_item(title, None, None, None)) } - /// Returns a standard "Hide" item. - pub fn show_all() -> Self { - make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None) + /// Configures the a custom item to have specified key equivalent. This does nothing if called + /// on a `MenuItem` type that is not `Custom`, + pub fn key(self, key: &str) -> Self { + if let MenuItem::Custom(objc) = self { + unsafe { + let key = NSString::new(key); + let _: () = msg_send![&*objc, setKeyEquivalent:key]; + } + + return MenuItem::Custom(objc); + } + + self } - /// Returns a standard "Close Window" item. - pub fn close_window() -> Self { - make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None) + /// Sets the modifier key flags for this menu item. This does nothing if called on a `MenuItem` + /// that is not `Custom`. + pub fn modifiers(self, modifiers: &[EventModifierFlag]) -> Self { + if let MenuItem::Custom(objc) = self { + let mut key_mask: NSUInteger = 0; + + for modifier in modifiers { + let y: NSUInteger = modifier.into(); + key_mask = key_mask | y; + } + + unsafe { + let _: () = msg_send![&*objc, setKeyEquivalentModifierMask:key_mask]; + } + + return MenuItem::Custom(objc); + } + + self } - /// Returns a standard "Quit" item. - pub fn quit() -> Self { - make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None) - } + /// Attaches a target/action handler to dispatch events. This does nothing if called on a + /// `MenuItem` that is not `Custom`. + /// + /// Note that we use an extra bit of unsafety here to pass over a heap'd block. We need to do + /// this as some menu items live in odd places (the system menu bar), and we need the handlers + /// to persist. We inject a custom dealloc method to pull the pointer back and drop the handler + /// whenever the menu item goes kaput. + pub fn action(self, action: F) -> Self { + if let MenuItem::Custom(mut objc) = self { + let handler = Box::new(Action(Box::new(action))); + let ptr = Box::into_raw(handler); + + unsafe { + (&mut *objc).set_ivar(BLOCK_PTR, ptr as usize); + let _: () = msg_send![&*objc, setTarget:&*objc]; + } - /// Returns a standard "Copy" item. - pub fn copy() -> Self { - make_menu_item("Copy", Some("c"), Some(sel!(copy:)), None) - } - - /// Returns a standard "Undo" item. - pub fn undo() -> Self { - make_menu_item("Undo", Some("z"), Some(sel!(undo:)), None) - } + return MenuItem::Custom(objc); + } - /// Returns a standard "Enter Full Screen" item - pub fn enter_full_screen() -> Self { - make_menu_item( - "Enter Full Screen", - Some("f"), - Some(sel!(toggleFullScreen:)), - Some(&[EventModifierFlag::Command, EventModifierFlag::Control]) - ) - } - - /// Returns a standard "Miniaturize" item - pub fn minimize() -> Self { - make_menu_item( - "Minimize", - Some("m"), - Some(sel!(performMiniaturize:)), - None - ) - } - - /// Returns a standard "Zoom" item - pub fn zoom() -> Self { - make_menu_item( - "Zoom", - None, - Some(sel!(performZoom:)), - None - ) - } - - /// Returns a standard "Redo" item. - pub fn redo() -> Self { - make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None) - } - - /// Returns a standard "Cut" item. - pub fn cut() -> Self { - make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None) - } - - /// Returns a standard "Select All" item. - pub fn select_all() -> Self { - make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None) - } - - /// Returns a standard "Paste" item. - pub fn paste() -> Self { - make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None) + self + } +} + +/// On the Objective-C side, we need to ensure our handler is dropped when this subclass +/// is deallocated. Note that NSMenuItem is seemingly odd outside of ARC contexts, and we +/// need to do some extra logic to ensure release calls are properly sent. +extern fn dealloc_cacao_menuitem(this: &Object, _: Sel) { + unsafe { + let ptr: usize = *this.get_ivar(BLOCK_PTR); + let obj = ptr as *mut Action; + + if !obj.is_null() { + let _handler = Box::from_raw(obj); + } + + // This should be fine to _not_ do, but considering we go out of our way to loop it back on + // itself, it's worth clearing out the slot. + //let _: () = msg_send![this, setTarget:nil]; + + let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; + } +} + +/// Called when our custom item needs to fire. +extern fn fire_block_action(this: &Object, _: Sel, _item: id) { + let action = crate::utils::load::(this, BLOCK_PTR); + (action.0)(); +} + +/// Injects a custom NSMenuItem subclass that contains a slot to hold a block, as well as a method +/// for calling the block. +/// +/// In general, we do not want to do more than we need to here - menus are one of the last areas +/// where Carbon still lurks, and subclassing things can get weird. +pub(crate) fn register_menu_item_class() -> *const Class { + static mut APP_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(NSMenuItem); + let mut decl = ClassDecl::new("CacaoMenuItem", superclass).unwrap(); + decl.add_ivar::(BLOCK_PTR); + + decl.add_method(sel!(dealloc), dealloc_cacao_menuitem as extern fn(&Object, _)); + decl.add_method(sel!(fireBlockAction:), fire_block_action as extern fn(&Object, _, id)); + + APP_CLASS = decl.register(); + }); + + unsafe { + APP_CLASS } } diff --git a/src/macos/menu/menu.rs b/src/macos/menu/menu.rs index 8d41a26..ee24b0f 100644 --- a/src/macos/menu/menu.rs +++ b/src/macos/menu/menu.rs @@ -6,17 +6,13 @@ use objc_id::{Id, ShareId}; use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; -use crate::foundation::{id, NSString}; +use crate::foundation::{id, NSInteger, NSString}; use crate::macos::menu::item::MenuItem; -use crate::invoker::TargetActionHandler; /// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting /// them throughout the application lifecycle. #[derive(Debug)] -pub struct Menu { - pub inner: Id, - pub actions: Vec -} +pub struct Menu(pub Id); impl Menu { /// Creates a new `Menu` with the given title, and uses the passed items as submenu items. @@ -29,41 +25,49 @@ impl Menu { /// to get the menu functioning. /// pub fn new(title: &str, items: Vec) -> Self { - let inner = unsafe { + Menu(unsafe { let cls = class!(NSMenu); let alloc: id = msg_send![cls, alloc]; let title = NSString::new(title); - let inner: id = msg_send![alloc, initWithTitle:title]; - Id::from_ptr(inner) - }; + let menu: id = msg_send![alloc, initWithTitle:&*title]; - let mut actions = vec![]; + for item in items.into_iter() { + let objc = item.to_objc(); + let _: () = msg_send![menu, addItem:&*objc]; + } - for item in items { - match item { - MenuItem::Entry((item, action)) => { - unsafe { - let _: () = msg_send![&*inner, addItem:item]; - } + Id::from_retained_ptr(menu) + }) + } - if action.is_some() { - actions.push(action.unwrap()); - } - }, + /// Given a set of `MenuItem`s, merges them into an existing Menu (e.g, for a context menu on a + /// view). + pub fn append(menu: id, items: Vec) -> id { + // You might look at the code below and wonder why we can't just call `removeAllItems`. + // + // Basically: that doesn't seem to properly decrement the retain count on the underlying + // menu item, and we wind up leaking any callbacks for the returned `MenuItem` instances. + // + // Walking them and calling release after removing them from the underlying store gives us + // the correct behavior. + unsafe { + let mut count: NSInteger = msg_send![menu, numberOfItems]; - MenuItem::Separator => { - unsafe { - let cls = class!(NSMenuItem); - let separator: id = msg_send![cls, separatorItem]; - let _: () = msg_send![&*inner, addItem:separator]; - } - } + while count != 0 { + count -= 1; + let item: id = msg_send![menu, itemAtIndex:count]; + let _: () = msg_send![menu, removeItemAtIndex:count]; + let _: () = msg_send![item, release]; } } - Menu { - inner: inner, - actions: actions + for item in items.into_iter() { + unsafe { + let objc = item.to_objc(); + let _: () = msg_send![menu, addItem:&*objc]; + } } + + menu } } diff --git a/src/macos/mod.rs b/src/macos/mod.rs index f24877e..f4ec45a 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -1,19 +1,9 @@ -//! Mac-specific implementations. +//! This module implements the core components necessary for making a well-formed macOS +//! application. These components are ones that are uniquely macOS-specific, and don't have a true +//! equivalent on iOS and tvOS as the interaction patterns are significantly different. //! -//! macOS is a much older system than iOS, and as a result has some... quirks, in addition to just -//! plain different APIs. It's tempting to want to find a common one and just implement that, but -//! unfortunately doing so erases a lot of control and finer points of the macOS platform. -//! -//! With that said, this framework makes attempts to make things mostly work as you'd expect them -//! to from the iOS-side of things, which means we wrap things like `NSView` and `NSTableView` and -//! so on to act like their iOS counterparts (we also layer-back everything by default, as it's -//! typically what you want). -//! -//! _However_, there are some specific things that just can't be wrapped well - for example, -//! `NSToolbar`. Yes, `UIToolbar` exists, but it's really not close to `NSToolbar` in functionality -//! at all. For controls like these, we surface them here - the goal is to enable you to write 90% -//! of your app as a cross platform codebase, with the initial 10% being scaffolding code for the -//! platform (e.g, NSApplication vs UIApplication lifecycle). +//! The coverage here is not exhaustive, but should be sufficient enough for relatively complex +//! applications. For examples, check the `examples` folder in the repository. mod alert; pub use alert::Alert; @@ -25,7 +15,7 @@ mod cursor; pub use cursor::{Cursor, CursorType}; mod enums; -pub use enums::{FocusRingType}; +pub use enums::FocusRingType; mod event; pub use event::*; diff --git a/src/macos/toolbar/class.rs b/src/macos/toolbar/class.rs index be24010..741afe6 100644 --- a/src/macos/toolbar/class.rs +++ b/src/macos/toolbar/class.rs @@ -6,7 +6,7 @@ use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel}; use objc::{class, sel, sel_impl, msg_send}; -use crate::foundation::{load_or_register_class, id, NSArray, NSString}; +use crate::foundation::{load_or_register_class, id, BOOL, NSArray, NSString}; use crate::macos::toolbar::{TOOLBAR_PTR, ToolbarDelegate}; use crate::utils::load; @@ -15,39 +15,49 @@ extern fn allowed_item_identifiers(this: &Object, _: Sel, _: let toolbar = load::(this, TOOLBAR_PTR); let identifiers: NSArray = toolbar.allowed_item_identifiers().iter().map(|identifier| { - NSString::new(identifier).into_inner() + identifier.to_nsstring() }).collect::>().into(); - identifiers.into_inner() + identifiers.into() } /// Retrieves and passes the default item identifiers for this toolbar. extern fn default_item_identifiers(this: &Object, _: Sel, _: id) -> id { let toolbar = load::(this, TOOLBAR_PTR); - let identifiers: NSArray = toolbar.default_item_identifiers().iter().map(|identifier| { - NSString::new(identifier).into_inner() - }).collect::>().into(); + let identifiers: NSArray = toolbar.default_item_identifiers() + .iter() + .map(|identifier| identifier.to_nsstring()) + .collect::>() + .into(); - identifiers.into_inner() + identifiers.into() } /// Retrieves and passes the default item identifiers for this toolbar. extern fn selectable_item_identifiers(this: &Object, _: Sel, _: id) -> id { let toolbar = load::(this, TOOLBAR_PTR); - let identifiers: NSArray = toolbar.selectable_item_identifiers().iter().map(|identifier| { - NSString::new(identifier).into_inner() - }).collect::>().into(); + let identifiers: NSArray = toolbar.selectable_item_identifiers() + .iter() + .map(|identifier| identifier.to_nsstring()) + .collect::>() + .into(); - identifiers.into_inner() + identifiers.into() } /// Loads the controller, grabs whatever item is for this identifier, and returns what the /// Objective-C runtime needs. -extern fn item_for_identifier(this: &Object, _: Sel, _: id, identifier: id, _: id) -> id { +extern fn item_for_identifier( + this: &Object, + _: Sel, + _: id, + identifier: id, + _: BOOL +) -> id { let toolbar = load::(this, TOOLBAR_PTR); - let identifier = NSString::wrap(identifier); + let identifier = NSString::from_retained(identifier); let item = toolbar.item_for(identifier.to_str()); unsafe { @@ -64,9 +74,21 @@ pub(crate) fn register_toolbar_class(instance: &T) -> *const decl.add_ivar::(TOOLBAR_PTR); // Add callback methods - decl.add_method(sel!(toolbarAllowedItemIdentifiers:), allowed_item_identifiers:: as extern fn(&Object, _, _) -> id); - decl.add_method(sel!(toolbarDefaultItemIdentifiers:), default_item_identifiers:: as extern fn(&Object, _, _) -> id); - decl.add_method(sel!(toolbarSelectableItemIdentifiers:), selectable_item_identifiers:: as extern fn(&Object, _, _) -> id); - decl.add_method(sel!(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:), item_for_identifier:: as extern fn(&Object, _, _, _, _) -> id); + decl.add_method( + sel!(toolbarAllowedItemIdentifiers:), + allowed_item_identifiers:: as extern fn(&Object, _, _) -> id + ); + decl.add_method( + sel!(toolbarDefaultItemIdentifiers:), + default_item_identifiers:: as extern fn(&Object, _, _) -> id + ); + decl.add_method( + sel!(toolbarSelectableItemIdentifiers:), + selectable_item_identifiers:: as extern fn(&Object, _, _) -> id + ); + decl.add_method( + sel!(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:), + item_for_identifier:: as extern fn(&Object, _, _, _, _) -> id + ); }) } diff --git a/src/macos/toolbar/enums.rs b/src/macos/toolbar/enums.rs index edd20fd..8ab0c1e 100644 --- a/src/macos/toolbar/enums.rs +++ b/src/macos/toolbar/enums.rs @@ -1,6 +1,6 @@ //! Various types used for Toolbar configuration. -use crate::foundation::NSUInteger; +use crate::foundation::{id, NSString, NSUInteger}; /// Represents the display mode(s) a Toolbar can render in. #[derive(Clone, Copy, Debug)] @@ -29,6 +29,86 @@ impl From for NSUInteger { } } +/// Represents an item identifier for items in a Toolbar. +#[derive(Clone, Debug)] +pub enum ItemIdentifier { + /// Represents a custom item. Use this when you need to handle your own item types. + Custom(&'static str), + + /// Represents a standard cloud-sharing icon. Available from 10.12 onwards. + CloudSharing, + + /// A flexible space identifier. Fills space, flexibly. + FlexibleSpace, + + /// A standard print toolbar item. Will send the necessary print calls to the first responder. + Print, + + /// A standard identifier for showing the colors panel. + Colors, + + /// A standard identifier for showing the fonts panel. + Fonts, + + /// A standard identifier for showing blank space. + Space, + + /// Standard toolbar item identifier for a sidebar. Will handle automatically hiding and + /// showing a SplitViewController sidebar if it's the window content view controller and the + /// first responder. + /// + /// Note that this API was introduced in Big Sur (11.0), and you may need to check against this + /// at runtime to ensure behavior is appropriate on older OS versions (if you support them). + ToggleSidebar, + + /// Standard toolbar item for a spot that tracks the sidebar border. In your delegate, use this + /// to indicate what items should be on the side of the sidebar and content. + /// + /// For example: + /// + /// ``` rust + /// vec![ItemIdentifier::ToggleSidebar, ItemIdentifier::SidebarTracker, ItemIdentifier::Print] + /// ``` + /// + /// Would result in the toggle sidebar item showing up in the sidebar on the left, and the + /// print item showing up in the content area on the right. + /// + /// Note that this API was introduced in Big Sur (11.0), and you may need to check against this + /// at runtime to ensure behavior is appropriate on older OS versions (if you support them). + /// + SidebarTracker +} + +extern "C" { + static NSToolbarToggleSidebarItemIdentifier: id; + static NSToolbarCloudSharingItemIdentifier: id; + static NSToolbarFlexibleSpaceItemIdentifier: id; + static NSToolbarPrintItemIdentifier: id; + static NSToolbarShowColorsItemIdentifier: id; + static NSToolbarShowFontsItemIdentifier: id; + static NSToolbarSpaceItemIdentifier: id; + static NSToolbarSidebarTrackingSeparatorItemIdentifier: id; +} + +impl ItemIdentifier { + /// Returns the NSString necessary for the toolbar to operate. + pub(crate) fn to_nsstring(&self) -> id { + unsafe { + match self { + Self::Custom(s) => NSString::new(s).into(), + Self::CloudSharing => NSToolbarCloudSharingItemIdentifier, + Self::FlexibleSpace => NSToolbarFlexibleSpaceItemIdentifier, + Self::Print => NSToolbarPrintItemIdentifier, + Self::Colors => NSToolbarShowColorsItemIdentifier, + Self::Fonts => NSToolbarShowFontsItemIdentifier, + Self::Space => NSToolbarSpaceItemIdentifier, + Self::ToggleSidebar => NSToolbarToggleSidebarItemIdentifier, + Self::SidebarTracker => NSToolbarSidebarTrackingSeparatorItemIdentifier + } + } + } +} + /// Represents the size mode a Toolbar can use. #[derive(Clone, Copy, Debug)] pub enum ToolbarSizeMode { diff --git a/src/macos/toolbar/item.rs b/src/macos/toolbar/item.rs index 7693691..57df07b 100644 --- a/src/macos/toolbar/item.rs +++ b/src/macos/toolbar/item.rs @@ -7,10 +7,10 @@ use std::fmt; use core_graphics::geometry::CGSize; use objc_id::{Id, ShareId}; -use objc::runtime::{Object}; +use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; -use crate::foundation::{id, NSString}; +use crate::foundation::{id, YES, NO, NSString}; use crate::invoker::TargetActionHandler; use crate::button::{Button, BezelStyle}; use crate::image::Image; @@ -39,8 +39,18 @@ impl ToolbarItem { }; ToolbarItem { - identifier: identifier, - objc: objc, + identifier, + objc, + button: None, + image: None, + handler: None + } + } + + pub(crate) fn wrap(item: id) -> Self { + ToolbarItem { + identifier: "".to_string(), + objc: unsafe { Id::from_retained_ptr(item) }, button: None, image: None, handler: None @@ -50,7 +60,7 @@ impl ToolbarItem { /// Sets the title for this item. pub fn set_title(&mut self, title: &str) { unsafe { - let title = NSString::new(title).into_inner(); + let title = NSString::new(title); let _: () = msg_send![&*self.objc, setLabel:&*title]; } } @@ -91,8 +101,18 @@ impl ToolbarItem { } } + /// Sets an action on this item. pub fn set_action(&mut self, action: F) { let handler = TargetActionHandler::new(&*self.objc, action); self.handler = Some(handler); } + + pub fn set_bordered(&self, bordered: bool) { + unsafe { + let _: () = msg_send![&*self.objc, setBordered:match bordered { + true => YES, + false => NO + }]; + } + } } diff --git a/src/macos/toolbar/mod.rs b/src/macos/toolbar/mod.rs index feb402d..8e3be37 100644 --- a/src/macos/toolbar/mod.rs +++ b/src/macos/toolbar/mod.rs @@ -19,7 +19,7 @@ mod traits; pub use traits::ToolbarDelegate; mod enums; -pub use enums::{ToolbarDisplayMode, ToolbarSizeMode}; +pub use enums::{ToolbarDisplayMode, ToolbarSizeMode, ItemIdentifier}; pub(crate) static TOOLBAR_PTR: &str = "rstToolbarPtr"; @@ -67,9 +67,9 @@ impl Toolbar where T: ToolbarDelegate + 'static { }); Toolbar { - identifier: identifier, - objc: objc, - objc_delegate: objc_delegate, + identifier, + objc, + objc_delegate, delegate: Some(delegate), } } @@ -117,10 +117,10 @@ impl Toolbar { /// Sets the item represented by the item identifier to be selected. pub fn set_selected(&self, item_identifier: &str) { - let identifier = NSString::new(item_identifier).into_inner(); + let identifier = NSString::new(item_identifier); unsafe { - let _: () = msg_send![&*self.objc, setSelectedItemIdentifier:identifier]; + let _: () = msg_send![&*self.objc, setSelectedItemIdentifier:&*identifier]; } } } diff --git a/src/macos/toolbar/traits.rs b/src/macos/toolbar/traits.rs index 66dbe45..22a559b 100644 --- a/src/macos/toolbar/traits.rs +++ b/src/macos/toolbar/traits.rs @@ -2,7 +2,7 @@ //! go. Currently a bit incomplete in that we don't support the customizing workflow, but feel free //! to pull request it. -use crate::macos::toolbar::{Toolbar, ToolbarItem}; +use crate::macos::toolbar::{Toolbar, ToolbarItem, ItemIdentifier}; /// A trait that you can implement to have your struct/etc act as an `NSToolbarDelegate`. pub trait ToolbarDelegate { @@ -23,14 +23,14 @@ pub trait ToolbarDelegate { fn did_load(&mut self, _toolbar: Toolbar) {} /// What items are allowed in this toolbar. - fn allowed_item_identifiers(&self) -> Vec<&'static str>; + fn allowed_item_identifiers(&self) -> Vec; /// The default items in this toolbar. - fn default_item_identifiers(&self) -> Vec<&'static str>; + fn default_item_identifiers(&self) -> Vec; /// The default items in this toolbar. This defaults to a blank `Vec`, and is an optional /// method - mostly useful for Preferences windows. - fn selectable_item_identifiers(&self) -> Vec<&'static str> { vec![] } + fn selectable_item_identifiers(&self) -> Vec { vec![] } /// For a given `identifier`, return the `ToolbarItem` that should be displayed. fn item_for(&self, _identifier: &str) -> &ToolbarItem; diff --git a/src/macos/window/config.rs b/src/macos/window/config.rs index 1d22b8d..2cc0a18 100644 --- a/src/macos/window/config.rs +++ b/src/macos/window/config.rs @@ -46,7 +46,7 @@ impl Default for WindowConfig { config.set_styles(&[ WindowStyle::Resizable, WindowStyle::Miniaturizable, WindowStyle::UnifiedTitleAndToolbar, - WindowStyle::Closable, WindowStyle::Titled + WindowStyle::Closable, WindowStyle::Titled, WindowStyle::FullSizeContentView ]); config diff --git a/src/macos/window/mod.rs b/src/macos/window/mod.rs index c27534e..1ee2925 100644 --- a/src/macos/window/mod.rs +++ b/src/macos/window/mod.rs @@ -229,6 +229,14 @@ impl Window { } } + /// Sets the content size for this window. + pub fn set_content_size>(&self, width: F, height: F) { + unsafe { + let size = CGSize::new(width.into(), height.into()); + let _: () = msg_send![&*self.objc, setContentSize:size]; + } + } + /// Sets the minimum size this window can shrink to. pub fn set_minimum_content_size>(&self, width: F, height: F) { unsafe { @@ -244,6 +252,14 @@ impl Window { let _: () = msg_send![&*self.objc, setContentMaxSize:size]; } } + + /// Sets the minimum size this window can shrink to. + pub fn set_minimum_size>(&self, width: F, height: F) { + unsafe { + let size = CGSize::new(width.into(), height.into()); + let _: () = msg_send![&*self.objc, setMinSize:size]; + } + } /// Used for setting a toolbar on this window. pub fn set_toolbar(&self, toolbar: &Toolbar) { @@ -429,6 +445,12 @@ impl Window { }]; } } + + pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { + unsafe { + let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; + } + } /// Returns the backing scale (e.g, `1.0` for non retina, `2.0` for retina) used on this /// window. diff --git a/src/networking/mod.rs b/src/networking/mod.rs index 010229b..b17087f 100644 --- a/src/networking/mod.rs +++ b/src/networking/mod.rs @@ -20,7 +20,7 @@ impl URLRequest { } pub fn url(&self) -> String { - NSString::wrap(unsafe { + NSString::from_retained(unsafe { let url: id = msg_send![&*self.inner, URL]; msg_send![url, absoluteString] }).to_string() diff --git a/src/notification_center/name.rs b/src/notification_center/name.rs index 65a6c15..6dcf155 100644 --- a/src/notification_center/name.rs +++ b/src/notification_center/name.rs @@ -1314,10 +1314,10 @@ pub enum NotificationName { WKAccessibilityReduceMotionStatusDidChange } -impl From for NSString { +impl From for NSString<'_> { fn from(name: NotificationName) -> Self { match name { - _ => NSString::new("") + _ => NSString::no_copy("") } } } diff --git a/src/pasteboard/mod.rs b/src/pasteboard/mod.rs index 5e1f39d..dd9a947 100644 --- a/src/pasteboard/mod.rs +++ b/src/pasteboard/mod.rs @@ -38,7 +38,7 @@ impl Pasteboard { pub fn named(name: PasteboardName) -> Self { Pasteboard(unsafe { let name: NSString = name.into(); - ShareId::from_ptr(msg_send![class!(NSPasteboard), pasteboardWithName:&*name.0]) + ShareId::from_ptr(msg_send![class!(NSPasteboard), pasteboardWithName:&*name]) }) } @@ -50,6 +50,16 @@ impl Pasteboard { }) } + /// A shorthand helper method for copying some text to the clipboard. + pub fn copy_text(&self, text: &str) { + let contents = NSString::new(text); + let ptype: NSString = PasteboardType::String.into(); + + unsafe { + let _: () = msg_send![&*self.0, setString:&*contents forType:ptype]; + } + } + /// Releases the receiver’s resources in the pasteboard server. It's rare-ish to need to use /// this, but considering this stuff happens on the Objective-C side you may need it. pub fn release_globally(&self) { @@ -85,8 +95,8 @@ impl Pasteboard { })); } - let urls = NSArray::wrap(contents).map(|url| { - let path = NSString::wrap(msg_send![url, path]); + 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(); @@ -114,8 +124,8 @@ impl Pasteboard { })); } - let urls = NSArray::wrap(contents).map(|url| { - let path = NSString::wrap(msg_send![url, path]).to_str().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(); diff --git a/src/pasteboard/types.rs b/src/pasteboard/types.rs index b369fa4..8b03b67 100644 --- a/src/pasteboard/types.rs +++ b/src/pasteboard/types.rs @@ -22,7 +22,7 @@ pub enum PasteboardName { Ruler } -impl From for NSString { +impl From for NSString<'_> { fn from(name: PasteboardName) -> Self { NSString::new(match name { PasteboardName::Drag => "Apple CFPasteboard drag", @@ -83,7 +83,7 @@ pub enum PasteboardType { TIFF } -impl From for NSString { +impl From for NSString<'_> { fn from(pboard_type: PasteboardType) -> Self { NSString::new(match pboard_type { PasteboardType::URL => "public.url", diff --git a/src/quicklook/config.rs b/src/quicklook/config.rs index 75b14b1..c4dc512 100644 --- a/src/quicklook/config.rs +++ b/src/quicklook/config.rs @@ -109,7 +109,7 @@ impl ThumbnailConfig { unsafe { let size = CGSize::new(self.size.0, self.size.1); // @TODO: Check nil here, or other bad conversion - let from_url: id = msg_send![class!(NSURL), fileURLWithPath:file.into_inner()]; + let from_url: id = msg_send![class!(NSURL), fileURLWithPath:&*file]; let request: id = msg_send![class!(QLThumbnailGenerationRequest), alloc]; let request: id = msg_send![request, initWithFileAtURL:from_url diff --git a/src/text/font.rs b/src/text/font.rs index 78886c2..ac350d2 100644 --- a/src/text/font.rs +++ b/src/text/font.rs @@ -19,7 +19,7 @@ impl Default for Font { objc: unsafe { let cls = class!(NSFont); let default_size: id = msg_send![cls, labelFontSize]; - msg_send![cls, labelFontOfSize:default_size] + ShareId::from_ptr(msg_send![cls, labelFontOfSize:default_size]) } } } @@ -29,7 +29,15 @@ impl Font { pub fn system(size: CGFloat) -> Self { Font { objc: unsafe { - msg_send![class!(NSFont), systemFontOfSize:size] + ShareId::from_ptr(msg_send![class!(NSFont), systemFontOfSize:size]) + } + } + } + + pub fn bold_system(size: CGFloat) -> Self { + Font { + objc: unsafe { + ShareId::from_ptr(msg_send![class!(NSFont), boldSystemFontOfSize:size]) } } } diff --git a/src/text/label/mod.rs b/src/text/label/mod.rs index fa75497..b04e1d9 100644 --- a/src/text/label/mod.rs +++ b/src/text/label/mod.rs @@ -61,9 +61,6 @@ mod ios; #[cfg(target_os = "ios")] use ios::{register_view_class, register_view_class_with_delegate}; -//mod controller; -//pub use controller::LabelController; - mod traits; pub use traits::LabelDelegate; @@ -75,8 +72,8 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { #[cfg(target_os = "macos")] let view: id = { // This sucks, but for now, sure. - let blank = NSString::new(""); - msg_send![registration_fn(), labelWithString:blank.into_inner()] + let blank = NSString::no_copy(""); + msg_send![registration_fn(), labelWithString:&*blank] }; #[cfg(target_os = "ios")] @@ -230,17 +227,17 @@ impl Label { let s = NSString::new(text); unsafe { - let _: () = msg_send![&*self.objc, setStringValue:s.into_inner()]; + let _: () = msg_send![&*self.objc, setStringValue:&*s]; } } /// Retrieve the text currently held in the label. - pub fn text(&self) -> String { - let s = NSString::wrap(unsafe { + pub fn get_text(&self) -> String { + let s = NSString::retain(unsafe { msg_send![&*self.objc, stringValue] }); - s.to_str().to_string() + s.to_string() } pub fn set_text_alignment(&self, alignment: TextAlign) { @@ -256,6 +253,15 @@ impl Label { } } + pub fn set_hidden(&self, hidden: bool) { + unsafe { + let _: () = msg_send![&*self.objc, setHidden:match hidden { + true => YES, + false => NO + }]; + } + } + pub fn set_line_break_mode(&self, mode: LineBreakMode) { #[cfg(target_os = "macos")] unsafe { diff --git a/src/view/mod.rs b/src/view/mod.rs index b5fb924..c0c5e53 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -68,6 +68,9 @@ use ios::{register_view_class, register_view_class_with_delegate}; mod controller; pub use controller::ViewController; +mod splitviewcontroller; +pub use splitviewcontroller::SplitViewController; + mod traits; pub use traits::ViewDelegate; @@ -199,7 +202,7 @@ impl View { height: self.height.clone(), center_x: self.center_x.clone(), center_y: self.center_y.clone(), - objc: Rc::clone(&self.objc) //.clone() + objc: Rc::clone(&self.objc) } } @@ -210,27 +213,18 @@ impl View { unsafe { (&mut **objc).set_ivar(BACKGROUND_COLOR, color.as_ref().to_objc()); } - /*let bg = color.as_ref().into_platform_specific_color(); - - unsafe { - let cg: id = msg_send![bg, CGColor]; - let layer: id = msg_send![&*self.objc, layer]; - let _: () = msg_send![layer, setBackgroundColor:cg]; - }*/ } /// Register this view for drag and drop operations. pub fn register_for_dragged_types(&self, types: &[PasteboardType]) { unsafe { let types: NSArray = types.into_iter().map(|t| { - // This clone probably doesn't need to be here, but it should also be cheap as - // this is just an enum... and this is not an oft called method. - let x: NSString = t.clone().into(); - x.into_inner() + let x: NSString = (*t).into(); + x.into() }).collect::>().into(); let objc = self.objc.borrow(); - let _: () = msg_send![&**objc, registerForDraggedTypes:types.into_inner()]; + let _: () = msg_send![&**objc, registerForDraggedTypes:&*types]; } } } diff --git a/src/view/splitviewcontroller/ios.rs b/src/view/splitviewcontroller/ios.rs new file mode 100644 index 0000000..3db20a5 --- /dev/null +++ b/src/view/splitviewcontroller/ios.rs @@ -0,0 +1,72 @@ +use std::sync::Once; +use std::unreachable; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::foundation::{BOOL}; +use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate}; +use crate::utils::{load, as_bool}; + +/// Called when the view controller receives a `viewWillAppear:` message. +extern fn will_appear(this: &mut Object, _: Sel, animated: BOOL) { + unsafe { + let _: () = msg_send![super(this, class!(UIViewController)), viewWillAppear:animated]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.will_appear(as_bool(animated)); +} + +/// Called when the view controller receives a `viewDidAppear:` message. +extern fn did_appear(this: &mut Object, _: Sel, animated: BOOL) { + unsafe { + let _: () = msg_send![super(this, class!(UIViewController)), viewDidAppear:animated]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.did_appear(as_bool(animated)); +} + +/// Called when the view controller receives a `viewWillDisappear:` message. +extern fn will_disappear(this: &mut Object, _: Sel, animated: BOOL) { + unsafe { + let _: () = msg_send![super(this, class!(UIViewController)), viewWillDisappear:animated]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.will_disappear(as_bool(animated)); +} + +/// Called when the view controller receives a `viewDidDisappear:` message. +extern fn did_disappear(this: &mut Object, _: Sel, animated: BOOL) { + unsafe { + let _: () = msg_send![super(this, class!(UIViewController)), viewDidDisappear:animated]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.did_disappear(as_bool(animated)); +} + +/// Registers an `NSViewDelegate`. +pub(crate) fn register_view_controller_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!(UIViewController); + let mut decl = ClassDecl::new("RSTViewController", superclass).unwrap(); + + decl.add_ivar::(VIEW_DELEGATE_PTR); + + decl.add_method(sel!(viewWillAppear:), will_appear:: as extern fn(&mut Object, _, BOOL)); + decl.add_method(sel!(viewDidAppear:), did_appear:: as extern fn(&mut Object, _, BOOL)); + decl.add_method(sel!(viewWillDisappear:), will_disappear:: as extern fn(&mut Object, _, BOOL)); + decl.add_method(sel!(viewDidDisappear:), did_disappear:: as extern fn(&mut Object, _, BOOL)); + + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/src/view/splitviewcontroller/macos.rs b/src/view/splitviewcontroller/macos.rs new file mode 100644 index 0000000..dd95235 --- /dev/null +++ b/src/view/splitviewcontroller/macos.rs @@ -0,0 +1,63 @@ +//! Hoists a basic `NSViewController`. + +use std::sync::Once; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::foundation::load_or_register_class; +use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate}; +use crate::utils::load; + +/// Called when the view controller receives a `viewWillAppear` message. +extern fn will_appear(this: &mut Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSViewController)), viewWillAppear]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.will_appear(false); +} + +/// Called when the view controller receives a `viewDidAppear` message. +extern fn did_appear(this: &mut Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSViewController)), viewDidAppear]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.did_appear(false); +} + +/// Called when the view controller receives a `viewWillDisappear` message. +extern fn will_disappear(this: &mut Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSViewController)), viewWillDisappear]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.will_disappear(false); +} + +/// Called when the view controller receives a `viewDidDisappear` message. +extern fn did_disappear(this: &mut Object, _: Sel) { + unsafe { + let _: () = msg_send![super(this, class!(NSViewController)), viewDidDisappear]; + } + + let controller = load::(this, VIEW_DELEGATE_PTR); + controller.did_disappear(false); +} + +/// Registers an `NSViewDelegate`. +pub(crate) fn register_view_controller_class(instance: &T) -> *const Class { + load_or_register_class("NSViewController", instance.subclass_name(), |decl| unsafe { + decl.add_ivar::(VIEW_DELEGATE_PTR); + + decl.add_method(sel!(viewWillAppear), will_appear:: as extern fn(&mut Object, _)); + decl.add_method(sel!(viewDidAppear), did_appear:: as extern fn(&mut Object, _)); + decl.add_method(sel!(viewWillDisappear), will_disappear:: as extern fn(&mut Object, _)); + decl.add_method(sel!(viewDidDisappear), did_disappear:: as extern fn(&mut Object, _)); + }) +} diff --git a/src/view/splitviewcontroller/mod.rs b/src/view/splitviewcontroller/mod.rs new file mode 100644 index 0000000..a49beb2 --- /dev/null +++ b/src/view/splitviewcontroller/mod.rs @@ -0,0 +1,123 @@ +use objc_id::ShareId; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::foundation::{id, nil, NSString}; +use crate::layout::{Layout}; +use crate::macos::toolbar::ToolbarItem; +use crate::view::{View, ViewController, ViewDelegate}; +use crate::utils::Controller; + +#[derive(Debug)] +pub struct SplitViewItem { + pub objc: ShareId, + pub view_controller: ViewController +} + +impl SplitViewItem +where + T: ViewDelegate + 'static +{ + pub fn item(view: T) -> Self { + let view_controller = ViewController::new(view); + + let objc = unsafe { + ShareId::from_ptr(msg_send![class!(NSSplitViewItem), splitViewItemWithViewController:&*view_controller.objc]) + }; + + SplitViewItem { + objc, + view_controller + } + } + + pub fn sidebar(view: T) -> Self { + let view_controller = ViewController::new(view); + + let objc = unsafe { + ShareId::from_ptr(msg_send![class!(NSSplitViewItem), sidebarWithViewController:&*view_controller.objc]) + }; + + SplitViewItem { + objc, + view_controller + } + } + + pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { + unsafe { + let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; + } + } +} + +#[derive(Debug)] +pub struct SplitViewController { + pub objc: ShareId, + pub sidebar: SplitViewItem, + pub content: SplitViewItem, +} + +impl SplitViewController +where + Sidebar: ViewDelegate + 'static, + Content: ViewDelegate + 'static +{ + pub fn new(sidebar: Sidebar, content: Content) -> Self { + let sidebar = SplitViewItem::sidebar(sidebar); + let content = SplitViewItem::item(content); + + let objc = unsafe { + let vc: id = msg_send![class!(NSSplitViewController), new]; + let _: () = msg_send![vc, addSplitViewItem:&*sidebar.objc]; + let _: () = msg_send![vc, addSplitViewItem:&*content.objc]; + ShareId::from_ptr(vc) + }; + + SplitViewController { objc, sidebar, content } + } + + /// Toggles the sidebar, if it exists, with an animation. If there's no sidebar in this split view + /// (which is highly unlikely, unless you went out of your way to duck this) then it will do + /// nothing. + pub fn toggle_sidebar(&self) { + unsafe { + let _: () = msg_send![&*self.objc, toggleSidebar:nil]; + } + } + + pub fn set_autosave_name(&self, name: &str) { + let name = NSString::new(name); + + unsafe { + let split_view: id = msg_send![&*self.objc, splitView]; + let _: () = msg_send![split_view, setAutosaveName:&*name]; + } + } + + /*/// This method can be used to acquire an item for Toolbar instances that tracks a specified + /// divider (`divider_index`) of this split view. This method is only supported on macOS 11.0+; + /// it will return `None` on 10.15 and below. + /// + /// You should call this and pass + store the item in your Toolbar, and vend it to the system + /// with your `ToolbarDelegate`. + pub fn tracking_separator_toolbar_item(&self, divider_index: usize) -> Option { + if crate::utils::os::is_minimum_version(11) { + unsafe { + let split_view: id = msg_send![&*self.objc, splitView]; + let item: id = msg_send![class!(NSTrackingSeparatorToolbarItem), trackingSeparatorToolbarItemWithIdentifier: + splitView:split_view + dividerIndex:divider_index as NSInteger + ]; + } + } + + None + }*/ +} + +impl Controller for SplitViewController { + fn get_backing_node(&self) -> ShareId { + self.objc.clone() + } +}