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.
This commit is contained in:
Ryan McGrath 2021-03-04 17:24:39 -08:00
parent 5cd59b5636
commit 10c513edad
No known key found for this signature in database
GPG key ID: DA6CBD9233593DEA
66 changed files with 1634 additions and 592 deletions

View file

@ -10,7 +10,9 @@ categories = ["gui", "os::macos-apis", "os::ios-apis"]
keywords = ["gui", "macos", "ios", "appkit", "uikit"] keywords = ["gui", "macos", "ios", "appkit", "uikit"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true
default-target = "x86_64-apple-darwin" default-target = "x86_64-apple-darwin"
rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
block = "0.1.6" block = "0.1.6"
@ -37,4 +39,4 @@ color_fallbacks = []
quicklook = [] quicklook = []
user-notifications = ["uuid"] user-notifications = ["uuid"]
webview = [] webview = []
webview-downloading = [] webview-downloading-macos = []

View file

@ -51,16 +51,16 @@ impl WindowDelegate for AppWindow {
window.set_content_view(&self.content); window.set_content_view(&self.content);
LayoutConstraint::activate(&[ 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.leading.constraint_equal_to(&self.content.leading).offset(16.),
self.blue.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), self.blue.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),
self.blue.width.constraint_equal_to_constant(100.), 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.leading.constraint_equal_to(&self.blue.trailing).offset(16.),
self.red.bottom.constraint_equal_to(&self.content.bottom).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.leading.constraint_equal_to(&self.red.trailing).offset(16.),
self.green.trailing.constraint_equal_to(&self.content.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.), self.green.bottom.constraint_equal_to(&self.content.bottom).offset(-16.),

View file

@ -2,7 +2,6 @@ use std::sync::{Arc, RwLock};
use cacao::lazy_static::lazy_static; use cacao::lazy_static::lazy_static;
use cacao::macos::App; use cacao::macos::App;
use cacao::notification_center::Dispatcher;
use crate::CalculatorApp; use crate::CalculatorApp;
@ -24,7 +23,6 @@ pub enum Msg {
/// on the main thread. /// on the main thread.
pub fn dispatch(msg: Msg) { pub fn dispatch(msg: Msg) {
println!("Dispatching UI message: {:?}", msg); println!("Dispatching UI message: {:?}", msg);
//App::<CalculatorApp, Msg>::dispatch_main(msg)
CALCULATOR.run(msg) CALCULATOR.run(msg)
} }

View file

@ -107,7 +107,7 @@ impl ViewDelegate for CalculatorView {
self.results_wrapper.top.constraint_equal_to(&view.top), self.results_wrapper.top.constraint_equal_to(&view.top),
self.results_wrapper.leading.constraint_equal_to(&view.leading), self.results_wrapper.leading.constraint_equal_to(&view.leading),
self.results_wrapper.trailing.constraint_equal_to(&view.trailing), 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.leading.constraint_equal_to(&self.results_wrapper.leading).offset(22.),
self.label.trailing.constraint_equal_to(&self.results_wrapper.trailing).offset(-16.), self.label.trailing.constraint_equal_to(&self.results_wrapper.trailing).offset(-16.),

View file

@ -16,11 +16,10 @@ use cacao::macos::window::{Window, WindowConfig, TitleVisibility};
use cacao::macos::{Event, EventMask, EventMonitor}; use cacao::macos::{Event, EventMask, EventMonitor};
use cacao::color::Color; use cacao::color::Color;
use cacao::notification_center::Dispatcher; use cacao::notification_center::Dispatcher;
use cacao::view::{View, ViewDelegate}; use cacao::view::View;
mod button_row; mod button_row;
mod calculator; mod calculator;
use calculator::{dispatch, Msg};
mod content_view; mod content_view;
use content_view::CalculatorView; use content_view::CalculatorView;
@ -38,7 +37,7 @@ impl AppDelegate for CalculatorApp {
// Event Monitors need to be started after the App has been activated. // 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 // 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. // &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_title("Calculator");
self.window.set_background_color(Color::rgb(49,49,49)); self.window.set_background_color(Color::rgb(49,49,49));
@ -70,7 +69,8 @@ impl CalculatorApp {
let characters = evt.characters(); let characters = evt.characters();
println!("{}", characters); println!("{}", characters);
match characters.as_ref() { //use calculator::{dispatch, Msg};
/*match characters.as_ref() {
"0" => dispatch(Msg::Push(0)), "0" => dispatch(Msg::Push(0)),
"1" => dispatch(Msg::Push(1)), "1" => dispatch(Msg::Push(1)),
"2" => dispatch(Msg::Push(2)), "2" => dispatch(Msg::Push(2)),
@ -90,7 +90,7 @@ impl CalculatorApp {
"c" => dispatch(Msg::Clear), "c" => dispatch(Msg::Clear),
"." => dispatch(Msg::Decimal), "." => dispatch(Msg::Decimal),
_ => {} _ => {}
} }*/
None None
})); }));

View file

@ -12,58 +12,58 @@ use crate::storage::{dispatch_ui, Message};
pub fn menu() -> Vec<Menu> { pub fn menu() -> Vec<Menu> {
vec![ vec![
Menu::new("", vec![ Menu::new("", vec![
MenuItem::about("Todos"), MenuItem::About("Todos".to_string()),
MenuItem::Separator, MenuItem::Separator,
MenuItem::entry("Preferences").key(",").action(|| { MenuItem::new("Preferences").key(",").action(|| {
dispatch_ui(Message::OpenPreferencesWindow); dispatch_ui(Message::OpenPreferencesWindow);
}), }),
MenuItem::Separator, MenuItem::Separator,
MenuItem::services(), MenuItem::Services,
MenuItem::Separator, MenuItem::Separator,
MenuItem::hide(), MenuItem::Hide,
MenuItem::hide_others(), MenuItem::HideOthers,
MenuItem::show_all(), MenuItem::ShowAll,
MenuItem::Separator, MenuItem::Separator,
MenuItem::quit() MenuItem::Quit
]), ]),
Menu::new("File", vec![ Menu::new("File", vec![
MenuItem::entry("Open/Show Window").key("n").action(|| { MenuItem::new("Open/Show Window").key("n").action(|| {
dispatch_ui(Message::OpenMainWindow); dispatch_ui(Message::OpenMainWindow);
}), }),
MenuItem::Separator, MenuItem::Separator,
MenuItem::entry("Add Todo").key("+").action(|| { MenuItem::new("Add Todo").key("+").action(|| {
dispatch_ui(Message::OpenNewTodoSheet); dispatch_ui(Message::OpenNewTodoSheet);
}), }),
MenuItem::Separator, MenuItem::Separator,
MenuItem::close_window(), MenuItem::CloseWindow
]), ]),
Menu::new("Edit", vec![ Menu::new("Edit", vec![
MenuItem::undo(), MenuItem::Undo,
MenuItem::redo(), MenuItem::Redo,
MenuItem::Separator, MenuItem::Separator,
MenuItem::cut(), MenuItem::Cut,
MenuItem::copy(), MenuItem::Copy,
MenuItem::paste(), MenuItem::Paste,
MenuItem::Separator, MenuItem::Separator,
MenuItem::select_all() MenuItem::SelectAll
]), ]),
Menu::new("View", vec![ Menu::new("View", vec![
MenuItem::enter_full_screen() MenuItem::EnterFullScreen
]), ]),
Menu::new("Window", vec![ Menu::new("Window", vec![
MenuItem::minimize(), MenuItem::Minimize,
MenuItem::zoom(), MenuItem::Zoom,
MenuItem::Separator, MenuItem::Separator,
MenuItem::entry("Bring All to Front") MenuItem::new("Bring All to Front")
]), ]),
Menu::new("Help", vec![]) Menu::new("Help", vec![])

View file

@ -1,7 +1,7 @@
//! Implements an example toolbar for a Preferences app. Could be cleaner, probably worth cleaning //! Implements an example toolbar for a Preferences app. Could be cleaner, probably worth cleaning
//! up at some point. //! 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 cacao::image::{Image, MacSystemIcon};
use crate::storage::{dispatch_ui, Message}; use crate::storage::{dispatch_ui, Message};
@ -46,16 +46,16 @@ impl ToolbarDelegate for PreferencesToolbar {
toolbar.set_selected("general"); toolbar.set_selected("general");
} }
fn allowed_item_identifiers(&self) -> Vec<&'static str> { fn allowed_item_identifiers(&self) -> Vec<ItemIdentifier> {
vec!["general", "advanced"] vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")]
} }
fn default_item_identifiers(&self) -> Vec<&'static str> { fn default_item_identifiers(&self) -> Vec<ItemIdentifier> {
vec!["general", "advanced"] vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")]
} }
fn selectable_item_identifiers(&self) -> Vec<&'static str> { fn selectable_item_identifiers(&self) -> Vec<ItemIdentifier> {
vec!["general", "advanced"] vec![ItemIdentifier::Custom("general"), ItemIdentifier::Custom("advanced")]
} }
fn item_for(&self, identifier: &str) -> &ToolbarItem { fn item_for(&self, identifier: &str) -> &ToolbarItem {

View file

@ -48,7 +48,7 @@ impl TodosListView {
self.view.as_ref().unwrap().perform_batch_updates(|listview| { self.view.as_ref().unwrap().perform_batch_updates(|listview| {
// We know we always insert at the 0 index, so this is a simple calculation. // 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. // You'd need to diff yourself for anything more complicated.
listview.insert_rows(0..1, RowAnimation::SlideDown); listview.insert_rows(&[0], RowAnimation::SlideDown);
}); });
}, },

View file

@ -1,7 +1,10 @@
//! The main Todos window toolbar. Contains a button to enable adding a new task. //! The main Todos window toolbar. Contains a button to enable adding a new task.
use cacao::button::Button; 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}; use crate::storage::{dispatch_ui, Message};
@ -31,12 +34,12 @@ impl ToolbarDelegate for TodosToolbar {
toolbar.set_display_mode(ToolbarDisplayMode::IconOnly); toolbar.set_display_mode(ToolbarDisplayMode::IconOnly);
} }
fn allowed_item_identifiers(&self) -> Vec<&'static str> { fn allowed_item_identifiers(&self) -> Vec<ItemIdentifier> {
vec!["AddTodoButton"] vec![ItemIdentifier::Custom("AddTodoButton")]
} }
fn default_item_identifiers(&self) -> Vec<&'static str> { fn default_item_identifiers(&self) -> Vec<ItemIdentifier> {
vec!["AddTodoButton"] vec![ItemIdentifier::Custom("AddTodoButton")]
} }
// We only have one item, so we don't care about the identifier. // We only have one item, so we don't care about the identifier.

View file

@ -10,6 +10,7 @@ use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::color::Color; use crate::color::Color;
use crate::image::Image;
use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger}; use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger};
use crate::invoker::TargetActionHandler; use crate::invoker::TargetActionHandler;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
@ -28,6 +29,7 @@ extern "C" {
#[derive(Debug)] #[derive(Debug)]
pub struct Button { pub struct Button {
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
pub image: Option<Image>,
handler: Option<TargetActionHandler>, handler: Option<TargetActionHandler>,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
@ -62,7 +64,7 @@ impl Button {
let title = NSString::new(text); let title = NSString::new(text);
let view: id = unsafe { 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, setWantsLayer:YES];
let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO]; let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO];
button button
@ -70,6 +72,7 @@ impl Button {
Button { Button {
handler: None, handler: None,
image: None,
top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }), top: LayoutAnchorY::new(unsafe { msg_send![view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![view, leadingAnchor] }),
trailing: LayoutAnchorX::new(unsafe { msg_send![view, trailingAnchor] }), 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. /// Sets the bezel style for this button.
#[cfg(feature = "macos")] #[cfg(feature = "macos")]
pub fn set_bezel_style(&self, bezel_style: BezelStyle) { pub fn set_bezel_style(&self, bezel_style: BezelStyle) {
@ -115,10 +126,10 @@ impl Button {
} }
pub fn set_key_equivalent(&self, key: &str) { pub fn set_key_equivalent(&self, key: &str) {
let key = NSString::new(key).into_inner(); let key = NSString::new(key);
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setKeyEquivalent:key]; let _: () = msg_send![&*self.objc, setKeyEquivalent:&*key];
} }
} }

View file

@ -10,6 +10,7 @@
//! that enables this functionality, we want to be able to provide this with some level of //! 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. //! backwards compatibility for Mojave, as that's still a supported OS.
use std::os::raw::c_void;
use std::sync::Once; use std::sync::Once;
use core_graphics::base::CGFloat; 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_NORMAL_CONTRAST: &'static str = "AQUA_DARK_COLOR_NORMAL_CONTRAST";
pub(crate) const AQUA_DARK_COLOR_HIGH_CONTRAST: &'static str = "AQUA_DARK_COLOR_HIGH_CONTRAST"; pub(crate) const AQUA_DARK_COLOR_HIGH_CONTRAST: &'static str = "AQUA_DARK_COLOR_HIGH_CONTRAST";
use std::os::raw::c_void;
extern "C" { extern "C" {
static NSAppearanceNameAqua: id; static NSAppearanceNameAqua: id;
@ -56,7 +56,7 @@ fn get_effective_color(this: &Object) -> id {
NSAppearanceNameAccessibilityHighContrastDarkAqua NSAppearanceNameAccessibilityHighContrastDarkAqua
]); ]);
let style: id = msg_send![appearance, bestMatchFromAppearancesWithNames:names.into_inner()]; let style: id = msg_send![appearance, bestMatchFromAppearancesWithNames:&*names];
if style == NSAppearanceNameDarkAqua { if style == NSAppearanceNameDarkAqua {
return *this.get_ivar(AQUA_DARK_COLOR_NORMAL_CONTRAST); return *this.get_ivar(AQUA_DARK_COLOR_NORMAL_CONTRAST);

View file

@ -242,7 +242,11 @@ pub enum Color {
DarkText, DarkText,
/// The un-adaptable color for text on a dark background. /// The un-adaptable color for text on a dark background.
LightText LightText,
WindowBackgroundColor,
UnderPageBackgroundColor
} }
impl Color { impl Color {
@ -484,5 +488,7 @@ unsafe fn to_objc(obj: &Color) -> id {
Color::Link => system_color_with_fallback!(color, linkColor, blueColor), Color::Link => system_color_with_fallback!(color, linkColor, blueColor),
Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor), Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor),
Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor), Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor),
Color::WindowBackgroundColor => system_color_with_fallback!(color, windowBackgroundColor, clearColor),
Color::UnderPageBackgroundColor => system_color_with_fallback!(color, underPageBackgroundColor, clearColor),
} }
} }

View file

@ -15,7 +15,7 @@
//! a loop. //! a loop.
//! //!
//! ## Example //! ## Example
//! ```rust //! ```rust,no_run
//! use std::collections::HashMap; //! use std::collections::HashMap;
//! use cacao::defaults::{UserDefaults, Value}; //! use cacao::defaults::{UserDefaults, Value};
//! //!
@ -27,7 +27,6 @@
//! map //! map
//! }); //! });
//! //!
//! // Ignore the unwrap() calls, it's a demo ;P
//! let value = defaults.get("test").unwrap().as_str().unwrap(); //! let value = defaults.get("test").unwrap().as_str().unwrap();
//! assert_eq!(value, "value"); //! assert_eq!(value, "value");
//! ``` //! ```
@ -38,7 +37,10 @@ use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::Id; 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; mod value;
pub use value::Value; pub use value::Value;
@ -92,7 +94,7 @@ impl UserDefaults {
UserDefaults(unsafe { UserDefaults(unsafe {
let alloc: id = msg_send![class!(NSUserDefaults), alloc]; 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<K: AsRef<str>>(&mut self, values: HashMap<K, Value>) { pub fn register<K: AsRef<str>>(&mut self, values: HashMap<K, Value>) {
let dictionary = NSDictionary::from(values); let dictionary = NSMutableDictionary::from(values);
unsafe { 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()); let key = NSString::new(key.as_ref());
unsafe { 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 key = NSString::new(key.as_ref());
let result: id = unsafe { let result: id = unsafe {
msg_send![&*self.0, objectForKey:key.into_inner()] msg_send![&*self.0, objectForKey:&*key]
}; };
if result == nil { if result == nil {
@ -184,12 +186,12 @@ impl UserDefaults {
} }
if NSData::is(result) { if NSData::is(result) {
let data = NSData::wrap(result); let data = NSData::retain(result);
return Some(Value::Data(data.into_vec())); return Some(Value::Data(data.into_vec()));
} }
if NSString::is(result) { 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)); return Some(Value::String(s));
} }
@ -213,7 +215,7 @@ impl UserDefaults {
x => { x => {
// Debugging code that should be removed at some point. // Debugging code that should be removed at some point.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
println!("Code: {}", x); println!("Unexpected code type found: {}", x);
None None
} }
@ -241,7 +243,7 @@ impl UserDefaults {
pub fn is_forced_for_key<K: AsRef<str>>(&self, key: K) -> bool { pub fn is_forced_for_key<K: AsRef<str>>(&self, key: K) -> bool {
let result: BOOL = unsafe { let result: BOOL = unsafe {
let key = NSString::new(key.as_ref()); let key = NSString::new(key.as_ref());
msg_send![&*self.0, objectIsForcedForKey:key.into_inner()] msg_send![&*self.0, objectIsForcedForKey:&*key]
}; };
to_bool(result) to_bool(result)

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; 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`. /// Represents a Value that can be stored or queried with `UserDefaults`.
/// ///
@ -136,22 +136,22 @@ impl From<Value> for id {
// period. // period.
fn from(value: Value) -> Self { fn from(value: Value) -> Self {
match value { match value {
Value::Bool(b) => NSNumber::bool(b).into_inner(), Value::Bool(b) => NSNumber::bool(b).into(),
Value::String(s) => NSString::new(&s).into_inner(), Value::String(s) => NSString::new(&s).into(),
Value::Float(f) => NSNumber::float(f).into_inner(), Value::Float(f) => NSNumber::float(f).into(),
Value::Integer(i) => NSNumber::integer(i).into_inner(), Value::Integer(i) => NSNumber::integer(i).into(),
Value::Data(data) => NSData::new(data).into_inner() Value::Data(data) => NSData::new(data).into()
} }
} }
} }
impl<K> From<HashMap<K, Value>> for NSDictionary impl<K> From<HashMap<K, Value>> for NSMutableDictionary
where where
K: AsRef<str> K: AsRef<str>
{ {
/// Translates a `HashMap` of `Value`s into an `NSDictionary`. /// Translates a `HashMap` of `Value`s into an `NSDictionary`.
fn from(map: HashMap<K, Value>) -> Self { fn from(map: HashMap<K, Value>) -> Self {
let mut dictionary = NSDictionary::new(); let mut dictionary = NSMutableDictionary::new();
for (key, value) in map.into_iter() { for (key, value) in map.into_iter() {
let k = NSString::new(key.as_ref()); let k = NSString::new(key.as_ref());

View file

@ -33,16 +33,16 @@ impl Error {
pub fn new(error: id) -> Self { pub fn new(error: id) -> Self {
let (code, domain, description) = unsafe { let (code, domain, description) = unsafe {
let code: usize = msg_send![error, code]; let code: usize = msg_send![error, code];
let domain = NSString::wrap(msg_send![error, domain]); let domain = NSString::retain(msg_send![error, domain]);
let description = NSString::wrap(msg_send![error, localizedDescription]); let description = NSString::retain(msg_send![error, localizedDescription]);
(code, domain, description) (code, domain, description)
}; };
Error { Error {
code: code, code,
domain: domain.to_str().to_string(), domain: domain.to_string(),
description: description.to_str().to_string() description: description.to_string()
} }
} }

View file

@ -56,7 +56,7 @@ impl FileManager {
create:NO create:NO
error:nil]; error:nil];
NSString::wrap(msg_send![dir, absoluteString]) NSString::retain(msg_send![dir, absoluteString])
}; };
Url::parse(directory.to_str()).map_err(|e| e.into()) Url::parse(directory.to_str()).map_err(|e| e.into())
@ -70,8 +70,8 @@ impl FileManager {
let to = NSString::new(to.as_str()); let to = NSString::new(to.as_str());
unsafe { unsafe {
let from_url: id = msg_send![class!(NSURL), URLWithString:from.into_inner()]; let from_url: id = msg_send![class!(NSURL), URLWithString:&*from];
let to_url: id = msg_send![class!(NSURL), URLWithString:to.into_inner()]; let to_url: id = msg_send![class!(NSURL), URLWithString:&*to];
// This should potentially be write(), but the backing class handles this logic // This should potentially be write(), but the backing class handles this logic
// already, so... going to leave it as read. // already, so... going to leave it as read.

View file

@ -102,7 +102,7 @@ pub fn get_url(panel: &Object) -> Option<String> {
None None
} else { } else {
let path: id = msg_send![url, path]; let path: id = msg_send![url, path];
Some(NSString::wrap(path).to_str().to_string()) Some(NSString::retain(path).to_string())
} }
} }
} }

View file

@ -13,6 +13,8 @@ use objc_id::ShareId;
use crate::foundation::{id, YES, NO, NSInteger, NSString}; use crate::foundation::{id, YES, NO, NSInteger, NSString};
use crate::filesystem::enums::ModalResponse; use crate::filesystem::enums::ModalResponse;
use crate::macos::window::{Window, WindowDelegate};
#[derive(Debug)] #[derive(Debug)]
pub struct FileSelectPanel { pub struct FileSelectPanel {
/// The internal Objective C `NSOpenPanel` instance. /// 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 /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as
/// the system runs and manages that in another process, and we're still abiding by the general /// the system runs and manages that in another process, and we're still abiding by the general
/// retain/ownership rules here. /// retain/ownership rules here.
pub fn show<F: Fn(Vec<PathBuf>) + '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<F>(&self, handler: F)
where
F: Fn(Vec<PathBuf>) + 'static
{
let panel = self.panel.clone(); let panel = self.panel.clone();
let completion = ConcreteBlock::new(move |result: NSInteger| { let completion = ConcreteBlock::new(move |result: NSInteger| {
let response: ModalResponse = result.into(); let response: ModalResponse = result.into();
@ -137,30 +145,46 @@ impl FileSelectPanel {
let _: () = msg_send![&*self.panel, beginWithCompletionHandler:completion.copy()]; 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<T, F>(&self, window: &Window<T>, handler: F)
where
F: Fn(Vec<PathBuf>) + '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. /// Retrieves the selected URLs from the provided panel.
/// This is currently a bit ugly, but it's also not something that needs to be the best thing in /// This is currently a bit ugly, but it's also not something that needs to be the best thing in
/// the world as it (ideally) shouldn't be called repeatedly in hot spots. /// the world as it (ideally) shouldn't be called repeatedly in hot spots.
pub fn get_urls(panel: &Object) -> Vec<PathBuf> { pub fn get_urls(panel: &Object) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = vec![];
unsafe { unsafe {
let urls: id = msg_send![&*panel, URLs]; let urls: id = msg_send![&*panel, URLs];
let mut count: usize = msg_send![urls, count]; let count: usize = msg_send![urls, count];
loop { (0..count).map(|index| {
if count == 0 { let url: id = msg_send![urls, objectAtIndex:index];
break; let path = NSString::retain(msg_send![url, path]).to_string();
} path.into()
}).collect()
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;
}
} }
paths.reverse();
paths
} }

View file

@ -1,12 +1,4 @@
//! A wrapper type for `NSArray`. use std::ops::{Deref, DerefMut};
//!
//! 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 objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
@ -15,15 +7,68 @@ use objc_id::Id;
use crate::foundation::id; use crate::foundation::id;
/// A wrapper for `NSArray` that makes common operations in our framework a bit easier to handle /// 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)] #[derive(Debug)]
pub struct NSArray(pub Id<Object>); pub struct NSArray(pub Id<Object>);
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<T>`.
/// 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, F: Fn(id) -> T>(&self, transform: F) -> Vec<T> {
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<Vec<&Object>> for NSArray { impl From<Vec<&Object>> for NSArray {
/// Given a set of `Object`s, creates an `NSArray` that holds them. /// Given a set of `Object`s, creates an `NSArray` that holds them.
fn from(objects: Vec<&Object>) -> Self { fn from(objects: Vec<&Object>) -> Self {
NSArray(unsafe { 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<Vec<id>> for NSArray {
/// Given a set of `*mut Object`s, creates an `NSArray` that holds them. /// Given a set of `*mut Object`s, creates an `NSArray` that holds them.
fn from(objects: Vec<id>) -> Self { fn from(objects: Vec<id>) -> Self {
NSArray(unsafe { 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 { impl From<NSArray> for id {
/// Given a set of `Object`s, creates an `NSArray` that holds them. /// Consumes and returns the pointer to the underlying NSArray.
pub fn new(objects: &[id]) -> Self { fn from(mut array: NSArray) -> Self {
NSArray(unsafe { &mut *array
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, and it's ideal to not retain that. impl Deref for NSArray {
/// This handles that edge case. type Target = Object;
pub fn wrap(array: id) -> Self {
NSArray(unsafe { /// Derefs to the underlying Objective-C Object.
Id::from_ptr(array) fn deref(&self) -> &Object {
}) &*self.0
} }
}
/// Consumes and returns the underlying Objective-C value. impl DerefMut for NSArray {
pub fn into_inner(mut self) -> id { /// Derefs to the underlying Objective-C Object.
fn deref_mut(&mut self) -> &mut Object {
&mut *self.0 &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, F: Fn(id) -> T>(&self, transform: F) -> Vec<T> {
let count = self.count();
let mut ret: Vec<T> = 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
}
} }

View file

@ -1,19 +1,46 @@
//! A lightweight wrapper around `NSAutoreleasePool`.
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::Id; 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<Object>); pub struct AutoReleasePool(pub Id<Object>);
impl AutoReleasePool { 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 { pub fn new() -> Self {
AutoReleasePool(unsafe { AutoReleasePool(unsafe {
Id::from_retained_ptr(msg_send![class!(NSAutoreleasePool), new]) Id::from_retained_ptr(msg_send![class!(NSAutoreleasePool), new])
}) })
} }
/// Drains the underlying AutoreleasePool.
pub fn drain(&self) { pub fn drain(&self) {
let _: () = unsafe { msg_send![&*self.0, drain] }; 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<F>(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] };
}
} }

View file

@ -15,7 +15,7 @@ lazy_static! {
/// constantly calling into the runtime, we store pointers to Class types here after first lookup /// 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: /// and/or creation. The general store format is (roughly speaking) as follows:
/// ///
/// ``` /// ```no_run
/// { /// {
/// "subclass_type": { /// "subclass_type": {
/// "superclass_type": *const Class as usize /// "superclass_type": *const Class as usize
@ -39,8 +39,8 @@ impl ClassMap {
let mut map = HashMap::new(); let mut map = HashMap::new();
// Top-level classes, like `NSView`, we cache here. The reasoning is that if a subclass // 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 // is being created, we can avoid querying the runtime for the superclass - i.e, many
// will have `NSView` as their superclass. // subclasses will have `NSView` as their superclass.
map.insert("_supers", HashMap::new()); map.insert("_supers", HashMap::new());
map map
@ -48,7 +48,11 @@ impl ClassMap {
} }
/// Attempts to load a previously registered subclass. /// 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(); let reader = self.0.read().unwrap();
if let Some(inner) = (*reader).get(subclass_name) { if let Some(inner) = (*reader).get(subclass_name) {
@ -152,7 +156,11 @@ where
}, },
None => { 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
);
} }
} }
} }

View file

@ -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<u8>` 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::mem;
use std::ops::{Deref, DerefMut};
use std::os::raw::c_void; use std::os::raw::c_void;
use std::slice; 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<u8>`, wrapping and retaining an existing /// Supports constructing a new `NSData` from a `Vec<u8>`, wrapping and retaining an existing
/// pointer from the Objective-C side, and turning an `NSData` into a `Vec<u8>`. /// pointer from the Objective-C side, and turning an `NSData` into a `Vec<u8>`.
///
/// This is an intentionally limited API.
#[derive(Debug)] #[derive(Debug)]
pub struct NSData(pub Id<Object>); pub struct NSData(pub Id<Object>);
@ -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 /// Given a (presumably) `NSData`, wraps and retains it.
/// wrap it while we figure out what to do with it. This does that. pub fn retain(data: id) -> Self {
pub fn wrap(data: id) -> Self {
NSData(unsafe { NSData(unsafe {
Id::from_ptr(data) 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`. /// A helper method for determining if a given `NSObject` is an `NSData`.
pub fn is(obj: id) -> bool { pub fn is(obj: id) -> bool {
let result: BOOL = unsafe { let result: BOOL = unsafe {
@ -117,9 +118,27 @@ impl NSData {
data data
} }
}
impl From<NSData> for id {
/// Consumes and returns the underlying `NSData`. /// 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 &mut *self.0
} }
} }

View file

@ -1,21 +1,24 @@
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::Id; use objc_id::Id;
use crate::foundation::{id, NSString}; use crate::foundation::{id, NSString};
/// A wrapper for `NSDictionary`. Behind the scenes we actually wrap `NSMutableDictionary`, and /// A wrapper for `NSMutableDictionary`.
/// rely on Rust doing the usual borrow-checking guards that it does so well.
#[derive(Debug)] #[derive(Debug)]
pub struct NSDictionary(pub Id<Object>); pub struct NSMutableDictionary(pub Id<Object>);
impl Default for NSDictionary { impl Default for NSMutableDictionary {
/// Returns a blank NSMutableDictionary.
fn default() -> Self { fn default() -> Self {
NSDictionary::new() NSMutableDictionary::new()
} }
} }
impl NSDictionary { impl NSMutableDictionary {
/// Constructs an `NSMutableDictionary` and retains it. /// Constructs an `NSMutableDictionary` and retains it.
/// ///
/// Why mutable? It's just easier for working with it, as they're (mostly) interchangeable when /// 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 /// 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. /// `unsafe {}` block... so you'll know you're in special territory then.
pub fn new() -> Self { pub fn new() -> Self {
NSDictionary(unsafe { NSMutableDictionary(unsafe {
Id::from_ptr(msg_send![class!(NSMutableDictionary), new]) Id::from_ptr(msg_send![class!(NSMutableDictionary), new])
}) })
} }
@ -33,7 +36,7 @@ impl NSDictionary {
/// This intentionally requires `NSString` be allocated ahead of time. /// This intentionally requires `NSString` be allocated ahead of time.
pub fn insert(&mut self, key: NSString, object: id) { pub fn insert(&mut self, key: NSString, object: id) {
unsafe { 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 &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
}
}

View file

@ -34,7 +34,7 @@ mod data;
pub use data::NSData; pub use data::NSData;
mod dictionary; mod dictionary;
pub use dictionary::NSDictionary; pub use dictionary::NSMutableDictionary;
mod number; mod number;
pub use number::NSNumber; pub use number::NSNumber;

View file

@ -7,7 +7,7 @@ use objc_id::Id;
use crate::foundation::{id, to_bool, BOOL, YES, NO, NSInteger}; 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 /// 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`). /// 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<Object>); pub struct NSNumber(pub Id<Object>);
impl NSNumber { 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 /// 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. /// wrap it while we figure out what to do with it. This does that.
pub fn wrap(data: id) -> Self { pub fn wrap(data: id) -> Self {
NSNumber(unsafe { NSNumber(unsafe {
Id::from_ptr(data) Id::from_retained_ptr(data)
}) })
} }
/// Constructs a `numberWithBool` instance of `NSNumber` and retains it. /// Constructs a `numberWithBool` instance of `NSNumber` and retains it.
pub fn bool(value: bool) -> Self { pub fn bool(value: bool) -> Self {
NSNumber(unsafe { 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, true => YES,
false => NO false => NO
}]) }])
@ -36,14 +44,14 @@ impl NSNumber {
/// Constructs a `numberWithInteger` instance of `NSNumber` and retains it. /// Constructs a `numberWithInteger` instance of `NSNumber` and retains it.
pub fn integer(value: i64) -> Self { pub fn integer(value: i64) -> Self {
NSNumber(unsafe { 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. /// Constructs a `numberWithDouble` instance of `NSNumber` and retains it.
pub fn float(value: f64) -> Self { pub fn float(value: f64) -> Self {
NSNumber(unsafe { 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`. /// to inform you how you should pull the underlying data out of the `NSNumber`.
/// ///
/// For more information: /// For more information:
/// [https://nshipster.com/type-encodings/](https://nshipster.com/type-encodings/) /// <https://nshipster.com/type-encodings/>
pub fn objc_type(&self) -> &str { pub fn objc_type(&self) -> &str {
unsafe { unsafe {
let t: *const c_char = msg_send![&*self.0, objCType]; let t: *const c_char = msg_send![&*self.0, objCType];
@ -101,9 +109,11 @@ impl NSNumber {
to_bool(result) to_bool(result)
} }
}
impl From<NSNumber> for id {
/// Consumes and returns the underlying `NSNumber`. /// Consumes and returns the underlying `NSNumber`.
pub fn into_inner(mut self) -> id { fn from(mut number: NSNumber) -> Self {
&mut *self.0 &mut *number.0
} }
} }

View file

@ -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 std::os::raw::c_char;
use objc::{class, msg_send, sel, sel_impl}; 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 /// We can make a few safety guarantees in this module as the UTF8 code on the Foundation
/// side is fairly battle tested. /// side is fairly battle tested.
#[derive(Debug)] #[derive(Debug)]
pub struct NSString(pub Id<Object>); pub struct NSString<'a> {
pub objc: Id<Object>,
phantom: PhantomData<&'a ()>
}
impl NSString { impl<'a> NSString<'a> {
/// Creates a new `NSString`. Note that `NSString` lives on the heap, so this allocates /// Creates a new `NSString`.
/// accordingly.
pub fn new(s: &str) -> Self { pub fn new(s: &str) -> Self {
NSString(unsafe { NSString {
let nsstring: *mut Object = msg_send![class!(NSString), alloc]; objc: unsafe {
//msg_send![nsstring, initWithBytesNoCopy:s.as_ptr() length:s.len() encoding:4 freeWhenDone:NO] 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]) 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 /// In cases where we're vended an `NSString` by the system, this can be used to wrap and
/// retain it. /// retain it.
pub fn wrap(object: id) -> Self { pub fn retain(object: id) -> Self {
NSString(unsafe { NSString {
Id::from_ptr(object) 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`. /// 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`. /// Helper method for returning the UTF8 bytes for this `NSString`.
fn bytes(&self) -> *const u8 { fn bytes(&self) -> *const u8 {
unsafe { 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 bytes as *const u8
} }
} }
@ -52,7 +87,7 @@ impl NSString {
/// Helper method for grabbing the proper byte length for this `NSString` (the UTF8 variant). /// Helper method for grabbing the proper byte length for this `NSString` (the UTF8 variant).
fn bytes_len(&self) -> usize { fn bytes_len(&self) -> usize {
unsafe { 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 { pub fn to_string(&self) -> String {
self.to_str().to_string() self.to_str().to_string()
} }
}
/// Consumes and returns the underlying `NSString` instance. impl fmt::Display for NSString<'_> {
pub fn into_inner(mut self) -> id { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
&mut *self.0 write!(f, "{}", self.to_str())
}
}
impl From<NSString<'_>> for id {
/// Consumes and returns the pointer to the underlying NSString instance.
fn from(mut string: NSString) -> Self {
&mut *string.objc
}
}
impl Deref for 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
} }
} }

View file

@ -19,9 +19,14 @@ pub enum MacSystemIcon {
PreferencesAdvanced, PreferencesAdvanced,
/// A standard "Accounts" preferences icon. This is intended for usage in Preferences toolbars. /// 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 { impl MacSystemIcon {
/// Maps system icons to their pre-11.0 framework identifiers. /// Maps system icons to their pre-11.0 framework identifiers.
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
@ -29,6 +34,7 @@ impl MacSystemIcon {
MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral", MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral",
MacSystemIcon::PreferencesAdvanced => "NSAdvanced", MacSystemIcon::PreferencesAdvanced => "NSAdvanced",
MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts", MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts",
MacSystemIcon::Add => "NSImageNameAddTemplate"
} }
} }
@ -37,7 +43,31 @@ impl MacSystemIcon {
match self { match self {
MacSystemIcon::PreferencesGeneral => "gearshape", MacSystemIcon::PreferencesGeneral => "gearshape",
MacSystemIcon::PreferencesAdvanced => "slider.vertical.3", 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"
}
}
}

View file

@ -119,14 +119,36 @@ impl Image {
Image(unsafe { Image(unsafe {
ShareId::from_ptr(match os::is_minimum_version(11) { ShareId::from_ptr(match os::is_minimum_version(11) {
true => { true => {
let icon = NSString::new(icon.to_sfsymbol_str()).into_inner(); let icon = NSString::new(icon.to_sfsymbol_str());
let desc = NSString::new(accessibility_description).into_inner(); let desc = NSString::new(accessibility_description);
msg_send![class!(NSImage), imageWithSystemSymbolName:icon accessibilityDescription:desc] msg_send![class!(NSImage), imageWithSystemSymbolName:&*icon
accessibilityDescription:&*desc]
}, },
false => { false => {
let icon = NSString::new(icon.to_str()).into_inner(); let icon = NSString::new(icon.to_str());
msg_send![class!(NSImage), imageNamed:icon] 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.");
} }
}) })
}) })

View file

@ -110,6 +110,15 @@ impl ImageView {
let _: () = msg_send![&*self.objc, setImage:&*image.0]; 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 { impl Layout for ImageView {

View file

@ -61,9 +61,6 @@ mod ios;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate}; use ios::{register_view_class, register_view_class_with_delegate};
//mod controller;
//pub use controller::TextFieldController;
mod traits; mod traits;
pub use traits::TextFieldDelegate; pub use traits::TextFieldDelegate;
@ -199,16 +196,16 @@ impl<T> TextField<T> {
/// Grabs the value from the textfield and returns it as an owned String. /// Grabs the value from the textfield and returns it as an owned String.
pub fn get_value(&self) -> String { pub fn get_value(&self) -> String {
let value = NSString::wrap(unsafe { let value = NSString::retain(unsafe {
msg_send![&*self.objc, stringValue] msg_send![&*self.objc, stringValue]
}); });
value.to_str().to_string() value.to_string()
} }
/// Call this to set the background color for the backing layer. /// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) { pub fn set_background_color(&self, color: Color) {
let bg = color.into_platform_specific_color(); let bg = color.to_objc();
unsafe { unsafe {
let cg: id = msg_send![bg, CGColor]; let cg: id = msg_send![bg, CGColor];
@ -222,7 +219,7 @@ impl<T> TextField<T> {
let s = NSString::new(text); let s = NSString::new(text);
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setStringValue:s.into_inner()]; let _: () = msg_send![&*self.objc, setStringValue:&*s];
} }
} }

View file

@ -121,9 +121,9 @@ where
let dl = register_app_delegate_class::<T>(); let dl = register_app_delegate_class::<T>();
let w = register_window_scene_delegate_class::<W, F>(); let w = register_window_scene_delegate_class::<W, F>();
// This probably needs to be Arc<Mutex<>>'d at some point, but this is still exploratory.
let app_delegate = Box::new(delegate); let app_delegate = Box::new(delegate);
let vendor = Box::new(scene_delegate_vendor); let vendor = Box::new(scene_delegate_vendor);
unsafe { unsafe {
let delegate_ptr: *const T = &*app_delegate; let delegate_ptr: *const T = &*app_delegate;
APP_DELEGATE = delegate_ptr as usize; APP_DELEGATE = delegate_ptr as usize;
@ -148,11 +148,11 @@ impl<T, W, F> App<T, W, F> {
let args = std::env::args().map(|arg| CString::new(arg).unwrap() ).collect::<Vec<CString>>(); let args = std::env::args().map(|arg| CString::new(arg).unwrap() ).collect::<Vec<CString>>();
let c_args = args.iter().map(|arg| arg.as_ptr()).collect::<Vec<*const c_char>>(); let c_args = args.iter().map(|arg| arg.as_ptr()).collect::<Vec<*const c_char>>();
let s = NSString::new("RSTApplication"); let s = NSString::no_copy("RSTApplication");
let s2 = NSString::new("RSTAppDelegate"); let s2 = NSString::no_copy("RSTAppDelegate");
unsafe { 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(); self.pool.drain();

View file

@ -13,18 +13,18 @@ pub enum SessionRole {
//CarPlayApplication //CarPlayApplication
} }
impl From<SessionRole> for NSString { impl From<SessionRole> for NSString<'_> {
fn from(role: SessionRole) -> Self { fn from(role: SessionRole) -> Self {
NSString::new(match role { NSString::new(match role {
SessionRole::Application => "UIWindowSceneSessionRoleApplication", SessionRole::Application => NSString::no_copy("UIWindowSceneSessionRoleApplication"),
SessionRole::ExternalDisplay => "UIWindowSceneSessionRoleExternalDisplay", SessionRole::ExternalDisplay => NSString::no_copy("UIWindowSceneSessionRoleExternalDisplay"),
//SessionRole::CarPlayApplication => "" //SessionRole::CarPlayApplication => ""
}) })
} }
} }
impl From<NSString> for SessionRole { impl From<NSString<'_>> for SessionRole {
fn from(value: NSString) -> Self { fn from(value: NSString<'_>) -> Self {
match value.to_str() { match value.to_str() {
"UIWindowSceneSessionRoleApplication" => SessionRole::Application, "UIWindowSceneSessionRoleApplication" => SessionRole::Application,
"UIWindowSceneSessionRoleExternalDisplay" => SessionRole::ExternalDisplay, "UIWindowSceneSessionRoleExternalDisplay" => SessionRole::ExternalDisplay,

View file

@ -8,7 +8,7 @@ use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::Object; use objc::runtime::Object;
use objc_id::ShareId; use objc_id::ShareId;
use crate::foundation::id; use crate::foundation::{id, YES, NO};
/// A wrapper for `NSLayoutConstraint`. This both acts as a central path through which to activate /// A wrapper for `NSLayoutConstraint`. This both acts as a central path through which to activate
/// constraints, as well as a wrapper for layout constraints that are not axis bound (e.g, width or /// constraints, as well as a wrapper for layout constraints that are not axis bound (e.g, width or
@ -43,12 +43,12 @@ impl LayoutConstraint {
/// Sets the offset for this constraint. /// Sets the offset for this constraint.
pub fn offset<F: Into<f64>>(self, offset: F) -> Self { pub fn offset<F: Into<f64>>(self, offset: F) -> Self {
let offset: f64 = offset.into(); let offset: f64 = offset.into();
unsafe { unsafe {
let o = offset as CGFloat; let o = offset as CGFloat;
let _: () = msg_send![&*self.constraint, setConstant:o]; let _: () = msg_send![&*self.constraint, setConstant:o];
} }
LayoutConstraint { LayoutConstraint {
constraint: self.constraint, constraint: self.constraint,
offset: offset, offset: offset,
@ -57,6 +57,27 @@ impl LayoutConstraint {
} }
} }
pub fn set_offset<F: Into<f64>>(&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. /// 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 // 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 // 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]; 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];
}
}
} }

View file

@ -3,13 +3,15 @@
use core_graphics::base::CGFloat; 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::runtime::Object;
use objc_id::ShareId; use objc_id::ShareId;
use crate::foundation::id; use crate::foundation::{id, nil, NSInteger};
use crate::layout::constraint::LayoutConstraint; 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 /// 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. /// factory/helper for creating `LayoutConstraint` objects based on your views.
#[derive(Clone, Debug, Default)] #[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. /// Return a constraint less than or equal to another dimension anchor.
pub fn constraint_less_than_or_equal_to(&self, anchor_to: &LayoutAnchorDimension) -> LayoutConstraint { pub fn constraint_less_than_or_equal_to(&self, anchor_to: &LayoutAnchorDimension) -> LayoutConstraint {
match (&self.0, &anchor_to.0) { match (&self.0, &anchor_to.0) {
@ -67,4 +81,16 @@ impl LayoutAnchorDimension {
_ => { panic!("Attempted to create constraints with an uninitialized anchor!"); } _ => { 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!"); }
}
}
} }

View file

@ -1,6 +1,8 @@
//#![deny(missing_docs)] //#![deny(missing_docs)]
//#![deny(missing_debug_implementations)] //#![deny(missing_debug_implementations)]
#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] #![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. // Copyright 2019+, the Cacao developers.
// See the COPYRIGHT file at the top-level directory of this distribution. // See the COPYRIGHT file at the top-level directory of this distribution.
@ -8,8 +10,8 @@
//! # Cacao //! # Cacao
//! //!
//! This library provides safe Rust bindings for `AppKit` on macOS and (eventually) `UIKit` on iOS. It //! This library provides safe Rust bindings for `AppKit` on macOS and (eventually) `UIKit` on iOS and tvOS.
//! tries to do so in a way that, if you've done programming for the framework before (in Swift or //! 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 //! 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. //! creative coding and assumptions can get us pretty far.
//! //!
@ -18,14 +20,14 @@
//! already fine for some apps. //! already fine for some apps.
//! //!
//! _Note that this crate relies on the Objective-C runtime. Interfacing with the runtime *requires* //! _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 //! 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 //! 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._ //! usage - just know it's happening, and in large part it's not going away._
//! //!
//! # Hello World //! # Hello World
//! //!
//! ```rust //! ```rust,no_run
//! use cacao::macos::app::{App, AppDelegate}; //! use cacao::macos::app::{App, AppDelegate};
//! use cacao::macos::window::Window; //! use cacao::macos::window::Window;
//! //!
@ -48,23 +50,41 @@
//! ``` //! ```
//! //!
//! ## Initialization //! ## Initialization
//!
//! Due to the way that AppKit and UIKit programs typically work, you're encouraged to do the bulk //! 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 //! 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 //! ensures the application has had time to initialize and do any housekeeping necessary behind the
//! scenes. //! 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 //! ## Optional Features
//! //!
//! The following are a list of [Cargo features][cargo-features] that can be enabled or disabled. //! 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. //! 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 //! emitting notifications on macOS and iOS. Note that this _requires_ your application be
//! code-signed, and will not work without it. //! code-signed, and will not work without it.
//! - **webview**: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. //! - `webview`: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`.
//! - **webview-downloading**: Enables downloading files from the `WebView` via a private //! This feature is not supported on tvOS, as the platform has no webview control.
//! interface. This is not an App-Store-safe feature, so be aware of that before enabling. //! - `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 //! [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 url;
pub use lazy_static; pub use lazy_static;
/// Until we figure out a better way to handle reusable views (i.e, the
/// "correct" way for a list view to work), just let the delegates pass
/// back the pointer and handle keeping the pools for themselves.
pub type Node = objc_id::ShareId<objc::runtime::Object>;
#[cfg(feature = "macos")] #[cfg(feature = "macos")]
#[cfg_attr(docsrs, doc(cfg(feature = "macos")))]
pub mod macos; pub mod macos;
#[cfg(feature = "ios")] #[cfg(feature = "ios")]
#[cfg_attr(docsrs, doc(cfg(feature = "ios")))]
pub mod ios; pub mod ios;
pub mod button; pub mod button;
#[cfg(feature = "cloudkit")] #[cfg(any(feature = "cloudkit", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "cloudkit")))]
pub mod cloudkit; pub mod cloudkit;
pub mod color; pub mod color;
@ -112,15 +130,18 @@ pub mod switch;
pub mod text; pub mod text;
#[cfg(feature = "quicklook")] #[cfg(feature = "quicklook")]
#[cfg_attr(docsrs, doc(cfg(feature = "quicklook")))]
pub mod quicklook; pub mod quicklook;
#[cfg(feature = "user-notifications")] #[cfg(feature = "user-notifications")]
#[cfg_attr(docsrs, doc(cfg(feature = "user-notifications")))]
pub mod user_notifications; pub mod user_notifications;
pub mod user_activity; pub mod user_activity;
pub(crate) mod utils; pub mod utils;
pub mod view; pub mod view;
#[cfg(feature = "webview")] #[cfg(any(feature = "webview", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "webview")))]
pub mod webview; pub mod webview;

View file

@ -47,9 +47,12 @@ impl RowAction {
/// on your ListViewRow. /// on your ListViewRow.
/// ///
/// Additional configuration can be done after initialization, if need be. /// 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<F>(title: &str, style: RowActionStyle, handler: F) -> Self pub fn new<F>(title: &str, style: RowActionStyle, handler: F) -> Self
where where
F: Fn(RowAction, usize) + Send + Sync + 'static F: Fn(RowAction, usize) + 'static
{ {
let title = NSString::new(title); let title = NSString::new(title);
let block = ConcreteBlock::new(move |action: id, row: NSUInteger| { let block = ConcreteBlock::new(move |action: id, row: NSUInteger| {
@ -65,7 +68,7 @@ impl RowAction {
RowAction(unsafe { RowAction(unsafe {
let cls = class!(NSTableViewRowAction); let cls = class!(NSTableViewRowAction);
Id::from_ptr(msg_send![cls, rowActionWithStyle:style Id::from_ptr(msg_send![cls, rowActionWithStyle:style
title:title.into_inner() title:&*title
handler:block handler:block
]) ])
}) })
@ -76,13 +79,13 @@ impl RowAction {
let title = NSString::new(title); let title = NSString::new(title);
unsafe { unsafe {
let _: () = msg_send![&*self.0, setTitle:title.into_inner()]; let _: () = msg_send![&*self.0, setTitle:&*title];
} }
} }
/// Sets the background color of this action. /// Sets the background color of this action.
pub fn set_background_color(&mut self, color: Color) { pub fn set_background_color(&mut self, color: Color) {
let color = color.into_platform_specific_color(); let color = color.to_objc();
unsafe { unsafe {
let _: () = msg_send![&*self.0, setBackgroundColor:color]; let _: () = msg_send![&*self.0, setBackgroundColor:color];

View file

@ -14,7 +14,8 @@ use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl, msg_send}; use objc::{class, sel, sel_impl, msg_send};
use objc_id::Id; 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::dragdrop::DragInfo;
use crate::listview::{ use crate::listview::{
LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR, LISTVIEW_DELEGATE_PTR, LISTVIEW_CELL_VENDOR_PTR,
@ -57,6 +58,45 @@ extern fn view_for_column<T: ListViewDelegate>(
} }
} }
extern fn will_display_cell<T: ListViewDelegate>(
this: &Object,
_: Sel,
_table_view: id,
_cell: id,
_column: id,
item: NSInteger
) {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.will_display_item(item as usize);
}
extern fn menu_needs_update<T: ListViewDelegate>(
this: &Object,
_: Sel,
menu: id
) {
let view = load::<T>(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<T: ListViewDelegate>(
this: &Object,
_: Sel,
_table_view: id,
item: NSInteger
) -> BOOL {
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR);
view.item_selected(item as usize);
YES
}
extern fn row_actions_for_row<T: ListViewDelegate>( extern fn row_actions_for_row<T: ListViewDelegate>(
this: &Object, this: &Object,
_: Sel, _: Sel,
@ -67,14 +107,13 @@ extern fn row_actions_for_row<T: ListViewDelegate>(
let edge: RowEdge = edge.into(); let edge: RowEdge = edge.into();
let view = load::<T>(this, LISTVIEW_DELEGATE_PTR); let view = load::<T>(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::<Vec<&Object>>()
.into();
//if actions.len() > 0 { &mut *ids
let ids: Vec<&Object> = actions.iter().map(|action| &*action.0).collect();
NSArray::from(ids).into_inner()
//} else {
// NSArray::new(&[]).into_inner()
//}
} }
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. /// 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<T: ListViewDelegate>(instanc
// Tableview-specific // Tableview-specific
decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger); decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger);
decl.add_method(sel!(tableView:willDisplayCell:forTableColumn:row:), will_display_cell::<T> as extern fn(&Object, _, id, id, id, NSInteger));
decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id); decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column::<T> as extern fn(&Object, _, id, id, NSInteger) -> id);
decl.add_method(sel!(tableView:shouldSelectRow:), select_row::<T> as extern fn(&Object, _, id, NSInteger) -> BOOL);
decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row::<T> as extern fn(&Object, _, id, NSInteger, NSInteger) -> id); decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row::<T> as extern fn(&Object, _, id, NSInteger, NSInteger) -> id);
// A slot for some menu handling; we just let it be done here for now rather than do the
// whole delegate run, since things are fast enough nowadays to just replace the entire
// menu.
decl.add_method(sel!(menuNeedsUpdate:), menu_needs_update::<T> as extern fn(&Object, _, id));
// Drag and drop operations (e.g, accepting files) // Drag and drop operations (e.g, accepting files)
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger); decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);
decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL); decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation::<T> as extern fn (&mut Object, _, _) -> BOOL);

View file

@ -48,13 +48,16 @@ use objc_id::ShareId;
use objc::runtime::{Class, Object}; use objc::runtime::{Class, Object};
use objc::{class, msg_send, sel, sel_impl}; 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::color::Color;
use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension};
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::scrollview::ScrollView; use crate::scrollview::ScrollView;
use crate::utils::CGSize; use crate::utils::CGSize;
#[cfg(target_os = "macos")]
use crate::macos::menu::MenuItem;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
@ -148,21 +151,24 @@ fn common_init(class: *const Class) -> id {
// Let's... make NSTableView into UITableView-ish. // Let's... make NSTableView into UITableView-ish.
#[cfg(target_os = "macos")] #[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, setWantsLayer:YES];
let _: () = msg_send![tableview, setUsesAutomaticRowHeights:YES]; let _: () = msg_send![tableview, setUsesAutomaticRowHeights:YES];
let _: () = msg_send![tableview, setFloatsGroupRows: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]; let _: () = msg_send![tableview, setColumnAutoresizingStyle:1];
//msg_send![tableview, setSelectionHighlightStyle:-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]; let _: () = msg_send![tableview, setHeaderView:nil];
// NSTableView requires at least one column to be manually added if doing so by code. // 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::no_copy("CacaoListViewColumn");
let identifier = NSString::new("CacaoListViewColumn");
let default_column_alloc: id = msg_send![class!(NSTableColumn), new]; 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![default_column, setResizingMask:(1<<0)];
let _: () = msg_send![tableview, addTableColumn:default_column]; 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<RefCell<Id<Object>>>);
impl ObjcProperty {
pub fn new(obj: id) -> Self {
Self(Rc::new(RefCell::new(unsafe {
Id::from_ptr(obj)
})))
}
pub fn with<F>(&self, handler: F)
where
F: Fn(&Object)
{
let borrow = self.0.borrow();
handler(&borrow);
}
}
#[derive(Debug, Default)]
pub struct PropertyNullable<T>(Rc<RefCell<Option<T>>>);
impl<T> PropertyNullable<T> {
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<F>(&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)] #[derive(Debug)]
pub struct ListView<T = ()> { pub struct ListView<T = ()> {
/// Internal map of cell identifers/vendors. These are used for handling dynamic cell /// Internal map of cell identifers/vendors. These are used for handling dynamic cell
/// allocation and reuse, which is necessary for an "infinite" listview. /// allocation and reuse, which is necessary for an "infinite" listview.
cell_factory: CellFactory, cell_factory: CellFactory,
pub menu: PropertyNullable<Vec<MenuItem>>,
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ShareId<Object>, pub objc: ShareId<Object>,
@ -245,6 +302,7 @@ impl ListView {
ListView { ListView {
cell_factory: CellFactory::new(), cell_factory: CellFactory::new(),
menu: PropertyNullable::default(),
delegate: None, delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }),
@ -302,6 +360,7 @@ impl<T> ListView<T> where T: ListViewDelegate + 'static {
let mut view = ListView { let mut view = ListView {
cell_factory: cell, cell_factory: cell,
menu: PropertyNullable::default(),
delegate: None, delegate: None,
top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }), top: LayoutAnchorY::new(unsafe { msg_send![anchor_view, topAnchor] }),
leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }), leading: LayoutAnchorX::new(unsafe { msg_send![anchor_view, leadingAnchor] }),
@ -331,6 +390,7 @@ impl<T> ListView<T> {
pub(crate) fn clone_as_handle(&self) -> ListView { pub(crate) fn clone_as_handle(&self) -> ListView {
ListView { ListView {
cell_factory: CellFactory::new(), cell_factory: CellFactory::new(),
menu: self.menu.clone(),
delegate: None, delegate: None,
top: self.top.clone(), top: self.top.clone(),
leading: self.leading.clone(), leading: self.leading.clone(),
@ -361,8 +421,8 @@ impl<T> ListView<T> {
pub fn dequeue<R: ViewDelegate + 'static>(&self, identifier: &'static str) -> ListViewRow<R> { pub fn dequeue<R: ViewDelegate + 'static>(&self, identifier: &'static str) -> ListViewRow<R> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
let key = NSString::new(identifier).into_inner(); let key = NSString::new(identifier);
let cell: id = msg_send![&*self.objc, makeViewWithIdentifier:key owner:nil]; let cell: id = msg_send![&*self.objc, makeViewWithIdentifier:&*key owner:nil];
if cell != nil { if cell != nil {
ListViewRow::from_cached(cell) ListViewRow::from_cached(cell)
@ -386,9 +446,46 @@ impl<T> ListView<T> {
} }
} }
/// 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<F: Fn(ListView)>(&self, update: F) { pub fn perform_batch_updates<F: Fn(ListView)>(&self, update: F) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
let _: () = msg_send![&*self.objc, beginUpdates]; let _: () = msg_send![&*self.objc, beginUpdates];
let handle = self.clone_as_handle(); let handle = self.clone_as_handle();
@ -398,13 +495,13 @@ impl<T> ListView<T> {
} }
} }
pub fn insert_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animation: RowAnimation) { pub fn insert_rows(&self, indexes: &[usize], animation: RowAnimation) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new]; let index_set: id = msg_send![class!(NSMutableIndexSet), new];
for index in indexes { for index in indexes {
let x: NSUInteger = index as NSUInteger; let x: NSUInteger = *index as NSUInteger;
let _: () = msg_send![index_set, addIndex:x]; let _: () = msg_send![index_set, addIndex:x];
} }
@ -435,13 +532,13 @@ impl<T> ListView<T> {
} }
} }
pub fn remove_rows<I: IntoIterator<Item = usize>>(&self, indexes: I, animations: RowAnimation) { pub fn remove_rows(&self, indexes: &[usize], animations: RowAnimation) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {
let index_set: id = msg_send![class!(NSMutableIndexSet), new]; let index_set: id = msg_send![class!(NSMutableIndexSet), new];
for index in indexes { for index in indexes {
let x: NSUInteger = index as NSUInteger; let x: NSUInteger = *index as NSUInteger;
let _: () = msg_send![index_set, addIndex:x]; let _: () = msg_send![index_set, addIndex:x];
} }
@ -507,11 +604,11 @@ impl<T> ListView<T> {
let types: NSArray = types.into_iter().map(|t| { 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 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. // this is just an enum... and this is not an oft called method.
let x: NSString = t.clone().into(); let x: NSString = (*t).into();
x.into_inner() x.into()
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
let _: () = msg_send![&*self.objc, registerForDraggedTypes:types.into_inner()]; let _: () = msg_send![&*self.objc, registerForDraggedTypes:&*types];
} }
} }
@ -520,6 +617,14 @@ impl<T> ListView<T> {
let _: () = msg_send![&*self.objc, reloadData]; 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<T> Layout for ListView<T> { impl<T> Layout for ListView<T> {

View file

@ -14,9 +14,9 @@ use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id; 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::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; use crate::utils::load;
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. /// 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<T: ViewDelegate>(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 /// 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 /// 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 /// 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<T: ViewDelegate>() -> *c
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't // A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move. // move.
decl.add_ivar::<usize>(LISTVIEW_ROW_DELEGATE_PTR); decl.add_ivar::<usize>(LISTVIEW_ROW_DELEGATE_PTR);
decl.add_ivar::<id>(BACKGROUND_COLOR);
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL); 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) // Drag and drop operations (e.g, accepting files)
decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger); decl.add_method(sel!(draggingEntered:), dragging_entered::<T> as extern fn (&mut Object, _, _) -> NSUInteger);

View file

@ -66,6 +66,7 @@ mod ios;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use ios::{register_listview_row_view_class, register_listview_row_class_with_delegate}; 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"; pub(crate) static LISTVIEW_ROW_DELEGATE_PTR: &str = "rstListViewRowDelegatePtr";
/// A helper method for instantiating view classes and applying default settings to them. /// A helper method for instantiating view classes and applying default settings to them.
@ -264,23 +265,20 @@ impl<T> ListViewRow<T> {
/// Sets the identifier, which enables cells to be reused and dequeued properly. /// Sets the identifier, which enables cells to be reused and dequeued properly.
pub fn set_identifier(&self, identifier: &'static str) { 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(); let objc = self.objc.borrow();
unsafe { unsafe {
let _: () = msg_send![&**objc, setIdentifier:identifier]; let _: () = msg_send![&**objc, setIdentifier:&*identifier];
} }
} }
/// Call this to set the background color for the backing layer. /// Call this to set the background color for the backing layer.
pub fn set_background_color(&self, color: Color) { 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 { unsafe {
let cg: id = msg_send![bg, CGColor]; (&mut **objc).set_ivar(BACKGROUND_COLOR, color.as_ref().to_objc());
let layer: id = msg_send![&**objc, layer];
let _: () = msg_send![layer, setBackgroundColor:cg];
} }
} }
@ -290,12 +288,12 @@ impl<T> ListViewRow<T> {
let types: NSArray = types.into_iter().map(|t| { 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 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. // this is just an enum... and this is not an oft called method.
let x: NSString = t.clone().into(); let x: NSString = (*t).into();
x.into_inner() x.into()
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
let objc = self.objc.borrow(); let objc = self.objc.borrow();
let _: () = msg_send![&**objc, registerForDraggedTypes:types.into_inner()]; let _: () = msg_send![&**objc, registerForDraggedTypes:&*types];
} }
} }
} }

View file

@ -1,6 +1,6 @@
//! Various traits used for Views. //! Various traits used for Views.
use crate::Node; use crate::macos::menu::MenuItem;
use crate::dragdrop::{DragInfo, DragOperation}; use crate::dragdrop::{DragInfo, DragOperation};
use crate::listview::{ListView, ListViewRow, RowAction, RowEdge}; use crate::listview::{ListView, ListViewRow, RowAction, RowEdge};
use crate::layout::Layout; use crate::layout::Layout;
@ -27,11 +27,22 @@ pub trait ListViewDelegate {
/// Returns the number of items in the list view. /// Returns the number of items in the list view.
fn number_of_items(&self) -> usize; 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 /// 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 /// 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 /// 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. /// had time to sit down and figure them out properly yet.
fn item_for(&self, row: usize) -> ListViewRow; fn item_for(&self, row: usize) -> ListViewRow;
/// Called when an item has been selected (clicked/tapped on).
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<MenuItem> { vec![] }
/// An optional delegate method; implement this if you'd like swipe-to-reveal to be /// 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. /// supported for a given row by returning a vector of actions to show.

View file

@ -32,6 +32,7 @@ use crate::foundation::{id, NSString};
/// Represents an `NSAlert`. Has no information other than the retained pointer to the Objective C /// Represents an `NSAlert`. Has no information other than the retained pointer to the Objective C
/// side, so... don't bother inspecting this. /// side, so... don't bother inspecting this.
#[derive(Debug)]
pub struct Alert(Id<Object>); pub struct Alert(Id<Object>);
impl Alert { impl Alert {
@ -40,13 +41,13 @@ impl Alert {
pub fn new(title: &str, message: &str) -> Self { pub fn new(title: &str, message: &str) -> Self {
let title = NSString::new(title); let title = NSString::new(title);
let message = NSString::new(message); let message = NSString::new(message);
let x = NSString::new("OK"); let ok = NSString::new("OK");
Alert(unsafe { Alert(unsafe {
let alert: id = msg_send![class!(NSAlert), new]; let alert: id = msg_send![class!(NSAlert), new];
let _: () = msg_send![alert, setMessageText:title]; let _: () = msg_send![alert, setMessageText:title];
let _: () = msg_send![alert, setInformativeText:message]; let _: () = msg_send![alert, setInformativeText:message];
let _: () = msg_send![alert, addButtonWithTitle:x]; let _: () = msg_send![alert, addButtonWithTitle:ok];
Id::from_ptr(alert) Id::from_ptr(alert)
}) })
} }

View file

@ -112,9 +112,10 @@ extern fn should_handle_reopen<T: AppDelegate>(this: &Object, _: Sel, _: id, has
} }
/// Fires when the application delegate receives a `applicationDockMenu:` request. /// Fires when the application delegate receives a `applicationDockMenu:` request.
// @TODO: Make this return Vec<MenuItem>.
extern fn dock_menu<T: AppDelegate>(this: &Object, _: Sel, _: id) -> id { extern fn dock_menu<T: AppDelegate>(this: &Object, _: Sel, _: id) -> id {
match app::<T>(this).dock_menu() { match app::<T>(this).dock_menu() {
Some(mut menu) => &mut *menu.inner, Some(mut menu) => &mut *menu.0,
None => nil None => nil
} }
} }
@ -133,7 +134,7 @@ extern fn did_change_screen_parameters<T: AppDelegate>(this: &Object, _: Sel, _:
/// Fires when the application receives a `application:willContinueUserActivityWithType:` /// Fires when the application receives a `application:willContinueUserActivityWithType:`
/// notification. /// notification.
extern fn will_continue_user_activity_with_type<T: AppDelegate>(this: &Object, _: Sel, _: id, activity_type: id) -> BOOL { extern fn will_continue_user_activity_with_type<T: AppDelegate>(this: &Object, _: Sel, _: id, activity_type: id) -> BOOL {
let activity = NSString::wrap(activity_type); let activity = NSString::retain(activity_type);
match app::<T>(this).will_continue_user_activity(activity.to_str()) { match app::<T>(this).will_continue_user_activity(activity.to_str()) {
true => YES, true => YES,
@ -144,7 +145,7 @@ extern fn will_continue_user_activity_with_type<T: AppDelegate>(this: &Object, _
/// Fires when the application receives a `application:continueUserActivity:restorationHandler:` notification. /// Fires when the application receives a `application:continueUserActivity:restorationHandler:` notification.
extern fn continue_user_activity<T: AppDelegate>(this: &Object, _: Sel, _: id, activity: id, handler: id) -> BOOL { extern fn continue_user_activity<T: AppDelegate>(this: &Object, _: Sel, _: id, activity: id, handler: id) -> BOOL {
// @TODO: This needs to support restorable objects, but it involves a larger question about how // @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. // useful.
let activity = UserActivity::with_inner(activity); let activity = UserActivity::with_inner(activity);
@ -161,7 +162,7 @@ extern fn continue_user_activity<T: AppDelegate>(this: &Object, _: Sel, _: id, a
/// `application:didFailToContinueUserActivityWithType:error:` message. /// `application:didFailToContinueUserActivityWithType:error:` message.
extern fn failed_to_continue_user_activity<T: AppDelegate>(this: &Object, _: Sel, _: id, activity_type: id, error: id) { extern fn failed_to_continue_user_activity<T: AppDelegate>(this: &Object, _: Sel, _: id, activity_type: id, error: id) {
app::<T>(this).failed_to_continue_user_activity( app::<T>(this).failed_to_continue_user_activity(
NSString::wrap(activity_type).to_str(), NSString::retain(activity_type).to_str(),
Error::new(error) Error::new(error)
); );
} }
@ -197,8 +198,8 @@ extern fn accepted_cloudkit_share<T: AppDelegate>(this: &Object, _: Sel, _: id,
/// Fires when the application receives an `application:openURLs` message. /// Fires when the application receives an `application:openURLs` message.
extern fn open_urls<T: AppDelegate>(this: &Object, _: Sel, _: id, file_urls: id) { extern fn open_urls<T: AppDelegate>(this: &Object, _: Sel, _: id, file_urls: id) {
let urls = NSArray::wrap(file_urls).map(|url| { let urls = NSArray::retain(file_urls).map(|url| {
let uri = NSString::wrap(unsafe { let uri = NSString::retain(unsafe {
msg_send![url, absoluteString] msg_send![url, absoluteString]
}); });
@ -210,7 +211,7 @@ extern fn open_urls<T: AppDelegate>(this: &Object, _: Sel, _: id, file_urls: id)
/// Fires when the application receives an `application:openFileWithoutUI:` message. /// Fires when the application receives an `application:openFileWithoutUI:` message.
extern fn open_file_without_ui<T: AppDelegate>(this: &Object, _: Sel, _: id, file: id) -> BOOL { extern fn open_file_without_ui<T: AppDelegate>(this: &Object, _: Sel, _: id, file: id) -> BOOL {
let filename = NSString::wrap(file); let filename = NSString::retain(file);
match app::<T>(this).open_file_without_ui(filename.to_str()) { match app::<T>(this).open_file_without_ui(filename.to_str()) {
true => YES, true => YES,
@ -236,7 +237,7 @@ extern fn open_untitled_file<T: AppDelegate>(this: &Object, _: Sel, _: id) -> BO
/// Fired when the application receives an `application:openTempFile:` message. /// Fired when the application receives an `application:openTempFile:` message.
extern fn open_temp_file<T: AppDelegate>(this: &Object, _: Sel, _: id, filename: id) -> BOOL { extern fn open_temp_file<T: AppDelegate>(this: &Object, _: Sel, _: id, filename: id) -> BOOL {
let filename = NSString::wrap(filename); let filename = NSString::retain(filename);
match app::<T>(this).open_temp_file(filename.to_str()) { match app::<T>(this).open_temp_file(filename.to_str()) {
true => YES, true => YES,
@ -246,7 +247,7 @@ extern fn open_temp_file<T: AppDelegate>(this: &Object, _: Sel, _: id, filename:
/// Fired when the application receives an `application:printFile:` message. /// Fired when the application receives an `application:printFile:` message.
extern fn print_file<T: AppDelegate>(this: &Object, _: Sel, _: id, file: id) -> BOOL { extern fn print_file<T: AppDelegate>(this: &Object, _: Sel, _: id, file: id) -> BOOL {
let filename = NSString::wrap(file); let filename = NSString::retain(file);
match app::<T>(this).print_file(filename.to_str()) { match app::<T>(this).print_file(filename.to_str()) {
true => YES, true => YES,
@ -257,8 +258,8 @@ extern fn print_file<T: AppDelegate>(this: &Object, _: Sel, _: id, file: id) ->
/// Fired when the application receives an `application:printFiles:withSettings:showPrintPanels:` /// Fired when the application receives an `application:printFiles:withSettings:showPrintPanels:`
/// message. /// message.
extern fn print_files<T: AppDelegate>(this: &Object, _: Sel, _: id, files: id, settings: id, show_print_panels: BOOL) -> NSUInteger { extern fn print_files<T: AppDelegate>(this: &Object, _: Sel, _: id, files: id, settings: id, show_print_panels: BOOL) -> NSUInteger {
let files = NSArray::wrap(files).map(|file| { let files = NSArray::retain(files).map(|file| {
NSString::wrap(file).to_str().to_string() NSString::retain(file).to_str().to_string()
}); });
let settings = PrintSettings::with_inner(settings); let settings = PrintSettings::with_inner(settings);
@ -275,7 +276,7 @@ extern fn did_change_occlusion_state<T: AppDelegate>(this: &Object, _: Sel, _: i
/// Note: this may not fire in sandboxed applications. Apple's documentation is unclear on the /// Note: this may not fire in sandboxed applications. Apple's documentation is unclear on the
/// matter. /// matter.
extern fn delegate_handles_key<T: AppDelegate>(this: &Object, _: Sel, _: id, key: id) -> BOOL { extern fn delegate_handles_key<T: AppDelegate>(this: &Object, _: Sel, _: id, key: id) -> BOOL {
let key = NSString::wrap(key); let key = NSString::retain(key);
match app::<T>(this).delegate_handles_key(key.to_str()) { match app::<T>(this).delegate_handles_key(key.to_str()) {
true => YES, true => YES,

View file

@ -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 // 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 // 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. // 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<Mutex<Vec<TargetActionHandler>>> = Arc::new(Mutex::new(Vec::new())); static ref MENU_ITEMS_HANDLER_CACHE: Arc<Mutex<Vec<TargetActionHandler>>> = Arc::new(Mutex::new(Vec::new()));
} }*/
/// A handler to make some boilerplate less annoying. /// A handler to make some boilerplate less annoying.
#[inline] #[inline]
@ -257,36 +257,21 @@ impl App {
/// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind /// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind
/// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and /// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and
/// you shouldn't bother to either. /// you shouldn't bother to either.
///
/// The one note here: we internally cache actions to avoid them dropping without the
/// Objective-C side knowing. The the comments on the
pub fn set_menu(mut menus: Vec<Menu>) { pub fn set_menu(mut menus: Vec<Menu>) {
let mut handlers = vec![];
let main_menu = unsafe { let main_menu = unsafe {
let menu_cls = class!(NSMenu); let menu_cls = class!(NSMenu);
let item_cls = class!(NSMenuItem); let item_cls = class!(NSMenuItem);
let main_menu: id = msg_send![menu_cls, new]; let main_menu: id = msg_send![menu_cls, new];
for menu in menus.iter_mut() { for menu in menus.iter_mut() {
handlers.append(&mut menu.actions);
let item: id = msg_send![item_cls, new]; let item: id = msg_send![item_cls, new];
let _: () = msg_send![item, setSubmenu:&*menu.inner]; let _: () = msg_send![item, setSubmenu:&*menu.0];
let _: () = msg_send![main_menu, addItem:item]; let _: () = msg_send![main_menu, addItem:item];
} }
main_menu 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 { shared_application(move |app| unsafe {
let _: () = msg_send![app, setMainMenu:main_menu]; let _: () = msg_send![app, setMainMenu:main_menu];
}); });

View file

@ -32,11 +32,11 @@ impl Event {
// @TODO: Check here if key event, invalid otherwise. // @TODO: Check here if key event, invalid otherwise.
// @TODO: Figure out if we can just return &str here, since the Objective-C side // @TODO: Figure out if we can just return &str here, since the Objective-C side
// should... make it work, I think. // should... make it work, I think.
let characters = NSString::wrap(unsafe { let characters = NSString::from_retained(unsafe {
msg_send![&*self.0, characters] msg_send![&*self.0, characters]
}); });
characters.to_str().to_string() characters.to_string()
} }
/*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool { /*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool {

View file

@ -2,24 +2,38 @@
//! one level deep; this could change in the future but is fine for //! one level deep; this could change in the future but is fine for
//! now. //! now.
use objc::{class, msg_send, sel, sel_impl};
use objc::runtime::{Object, Sel};
use objc_id::ShareId;
use std::sync::Once; 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::foundation::{id, nil, NSString, NSUInteger};
use crate::events::EventModifierFlag; 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<dyn Fn() + 'static>);
/// Internal method (shorthand) for generating `NSMenuItem` holders. /// Internal method (shorthand) for generating `NSMenuItem` holders.
fn make_menu_item( fn make_menu_item<S: AsRef<str>>(
title: &str, title: S,
key: Option<&str>, key: Option<&str>,
action: Option<Sel>, action: Option<Sel>,
modifiers: Option<&[EventModifierFlag]> modifiers: Option<&[EventModifierFlag]>
) -> MenuItem { ) -> Id<Object> {
unsafe { unsafe {
let title = NSString::new(title); let title = NSString::new(title.as_ref());
// Note that AppKit requires a blank string if nil, not nil. // Note that AppKit requires a blank string if nil, not nil.
let key = NSString::new(match key { let key = NSString::new(match key {
@ -27,10 +41,16 @@ fn make_menu_item(
None => "" None => ""
}); });
let alloc: id = msg_send![class!(NSMenuItem), alloc]; // Stock menu items that use selectors targeted at system pieces are just standard
let item = ShareId::from_ptr(match action { // `NSMenuItem`s. If there's no custom ones, we use our subclass that has a slot to store a
Some(a) => msg_send![alloc, initWithTitle:title action:a keyEquivalent:key], // handler pointer.
None => msg_send![alloc, initWithTitle:title action:nil keyEquivalent:key] 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 { if let Some(modifiers) = modifiers {
@ -44,169 +64,262 @@ fn make_menu_item(
let _: () = msg_send![&*item, setKeyEquivalentModifierMask:key_mask]; 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)] #[derive(Debug)]
pub enum MenuItem { pub enum MenuItem {
/// Represents a Menu item that's not a separator - for all intents and purposes, you can consider /// A custom MenuItem. This type functions as a builder, so you can customize it easier.
/// this the real `NSMenuItem`. /// You can (and should) create this variant via the `new(title)` method, but if you need to do
Entry((ShareId<Object>, Option<TargetActionHandler>)), /// something crazier, then wrap it in this and you can hook into the Cacao menu system
/// accordingly.
Custom(Id<Object>),
/// 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. /// separating out pieces of the `NSMenu` structure.
Separator Separator
} }
impl MenuItem { impl MenuItem {
/// Creates and returns a `MenuItem::Entry` with the specified title. /// Consumes and returns a handle for the underlying MenuItem. This is internal as we make a few assumptions
pub fn entry(title: &str) -> Self { /// for how it interacts with our `Menu` setup, but this could be made public in the future.
make_menu_item(title, None, None, None) pub(crate) unsafe fn to_objc(self) -> Id<Object> {
}
/// Configures the menu item, if it's not a separator, to support a key equivalent.
pub fn key(self, key: &str) -> Self {
match self { match self {
MenuItem::Separator => MenuItem::Separator, Self::Custom(objc) => objc,
MenuItem::Entry((item, action)) => { Self::About(app_name) => {
unsafe { let title = format!("About {}", app_name);
let key = NSString::new(key); make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None)
let _: () = msg_send![&*item, setKeyEquivalent:key];
}
MenuItem::Entry((item, action))
}
}
}
/// Attaches a target/action handler to dispatch events.
pub fn action<F: Fn() + Send + Sync + 'static>(self, action: F) -> Self {
match self {
MenuItem::Separator => MenuItem::Separator,
MenuItem::Entry((item, _)) => {
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))
}, },
// Should never be hit Self::Hide => make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None),
MenuItem::Separator => MenuItem::Separator
// 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. /// Returns a `Custom` menu item, with the given title. You can configure this further with the
pub fn hide_others() -> Self { /// builder methods on this object.
make_menu_item( pub fn new<S: AsRef<str>>(title: S) -> Self {
"Hide Others", MenuItem::Custom(make_menu_item(title, None, None, None))
Some("h"),
Some(sel!(hide:)),
Some(&[EventModifierFlag::Command, EventModifierFlag::Option])
)
} }
/// Returns a standard "Hide" item. /// Configures the a custom item to have specified key equivalent. This does nothing if called
pub fn show_all() -> Self { /// on a `MenuItem` type that is not `Custom`,
make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None) 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. /// Sets the modifier key flags for this menu item. This does nothing if called on a `MenuItem`
pub fn close_window() -> Self { /// that is not `Custom`.
make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None) 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. /// Attaches a target/action handler to dispatch events. This does nothing if called on a
pub fn quit() -> Self { /// `MenuItem` that is not `Custom`.
make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None) ///
} /// 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<F: Fn() + 'static>(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. return MenuItem::Custom(objc);
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)
}
/// Returns a standard "Enter Full Screen" item self
pub fn enter_full_screen() -> Self { }
make_menu_item( }
"Enter Full Screen",
Some("f"), /// On the Objective-C side, we need to ensure our handler is dropped when this subclass
Some(sel!(toggleFullScreen:)), /// is deallocated. Note that NSMenuItem is seemingly odd outside of ARC contexts, and we
Some(&[EventModifierFlag::Command, EventModifierFlag::Control]) /// 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);
/// Returns a standard "Miniaturize" item let obj = ptr as *mut Action;
pub fn minimize() -> Self {
make_menu_item( if !obj.is_null() {
"Minimize", let _handler = Box::from_raw(obj);
Some("m"), }
Some(sel!(performMiniaturize:)),
None // 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];
/// Returns a standard "Zoom" item let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc];
pub fn zoom() -> Self { }
make_menu_item( }
"Zoom",
None, /// Called when our custom item needs to fire.
Some(sel!(performZoom:)), extern fn fire_block_action(this: &Object, _: Sel, _item: id) {
None let action = crate::utils::load::<Action>(this, BLOCK_PTR);
) (action.0)();
} }
/// Returns a standard "Redo" item. /// Injects a custom NSMenuItem subclass that contains a slot to hold a block, as well as a method
pub fn redo() -> Self { /// for calling the block.
make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None) ///
} /// 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.
/// Returns a standard "Cut" item. pub(crate) fn register_menu_item_class() -> *const Class {
pub fn cut() -> Self { static mut APP_CLASS: *const Class = 0 as *const Class;
make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None) static INIT: Once = Once::new();
}
INIT.call_once(|| unsafe {
/// Returns a standard "Select All" item. let superclass = class!(NSMenuItem);
pub fn select_all() -> Self { let mut decl = ClassDecl::new("CacaoMenuItem", superclass).unwrap();
make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None) decl.add_ivar::<usize>(BLOCK_PTR);
}
decl.add_method(sel!(dealloc), dealloc_cacao_menuitem as extern fn(&Object, _));
/// Returns a standard "Paste" item. decl.add_method(sel!(fireBlockAction:), fire_block_action as extern fn(&Object, _, id));
pub fn paste() -> Self {
make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None) APP_CLASS = decl.register();
});
unsafe {
APP_CLASS
} }
} }

View file

@ -6,17 +6,13 @@ use objc_id::{Id, ShareId};
use objc::runtime::Object; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, NSString}; use crate::foundation::{id, NSInteger, NSString};
use crate::macos::menu::item::MenuItem; use crate::macos::menu::item::MenuItem;
use crate::invoker::TargetActionHandler;
/// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting /// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting
/// them throughout the application lifecycle. /// them throughout the application lifecycle.
#[derive(Debug)] #[derive(Debug)]
pub struct Menu { pub struct Menu(pub Id<Object>);
pub inner: Id<Object>,
pub actions: Vec<TargetActionHandler>
}
impl Menu { impl Menu {
/// Creates a new `Menu` with the given title, and uses the passed items as submenu items. /// Creates a new `Menu` with the given title, and uses the passed items as submenu items.
@ -29,41 +25,49 @@ impl Menu {
/// to get the menu functioning. /// to get the menu functioning.
/// ///
pub fn new(title: &str, items: Vec<MenuItem>) -> Self { pub fn new(title: &str, items: Vec<MenuItem>) -> Self {
let inner = unsafe { Menu(unsafe {
let cls = class!(NSMenu); let cls = class!(NSMenu);
let alloc: id = msg_send![cls, alloc]; let alloc: id = msg_send![cls, alloc];
let title = NSString::new(title); let title = NSString::new(title);
let inner: id = msg_send![alloc, initWithTitle:title]; let menu: id = msg_send![alloc, initWithTitle:&*title];
Id::from_ptr(inner)
};
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 { Id::from_retained_ptr(menu)
match item { })
MenuItem::Entry((item, action)) => { }
unsafe {
let _: () = msg_send![&*inner, addItem:item];
}
if action.is_some() { /// Given a set of `MenuItem`s, merges them into an existing Menu (e.g, for a context menu on a
actions.push(action.unwrap()); /// view).
} pub fn append(menu: id, items: Vec<MenuItem>) -> 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 => { while count != 0 {
unsafe { count -= 1;
let cls = class!(NSMenuItem); let item: id = msg_send![menu, itemAtIndex:count];
let separator: id = msg_send![cls, separatorItem]; let _: () = msg_send![menu, removeItemAtIndex:count];
let _: () = msg_send![&*inner, addItem:separator]; let _: () = msg_send![item, release];
}
}
} }
} }
Menu { for item in items.into_iter() {
inner: inner, unsafe {
actions: actions let objc = item.to_objc();
let _: () = msg_send![menu, addItem:&*objc];
}
} }
menu
} }
} }

View file

@ -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 //! The coverage here is not exhaustive, but should be sufficient enough for relatively complex
//! plain different APIs. It's tempting to want to find a common one and just implement that, but //! applications. For examples, check the `examples` folder in the repository.
//! 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).
mod alert; mod alert;
pub use alert::Alert; pub use alert::Alert;
@ -25,7 +15,7 @@ mod cursor;
pub use cursor::{Cursor, CursorType}; pub use cursor::{Cursor, CursorType};
mod enums; mod enums;
pub use enums::{FocusRingType}; pub use enums::FocusRingType;
mod event; mod event;
pub use event::*; pub use event::*;

View file

@ -6,7 +6,7 @@ use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel}; use objc::runtime::{Class, Object, Sel};
use objc::{class, sel, sel_impl, msg_send}; 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::macos::toolbar::{TOOLBAR_PTR, ToolbarDelegate};
use crate::utils::load; use crate::utils::load;
@ -15,39 +15,49 @@ extern fn allowed_item_identifiers<T: ToolbarDelegate>(this: &Object, _: Sel, _:
let toolbar = load::<T>(this, TOOLBAR_PTR); let toolbar = load::<T>(this, TOOLBAR_PTR);
let identifiers: NSArray = toolbar.allowed_item_identifiers().iter().map(|identifier| { let identifiers: NSArray = toolbar.allowed_item_identifiers().iter().map(|identifier| {
NSString::new(identifier).into_inner() identifier.to_nsstring()
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
identifiers.into_inner() identifiers.into()
} }
/// Retrieves and passes the default item identifiers for this toolbar. /// Retrieves and passes the default item identifiers for this toolbar.
extern fn default_item_identifiers<T: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id { extern fn default_item_identifiers<T: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id {
let toolbar = load::<T>(this, TOOLBAR_PTR); let toolbar = load::<T>(this, TOOLBAR_PTR);
let identifiers: NSArray = toolbar.default_item_identifiers().iter().map(|identifier| { let identifiers: NSArray = toolbar.default_item_identifiers()
NSString::new(identifier).into_inner() .iter()
}).collect::<Vec<id>>().into(); .map(|identifier| identifier.to_nsstring())
.collect::<Vec<id>>()
.into();
identifiers.into_inner() identifiers.into()
} }
/// Retrieves and passes the default item identifiers for this toolbar. /// Retrieves and passes the default item identifiers for this toolbar.
extern fn selectable_item_identifiers<T: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id { extern fn selectable_item_identifiers<T: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id {
let toolbar = load::<T>(this, TOOLBAR_PTR); let toolbar = load::<T>(this, TOOLBAR_PTR);
let identifiers: NSArray = toolbar.selectable_item_identifiers().iter().map(|identifier| { let identifiers: NSArray = toolbar.selectable_item_identifiers()
NSString::new(identifier).into_inner() .iter()
}).collect::<Vec<id>>().into(); .map(|identifier| identifier.to_nsstring())
.collect::<Vec<id>>()
.into();
identifiers.into_inner() identifiers.into()
} }
/// Loads the controller, grabs whatever item is for this identifier, and returns what the /// Loads the controller, grabs whatever item is for this identifier, and returns what the
/// Objective-C runtime needs. /// Objective-C runtime needs.
extern fn item_for_identifier<T: ToolbarDelegate>(this: &Object, _: Sel, _: id, identifier: id, _: id) -> id { extern fn item_for_identifier<T: ToolbarDelegate>(
this: &Object,
_: Sel,
_: id,
identifier: id,
_: BOOL
) -> id {
let toolbar = load::<T>(this, TOOLBAR_PTR); let toolbar = load::<T>(this, TOOLBAR_PTR);
let identifier = NSString::wrap(identifier); let identifier = NSString::from_retained(identifier);
let item = toolbar.item_for(identifier.to_str()); let item = toolbar.item_for(identifier.to_str());
unsafe { unsafe {
@ -64,9 +74,21 @@ pub(crate) fn register_toolbar_class<T: ToolbarDelegate>(instance: &T) -> *const
decl.add_ivar::<usize>(TOOLBAR_PTR); decl.add_ivar::<usize>(TOOLBAR_PTR);
// Add callback methods // Add callback methods
decl.add_method(sel!(toolbarAllowedItemIdentifiers:), allowed_item_identifiers::<T> as extern fn(&Object, _, _) -> id); decl.add_method(
decl.add_method(sel!(toolbarDefaultItemIdentifiers:), default_item_identifiers::<T> as extern fn(&Object, _, _) -> id); sel!(toolbarAllowedItemIdentifiers:),
decl.add_method(sel!(toolbarSelectableItemIdentifiers:), selectable_item_identifiers::<T> as extern fn(&Object, _, _) -> id); allowed_item_identifiers::<T> as extern fn(&Object, _, _) -> id
decl.add_method(sel!(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:), item_for_identifier::<T> as extern fn(&Object, _, _, _, _) -> id); );
decl.add_method(
sel!(toolbarDefaultItemIdentifiers:),
default_item_identifiers::<T> as extern fn(&Object, _, _) -> id
);
decl.add_method(
sel!(toolbarSelectableItemIdentifiers:),
selectable_item_identifiers::<T> as extern fn(&Object, _, _) -> id
);
decl.add_method(
sel!(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:),
item_for_identifier::<T> as extern fn(&Object, _, _, _, _) -> id
);
}) })
} }

View file

@ -1,6 +1,6 @@
//! Various types used for Toolbar configuration. //! 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. /// Represents the display mode(s) a Toolbar can render in.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -29,6 +29,86 @@ impl From<ToolbarDisplayMode> 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. /// Represents the size mode a Toolbar can use.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum ToolbarSizeMode { pub enum ToolbarSizeMode {

View file

@ -7,10 +7,10 @@ use std::fmt;
use core_graphics::geometry::CGSize; use core_graphics::geometry::CGSize;
use objc_id::{Id, ShareId}; use objc_id::{Id, ShareId};
use objc::runtime::{Object}; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use crate::foundation::{id, NSString}; use crate::foundation::{id, YES, NO, NSString};
use crate::invoker::TargetActionHandler; use crate::invoker::TargetActionHandler;
use crate::button::{Button, BezelStyle}; use crate::button::{Button, BezelStyle};
use crate::image::Image; use crate::image::Image;
@ -39,8 +39,18 @@ impl ToolbarItem {
}; };
ToolbarItem { ToolbarItem {
identifier: identifier, identifier,
objc: objc, 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, button: None,
image: None, image: None,
handler: None handler: None
@ -50,7 +60,7 @@ impl ToolbarItem {
/// Sets the title for this item. /// Sets the title for this item.
pub fn set_title(&mut self, title: &str) { pub fn set_title(&mut self, title: &str) {
unsafe { unsafe {
let title = NSString::new(title).into_inner(); let title = NSString::new(title);
let _: () = msg_send![&*self.objc, setLabel:&*title]; let _: () = msg_send![&*self.objc, setLabel:&*title];
} }
} }
@ -91,8 +101,18 @@ impl ToolbarItem {
} }
} }
/// Sets an action on this item.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) { pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) {
let handler = TargetActionHandler::new(&*self.objc, action); let handler = TargetActionHandler::new(&*self.objc, action);
self.handler = Some(handler); self.handler = Some(handler);
} }
pub fn set_bordered(&self, bordered: bool) {
unsafe {
let _: () = msg_send![&*self.objc, setBordered:match bordered {
true => YES,
false => NO
}];
}
}
} }

View file

@ -19,7 +19,7 @@ mod traits;
pub use traits::ToolbarDelegate; pub use traits::ToolbarDelegate;
mod enums; mod enums;
pub use enums::{ToolbarDisplayMode, ToolbarSizeMode}; pub use enums::{ToolbarDisplayMode, ToolbarSizeMode, ItemIdentifier};
pub(crate) static TOOLBAR_PTR: &str = "rstToolbarPtr"; pub(crate) static TOOLBAR_PTR: &str = "rstToolbarPtr";
@ -67,9 +67,9 @@ impl<T> Toolbar<T> where T: ToolbarDelegate + 'static {
}); });
Toolbar { Toolbar {
identifier: identifier, identifier,
objc: objc, objc,
objc_delegate: objc_delegate, objc_delegate,
delegate: Some(delegate), delegate: Some(delegate),
} }
} }
@ -117,10 +117,10 @@ impl<T> Toolbar<T> {
/// Sets the item represented by the item identifier to be selected. /// Sets the item represented by the item identifier to be selected.
pub fn set_selected(&self, item_identifier: &str) { pub fn set_selected(&self, item_identifier: &str) {
let identifier = NSString::new(item_identifier).into_inner(); let identifier = NSString::new(item_identifier);
unsafe { unsafe {
let _: () = msg_send![&*self.objc, setSelectedItemIdentifier:identifier]; let _: () = msg_send![&*self.objc, setSelectedItemIdentifier:&*identifier];
} }
} }
} }

View file

@ -2,7 +2,7 @@
//! go. Currently a bit incomplete in that we don't support the customizing workflow, but feel free //! go. Currently a bit incomplete in that we don't support the customizing workflow, but feel free
//! to pull request it. //! 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`. /// A trait that you can implement to have your struct/etc act as an `NSToolbarDelegate`.
pub trait ToolbarDelegate { pub trait ToolbarDelegate {
@ -23,14 +23,14 @@ pub trait ToolbarDelegate {
fn did_load(&mut self, _toolbar: Toolbar) {} fn did_load(&mut self, _toolbar: Toolbar) {}
/// What items are allowed in this toolbar. /// What items are allowed in this toolbar.
fn allowed_item_identifiers(&self) -> Vec<&'static str>; fn allowed_item_identifiers(&self) -> Vec<ItemIdentifier>;
/// The default items in this toolbar. /// The default items in this toolbar.
fn default_item_identifiers(&self) -> Vec<&'static str>; fn default_item_identifiers(&self) -> Vec<ItemIdentifier>;
/// The default items in this toolbar. This defaults to a blank `Vec`, and is an optional /// The default items in this toolbar. This defaults to a blank `Vec`, and is an optional
/// method - mostly useful for Preferences windows. /// method - mostly useful for Preferences windows.
fn selectable_item_identifiers(&self) -> Vec<&'static str> { vec![] } fn selectable_item_identifiers(&self) -> Vec<ItemIdentifier> { vec![] }
/// For a given `identifier`, return the `ToolbarItem` that should be displayed. /// For a given `identifier`, return the `ToolbarItem` that should be displayed.
fn item_for(&self, _identifier: &str) -> &ToolbarItem; fn item_for(&self, _identifier: &str) -> &ToolbarItem;

View file

@ -46,7 +46,7 @@ impl Default for WindowConfig {
config.set_styles(&[ config.set_styles(&[
WindowStyle::Resizable, WindowStyle::Miniaturizable, WindowStyle::UnifiedTitleAndToolbar, WindowStyle::Resizable, WindowStyle::Miniaturizable, WindowStyle::UnifiedTitleAndToolbar,
WindowStyle::Closable, WindowStyle::Titled WindowStyle::Closable, WindowStyle::Titled, WindowStyle::FullSizeContentView
]); ]);
config config

View file

@ -229,6 +229,14 @@ impl<T> Window<T> {
} }
} }
/// Sets the content size for this window.
pub fn set_content_size<F: Into<f64>>(&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. /// Sets the minimum size this window can shrink to.
pub fn set_minimum_content_size<F: Into<f64>>(&self, width: F, height: F) { pub fn set_minimum_content_size<F: Into<f64>>(&self, width: F, height: F) {
unsafe { unsafe {
@ -244,6 +252,14 @@ impl<T> Window<T> {
let _: () = msg_send![&*self.objc, setContentMaxSize:size]; let _: () = msg_send![&*self.objc, setContentMaxSize:size];
} }
} }
/// Sets the minimum size this window can shrink to.
pub fn set_minimum_size<F: Into<f64>>(&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. /// Used for setting a toolbar on this window.
pub fn set_toolbar<TC: ToolbarDelegate>(&self, toolbar: &Toolbar<TC>) { pub fn set_toolbar<TC: ToolbarDelegate>(&self, toolbar: &Toolbar<TC>) {
@ -429,6 +445,12 @@ impl<T> Window<T> {
}]; }];
} }
} }
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 /// Returns the backing scale (e.g, `1.0` for non retina, `2.0` for retina) used on this
/// window. /// window.

View file

@ -20,7 +20,7 @@ impl URLRequest {
} }
pub fn url(&self) -> String { pub fn url(&self) -> String {
NSString::wrap(unsafe { NSString::from_retained(unsafe {
let url: id = msg_send![&*self.inner, URL]; let url: id = msg_send![&*self.inner, URL];
msg_send![url, absoluteString] msg_send![url, absoluteString]
}).to_string() }).to_string()

View file

@ -1314,10 +1314,10 @@ pub enum NotificationName {
WKAccessibilityReduceMotionStatusDidChange WKAccessibilityReduceMotionStatusDidChange
} }
impl From<NotificationName> for NSString { impl From<NotificationName> for NSString<'_> {
fn from(name: NotificationName) -> Self { fn from(name: NotificationName) -> Self {
match name { match name {
_ => NSString::new("") _ => NSString::no_copy("")
} }
} }
} }

View file

@ -38,7 +38,7 @@ impl Pasteboard {
pub fn named(name: PasteboardName) -> Self { pub fn named(name: PasteboardName) -> Self {
Pasteboard(unsafe { Pasteboard(unsafe {
let name: NSString = name.into(); let name: NSString = name.into();
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 receivers resources in the pasteboard server. It's rare-ish to need to use /// Releases the receivers 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. /// this, but considering this stuff happens on the Objective-C side you may need it.
pub fn release_globally(&self) { pub fn release_globally(&self) {
@ -85,8 +95,8 @@ impl Pasteboard {
})); }));
} }
let urls = NSArray::wrap(contents).map(|url| { let urls = NSArray::retain(contents).map(|url| {
let path = NSString::wrap(msg_send![url, path]); let path = NSString::retain(msg_send![url, path]);
Url::parse(&format!("file://{}", path.to_str())) Url::parse(&format!("file://{}", path.to_str()))
}).into_iter().filter_map(|r| r.ok()).collect(); }).into_iter().filter_map(|r| r.ok()).collect();
@ -114,8 +124,8 @@ impl Pasteboard {
})); }));
} }
let urls = NSArray::wrap(contents).map(|url| { let urls = NSArray::retain(contents).map(|url| {
let path = NSString::wrap(msg_send![url, path]).to_str().to_string(); let path = NSString::retain(msg_send![url, path]).to_str().to_string();
PathBuf::from(path) PathBuf::from(path)
}).into_iter().collect(); }).into_iter().collect();

View file

@ -22,7 +22,7 @@ pub enum PasteboardName {
Ruler Ruler
} }
impl From<PasteboardName> for NSString { impl From<PasteboardName> for NSString<'_> {
fn from(name: PasteboardName) -> Self { fn from(name: PasteboardName) -> Self {
NSString::new(match name { NSString::new(match name {
PasteboardName::Drag => "Apple CFPasteboard drag", PasteboardName::Drag => "Apple CFPasteboard drag",
@ -83,7 +83,7 @@ pub enum PasteboardType {
TIFF TIFF
} }
impl From<PasteboardType> for NSString { impl From<PasteboardType> for NSString<'_> {
fn from(pboard_type: PasteboardType) -> Self { fn from(pboard_type: PasteboardType) -> Self {
NSString::new(match pboard_type { NSString::new(match pboard_type {
PasteboardType::URL => "public.url", PasteboardType::URL => "public.url",

View file

@ -109,7 +109,7 @@ impl ThumbnailConfig {
unsafe { unsafe {
let size = CGSize::new(self.size.0, self.size.1); let size = CGSize::new(self.size.0, self.size.1);
// @TODO: Check nil here, or other bad conversion // @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![class!(QLThumbnailGenerationRequest), alloc];
let request: id = msg_send![request, initWithFileAtURL:from_url let request: id = msg_send![request, initWithFileAtURL:from_url

View file

@ -19,7 +19,7 @@ impl Default for Font {
objc: unsafe { objc: unsafe {
let cls = class!(NSFont); let cls = class!(NSFont);
let default_size: id = msg_send![cls, labelFontSize]; 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 { pub fn system(size: CGFloat) -> Self {
Font { Font {
objc: unsafe { 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])
} }
} }
} }

View file

@ -61,9 +61,6 @@ mod ios;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
use ios::{register_view_class, register_view_class_with_delegate}; use ios::{register_view_class, register_view_class_with_delegate};
//mod controller;
//pub use controller::LabelController;
mod traits; mod traits;
pub use traits::LabelDelegate; pub use traits::LabelDelegate;
@ -75,8 +72,8 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let view: id = { let view: id = {
// This sucks, but for now, sure. // This sucks, but for now, sure.
let blank = NSString::new(""); let blank = NSString::no_copy("");
msg_send![registration_fn(), labelWithString:blank.into_inner()] msg_send![registration_fn(), labelWithString:&*blank]
}; };
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
@ -230,17 +227,17 @@ impl<T> Label<T> {
let s = NSString::new(text); let s = NSString::new(text);
unsafe { 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. /// Retrieve the text currently held in the label.
pub fn text(&self) -> String { pub fn get_text(&self) -> String {
let s = NSString::wrap(unsafe { let s = NSString::retain(unsafe {
msg_send![&*self.objc, stringValue] msg_send![&*self.objc, stringValue]
}); });
s.to_str().to_string() s.to_string()
} }
pub fn set_text_alignment(&self, alignment: TextAlign) { pub fn set_text_alignment(&self, alignment: TextAlign) {
@ -256,6 +253,15 @@ impl<T> Label<T> {
} }
} }
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) { pub fn set_line_break_mode(&self, mode: LineBreakMode) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
unsafe { unsafe {

View file

@ -68,6 +68,9 @@ use ios::{register_view_class, register_view_class_with_delegate};
mod controller; mod controller;
pub use controller::ViewController; pub use controller::ViewController;
mod splitviewcontroller;
pub use splitviewcontroller::SplitViewController;
mod traits; mod traits;
pub use traits::ViewDelegate; pub use traits::ViewDelegate;
@ -199,7 +202,7 @@ impl<T> View<T> {
height: self.height.clone(), height: self.height.clone(),
center_x: self.center_x.clone(), center_x: self.center_x.clone(),
center_y: self.center_y.clone(), center_y: self.center_y.clone(),
objc: Rc::clone(&self.objc) //.clone() objc: Rc::clone(&self.objc)
} }
} }
@ -210,27 +213,18 @@ impl<T> View<T> {
unsafe { unsafe {
(&mut **objc).set_ivar(BACKGROUND_COLOR, color.as_ref().to_objc()); (&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. /// Register this view for drag and drop operations.
pub fn register_for_dragged_types(&self, types: &[PasteboardType]) { pub fn register_for_dragged_types(&self, types: &[PasteboardType]) {
unsafe { unsafe {
let types: NSArray = types.into_iter().map(|t| { let types: NSArray = types.into_iter().map(|t| {
// This clone probably doesn't need to be here, but it should also be cheap as let x: NSString = (*t).into();
// this is just an enum... and this is not an oft called method. x.into()
let x: NSString = t.clone().into();
x.into_inner()
}).collect::<Vec<id>>().into(); }).collect::<Vec<id>>().into();
let objc = self.objc.borrow(); let objc = self.objc.borrow();
let _: () = msg_send![&**objc, registerForDraggedTypes:types.into_inner()]; let _: () = msg_send![&**objc, registerForDraggedTypes:&*types];
} }
} }
} }

View file

@ -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<T: ViewDelegate>(this: &mut Object, _: Sel, animated: BOOL) {
unsafe {
let _: () = msg_send![super(this, class!(UIViewController)), viewWillAppear:animated];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.will_appear(as_bool(animated));
}
/// Called when the view controller receives a `viewDidAppear:` message.
extern fn did_appear<T: ViewDelegate>(this: &mut Object, _: Sel, animated: BOOL) {
unsafe {
let _: () = msg_send![super(this, class!(UIViewController)), viewDidAppear:animated];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.did_appear(as_bool(animated));
}
/// Called when the view controller receives a `viewWillDisappear:` message.
extern fn will_disappear<T: ViewDelegate>(this: &mut Object, _: Sel, animated: BOOL) {
unsafe {
let _: () = msg_send![super(this, class!(UIViewController)), viewWillDisappear:animated];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.will_disappear(as_bool(animated));
}
/// Called when the view controller receives a `viewDidDisappear:` message.
extern fn did_disappear<T: ViewDelegate>(this: &mut Object, _: Sel, animated: BOOL) {
unsafe {
let _: () = msg_send![super(this, class!(UIViewController)), viewDidDisappear:animated];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.did_disappear(as_bool(animated));
}
/// Registers an `NSViewDelegate`.
pub(crate) fn register_view_controller_class<T: ViewDelegate + 'static>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIViewController);
let mut decl = ClassDecl::new("RSTViewController", superclass).unwrap();
decl.add_ivar::<usize>(VIEW_DELEGATE_PTR);
decl.add_method(sel!(viewWillAppear:), will_appear::<T> as extern fn(&mut Object, _, BOOL));
decl.add_method(sel!(viewDidAppear:), did_appear::<T> as extern fn(&mut Object, _, BOOL));
decl.add_method(sel!(viewWillDisappear:), will_disappear::<T> as extern fn(&mut Object, _, BOOL));
decl.add_method(sel!(viewDidDisappear:), did_disappear::<T> as extern fn(&mut Object, _, BOOL));
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -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<T: ViewDelegate>(this: &mut Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSViewController)), viewWillAppear];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.will_appear(false);
}
/// Called when the view controller receives a `viewDidAppear` message.
extern fn did_appear<T: ViewDelegate>(this: &mut Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSViewController)), viewDidAppear];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.did_appear(false);
}
/// Called when the view controller receives a `viewWillDisappear` message.
extern fn will_disappear<T: ViewDelegate>(this: &mut Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSViewController)), viewWillDisappear];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.will_disappear(false);
}
/// Called when the view controller receives a `viewDidDisappear` message.
extern fn did_disappear<T: ViewDelegate>(this: &mut Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSViewController)), viewDidDisappear];
}
let controller = load::<T>(this, VIEW_DELEGATE_PTR);
controller.did_disappear(false);
}
/// Registers an `NSViewDelegate`.
pub(crate) fn register_view_controller_class<T: ViewDelegate + 'static>(instance: &T) -> *const Class {
load_or_register_class("NSViewController", instance.subclass_name(), |decl| unsafe {
decl.add_ivar::<usize>(VIEW_DELEGATE_PTR);
decl.add_method(sel!(viewWillAppear), will_appear::<T> as extern fn(&mut Object, _));
decl.add_method(sel!(viewDidAppear), did_appear::<T> as extern fn(&mut Object, _));
decl.add_method(sel!(viewWillDisappear), will_disappear::<T> as extern fn(&mut Object, _));
decl.add_method(sel!(viewDidDisappear), did_disappear::<T> as extern fn(&mut Object, _));
})
}

View file

@ -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<T> {
pub objc: ShareId<Object>,
pub view_controller: ViewController<T>
}
impl<T> SplitViewItem<T>
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<Sidebar, Content> {
pub objc: ShareId<Object>,
pub sidebar: SplitViewItem<Sidebar>,
pub content: SplitViewItem<Content>,
}
impl<Sidebar, Content> SplitViewController<Sidebar, Content>
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<ToolbarItem> {
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<Sidebar, Content> Controller for SplitViewController<Sidebar, Content> {
fn get_backing_node(&self) -> ShareId<Object> {
self.objc.clone()
}
}