From e4785bb50fd59110b7be2dcb6b8cd1fff1d6b3df Mon Sep 17 00:00:00 2001 From: simlay Date: Mon, 10 Jul 2023 03:42:46 -0400 Subject: [PATCH] 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); +}