From 5865bc6d1d66d7c9a5d4a6fbe595ffdb4e0046cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 09:01:32 +0000 Subject: [PATCH 1/9] Update core-graphics requirement from 0.22 to 0.23 Updates the requirements on [core-graphics](https://github.com/servo/core-foundation-rs) to permit the latest version. - [Commits](https://github.com/servo/core-foundation-rs/compare/core-graphics-v0.22.0...cocoa-v0.23.0) --- updated-dependencies: - dependency-name: core-graphics dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 30fd517..a496ee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] block = "0.1.6" core-foundation = "0.9" -core-graphics = "0.22" +core-graphics = "0.23" dispatch = "0.2.0" infer = { version = "0.13", optional = true } lazy_static = "1.4.0" From 81adefc1b5465538fc5837d9604e503ed8edcfd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 08:22:10 +0000 Subject: [PATCH 2/9] Update infer requirement from 0.13 to 0.15 Updates the requirements on [infer](https://github.com/bojand/infer) to permit the latest version. - [Release notes](https://github.com/bojand/infer/releases) - [Commits](https://github.com/bojand/infer/compare/v0.13.0...v0.15.0) --- updated-dependencies: - dependency-name: infer dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 30fd517..cdc78ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ block = "0.1.6" core-foundation = "0.9" core-graphics = "0.22" dispatch = "0.2.0" -infer = { version = "0.13", optional = true } +infer = { version = "0.15", optional = true } lazy_static = "1.4.0" libc = "0.2" objc = "0.2.7" From e4785bb50fd59110b7be2dcb6b8cd1fff1d6b3df Mon Sep 17 00:00:00 2001 From: simlay Date: Mon, 10 Jul 2023 03:42:46 -0400 Subject: [PATCH 3/9] iOS support for label, text input, font, more tests (#55) * Added a bunch of unit tests and added text input to uikit feature * cargo fmt * I dunno what this is but it wasnt checked in * Fix uikit unit tests * maybe fix cargo fmt * Fix iOS run * fix cargo fmt * Maybe fix cargo fmt * maybe fix cargo fmt * cargo fmt * Try to fix cargo fmt one more time * cargo fmt --- Cargo.toml | 3 + examples/ios-beta/main.rs | 73 ++++++++++++++++---- src/button/mod.rs | 11 ++- src/image/mod.rs | 7 ++ src/input/mod.rs | 74 ++++++++++++++------ src/input/uikit.rs | 95 ++++++++++++++++++++++++++ src/lib.rs | 7 +- src/listview/row/mod.rs | 7 ++ src/scrollview/mod.rs | 14 ++-- src/scrollview/traits.rs | 6 ++ src/scrollview/uikit.rs | 138 ++++++++++++++++++++++++++++++++++++++ src/text/font.rs | 33 +++++++-- src/text/label/mod.rs | 105 ++++++++++++++++------------- src/text/label/uikit.rs | 45 +++++++++++++ src/uikit/app/mod.rs | 15 +++-- src/uikit/scene/config.rs | 8 ++- src/view/mod.rs | 9 +++ 17 files changed, 545 insertions(+), 105 deletions(-) create mode 100644 src/input/uikit.rs create mode 100644 src/scrollview/uikit.rs create mode 100644 src/text/label/uikit.rs diff --git a/Cargo.toml b/Cargo.toml index 6c1ac0f..664c812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,3 +102,6 @@ required-features = ["appkit"] [[example]] name = "safe_area" required-features = ["appkit"] +[[example]] +name = "popover" +required-features = ["appkit"] \ No newline at end of file diff --git a/examples/ios-beta/main.rs b/examples/ios-beta/main.rs index ded7fc6..47283a0 100644 --- a/examples/ios-beta/main.rs +++ b/examples/ios-beta/main.rs @@ -1,5 +1,7 @@ use std::sync::RwLock; +use cacao::input::{TextField, TextFieldDelegate}; +use cacao::text::{Label, TextAlign}; use cacao::uikit::{App, AppDelegate, Scene, SceneConfig, SceneConnectionOptions, SceneSession, Window, WindowSceneDelegate}; use cacao::color::Color; @@ -15,22 +17,62 @@ impl AppDelegate for TestApp { SceneConfig::new("Default Configuration", session.role()) } } +#[derive(Debug, Default)] +pub struct ConsoleLogger(String); + +impl TextFieldDelegate for ConsoleLogger { + const NAME: &'static str = "ConsoleLogger"; + + fn text_should_begin_editing(&self, value: &str) -> bool { + println!("{} should begin editing: {}", self.0, value); + true + } + + fn text_did_change(&self, value: &str) { + println!("{} text did change to {}", self.0, value); + } + + fn text_did_end_editing(&self, value: &str) { + println!("{} did end editing: {}", self.0, value); + } + + fn text_should_end_editing(&self, value: &str) -> bool { + println!("{} should end editing: {}", self.0, value); + true + } +} -#[derive(Default)] pub struct RootView { - pub red: View, pub green: View, pub blue: View, - pub image: ImageView + pub label: Label, + pub image: ImageView, + pub input: TextField +} + +impl Default for RootView { + fn default() -> Self { + RootView { + green: View::new(), + blue: View::new(), + label: Label::new(), + image: ImageView::new(), + input: TextField::with(ConsoleLogger("input_1".to_string())) + } + } } impl ViewDelegate for RootView { const NAME: &'static str = "RootView"; fn did_load(&mut self, view: View) { - self.red.set_background_color(Color::SystemRed); - self.red.layer.set_corner_radius(16.); - view.add_subview(&self.red); + self.label.set_text("my label"); + self.label.set_text_color(Color::SystemWhite); + self.label.set_background_color(Color::SystemRed); + self.label.layer.set_corner_radius(16.); + self.label.set_text_alignment(TextAlign::Center); + + view.add_subview(&self.label); self.green.set_background_color(Color::SystemGreen); view.add_subview(&self.green); @@ -43,19 +85,26 @@ impl ViewDelegate for RootView { self.image.set_image(&Image::with_data(image_bytes)); view.add_subview(&self.image); + self.input.set_text("my input box 1"); + view.add_subview(&self.input); + LayoutConstraint::activate(&[ - self.red.top.constraint_equal_to(&view.top).offset(16.), - self.red.leading.constraint_equal_to(&view.leading).offset(16.), - self.red.trailing.constraint_equal_to(&view.trailing).offset(-16.), - self.red.height.constraint_equal_to_constant(100.), - self.green.top.constraint_equal_to(&self.red.bottom).offset(16.), + self.label.leading.constraint_equal_to(&view.leading).offset(16.), + self.label.top.constraint_equal_to(&view.top).offset(16.), + self.label.height.constraint_equal_to_constant(100.), + self.label.trailing.constraint_equal_to(&view.trailing).offset(-16.), + self.green.top.constraint_equal_to(&self.label.bottom).offset(16.), self.green.leading.constraint_equal_to(&view.leading).offset(16.), self.green.trailing.constraint_equal_to(&view.trailing).offset(-16.), self.green.height.constraint_equal_to_constant(120.), + self.input.center_x.constraint_equal_to(&self.green.center_x), + self.input.center_y.constraint_equal_to(&self.green.center_y), self.blue.top.constraint_equal_to(&self.green.bottom).offset(16.), self.blue.leading.constraint_equal_to(&view.leading).offset(16.), self.blue.trailing.constraint_equal_to(&view.trailing).offset(-16.), - self.blue.bottom.constraint_equal_to(&view.bottom).offset(-16.) + self.blue.bottom.constraint_equal_to(&view.bottom).offset(-16.), + self.image.center_x.constraint_equal_to(&self.blue.center_x), + self.image.center_y.constraint_equal_to(&self.blue.center_y) ]); } } diff --git a/src/button/mod.rs b/src/button/mod.rs index 6602bb8..2d0f03f 100644 --- a/src/button/mod.rs +++ b/src/button/mod.rs @@ -353,5 +353,14 @@ impl Drop for Button { /// Registers an `NSButton` subclass, and configures it to hold some ivars /// for various things we need to store. fn register_class() -> *const Class { - load_or_register_class("NSButton", "RSTButton", |decl| unsafe {}) + #[cfg(feature = "appkit")] + let super_class = "NSButton"; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let super_class = "UIButton"; + load_or_register_class(super_class, "RSTButton", |decl| unsafe {}) +} + +#[test] +fn test_button() { + let button = Button::new("foobar"); } diff --git a/src/image/mod.rs b/src/image/mod.rs index 73a5f48..a949da7 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -8,6 +8,7 @@ use crate::layout::Layout; use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; +use crate::layer::Layer; #[cfg(feature = "autolayout")] use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY}; @@ -52,6 +53,10 @@ pub struct ImageView { /// A pointer to the Objective-C runtime view controller. pub objc: ObjcProperty, + /// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit + /// we explicitly opt in to layer backed views. + pub layer: Layer, + /// A pointer to the Objective-C runtime top layout constraint. #[cfg(feature = "autolayout")] pub top: LayoutAnchorY, @@ -135,6 +140,8 @@ impl ImageView { #[cfg(feature = "autolayout")] center_y: LayoutAnchorY::center(view), + layer: Layer::wrap(unsafe { msg_send![view, layer] }), + objc: ObjcProperty::retain(view) } } diff --git a/src/input/mod.rs b/src/input/mod.rs index b4155e8..4af8bb0 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -44,7 +44,7 @@ //! For more information on Autolayout, view the module or check out the examples folder. use objc::runtime::{Class, Object}; -use objc::{msg_send, sel, sel_impl}; +use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; use crate::color::Color; @@ -64,11 +64,11 @@ mod appkit; #[cfg(feature = "appkit")] use appkit::{register_view_class, register_view_class_with_delegate}; -//#[cfg(feature = "uikit")] -//mod uikit; +#[cfg(feature = "uikit")] +mod uikit; -//#[cfg(feature = "uikit")] -//use uikit::{register_view_class, register_view_class_with_delegate}; +#[cfg(all(feature = "uikit", not(feature = "appkit")))] +use uikit::{register_view_class, register_view_class_with_delegate}; mod traits; pub use traits::TextFieldDelegate; @@ -200,50 +200,52 @@ where let class = register_view_class_with_delegate(&delegate); let mut delegate = Box::new(delegate); - let label = common_init(class); + let input = common_init(class); unsafe { let ptr: *const T = &*delegate; - (&mut *label).set_ivar(TEXTFIELD_DELEGATE_PTR, ptr as usize); + (&mut *input).set_ivar(TEXTFIELD_DELEGATE_PTR, ptr as usize); }; + #[cfg(feature = "uikit")] + let _: () = unsafe { msg_send![input, setDelegate: input] }; - let mut label = TextField { + let mut input = TextField { delegate: None, - objc: ObjcProperty::retain(label), + objc: ObjcProperty::retain(input), #[cfg(feature = "autolayout")] - top: LayoutAnchorY::top(label), + top: LayoutAnchorY::top(input), #[cfg(feature = "autolayout")] - left: LayoutAnchorX::left(label), + left: LayoutAnchorX::left(input), #[cfg(feature = "autolayout")] - leading: LayoutAnchorX::leading(label), + leading: LayoutAnchorX::leading(input), #[cfg(feature = "autolayout")] - right: LayoutAnchorX::right(label), + right: LayoutAnchorX::right(input), #[cfg(feature = "autolayout")] - trailing: LayoutAnchorX::trailing(label), + trailing: LayoutAnchorX::trailing(input), #[cfg(feature = "autolayout")] - bottom: LayoutAnchorY::bottom(label), + bottom: LayoutAnchorY::bottom(input), #[cfg(feature = "autolayout")] - width: LayoutAnchorDimension::width(label), + width: LayoutAnchorDimension::width(input), #[cfg(feature = "autolayout")] - height: LayoutAnchorDimension::height(label), + height: LayoutAnchorDimension::height(input), #[cfg(feature = "autolayout")] - center_x: LayoutAnchorX::center(label), + center_x: LayoutAnchorX::center(input), #[cfg(feature = "autolayout")] - center_y: LayoutAnchorY::center(label) + center_y: LayoutAnchorY::center(input) }; - (&mut delegate).did_load(label.clone_as_handle()); - label.delegate = Some(delegate); - label + (&mut delegate).did_load(input.clone_as_handle()); + input.delegate = Some(delegate); + input } } @@ -290,10 +292,16 @@ impl TextField { } /// Grabs the value from the textfield and returns it as an owned String. + #[cfg(feature = "appkit")] pub fn get_value(&self) -> String { self.objc .get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() }) } + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + pub fn get_value(&self) -> String { + self.objc + .get(|obj| unsafe { NSString::retain(msg_send![obj, text]).to_string() }) + } /// Call this to set the background color for the backing layer. pub fn set_background_color>(&self, color: C) { @@ -309,7 +317,10 @@ impl TextField { let s = NSString::new(text); self.objc.with_mut(|obj| unsafe { + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setStringValue:&*s]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setText:&*s]; }); } @@ -318,7 +329,10 @@ impl TextField { let s = NSString::new(text); self.objc.with_mut(|obj| unsafe { + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setPlaceholderString:&*s]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setPlaceholder:&*s]; }); } @@ -326,7 +340,10 @@ impl TextField { pub fn set_text_alignment(&self, alignment: TextAlign) { self.objc.with_mut(|obj| unsafe { let alignment: NSInteger = alignment.into(); + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setAlignment: alignment]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setTextAlignment: alignment]; }); } @@ -401,3 +418,16 @@ impl Drop for TextField { }*/ } } + +#[test] +fn test_text_view() { + let text_field = TextField::new(); + let value = text_field.get_value(); + assert!(value.is_empty()); + text_field.set_background_color(Color::SystemBlue); + text_field.set_text("foobar"); + let value = text_field.get_value(); + assert_eq!(value, "foobar".to_string()); + text_field.set_text_alignment(TextAlign::Left); + text_field.set_font(Font::default()); +} diff --git a/src/input/uikit.rs b/src/input/uikit.rs new file mode 100644 index 0000000..83f15a2 --- /dev/null +++ b/src/input/uikit.rs @@ -0,0 +1,95 @@ +use std::sync::Once; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel, BOOL}; +use objc::{class, msg_send, sel, sel_impl}; +use objc_id::Id; + +use crate::foundation::{id, load_or_register_class, NSString, NSUInteger, NO, YES}; +use crate::input::{TextFieldDelegate, TEXTFIELD_DELEGATE_PTR}; +use crate::utils::load; + +/// Called when editing this text field has ended (e.g. user pressed enter). +extern "C" fn text_did_end_editing(this: &mut Object, _: Sel, _info: id) { + let view = load::(this, TEXTFIELD_DELEGATE_PTR); + let s = NSString::retain(unsafe { msg_send![this, text] }); + view.text_did_end_editing(s.to_str()); +} + +extern "C" fn text_did_begin_editing(this: &mut Object, _: Sel, _info: id) { + let view = load::(this, TEXTFIELD_DELEGATE_PTR); + let s = NSString::retain(unsafe { msg_send![this, text] }); + view.text_did_begin_editing(s.to_str()); +} + +extern "C" fn text_did_change(this: &mut Object, _: Sel, _info: id) { + let view = load::(this, TEXTFIELD_DELEGATE_PTR); + let s = NSString::retain(unsafe { msg_send![this, text] }); + view.text_did_change(s.to_str()); +} + +extern "C" fn text_should_begin_editing(this: &mut Object, _: Sel, _info: id) -> BOOL { + let view = load::(this, TEXTFIELD_DELEGATE_PTR); + let s = NSString::retain(unsafe { msg_send![this, text] }); + + match view.text_should_begin_editing(s.to_str()) { + true => YES, + false => NO + } +} + +extern "C" fn text_should_end_editing(this: &mut Object, _: Sel, _info: id) -> BOOL { + let view = load::(this, TEXTFIELD_DELEGATE_PTR); + let s = NSString::retain(unsafe { msg_send![this, text] }); + match view.text_should_end_editing(s.to_str()) { + true => YES, + false => NO + } +} + +/// Injects an `UITextField` subclass. This is used for the default views that don't use delegates - we +/// have separate classes here since we don't want to waste cycles on methods that will never be +/// used if there's no delegates. +pub(crate) fn register_view_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(UITextField); + let decl = ClassDecl::new("RSTTextInputField", superclass).unwrap(); + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} + +/// Injects an `UITextField` subclass, with some callback and pointer ivars for what we +/// need to do. +pub(crate) fn register_view_class_with_delegate(instance: &T) -> *const Class { + load_or_register_class("UITextField", instance.subclass_name(), |decl| unsafe { + // A pointer to the "view controller" on the Rust side. It's expected that this doesn't + // move. + decl.add_ivar::(TEXTFIELD_DELEGATE_PTR); + + decl.add_method( + sel!(textFieldDidEndEditing:), + text_did_end_editing:: as extern "C" fn(&mut Object, _, _) + ); + decl.add_method( + sel!(textFieldDidBeginEditing:), + text_did_begin_editing:: as extern "C" fn(&mut Object, _, _) + ); + decl.add_method( + sel!(textFieldDidChangeSelection:), + text_did_change:: as extern "C" fn(&mut Object, _, _) + ); + decl.add_method( + sel!(textFieldShouldBeginEditing:), + text_should_begin_editing:: as extern "C" fn(&mut Object, Sel, id) -> BOOL + ); + decl.add_method( + sel!(textFieldShouldEndEditing:), + text_should_end_editing:: as extern "C" fn(&mut Object, Sel, id) -> BOOL + ); + }) +} diff --git a/src/lib.rs b/src/lib.rs index 35d9cd1..aff3551 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,7 @@ pub mod cloudkit; pub mod color; -#[cfg(feature = "appkit")] +#[cfg(any(feature = "appkit", feature = "uikit"))] pub mod control; #[cfg(feature = "appkit")] @@ -140,7 +140,7 @@ pub mod geometry; #[cfg(any(feature = "appkit", feature = "uikit"))] pub mod image; -#[cfg(feature = "appkit")] +#[cfg(any(feature = "appkit", feature = "uikit"))] pub mod input; pub(crate) mod invoker; @@ -161,7 +161,7 @@ pub mod pasteboard; #[cfg(feature = "appkit")] pub mod progress; -#[cfg(feature = "appkit")] +#[cfg(any(feature = "appkit", feature = "uikit"))] pub mod scrollview; #[cfg(feature = "appkit")] @@ -170,7 +170,6 @@ pub mod switch; #[cfg(feature = "appkit")] pub mod select; -#[cfg(feature = "appkit")] pub mod text; #[cfg(feature = "quicklook")] diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index 7ed7208..44c829f 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -55,6 +55,7 @@ use crate::layer::Layer; use crate::layout::Layout; use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; +#[cfg(all(feature = "appkit", target_os = "macos"))] use crate::view::{ViewAnimatorProxy, ViewDelegate}; #[cfg(feature = "autolayout")] @@ -96,6 +97,7 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { #[derive(Debug)] pub struct ListViewRow { /// An object that supports limited animations. Can be cloned into animation closures. + #[cfg(all(feature = "appkit", target_os = "macos"))] pub animator: ViewAnimatorProxy, /// A pointer to the Objective-C runtime view controller. @@ -163,6 +165,7 @@ impl ListViewRow { ListViewRow { delegate: None, objc: ObjcProperty::retain(view), + #[cfg(all(feature = "appkit", target_os = "macos"))] animator: ViewAnimatorProxy::new(view), #[cfg(feature = "autolayout")] @@ -227,6 +230,7 @@ where let view = ListViewRow { delegate: Some(delegate), objc: ObjcProperty::retain(view), + #[cfg(all(feature = "appkit", target_os = "macos"))] animator: ViewAnimatorProxy::new(view), #[cfg(feature = "autolayout")] @@ -283,6 +287,7 @@ where let mut view = ListViewRow { delegate: None, objc: ObjcProperty::retain(view), + #[cfg(all(feature = "appkit", target_os = "macos"))] animator: ViewAnimatorProxy::new(view), #[cfg(feature = "autolayout")] @@ -335,6 +340,7 @@ where ListViewRow { delegate: None, objc: self.objc.clone(), + #[cfg(all(feature = "appkit", target_os = "macos"))] animator: self.animator.clone(), #[cfg(feature = "autolayout")] @@ -384,6 +390,7 @@ impl ListViewRow { is_handle: true, layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row. objc: self.objc.clone(), + #[cfg(all(feature = "appkit", target_os = "macos"))] animator: self.animator.clone(), #[cfg(feature = "autolayout")] diff --git a/src/scrollview/mod.rs b/src/scrollview/mod.rs index d975d81..3f93918 100644 --- a/src/scrollview/mod.rs +++ b/src/scrollview/mod.rs @@ -50,7 +50,6 @@ use crate::color::Color; use crate::foundation::{id, nil, NSArray, NSString, NO, YES}; use crate::layout::Layout; use crate::objc_access::ObjcAccess; -use crate::pasteboard::PasteboardType; use crate::utils::properties::ObjcProperty; #[cfg(feature = "autolayout")] @@ -62,11 +61,11 @@ mod appkit; #[cfg(feature = "appkit")] use appkit::{register_scrollview_class, register_scrollview_class_with_delegate}; -//#[cfg(feature = "uikit")] -//mod ios; +#[cfg(feature = "uikit")] +mod uikit; -//#[cfg(feature = "uikit")] -//use ios::{register_view_class, register_view_class_with_delegate}; +#[cfg(all(feature = "uikit", not(feature = "appkit")))] +use uikit::{register_scrollview_class, register_scrollview_class_with_delegate}; mod traits; pub use traits::ScrollViewDelegate; @@ -334,3 +333,8 @@ impl Drop for ScrollView { }*/ } } + +#[test] +fn test_scrollview() { + let view = ScrollView::new(); +} diff --git a/src/scrollview/traits.rs b/src/scrollview/traits.rs index ca13308..480cd8f 100644 --- a/src/scrollview/traits.rs +++ b/src/scrollview/traits.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "appkit")] use crate::dragdrop::{DragInfo, DragOperation}; use crate::scrollview::ScrollView; @@ -22,24 +23,29 @@ pub trait ScrollViewDelegate { /// Called when this has been removed from the view heirarchy. fn did_disappear(&self, _animated: bool) {} + #[cfg(feature = "appkit")] /// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform. fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None } + #[cfg(feature = "appkit")] /// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation. fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false } + #[cfg(feature = "appkit")] /// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data. fn perform_drag_operation(&self, _info: DragInfo) -> bool { false } + #[cfg(feature = "appkit")] /// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up. fn conclude_drag_operation(&self, _info: DragInfo) {} + #[cfg(feature = "appkit")] /// Invoked when the dragged image exits the destination’s bounds rectangle (in the case of a view) or its frame /// rectangle (in the case of a window object). fn dragging_exited(&self, _info: DragInfo) {} diff --git a/src/scrollview/uikit.rs b/src/scrollview/uikit.rs new file mode 100644 index 0000000..f21caaf --- /dev/null +++ b/src/scrollview/uikit.rs @@ -0,0 +1,138 @@ +//! This module does one specific thing: register a custom `NSView` class that's... brought to the +//! modern era. +//! +//! I kid, I kid. +//! +//! It just enforces that coordinates are judged from the top-left, which is what most people look +//! for in the modern era. It also implements a few helpers for things like setting a background +//! color, and enforcing layer backing by default. + +use std::sync::Once; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel, BOOL}; +use objc::{class, sel, sel_impl}; +use objc_id::Id; + +use crate::foundation::{id, NSUInteger, NO, YES}; +use crate::scrollview::{ScrollViewDelegate, SCROLLVIEW_DELEGATE_PTR}; +use crate::utils::load; + +/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. +extern "C" fn enforce_normalcy(_: &Object, _: Sel) -> BOOL { + return YES; +} + +/* +use crate::dragdrop::DragInfo; +/// Called when a drag/drop operation has entered this view. +extern "C" fn dragging_entered(this: &mut Object, _: Sel, info: id) -> NSUInteger { + let view = load::(this, SCROLLVIEW_DELEGATE_PTR); + view.dragging_entered(DragInfo { + info: unsafe { Id::from_ptr(info) } + }) + .into() +} + +/// Called when a drag/drop operation has entered this view. +extern "C" fn prepare_for_drag_operation(this: &mut Object, _: Sel, info: id) -> BOOL { + let view = load::(this, SCROLLVIEW_DELEGATE_PTR); + + match view.prepare_for_drag_operation(DragInfo { + info: unsafe { Id::from_ptr(info) } + }) { + true => YES, + false => NO + } +} + +/// Called when a drag/drop operation has entered this view. +extern "C" fn perform_drag_operation(this: &mut Object, _: Sel, info: id) -> BOOL { + let view = load::(this, SCROLLVIEW_DELEGATE_PTR); + + match view.perform_drag_operation(DragInfo { + info: unsafe { Id::from_ptr(info) } + }) { + true => YES, + false => NO + } +} + +/// Called when a drag/drop operation has entered this view. +extern "C" fn conclude_drag_operation(this: &mut Object, _: Sel, info: id) { + let view = load::(this, SCROLLVIEW_DELEGATE_PTR); + + view.conclude_drag_operation(DragInfo { + info: unsafe { Id::from_ptr(info) } + }); +} + +/// Called when a drag/drop operation has entered this view. +extern "C" fn dragging_exited(this: &mut Object, _: Sel, info: id) { + let view = load::(this, SCROLLVIEW_DELEGATE_PTR); + + view.dragging_exited(DragInfo { + info: unsafe { Id::from_ptr(info) } + }); +} +*/ + +/// Injects an `UIScrollView` subclass. +pub(crate) fn register_scrollview_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(UIScrollView); + let decl = ClassDecl::new("RSTScrollView", superclass).unwrap(); + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} + +/// Injects an `NSView` subclass, with some callback and pointer ivars for what we +/// need to do. +pub(crate) fn register_scrollview_class_with_delegate() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(UIScrollView); + let mut decl = ClassDecl::new("RSTScrollViewWithDelegate", superclass).unwrap(); + + // A pointer to the "view controller" on the Rust side. It's expected that this doesn't + // move. + decl.add_ivar::(SCROLLVIEW_DELEGATE_PTR); + + decl.add_method(sel!(isFlipped), enforce_normalcy as extern "C" fn(&Object, _) -> BOOL); + + /* + // Drag and drop operations (e.g, accepting files) + decl.add_method( + sel!(draggingEntered:), + dragging_entered:: as extern "C" fn(&mut Object, _, _) -> NSUInteger + ); + decl.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation:: as extern "C" fn(&mut Object, _, _) -> BOOL + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation:: as extern "C" fn(&mut Object, _, _) -> BOOL + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation:: as extern "C" fn(&mut Object, _, _) + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited:: as extern "C" fn(&mut Object, _, _) + ); + */ + + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/src/text/font.rs b/src/text/font.rs index d0151fe..f65c0bc 100644 --- a/src/text/font.rs +++ b/src/text/font.rs @@ -19,27 +19,39 @@ pub struct Font(pub ShareId); impl Default for Font { /// Returns the default `labelFont` on macOS. fn default() -> Self { - Font(unsafe { - let cls = class!(NSFont); - let default_size: id = msg_send![cls, labelFontSize]; - ShareId::from_ptr(msg_send![cls, labelFontOfSize: default_size]) - }) + let cls = Self::class(); + let default_size: id = unsafe { msg_send![cls, labelFontSize] }; + + #[cfg(feature = "appkit")] + let font = Font(unsafe { ShareId::from_ptr(msg_send![cls, labelFontOfSize: default_size]) }); + + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let font = Font(unsafe { ShareId::from_ptr(msg_send![cls, systemFontOfSize: default_size]) }); + font } } impl Font { + fn class() -> &'static Class { + #[cfg(feature = "appkit")] + let class = class!(NSFont); + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let class = class!(UIFont); + + class + } /// Creates and returns a default system font at the specified size. pub fn system(size: f64) -> Self { let size = size as CGFloat; - Font(unsafe { ShareId::from_ptr(msg_send![class!(NSFont), systemFontOfSize: size]) }) + Font(unsafe { ShareId::from_ptr(msg_send![Self::class(), systemFontOfSize: size]) }) } /// Creates and returns a default bold system font at the specified size. pub fn bold_system(size: f64) -> Self { let size = size as CGFloat; - Font(unsafe { ShareId::from_ptr(msg_send![class!(NSFont), boldSystemFontOfSize: size]) }) + Font(unsafe { ShareId::from_ptr(msg_send![Self::class(), boldSystemFontOfSize: size]) }) } /// Creates and returns a monospace system font at the specified size and weight @@ -78,3 +90,10 @@ impl AsRef for Font { self } } + +#[test] +fn font_test() { + let default_font = Font::default(); + let system_font = Font::system(100.0); + let bold_system_font = Font::bold_system(100.0); +} diff --git a/src/text/label/mod.rs b/src/text/label/mod.rs index bda7d8f..8bdd3cd 100644 --- a/src/text/label/mod.rs +++ b/src/text/label/mod.rs @@ -49,6 +49,7 @@ use objc_id::ShareId; use crate::color::Color; use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NSUInteger, NO, YES}; +use crate::layer::Layer; use crate::layout::Layout; use crate::objc_access::ObjcAccess; use crate::text::{AttributedString, Font, LineBreakMode, TextAlign}; @@ -63,11 +64,11 @@ mod appkit; #[cfg(feature = "appkit")] use appkit::{register_view_class, register_view_class_with_delegate}; -//#[cfg(feature = "uikit")] -//mod uikit; +#[cfg(feature = "uikit")] +mod uikit; -//#[cfg(feature = "uikit")] -//use uikit::{register_view_class, register_view_class_with_delegate}; +#[cfg(all(feature = "uikit", not(feature = "appkit")))] +use uikit::{register_view_class, register_view_class_with_delegate}; mod traits; pub use traits::LabelDelegate; @@ -156,6 +157,10 @@ pub struct Label { /// A pointer to the delegate for this view. pub delegate: Option>, + /// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit + /// we explicitly opt in to layer backed views. + pub layer: Layer, + /// A pointer to the Objective-C runtime top layout constraint. #[cfg(feature = "autolayout")] pub top: LayoutAnchorY, @@ -207,9 +212,12 @@ impl Label { /// Returns a default `Label`, suitable for pub fn new() -> Self { let view = allocate_view(register_view_class); + Self::init(view, None) + } + pub(crate) fn init(view: id, delegate: Option>) -> Label { Label { - delegate: None, + delegate, #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(view), @@ -241,6 +249,8 @@ impl Label { #[cfg(feature = "autolayout")] center_y: LayoutAnchorY::center(view), + layer: Layer::wrap(unsafe { msg_send![view, layer] }), + objc: ObjcProperty::retain(view) } } @@ -255,51 +265,12 @@ where pub fn with(delegate: T) -> Label { let delegate = Box::new(delegate); - let label = allocate_view(register_view_class_with_delegate::); + let view = allocate_view(register_view_class_with_delegate::); unsafe { let ptr: *const T = &*delegate; - (&mut *label).set_ivar(LABEL_DELEGATE_PTR, ptr as usize); + (&mut *view).set_ivar(LABEL_DELEGATE_PTR, ptr as usize); }; - - let mut label = Label { - delegate: None, - - #[cfg(feature = "autolayout")] - top: LayoutAnchorY::top(label), - - #[cfg(feature = "autolayout")] - left: LayoutAnchorX::left(label), - - #[cfg(feature = "autolayout")] - leading: LayoutAnchorX::leading(label), - - #[cfg(feature = "autolayout")] - right: LayoutAnchorX::right(label), - - #[cfg(feature = "autolayout")] - trailing: LayoutAnchorX::trailing(label), - - #[cfg(feature = "autolayout")] - bottom: LayoutAnchorY::bottom(label), - - #[cfg(feature = "autolayout")] - width: LayoutAnchorDimension::width(label), - - #[cfg(feature = "autolayout")] - height: LayoutAnchorDimension::height(label), - - #[cfg(feature = "autolayout")] - center_x: LayoutAnchorX::center(label), - - #[cfg(feature = "autolayout")] - center_y: LayoutAnchorY::center(label), - - objc: ObjcProperty::retain(label) - }; - - //(&mut delegate).did_load(label.clone_as_handle()); - label.delegate = Some(delegate); - label + Label::init(view, Some(delegate)) } } @@ -342,6 +313,8 @@ impl Label { #[cfg(feature = "autolayout")] center_y: self.center_y.clone(), + layer: self.layer.clone(), + objc: self.objc.clone() } } @@ -372,28 +345,51 @@ impl Label { let s = NSString::new(text); self.objc.with_mut(|obj| unsafe { + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setStringValue:&*s]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setText:&*s]; }); } /// Sets the attributed string to be the attributed string value on this label. pub fn set_attributed_text(&self, text: AttributedString) { self.objc.with_mut(|obj| unsafe { + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setAttributedStringValue:&*text]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setAttributedText:&*text]; }); } /// Retrieve the text currently held in the label. + #[cfg(feature = "appkit")] pub fn get_text(&self) -> String { self.objc .get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() }) } + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + pub fn get_text(&self) -> String { + self.objc.get(|obj| { + let val: id = unsafe { msg_send![obj, text] }; + // Through trial and error, this seems to return a null pointer when there's no + // text. + if val.is_null() { + String::new() + } else { + NSString::retain(val).to_string() + } + }) + } /// Sets the text alignment for this label. pub fn set_text_alignment(&self, alignment: TextAlign) { self.objc.with_mut(|obj| unsafe { let alignment: NSInteger = alignment.into(); + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setAlignment: alignment]; + #[cfg(all(feature = "uikit", not(feature = "appkit")))] + let _: () = msg_send![obj, setTextAlignment: alignment]; }); } @@ -467,3 +463,18 @@ impl Drop for Label { }*/ } } + +#[test] +fn test_label() { + let label = Label::new(); + let text = label.get_text(); + assert!(text.is_empty()); + label.set_background_color(Color::SystemOrange); + label.set_text_color(Color::SystemRed); + label.set_text_alignment(TextAlign::Right); + label.set_text("foobar"); + let text = label.get_text(); + assert_eq!(text, "foobar".to_string()); + label.set_font(Font::system(10.0)); + label.set_attributed_text(AttributedString::new("foobar")); +} diff --git a/src/text/label/uikit.rs b/src/text/label/uikit.rs new file mode 100644 index 0000000..87d4c0b --- /dev/null +++ b/src/text/label/uikit.rs @@ -0,0 +1,45 @@ +use std::sync::Once; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel, BOOL}; +use objc::{class, sel, sel_impl}; +use objc_id::Id; + +use crate::foundation::{id, NSUInteger, NO, YES}; +use crate::text::label::{LabelDelegate, LABEL_DELEGATE_PTR}; + +/// Injects an `UILabel` subclass. This is used for the default views that don't use delegates - we +/// have separate classes here since we don't want to waste cycles on methods that will never be +/// used if there's no delegates. +pub(crate) fn register_view_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(UILabel); + let decl = ClassDecl::new("RSTTextField", superclass).unwrap(); + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} + +/// Injects an `UILabel` subclass, with some callback and pointer ivars for what we +/// need to do. +pub(crate) fn register_view_class_with_delegate() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(UIView); + let mut decl = ClassDecl::new("RSTTextFieldWithDelegate", superclass).unwrap(); + + // A pointer to the "view controller" on the Rust side. It's expected that this doesn't + // move. + decl.add_ivar::(LABEL_DELEGATE_PTR); + + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/src/uikit/app/mod.rs b/src/uikit/app/mod.rs index e4b03bd..1e0045c 100644 --- a/src/uikit/app/mod.rs +++ b/src/uikit/app/mod.rs @@ -139,7 +139,10 @@ where } } -impl App { +impl App +where + T: AppDelegate + 'static +{ /// Handles calling through to `UIApplicationMain()`, ensuring that it's using our custom /// `UIApplication` and `UIApplicationDelegate` classes. pub fn run(&self) { @@ -149,12 +152,14 @@ impl App { let c_args = args.iter().map(|arg| arg.as_ptr()).collect::>(); - let mut s = NSString::new("RSTApplication_UIApplication"); - let mut s2 = NSString::new("RSTAppDelegate_NSObject"); + let cls = register_app_class(); + let dl = register_app_delegate_class::(); + + let cls_name: id = unsafe { msg_send![cls, className] }; + let dl_name: id = unsafe { msg_send![dl, className] }; unsafe { - println!("RUNNING?!"); - UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), s.into(), s2.into()); + UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), cls_name, dl_name); } //self.pool.drain(); diff --git a/src/uikit/scene/config.rs b/src/uikit/scene/config.rs index c637004..46ff205 100644 --- a/src/uikit/scene/config.rs +++ b/src/uikit/scene/config.rs @@ -2,7 +2,8 @@ use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::Id; -use crate::foundation::{id, ClassMap, NSString}; +use crate::foundation::{id, load_or_register_class, ClassMap, NSString}; + use crate::uikit::scene::SessionRole; /// A wrapper for UISceneConfiguration. @@ -26,7 +27,10 @@ impl SceneConfig { let config: id = msg_send![cls, configurationWithName:name sessionRole:role]; let _: () = msg_send![config, setSceneClass: class!(UIWindowScene)]; - let _: () = msg_send![config, setDelegateClass: delegate_class]; + + // TODO: use register_window_scene_delegate_class rather than load_or_register_class. + let window_delegate = load_or_register_class("UIResponder", "RSTWindowSceneDelegate", |decl| unsafe {}); + let _: () = msg_send![config, setDelegateClass: window_delegate]; Id::from_ptr(config) }) diff --git a/src/view/mod.rs b/src/view/mod.rs index e99a53d..a61321b 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -77,7 +77,9 @@ mod splitviewcontroller; #[cfg(feature = "appkit")] pub use splitviewcontroller::SplitViewController; +#[cfg(feature = "appkit")] mod popover; +#[cfg(feature = "appkit")] pub use popover::*; mod traits; pub use traits::ViewDelegate; @@ -347,3 +349,10 @@ impl Drop for View { } } } + +#[test] +fn test_view() { + let view = View::new(); + let clone = view.clone_as_handle(); + view.set_background_color(Color::SystemGreen); +} From 9f8d946371f4a6dba7346052ca38011a101d3beb Mon Sep 17 00:00:00 2001 From: Ari Lotter Date: Mon, 10 Jul 2023 16:59:25 -0400 Subject: [PATCH 4/9] add haptics (#93) --- src/appkit/haptics.rs | 79 +++++++++++++++++++++++++++++++++++++++++++ src/appkit/mod.rs | 2 ++ 2 files changed, 81 insertions(+) create mode 100644 src/appkit/haptics.rs diff --git a/src/appkit/haptics.rs b/src/appkit/haptics.rs new file mode 100644 index 0000000..adf1356 --- /dev/null +++ b/src/appkit/haptics.rs @@ -0,0 +1,79 @@ +use std::convert::TryFrom; + +use objc::{class, msg_send, runtime::Object, sel, sel_impl}; +use objc_id::ShareId; + +use crate::foundation::NSUInteger; + +#[derive(Clone, Debug)] +pub struct HapticFeedbackPerformer(pub ShareId); + +impl HapticFeedbackPerformer { + pub fn perform(&self, pattern: FeedbackPattern, performance_time: PerformanceTime) { + unsafe { + let _: () = msg_send![&*self.0, performFeedbackPattern: pattern performanceTime: performance_time]; + } + } +} + +impl Default for HapticFeedbackPerformer { + /// Returns the default haptic feedback performer. + fn default() -> Self { + HapticFeedbackPerformer(unsafe { + let manager = msg_send![class!(NSHapticFeedbackManager), defaultPerformer]; + ShareId::from_ptr(manager) + }) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum PerformanceTime { + Default = 0, + Now = 1, + DrawCompleted = 2 +} + +impl Default for PerformanceTime { + fn default() -> Self { + Self::Default + } +} + +impl TryFrom for PerformanceTime { + type Error = &'static str; + + fn try_from(value: f64) -> Result { + match value as u8 { + 0 => Ok(Self::Default), + 1 => Ok(Self::Now), + 2 => Ok(Self::DrawCompleted), + _ => Err("Invalid performance time") + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum FeedbackPattern { + Generic = 0, + Alignment = 1, + LevelChange = 2 +} + +impl Default for FeedbackPattern { + fn default() -> Self { + Self::Generic + } +} + +impl TryFrom for FeedbackPattern { + type Error = &'static str; + + fn try_from(value: f64) -> Result { + match value as u8 { + 0 => Ok(Self::Generic), + 1 => Ok(Self::Alignment), + 2 => Ok(Self::LevelChange), + _ => Err("Invalid feedback pattern") + } + } +} diff --git a/src/appkit/mod.rs b/src/appkit/mod.rs index c96c132..eab6a63 100644 --- a/src/appkit/mod.rs +++ b/src/appkit/mod.rs @@ -27,3 +27,5 @@ pub mod menu; pub mod printing; pub mod toolbar; pub mod window; + +pub mod haptics; From aedcfe1c65041a1df6e596d38dd411c62168150d Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 13 Jul 2023 17:07:40 -0700 Subject: [PATCH 5/9] Mark how to run the nightly version of tools, and fix some text (#95) --- CONTRIBUTING.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98b2ca2..dc0808b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Have a look at the [issue tracker](https://github.com/ryanmcgrath/cacao/issues). describing your problem (or a very similar one) there, please open a new issue with the following details: -- Which versions of Rust and cacao (and macOS/iOS build/device) are you using? +- Which versions of Rust and Cacao (and macOS/iOS build/device) are you using? - Which feature flags are you using? - What are you trying to accomplish? - What is the full error you are seeing? @@ -34,17 +34,17 @@ If you can't find an issue (open or closed) describing your idea on the [issue tracker], open an issue. Adding answers to the following questions in your description is +1: -- What do you want to do, and how do you expect Alchemy to support you with that? -- How might this be added to Alchemy? +- What do you want to do, and how do you expect Cacao to support you with that? +- How might this be added to Cacao? - What are possible alternatives? - Are there any disadvantages? Thank you! -## Contribute code to Alchemy +## Contribute code to Cacao -### Setting up cacao locally +### Setting up Cacao locally 1. Install Rust. Stable should be fine. 2. Clone this repository and open it in your favorite editor. @@ -57,8 +57,7 @@ In a few cases, though, it's fine to deviate - a good example is branching match To run rustfmt tests locally: -1. Use rustup to set rust toolchain to the version specified in the - [rust-toolchain file](./rust-toolchain). +1. Use rustup to set Rust toolchain to the latest stable version of Rust. 2. Install the rustfmt and clippy by running ``` @@ -66,18 +65,18 @@ To run rustfmt tests locally: rustup component add clippy-preview ``` -3. Run clippy using cargo from the root of your alchemy repo. +3. Run clippy nightly using cargo from the root of your Cacao repo. ``` - cargo clippy + cargo +nightly clippy ``` Each PR needs to compile without warning. -4. Run rustfmt using cargo from the root of your alchemy repo. +4. Run rustfmt nightly using cargo from the root of your Cacao repo. To see changes that need to be made, run ``` - cargo fmt --all -- --check + cargo +nightly fmt --all -- --check ``` If all code is properly formatted (e.g. if you have not made any changes), @@ -89,15 +88,15 @@ To run rustfmt tests locally: Once you are ready to apply the formatting changes, run ``` - cargo fmt --all + cargo +nightly fmt --all ``` You won't see any output, but all your files will be corrected. You can also use rustfmt to make corrections or highlight issues in your editor. -Check out [their README](https://github.com/rust-lang-nursery/rustfmt) for details. +Check out [their README](https://github.com/rust-lang/rustfmt) for details. ### Notes This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's -worthwhile to ensure that it scans well. It also maps well to existing Cocoa/cacao idioms and is generally preferred. +worthwhile to ensure that it scans well. It also maps well to existing Cocoa/Cacao idioms and is generally preferred. From 01507f7642768ae0c39cd9a4d74ca290d13e09e3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 13 Jul 2023 17:22:54 -0700 Subject: [PATCH 6/9] Global NSEvent listener and some mouse methods (#94) * Support for all NSEvent types and configurable event monitoring * Useful mouse event methods * rustfmt nightly fixes * Use standard kind naming convention --- Cargo.toml | 3 +- src/appkit/event/mod.rs | 109 +++++++++++++++++++++++++++++++++++++--- src/events.rs | 42 +++++++++++++++- src/foundation/mod.rs | 2 + 4 files changed, 146 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 664c812..3398f75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ default-target = "x86_64-apple-darwin" rustdoc-args = ["--cfg", "docsrs"] [dependencies] +bitmask-enum = "2.2.1" block = "0.1.6" core-foundation = "0.9" core-graphics = "0.23" @@ -104,4 +105,4 @@ name = "safe_area" required-features = ["appkit"] [[example]] name = "popover" -required-features = ["appkit"] \ No newline at end of file +required-features = ["appkit"] diff --git a/src/appkit/event/mod.rs b/src/appkit/event/mod.rs index 5de45f1..851639f 100644 --- a/src/appkit/event/mod.rs +++ b/src/appkit/event/mod.rs @@ -1,15 +1,54 @@ +use bitmask_enum::bitmask; use block::ConcreteBlock; use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::Id; -use crate::foundation::{id, nil, NSString}; +use crate::events::EventType; +use crate::foundation::{id, nil, NSInteger, NSPoint, NSString}; /// An EventMask describes the type of event. -#[derive(Debug)] +#[bitmask(u64)] pub enum EventMask { - KeyDown + LeftMouseDown = 1 << 1, + LeftMouseUp = 1 << 2, + RightMouseDown = 1 << 3, + RightMouseUp = 1 << 4, + MouseMoved = 1 << 5, + LeftMouseDragged = 1 << 6, + RightMouseDragged = 1 << 7, + MouseEntered = 1 << 8, + MouseExited = 1 << 9, + KeyDown = 1 << 10, + KeyUp = 1 << 11, + FlagsChanged = 1 << 12, + AppKitDefined = 1 << 13, + SystemDefined = 1 << 14, + ApplicationDefined = 1 << 15, + Periodic = 1 << 16, + CursorUpdate = 1 << 17, + + ScrollWheel = 1 << 22, + TabletPoint = 1 << 23, + TabletProximity = 1 << 24, + OtherMouseDown = 1 << 25, + OtherMouseUp = 1 << 26, + OtherMouseDragged = 1 << 27, + + Gesture = 1 << 29, + Magnify = 1 << 30, + Swipe = 1 << 31, + Rotate = 1 << 18, + BeginGesture = 1 << 19, + EndGesture = 1 << 20, + + SmartMagnify = 1 << 32, + QuickLook = 1 << 33, + Pressure = 1 << 34, + DirectTouch = 1 << 37, + + ChangeMode = 1 << 38 } /// A wrapper over an `NSEvent`. @@ -25,6 +64,16 @@ impl Event { Event(unsafe { Id::from_ptr(objc) }) } + /// The event's type. + /// + /// Corresponds to the `type` getter. + pub fn kind(&self) -> EventType { + let kind: NSUInteger = unsafe { msg_send![&*self.0, type] }; + + unsafe { ::std::mem::transmute(kind) } + } + + /// The characters associated with a key-up or key-down event. pub fn characters(&self) -> String { // @TODO: Check here if key event, invalid otherwise. // @TODO: Figure out if we can just return &str here, since the Objective-C side @@ -34,6 +83,26 @@ impl Event { characters.to_string() } + /// The indices of the currently pressed mouse buttons. + pub fn pressed_mouse_buttons() -> NSUInteger { + unsafe { msg_send![class!(NSEvent), pressedMouseButtons] } + } + + /// Reports the current mouse position in screen coordinates. + pub fn mouse_location() -> NSPoint { + unsafe { msg_send![class!(NSEvent), mouseLocation] } + } + + /// The button number for a mouse event. + pub fn button_number(&self) -> NSInteger { + unsafe { msg_send![&*self.0, buttonNumber] } + } + + /// The number of mouse clicks associated with a mouse-down or mouse-up event. + pub fn click_count(&self) -> NSInteger { + unsafe { msg_send![&*self.0, clickCount] } + } + /*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool { let modifier_flags: NSUInteger = unsafe { msg_send![&*self.0, modifierFlags] @@ -47,13 +116,13 @@ impl Event { false }*/ - /// Register an event handler with the system event stream. This method + /// Register an event handler with the local system event stream. This method /// watches for events that occur _within the application_. Events outside - /// of the application require installing a `monitor_global_events` handler. + /// of the application require installing a `global_monitor` handler. /// /// Note that in order to monitor all possible events, both local and global /// monitors are required - the streams don't mix. - pub fn local_monitor(_mask: EventMask, handler: F) -> EventMonitor + pub fn local_monitor(mask: EventMask, handler: F) -> EventMonitor where F: Fn(Event) -> Option + Send + Sync + 'static { @@ -68,7 +137,33 @@ impl Event { let block = block.copy(); EventMonitor(unsafe { - msg_send![class!(NSEvent), addLocalMonitorForEventsMatchingMask:1024 + msg_send![class!(NSEvent), addLocalMonitorForEventsMatchingMask:mask.bits + handler:block] + }) + } + + /// Register an event handler with the global system event stream. This method + /// watches for events that occur _outside the application_. Events within + /// the application require installing a `local_monitor` handler. + /// + /// Note that in order to monitor all possible events, both local and global + /// monitors are required - the streams don't mix. + pub fn global_monitor(mask: EventMask, handler: F) -> EventMonitor + where + F: Fn(Event) -> Option + Send + Sync + 'static + { + let block = ConcreteBlock::new(move |event: id| { + let evt = Event::new(event); + + match handler(evt) { + Some(mut evt) => &mut *evt.0, + None => nil + } + }); + let block = block.copy(); + + EventMonitor(unsafe { + msg_send![class!(NSEvent), addGlobalMonitorForEventsMatchingMask:mask.bits handler:block] }) } diff --git a/src/events.rs b/src/events.rs index a4f3cc4..c0f3754 100644 --- a/src/events.rs +++ b/src/events.rs @@ -48,7 +48,45 @@ impl From<&EventModifierFlag> for NSUInteger { /// Represents an event type that you can request to be notified about. #[derive(Clone, Copy, Debug)] +#[cfg_attr(target_pointer_width = "32", repr(u32))] +#[cfg_attr(target_pointer_width = "64", repr(u64))] pub enum EventType { - /// A keydown event. - KeyDown + LeftMouseDown = 1, + LeftMouseUp = 2, + RightMouseDown = 3, + RightMouseUp = 4, + MouseMoved = 5, + LeftMouseDragged = 6, + RightMouseDragged = 7, + MouseEntered = 8, + MouseExited = 9, + KeyDown = 10, + KeyUp = 11, + FlagsChanged = 12, + AppKitDefined = 13, + SystemDefined = 14, + ApplicationDefined = 15, + Periodic = 16, + CursorUpdate = 17, + + ScrollWheel = 22, + TabletPoint = 23, + TabletProximity = 24, + OtherMouseDown = 25, + OtherMouseUp = 26, + OtherMouseDragged = 27, + + Gesture = 29, + Magnify = 30, + Swipe = 31, + Rotate = 18, + BeginGesture = 19, + EndGesture = 20, + + SmartMagnify = 32, + QuickLook = 33, + Pressure = 34, + DirectTouch = 37, + + ChangeMode = 38 } diff --git a/src/foundation/mod.rs b/src/foundation/mod.rs index e101255..f73c053 100644 --- a/src/foundation/mod.rs +++ b/src/foundation/mod.rs @@ -86,3 +86,5 @@ pub type NSInteger = libc::c_long; /// Platform-specific. #[cfg(target_pointer_width = "64")] pub type NSUInteger = libc::c_ulong; + +pub type NSPoint = core_graphics::geometry::CGPoint; From 9fbb332b38f87ea4b4491dfda253e920b4320026 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 19 Jul 2023 13:40:51 -0700 Subject: [PATCH 7/9] Idiomatic NSArray iteration (#97) --- src/appkit/app/delegate.rs | 12 ++++++----- src/foundation/array.rs | 44 +++++++++++++++++++++++++++----------- src/pasteboard/mod.rs | 2 +- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/appkit/app/delegate.rs b/src/appkit/app/delegate.rs index b833c46..0e90c2f 100644 --- a/src/appkit/app/delegate.rs +++ b/src/appkit/app/delegate.rs @@ -187,13 +187,12 @@ extern "C" fn accepted_cloudkit_share(this: &Object, _: Sel, _: /// Fires when the application receives an `application:openURLs` message. extern "C" fn open_urls(this: &Object, _: Sel, _: id, file_urls: id) { let urls = NSArray::retain(file_urls) - .map(|url| { + .iter() + .filter_map(|url| { let uri = NSString::retain(unsafe { msg_send![url, absoluteString] }); - Url::parse(uri.to_str()) + Url::parse(uri.to_str()).ok() }) - .into_iter() - .filter_map(|url| url.ok()) .collect(); app::(this).open_urls(urls); @@ -263,7 +262,10 @@ extern "C" fn print_files( settings: id, show_print_panels: BOOL ) -> NSUInteger { - let files = NSArray::retain(files).map(|file| NSString::retain(file).to_str().to_string()); + let files = NSArray::retain(files) + .iter() + .map(|file| NSString::retain(file).to_str().to_string()) + .collect(); let settings = PrintSettings::with_inner(settings); diff --git a/src/foundation/array.rs b/src/foundation/array.rs index cbeb772..800156c 100644 --- a/src/foundation/array.rs +++ b/src/foundation/array.rs @@ -41,21 +41,41 @@ impl NSArray { unsafe { msg_send![&*self.0, count] } } - /// A helper method for mapping over the backing `NSArray` items and producing a Rust `Vec`. - /// Often times we need to map in this framework to convert between Rust types, so isolating - /// this out makes life much easier. - pub fn map T>(&self, transform: F) -> Vec { - let count = self.count(); - let objc = &*self.0; + /// Returns an iterator over the `NSArray` + pub fn iter<'a>(&'a self) -> NSArrayIterator<'a> { + NSArrayIterator { + next_index: 0, + count: self.count(), + array: self + } + } +} +#[derive(Debug)] +pub struct NSArrayIterator<'a> { + next_index: usize, + count: usize, + + array: &'a NSArray +} + +impl Iterator for NSArrayIterator<'_> { + type Item = id; + + fn next(&mut self) -> Option { // 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() + if self.next_index < self.count { + let objc = &*self.array.0; + let index = self.next_index; + + let item: id = unsafe { msg_send![objc, objectAtIndex: index] }; + + self.next_index += 1; + Some(item) + } else { + None + } } } diff --git a/src/pasteboard/mod.rs b/src/pasteboard/mod.rs index c15764c..3847ff4 100644 --- a/src/pasteboard/mod.rs +++ b/src/pasteboard/mod.rs @@ -106,7 +106,7 @@ impl Pasteboard { })); } - let urls = NSArray::retain(contents).map(|url| NSURL::retain(url)).into_iter().collect(); + let urls = NSArray::retain(contents).iter().map(|url| NSURL::retain(url)).collect(); Ok(urls) } From 1b577506a779a27e3340ca96f05ce923e2773fa3 Mon Sep 17 00:00:00 2001 From: maxer137 Date: Wed, 19 Jul 2023 21:20:43 +0000 Subject: [PATCH 8/9] Fix Label::set_max_number_of_lines causing crash on iOS (#98) --- src/text/label/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/text/label/mod.rs b/src/text/label/mod.rs index 8bdd3cd..04844a3 100644 --- a/src/text/label/mod.rs +++ b/src/text/label/mod.rs @@ -417,7 +417,10 @@ impl Label { /// Sets the maximum number of lines. pub fn set_max_number_of_lines(&self, num: NSInteger) { self.objc.with_mut(|obj| unsafe { + #[cfg(feature = "appkit")] let _: () = msg_send![obj, setMaximumNumberOfLines: num]; + #[cfg(feature = "uikit")] + let _: () = msg_send![obj, setNumberOfLines: num]; }); } From e6696eaa3ee43a6f34e2c174bfa2b5cbe7bc26e9 Mon Sep 17 00:00:00 2001 From: Benedikt Terhechte Date: Tue, 1 Aug 2023 08:51:53 +0200 Subject: [PATCH 9/9] Multiple changes I had to make for Ebou (#89) * Add segmented control, icons, toolbar * add to demos * doc tests * format code * Additional SFSymbol definitions --- examples/calculator/content_view.rs | 2 +- examples/popover.rs | 25 +- examples/todos_list/add/view.rs | 2 +- .../preferences/toggle_option_view.rs | 3 +- examples/todos_list/preferences/toolbar.rs | 4 +- examples/todos_list/storage/defaults.rs | 3 +- examples/todos_list/todos/toolbar.rs | 2 +- src/appkit/mod.rs | 1 + src/appkit/segmentedcontrol.rs | 346 ++++++++++++++++++ src/appkit/toolbar/item.rs | 15 +- src/appkit/window/mod.rs | 16 + src/button/mod.rs | 6 +- src/image/icons.rs | 30 +- src/image/image.rs | 10 +- src/invoker.rs | 10 +- src/select/mod.rs | 2 +- src/switch.rs | 2 +- 17 files changed, 449 insertions(+), 30 deletions(-) create mode 100644 src/appkit/segmentedcontrol.rs diff --git a/examples/calculator/content_view.rs b/examples/calculator/content_view.rs index 53cd593..9ff00aa 100644 --- a/examples/calculator/content_view.rs +++ b/examples/calculator/content_view.rs @@ -16,7 +16,7 @@ pub fn button(text: &str, msg: Msg) -> Button { button.set_bordered(false); button.set_bezel_style(BezelStyle::SmallSquare); button.set_focus_ring_type(FocusRingType::None); - button.set_action(move || dispatch(msg.clone())); + button.set_action(move |_| dispatch(msg.clone())); button.set_key_equivalent(&*text.to_lowercase()); let font = Font::system(22.); diff --git a/examples/popover.rs b/examples/popover.rs index f3f8810..f3a42c0 100644 --- a/examples/popover.rs +++ b/examples/popover.rs @@ -5,13 +5,15 @@ //! - Another Controller / View use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::segmentedcontrol::SegmentedControl; use cacao::appkit::window::{Window, WindowConfig, WindowController, WindowDelegate}; use cacao::appkit::{App, AppDelegate}; use cacao::button::Button; +use cacao::foundation::NSArray; use cacao::geometry::{Edge, Rect}; +use cacao::image::Image; use cacao::layout::{Layout, LayoutConstraint}; use cacao::notification_center::Dispatcher; -use cacao::text::{Font, Label}; use cacao::view::{Popover, PopoverConfig, View, ViewController, ViewDelegate}; struct BasicApp { @@ -124,7 +126,7 @@ impl ViewDelegate for PopoverExampleContentView { fn did_load(&mut self, view: cacao::view::View) { let mut button = Button::new("Show"); - button.set_action(|| dispatch_ui(Msg::Click)); + button.set_action(|_| dispatch_ui(Msg::Click)); let controller = PopoverExampleContentViewController::new(); let config = PopoverConfig { @@ -164,16 +166,21 @@ impl Dispatcher for BasicApp { #[derive(Debug)] struct PopoverExampleContentViewController { - pub label: Label + pub control: SegmentedControl } impl PopoverExampleContentViewController { fn new() -> Self { - let label = Label::new(); - let font = Font::system(20.); - label.set_font(&font); - label.set_text("Hello"); - Self { label } + let images = NSArray::from(vec![ + &*Image::symbol(cacao::image::SFSymbol::AtSymbol, "Hello").0, + &*Image::symbol(cacao::image::SFSymbol::PaperPlane, "Hello").0, + &*Image::symbol(cacao::image::SFSymbol::PaperPlaneFilled, "Hello").0, + ]); + let mut control = SegmentedControl::new(images, cacao::appkit::segmentedcontrol::TrackingMode::SelectOne); + control.set_action(|index| { + println!("Selected Index {index}"); + }); + Self { control } } } @@ -181,6 +188,6 @@ impl ViewDelegate for PopoverExampleContentViewController { const NAME: &'static str = "PopoverExampleContentViewController"; fn did_load(&mut self, view: View) { - view.add_subview(&self.label); + view.add_subview(&self.control); } } diff --git a/examples/todos_list/add/view.rs b/examples/todos_list/add/view.rs index c8e962d..0e4c899 100644 --- a/examples/todos_list/add/view.rs +++ b/examples/todos_list/add/view.rs @@ -50,7 +50,7 @@ impl ViewDelegate for AddNewTodoContentView { let mut button = Button::new("Add"); button.set_key_equivalent("\r"); - button.set_action(|| dispatch_ui(Message::ProcessNewTodo)); + button.set_action(|_| dispatch_ui(Message::ProcessNewTodo)); view.add_subview(&instructions); view.add_subview(&input); diff --git a/examples/todos_list/preferences/toggle_option_view.rs b/examples/todos_list/preferences/toggle_option_view.rs index 517fb5f..9389e9f 100644 --- a/examples/todos_list/preferences/toggle_option_view.rs +++ b/examples/todos_list/preferences/toggle_option_view.rs @@ -2,6 +2,7 @@ use cacao::layout::{Layout, LayoutConstraint}; use cacao::switch::Switch; use cacao::text::Label; use cacao::view::View; +use objc::runtime::Object; /// A reusable widget for a toggle; this is effectively a standard checkbox/label combination for /// toggling a boolean value. @@ -55,7 +56,7 @@ impl ToggleOptionView { /// can toggle your settings and such there. pub fn configure(&mut self, text: &str, subtitle: &str, state: bool, handler: F) where - F: Fn() + Send + Sync + 'static + F: Fn(*const Object) + Send + Sync + 'static { self.title.set_text(text); self.subtitle.set_text(subtitle); diff --git a/examples/todos_list/preferences/toolbar.rs b/examples/todos_list/preferences/toolbar.rs index 0c8aa4e..d65573c 100644 --- a/examples/todos_list/preferences/toolbar.rs +++ b/examples/todos_list/preferences/toolbar.rs @@ -19,7 +19,7 @@ impl Default for PreferencesToolbar { let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "General"); item.set_image(icon); - item.set_action(|| { + item.set_action(|_| { dispatch_ui(Message::SwitchPreferencesToGeneralPane); }); @@ -32,7 +32,7 @@ impl Default for PreferencesToolbar { let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "Advanced"); item.set_image(icon); - item.set_action(|| { + item.set_action(|_| { dispatch_ui(Message::SwitchPreferencesToAdvancedPane); }); diff --git a/examples/todos_list/storage/defaults.rs b/examples/todos_list/storage/defaults.rs index d327171..582d216 100644 --- a/examples/todos_list/storage/defaults.rs +++ b/examples/todos_list/storage/defaults.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use cacao::defaults::{UserDefaults, Value}; +use objc::runtime::Object; const EXAMPLE: &str = "exampleSetting"; @@ -25,7 +26,7 @@ impl Defaults { } /// Toggles the example setting. - pub fn toggle_should_whatever() { + pub fn toggle_should_whatever(_object: *const Object) { toggle_bool(EXAMPLE); } diff --git a/examples/todos_list/todos/toolbar.rs b/examples/todos_list/todos/toolbar.rs index 12f1335..19d9b49 100644 --- a/examples/todos_list/todos/toolbar.rs +++ b/examples/todos_list/todos/toolbar.rs @@ -15,7 +15,7 @@ impl Default for TodosToolbar { item.set_title("Add Todo"); item.set_button(Button::new("+ New")); - item.set_action(|| { + item.set_action(|_| { dispatch_ui(Message::OpenNewTodoSheet); }); diff --git a/src/appkit/mod.rs b/src/appkit/mod.rs index eab6a63..0c5bdac 100644 --- a/src/appkit/mod.rs +++ b/src/appkit/mod.rs @@ -29,3 +29,4 @@ pub mod toolbar; pub mod window; pub mod haptics; +pub mod segmentedcontrol; diff --git a/src/appkit/segmentedcontrol.rs b/src/appkit/segmentedcontrol.rs new file mode 100644 index 0000000..ba9fbb7 --- /dev/null +++ b/src/appkit/segmentedcontrol.rs @@ -0,0 +1,346 @@ +//! Wraps `NSSegmentedControl` on appkit + +use std::fmt; +use std::sync::Once; + +use std::cell::RefCell; +use std::rc::Rc; + +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc::{class, msg_send, sel, sel_impl}; +use objc_id::ShareId; + +use crate::color::Color; +use crate::control::Control; +use crate::foundation::{id, nil, NSArray, NSString, NSUInteger, BOOL, NO, YES}; +use crate::image::Image; +use crate::invoker::TargetActionHandler; +use crate::keys::Key; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; +use crate::text::{AttributedString, Font}; +use crate::utils::{load, properties::ObjcProperty}; + +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY}; + +#[cfg(feature = "appkit")] +use crate::appkit::FocusRingType; + +/// Wraps `NSButton` on appkit, and `UIButton` on iOS and tvOS. +/// +/// You'd use this type to create a button that a user can interact with. Buttons can be configured +/// a number of ways, and support setting a callback to fire when they're clicked or tapped. +/// +/// Some properties are platform-specific; see the documentation for further information. +/// +/// ```rust,no_run +/// use cacao::button::Button; +/// use cacao::view::View; +/// use crate::cacao::layout::Layout; +/// let mut button = Button::new("My button title"); +/// button.set_key_equivalent("c"); +/// +/// button.set_action(|_| { +/// println!("My button was clicked."); +/// }); +/// let my_view : View<()> = todo!(); +/// +/// // Make sure you don't let your Button drop for as long as you need it. +/// my_view.add_subview(&button); +/// ``` +#[derive(Debug)] +pub struct SegmentedControl { + /// A handle for the underlying Objective-C object. + pub objc: ObjcProperty, + + /// Hold on to the images + images: NSArray, + + handler: Option, + + /// A pointer to the Objective-C runtime top layout constraint. + #[cfg(feature = "autolayout")] + pub top: LayoutAnchorY, + + /// A pointer to the Objective-C runtime leading layout constraint. + #[cfg(feature = "autolayout")] + pub leading: LayoutAnchorX, + + /// A pointer to the Objective-C runtime left layout constraint. + #[cfg(feature = "autolayout")] + pub left: LayoutAnchorX, + + /// A pointer to the Objective-C runtime trailing layout constraint. + #[cfg(feature = "autolayout")] + pub trailing: LayoutAnchorX, + + /// A pointer to the Objective-C runtime right layout constraint. + #[cfg(feature = "autolayout")] + pub right: LayoutAnchorX, + + /// A pointer to the Objective-C runtime bottom layout constraint. + #[cfg(feature = "autolayout")] + pub bottom: LayoutAnchorY, + + /// A pointer to the Objective-C runtime width layout constraint. + #[cfg(feature = "autolayout")] + pub width: LayoutAnchorDimension, + + /// A pointer to the Objective-C runtime height layout constraint. + #[cfg(feature = "autolayout")] + pub height: LayoutAnchorDimension, + + /// A pointer to the Objective-C runtime center X layout constraint. + #[cfg(feature = "autolayout")] + pub center_x: LayoutAnchorX, + + /// A pointer to the Objective-C runtime center Y layout constraint. + #[cfg(feature = "autolayout")] + pub center_y: LayoutAnchorY +} + +#[derive(Debug)] +#[repr(u8)] +pub enum TrackingMode { + SelectOne = 0, + SelectMany = 1, + SelectMomentary = 2 +} + +impl SegmentedControl { + /// Creates a new `NSSegmentedControl` instance, configures it appropriately, + /// and retains the necessary Objective-C runtime pointer. + pub fn new(images: NSArray, tracking_mode: TrackingMode) -> Self { + let view: id = unsafe { + let tracking_mode = tracking_mode as u8 as i32; + let control: id = msg_send![register_class(), segmentedControlWithImages:&*images trackingMode:tracking_mode + target:nil + action:nil + ]; + + let _: () = msg_send![control, setWantsLayer: YES]; + + #[cfg(feature = "autolayout")] + let _: () = msg_send![control, setTranslatesAutoresizingMaskIntoConstraints: NO]; + + control + }; + + SegmentedControl { + handler: None, + + images, + + #[cfg(feature = "autolayout")] + top: LayoutAnchorY::top(view), + + #[cfg(feature = "autolayout")] + left: LayoutAnchorX::left(view), + + #[cfg(feature = "autolayout")] + leading: LayoutAnchorX::leading(view), + + #[cfg(feature = "autolayout")] + right: LayoutAnchorX::right(view), + + #[cfg(feature = "autolayout")] + trailing: LayoutAnchorX::trailing(view), + + #[cfg(feature = "autolayout")] + bottom: LayoutAnchorY::bottom(view), + + #[cfg(feature = "autolayout")] + width: LayoutAnchorDimension::width(view), + + #[cfg(feature = "autolayout")] + height: LayoutAnchorDimension::height(view), + + #[cfg(feature = "autolayout")] + center_x: LayoutAnchorX::center(view), + + #[cfg(feature = "autolayout")] + center_y: LayoutAnchorY::center(view), + + objc: ObjcProperty::retain(view) + } + } + + /// Select the segment at index + pub fn set_tooltip_segment(&mut self, index: NSUInteger, tooltip: &str) { + self.objc.with_mut(|obj| unsafe { + let converted = NSString::new(tooltip); + let _: () = msg_send![obj, setToolTip: converted forSegment: index]; + }) + } + + /// Select the segment at index + pub fn select_segment(&mut self, index: NSUInteger) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setSelectedSegment: index]; + }) + } + + /// Sets an image on the underlying button. + pub fn set_image_segment(&mut self, image: Image, segment: NSUInteger) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setImage:&*image.0 forSegment: segment]; + }); + } + + /// Attaches a callback for button press events. Don't get too creative now... + /// best just to message pass or something. + pub fn set_action(&mut self, action: F) { + // @TODO: This probably isn't ideal but gets the job done for now; needs revisiting. + let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) }); + let handler = TargetActionHandler::new(&*this, move |obj: *const Object| unsafe { + let selected: i32 = msg_send![obj, selectedSegment]; + action(selected) + }); + self.handler = Some(handler); + } + + /// Call this to set the background color for the backing layer. + pub fn set_background_color>(&self, color: C) { + let color: id = color.as_ref().into(); + + #[cfg(feature = "appkit")] + self.objc.with_mut(|obj| unsafe { + let cell: id = msg_send![obj, cell]; + let _: () = msg_send![cell, setBackgroundColor: color]; + }); + } + + /// Set a key to be bound to this button. When the key is pressed, the action coupled to this + /// button will fire. + pub fn set_key_equivalent<'a, K>(&self, key: K) + where + K: Into> + { + let key: Key<'a> = key.into(); + + self.objc.with_mut(|obj| { + let keychar = match key { + Key::Char(s) => NSString::new(s), + Key::Delete => NSString::new("\u{08}") + }; + + unsafe { + let _: () = msg_send![obj, setKeyEquivalent:&*keychar]; + } + }); + } + + /// Sets the text color for this button. + /// + /// On appkit, this is done by way of an `AttributedString` under the hood. + pub fn set_text_color>(&self, color: C) { + #[cfg(feature = "appkit")] + self.objc.with_mut(move |obj| unsafe { + let text: id = msg_send![obj, attributedTitle]; + let len: isize = msg_send![text, length]; + + let mut attr_str = AttributedString::wrap(text); + attr_str.set_text_color(color.as_ref(), 0..len); + + let _: () = msg_send![obj, setAttributedTitle:&*attr_str]; + }); + } + + // @TODO: Figure out how to handle oddities like this. + /// For buttons on appkit, one might need to disable the border. This does that. + #[cfg(feature = "appkit")] + pub fn set_bordered(&self, is_bordered: bool) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setBordered:match is_bordered { + true => YES, + false => NO + }]; + }); + } + + /// Sets the font for this button. + pub fn set_font>(&self, font: F) { + let font = font.as_ref().clone(); + + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setFont:&*font]; + }); + } + + /// Sets how the control should draw a focus ring when a user is focused on it. + /// + /// This is an appkit-only method. + #[cfg(feature = "appkit")] + pub fn set_focus_ring_type(&self, focus_ring_type: FocusRingType) { + let ring_type: NSUInteger = focus_ring_type.into(); + + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setFocusRingType: ring_type]; + }); + } + + /// Toggles the highlighted status of the button. + pub fn set_highlighted(&self, highlight: bool) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, highlight:match highlight { + true => YES, + false => NO + }]; + }); + } +} + +impl ObjcAccess for SegmentedControl { + fn with_backing_obj_mut(&self, handler: F) { + self.objc.with_mut(handler); + } + + fn get_from_backing_obj R, R>(&self, handler: F) -> R { + self.objc.get(handler) + } +} + +impl Layout for SegmentedControl {} +impl Control for SegmentedControl {} + +impl ObjcAccess for &SegmentedControl { + fn with_backing_obj_mut(&self, handler: F) { + self.objc.with_mut(handler); + } + + fn get_from_backing_obj R, R>(&self, handler: F) -> R { + self.objc.get(handler) + } +} + +impl Layout for &SegmentedControl {} +impl Control for &SegmentedControl {} + +impl Drop for SegmentedControl { + /// Nils out references on the Objective-C side and removes this from the backing view. + // Just to be sure, let's... nil these out. They should be weak references, + // but I'd rather be paranoid and remove them later. + fn drop(&mut self) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setTarget: nil]; + let _: () = msg_send![obj, setAction: nil]; + }); + } +} + +/// Registers an `NSButton` subclass, and configures it to hold some ivars +/// for various things we need to store. +fn register_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(NSSegmentedControl); + let decl = ClassDecl::new("RSTSegmentedControl", superclass).unwrap(); + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/src/appkit/toolbar/item.rs b/src/appkit/toolbar/item.rs index 2258acd..201fc37 100644 --- a/src/appkit/toolbar/item.rs +++ b/src/appkit/toolbar/item.rs @@ -10,6 +10,7 @@ use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::{Id, ShareId}; +use crate::appkit::segmentedcontrol::SegmentedControl; use crate::button::{BezelStyle, Button}; use crate::foundation::{id, NSString, NO, YES}; use crate::image::Image; @@ -21,6 +22,7 @@ pub struct ToolbarItem { pub identifier: String, pub objc: Id, pub button: Option