diff --git a/Cargo.toml b/Cargo.toml index 3f79b70..270da49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cacao" -description = "Rust bindings for AppKit (macOS, beta) and UIKit (iOS/tvOS, alpha)." -version = "0.2.0" +description = "Rust bindings for AppKit (macOS/Airyx/GNUStep, beta) and UIKit (iOS/tvOS, alpha)." +version = "0.3.0" edition = "2018" authors = ["Ryan McGrath "] build = "build.rs" @@ -20,23 +20,26 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] block = "0.1.6" -core-foundation = { version = "0.9", features = ["with-chrono", "mac_os_10_8_features"] } +core-foundation = { version = "0.9", features = ["with-chrono"] } core-graphics = "0.22" dispatch = "0.2.0" +infer = { version = "0.4", optional = true } lazy_static = "1.4.0" libc = "0.2" objc = "0.2.7" objc_id = "0.1.1" os_info = "3.0.1" -uuid = { version = "0.8", features = ["v4"], optional = true } url = "2.1.1" -infer = { version = "0.4", optional = true } +uuid = { version = "0.8", features = ["v4"], optional = true } [dev-dependencies] eval = "0.4" [features] -default = [] +appkit = ["core-foundation/mac_os_10_8_features"] +uikit = [] +autolayout = [] +default = ["appkit", "autolayout"] cloudkit = [] color_fallbacks = [] quicklook = [] diff --git a/README.md b/README.md index 10cd2f8..e7c06f1 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,17 @@ It tries to do so in a way that, if you've done programming for the framework be Objective-C), will feel familiar. This is tricky in Rust due to the ownership model, but some creative coding and assumptions can get us pretty far. -`0.2.0` exists on crates.io in part to enable the project to see wider usage, which can +This exists on crates.io in part to enable the project to see wider usage, which can inform development. That said, this library is currently early stages and may have bugs - your usage of it is at your own risk. However, provided you follow the rules (regarding memory/ownership) it's already fine for some apps. The core repository has a wealth of examples to help you get started. +> **Important** +> +> If you are migrating from 0.2 to 0.3, you should elect either `appkit` or `uikit` as a feature in your `Cargo.toml`. This change was made to +> support platforms that aren't just macOS/iOS/tvOS (e.g, gnustep, airyx). One of these features is required to work; `appkit` is defaulted for +> ease of development. + >_Note that this crate relies on the Objective-C runtime. Interfacing with the runtime **requires** unsafe blocks; this crate handles those unsafe interactions for you and provides a safe wrapper, but by using this crate you understand that usage of `unsafe` is a given and will be somewhat @@ -17,11 +23,16 @@ rampant for wrapped controls. This does **not** mean you can't assess, review, o usage - just know it's happening, and in large part it's not going away. Issues pertaining to the mere existence of unsafe will be closed without comment._ +If you're looking to build the docs for this on your local machine, you'll want the following due to the way feature flags work +with `cargo doc`: + +`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --open` + # Hello World ```rust -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::Window; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::window::Window; #[derive(Default)] struct BasicApp { @@ -56,22 +67,28 @@ ensures the application has had time to initialize and do any housekeeping neces scenes. ## Currently Supported -In terms of mostly working pieces, the following currently work. This list is not exhaustive and you're encouraged to check out the documentation for more info: +In terms of mostly working pieces, the table below showcases the level of support for varying features. This list is not exhaustive just by virtue of documentation updating being hell - so you're encouraged to check out the code-built documentation for more info: -- `App` initialization and event delegation -- `Window` construction, handling, and event delegation -- `View` construction, basic styling, some event delegation -- `ViewController` construction, lifecycle delegation -- `Color`, for handling system-established color types -- `ListView` support, including cell reuse (still needs testing) -- `Button` support, as well as enabling them in Toolbars -- `Label` and `TextField` support for basic text handling. -- `Image`, `ImageView` and `SystemIcon` for image usage. Images can use a custom draw handler, and draw graphics with the [core_graphics]() crate -- `Toolbar` construction and basic API -- `SplitViewController` support, including some Big-Sur-only additions -- `WebView` with a basic API for handling callbacks -- `UserDefaults` for persisting small pieces of data per-application -- `Autolayout` for View layout and such +Note that while iOS has green checkmarks, some components still aren't as well defined (e.g, Views/ViewControllers are still very alpha there). + +Non-Apple platforms that shim or provide a form of AppKit may be able to use a good chunk of the AppKit support in this library. + +| Component | Description | AppKit | iOS | tvOS | +| ------------------- | ----------------------------------- | ------ | --- | ---- | +| App | Initialization & events | ✅ | ✅ | ❌ | +| Window | Construction, handling, events | ✅ | ✅ | ❌ | +| View | Construction, styling, events | ✅ | ✅ | ❌ | +| ViewController | Construction, lifecycle events | ✅ | ✅ | ❌ | +| Color | System-backed colors, theming | ✅ | ✅ | ❌ | +| ListView | Reusable list w/ cached rows | ✅ | ❌ | ❌ | +| Button | Styling, events, toolbar support | ✅ | ❌ | ❌ | +| Label/TextField | Text rendering & input | ✅ | ❌ | ❌ | +| Image/ImageView | Loading, drawing, etc | ✅ | ❌ | ❌ | +| Toolbar | Basic native toolbar | ✅ | ❌ | ❌ | +| SplitViewController | Split views (Big Sur friendly) | ✅ | ❌ | ❌ | +| WebView | Wrapper for WKWebView | ✅ | ❌ | ❌ | +| UserDefaults | Persisting small data | ✅ | ✅ | ❌ | +| Autolayout | View layout for varying screens | ✅ | ✅ | ❌ | ## Optional Features @@ -79,11 +96,13 @@ The following are a list of [Cargo features][cargo-features] that can be enabled The following are a list of [Cargo features][cargo-features] that can be enabled or disabled. +- `appkit`: Links `AppKit.framework`. +- `uikit`: Links `UIKit.framework` (iOS/tvOS only). - `cloudkit`: Links `CloudKit.framework` and provides some wrappers around CloudKit functionality. Currently not feature complete. - `color_fallbacks`: Provides fallback colors for older systems where `systemColor` types don't exist. This feature is very uncommon and you probably don't need it. - `quicklook`: Links `QuickLook.framework` and offers methods for generating preview images for files. - `user-notifications`: Links `UserNotifications.framework` and provides functionality for emitting notifications on macOS and iOS. Note that this _requires_ your application be code-signed, and will not work without it. -- `webview`: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. This feature is not supported on tvOS, as the platform has no webview control. +- `webview`: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. This feature is not supported on tvOS, as the platform has no webview control. This feature is also potentially only supported for macOS/iOS due to the WKWebView control and varying support on non-Apple platforms. - `webview-downloading-macos`: Enables downloading files from the `WebView` via a private interface. This is not an App-Store-safe feature, so be aware of that before enabling. This feature is not supported on iOS (a user would handle downloads very differently) or tvOS (there's no web browser there at all). [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section diff --git a/build.rs b/build.rs index 8b4e9b7..83ec90c 100644 --- a/build.rs +++ b/build.rs @@ -1,17 +1,13 @@ //! Emits linker flags depending on platforms and features. -//! -//! (iOS/macOS only right now... maybe tvOS one day?) fn main() { - let target = std::env::var("TARGET").unwrap(); - println!("cargo:rustc-link-lib=framework=Foundation"); + + #[cfg(feature = "appkit")] + println!("cargo:rustc-link-lib=framework=AppKit"); - if target.contains("-ios") { - println!("cargo:rustc-link-lib=framework=UIKit"); - } else { - println!("cargo:rustc-link-lib=framework=AppKit"); - } + #[cfg(feature = "uikit")] + println!("cargo:rustc-link-lib=framework=UIKit"); println!("cargo:rustc-link-lib=framework=CoreGraphics"); println!("cargo:rustc-link-lib=framework=QuartzCore"); diff --git a/examples/animation.rs b/examples/animation.rs new file mode 100644 index 0000000..9df26fb --- /dev/null +++ b/examples/animation.rs @@ -0,0 +1,179 @@ +//! This example builds on the AutoLayout example, but adds in animation +//! via `AnimationContext`. Views and layout anchors have special proxy objects that can be cloned +//! into handlers, enabling basic animation support within `AnimationContext`. +//! +//! This one is a bit kludgier than some other examples, but the comments throughout this should +//! clarify why that is. + +use cacao::color::Color; +use cacao::layout::{Layout, LayoutConstraint, LayoutConstraintAnimatorProxy}; +use cacao::view::{View, ViewAnimatorProxy}; + +use cacao::appkit::{App, AppDelegate, AnimationContext}; +use cacao::appkit::{Event, EventMask, EventMonitor}; +use cacao::appkit::menu::Menu; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; + +struct BasicApp { + window: Window +} + +impl AppDelegate for BasicApp { + fn did_finish_launching(&self) { + App::set_menu(Menu::standard()); + App::activate(); + + self.window.show(); + } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } +} + +/// This map is the four different animation frames that we display, per view type. +/// Why do we have this here? +/// +/// Well, it's because there's no random number generator in the standard library, and I really +/// dislike when examples need crates attached to 'em. +/// +/// The basic mapping logic is this: each entry is a view's frame(s), and each frame is an array +/// of: +/// +/// [top, left, width, height, alpha] +/// +/// We then treat each frame index as follows: +/// +/// w: 0 +/// a: 1 +/// s: 2 +/// d: 3 +const ANIMATIONS: [[[f64; 5]; 4]; 3] = [ + // Blue + [ + [44., 16., 100., 100., 1.], + [128., 84., 144., 124., 1.], + [32., 32., 44., 44., 0.7], + [328., 157., 200., 200., 0.7], + ], + + // Red + [ + [44., 132., 100., 100., 1.], + [40., 47., 80., 64., 0.7], + [84., 220., 600., 109., 1.0], + [48., 600., 340., 44., 0.7], + ], + + // Green + [ + [44., 248., 100., 100., 1.], + [420., 232., 420., 244., 0.7], + [310., 440., 150., 238., 0.7], + [32., 32., 44., 44., 1.], + ] +]; + +/// A helper method for generating frame constraints that we want to be animating. +fn apply_styles( + view: &View, + parent: &View, + background_color: Color, + animation_table_index: usize +) -> [LayoutConstraint; 4] { + view.set_background_color(background_color); + view.layer.set_corner_radius(16.); + parent.add_subview(view); + + let animation = ANIMATIONS[animation_table_index][0]; + + [ + view.top.constraint_equal_to(&parent.top).offset(animation[0]), + view.left.constraint_equal_to(&parent.left).offset(animation[1]), + view.width.constraint_equal_to_constant(animation[2]), + view.height.constraint_equal_to_constant(animation[3]) + ] +} + +#[derive(Default)] +struct AppWindow { + content: View, + blue: View, + red: View, + green: View, + key_monitor: Option +} + +impl WindowDelegate for AppWindow { + const NAME: &'static str = "WindowDelegate"; + + fn did_load(&mut self, window: Window) { + window.set_title("Animation Example (Use W/A/S/D to change state!)"); + window.set_minimum_content_size(300., 300.); + + window.set_content_view(&self.content); + + let blue_frame = apply_styles(&self.blue, &self.content, Color::SystemBlue, 0); + let red_frame = apply_styles(&self.red, &self.content, Color::SystemRed, 1); + let green_frame = apply_styles(&self.green, &self.content, Color::SystemGreen, 2); + + let alpha_animators = [&self.blue, &self.red, &self.green].iter().map(|view| { + view.animator.clone() + }).collect::>(); + + let constraint_animators = [blue_frame, red_frame, green_frame].iter().map(|frame| { + LayoutConstraint::activate(frame); + + vec![ + frame[0].animator.clone(), + frame[1].animator.clone(), + frame[2].animator.clone(), + frame[3].animator.clone(), + ] + }).collect::>>(); + + // Monitor key change events for w/a/s/d, and then animate each view to their correct + // frame and alpha value. + self.key_monitor = Some(Event::local_monitor(EventMask::KeyDown, move |evt| { + let characters = evt.characters(); + + let animation_index = match characters.as_ref() { + "w" => 0, + "a" => 1, + "s" => 2, + "d" => 3, + _ => 4 + }; + + if animation_index == 4 { + return None; + } + + let alpha_animators = alpha_animators.clone(); + let constraint_animators = constraint_animators.clone(); + + AnimationContext::run(move |_ctx| { + alpha_animators.iter().enumerate().for_each(move |(index, view)| { + let animation = ANIMATIONS[index][animation_index]; + view.set_alpha(animation[4]); + }); + + constraint_animators.iter().enumerate().for_each(move |(index, frame)| { + let animation = ANIMATIONS[index][animation_index]; + frame[0].set_offset(animation[0]); + frame[1].set_offset(animation[1]); + frame[2].set_offset(animation[2]); + frame[3].set_offset(animation[3]); + }); + }); + + None + })); + } +} + +fn main() { + App::new("com.test.window", BasicApp { + window: Window::with(WindowConfig::default(), AppWindow::default()) + }).run(); +} diff --git a/examples/autolayout.rs b/examples/autolayout.rs index fb564c4..880b3bf 100644 --- a/examples/autolayout.rs +++ b/examples/autolayout.rs @@ -5,8 +5,9 @@ use cacao::color::{Color, Theme}; use cacao::layout::{Layout, LayoutConstraint}; use cacao::view::View; -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::{Window, WindowConfig, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; struct BasicApp { window: Window @@ -14,9 +15,41 @@ struct BasicApp { impl AppDelegate for BasicApp { fn did_finish_launching(&self) { + App::set_menu(vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ]); + App::activate(); + self.window.show(); } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } } #[derive(Default)] diff --git a/examples/browser/main.rs b/examples/browser/main.rs index e34219d..5b8fdf1 100644 --- a/examples/browser/main.rs +++ b/examples/browser/main.rs @@ -4,10 +4,10 @@ use cacao::notification_center::Dispatcher; use cacao::webview::{WebView, WebViewConfig, WebViewDelegate}; -use cacao::macos::{App, AppDelegate}; -use cacao::macos::menu::{Menu, MenuItem}; -use cacao::macos::toolbar::Toolbar; -use cacao::macos::window::{Window, WindowConfig, WindowDelegate, WindowToolbarStyle}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::toolbar::Toolbar; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate, WindowToolbarStyle}; mod toolbar; use toolbar::BrowserToolbar; diff --git a/examples/browser/toolbar.rs b/examples/browser/toolbar.rs index 14f817b..b27628d 100644 --- a/examples/browser/toolbar.rs +++ b/examples/browser/toolbar.rs @@ -4,7 +4,7 @@ use cacao::objc::{msg_send, sel, sel_impl}; use cacao::button::Button; use cacao::input::{TextField, TextFieldDelegate}; -use cacao::macos::toolbar::{Toolbar, ToolbarDisplayMode, ToolbarItem, ItemIdentifier, ToolbarDelegate}; +use cacao::appkit::toolbar::{Toolbar, ToolbarDisplayMode, ToolbarItem, ItemIdentifier, ToolbarDelegate}; use super::Action; diff --git a/examples/calculator/calculator.rs b/examples/calculator/calculator.rs index 407a9c5..17783ff 100644 --- a/examples/calculator/calculator.rs +++ b/examples/calculator/calculator.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, RwLock}; use cacao::lazy_static::lazy_static; -use cacao::macos::App; +use cacao::appkit::App; use crate::CalculatorApp; diff --git a/examples/calculator/content_view.rs b/examples/calculator/content_view.rs index ca55804..cda706d 100644 --- a/examples/calculator/content_view.rs +++ b/examples/calculator/content_view.rs @@ -2,7 +2,7 @@ use cacao::text::{Font, Label, TextAlign}; use cacao::layout::{LayoutConstraint, Layout}; use cacao::button::{Button, BezelStyle}; use cacao::color::Color; -use cacao::macos::FocusRingType; +use cacao::appkit::FocusRingType; use cacao::view::{View, ViewDelegate}; use crate::button_row::ButtonRow; @@ -17,7 +17,7 @@ pub fn button(text: &str, msg: Msg) -> Button { button.set_bezel_style(BezelStyle::SmallSquare); button.set_focus_ring_type(FocusRingType::None); button.set_action(move || dispatch(msg.clone())); - button.set_key_equivalent(&text.to_lowercase()); + button.set_key_equivalent(&*text.to_lowercase()); let font = Font::system(22.); button.set_font(&font); diff --git a/examples/calculator/main.rs b/examples/calculator/main.rs index f7df4bc..d36ef02 100644 --- a/examples/calculator/main.rs +++ b/examples/calculator/main.rs @@ -11,9 +11,9 @@ use std::sync::RwLock; -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::{Window, WindowConfig, TitleVisibility}; -use cacao::macos::{Event, EventMask, EventMonitor}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::window::{Window, WindowConfig, TitleVisibility}; +use cacao::appkit::{Event, EventMask, EventMonitor}; use cacao::color::Color; use cacao::notification_center::Dispatcher; use cacao::view::View; @@ -37,7 +37,7 @@ impl AppDelegate for CalculatorApp { // Event Monitors need to be started after the App has been activated. // We use an RwLock here, but it's possible this entire method can be // &mut self and you wouldn't need these kinds of shenanigans. - self.start_monitoring(); + //self.start_monitoring(); self.window.set_title("Calculator"); self.window.set_background_color(Color::rgb(49,49,49)); @@ -48,6 +48,7 @@ impl AppDelegate for CalculatorApp { self.window.set_content_view(&self.content); self.window.show(); } + fn should_terminate_after_last_window_closed(&self) -> bool { true } diff --git a/examples/defaults.rs b/examples/defaults.rs index d01eed5..6502bb7 100644 --- a/examples/defaults.rs +++ b/examples/defaults.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use cacao::macos::{App, AppDelegate}; +use cacao::appkit::{App, AppDelegate}; use cacao::defaults::{UserDefaults, Value}; #[derive(Default)] diff --git a/examples/frame_layout.rs b/examples/frame_layout.rs new file mode 100644 index 0000000..ca84591 --- /dev/null +++ b/examples/frame_layout.rs @@ -0,0 +1,119 @@ +//! This example showcases setting up a basic application and window, setting up some views to +//! work with autolayout, and some basic ways to handle colors. + +use cacao::color::Color; +use cacao::geometry::Rect; +use cacao::layout::Layout; +use cacao::view::View; + +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; + +const CORNER_RADIUS: f64 = 16.; +const SPACING: f64 = 10.; +const TOP: f64 = 40.; +const WIDTH: f64 = 100.; +const HEIGHT: f64 = 100.; + +struct BasicApp { + window: Window +} + +impl AppDelegate for BasicApp { + fn did_finish_launching(&self) { + App::set_menu(vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ]); + + App::activate(); + + self.window.show(); + self.window.delegate.as_ref().unwrap().layout(); + } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } +} + +#[derive(Default)] +struct AppWindow { + content: View, + blue: View, + red: View, + green: View +} + +impl AppWindow { + pub fn layout(&self) { + self.blue.set_background_color(Color::SystemBlue); + self.blue.set_frame(Rect { + top: TOP, + left: SPACING, + width: WIDTH, + height: HEIGHT + }); + self.blue.layer.set_corner_radius(CORNER_RADIUS); + self.content.add_subview(&self.blue); + + self.red.set_background_color(Color::SystemRed); + self.red.set_frame(Rect { + top: TOP, + left: WIDTH + (SPACING * 2.), + width: WIDTH, + height: HEIGHT + }); + self.red.layer.set_corner_radius(CORNER_RADIUS); + self.content.add_subview(&self.red); + + self.green.set_background_color(Color::SystemGreen); + self.green.set_frame(Rect { + top: TOP, + left: (WIDTH * 2.) + (SPACING * 3.), + width: WIDTH, + height: HEIGHT + }); + self.green.layer.set_corner_radius(CORNER_RADIUS); + self.content.add_subview(&self.green); + } +} + +impl WindowDelegate for AppWindow { + const NAME: &'static str = "WindowDelegate"; + + fn did_load(&mut self, window: Window) { + window.set_title("Frame Layout Example"); + window.set_minimum_content_size(300., 300.); + window.set_content_view(&self.content); + } +} + +fn main() { + App::new("com.test.window", BasicApp { + window: Window::with(WindowConfig::default(), AppWindow::default()) + }).run(); +} diff --git a/examples/ios-beta/main.rs b/examples/ios-beta/main.rs index e8d362d..89b282f 100644 --- a/examples/ios-beta/main.rs +++ b/examples/ios-beta/main.rs @@ -1,6 +1,6 @@ use std::sync::RwLock; -use cacao::ios::{ +use cacao::uikit::{ App, AppDelegate, Scene, SceneConfig, SceneSession, SceneConnectionOptions, WindowSceneDelegate, Window }; diff --git a/examples/ios-beta/readme.md b/examples/ios-beta/readme.md index 08217c3..5dc4a32 100644 --- a/examples/ios-beta/readme.md +++ b/examples/ios-beta/readme.md @@ -6,8 +6,8 @@ Since this needs to run in an iOS simulator or on a device, you can't run it lik - Start a simulator (Simulator.app). - `cargo install cargo-bundle` -- `cargo bundle --example ios-beta --target x86_64-apple-ios` -- `xcrun simctl install booted target/x86_64-apple-ios/debug/examples/bundle/ios/cacao-ios-test.app` +- `cargo bundle --example ios-beta --no-default-features --features uikit,autolayout --target x86_64-apple-ios` +- `xcrun simctl install booted target/x86_64-apple-ios/debug/examples/bundle/ios/cacao-ios-beta.app` - `xcrun simctl launch --console booted com.cacao.ios-test` ## Current Support diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000..4c90ffe --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,64 @@ +# Cacao Examples +This directory contains example code for apps written in cacao. To run an example, check out the list of commands below - some require certain features to be enabled. + +## AutoLayout +An example that showcases layout out a view with AutoLayout. This requires the feature flag `autolayout` to be enabled, but it's defaulted for ease of use so doesn't need to be specified here. Platforms where AutoLayout is not supported will likely not work with this example. + +`cargo run --example autolayout` + +## Frame Layout +An example that showcases laying out with a more old school Frame-based approach. Platforms where AutoLayout are not supported will want to try this instead of the AutoLayout example. + +**macOS:** +`cargo run --example frame_layout` + +**Platforms lacking AutoLayout:** +`cargo run --example frame_layout --no-default-features --features appkit` + +## Defaults +This example isn't GUI-specific, but showcases accessing `NSUserDefaults` from Rust for persisting basic data. + +`cargo run --example defaults` + +## Window +This example showcases creating a basic `Window`. This should run on all AppKit-supporting platforms. + +`cargo run --example window` + +## Window Controller +This example showcases creating a basic `WindowController`. This may run on all AppKit-supporting platforms. + +`cargo run --example window_controller` + +## Window Delegate +This example showcases creating a basic `WindowDelegate` to receive and handle events. This may run on all AppKit-supporting platforms. + +`cargo run --example window_delegate` + +## Text Input +This example showcases text input, and logs it to the underlying console. It's mostly a testbed to ensure that the backing widget for input behaves as expected. + +`cargo run --example text_input` + +## Calculator +A Rust-rendition of the macOS Calculator app. + +`cargo run --example calculator` + +## To-Do List +A "kitchen sink" example that showcases how to do more advanced things, such as cached reusable ListView components. + +`cargo run --example todos_list` + +## Browser +A _very_ basic web browser. Platforms that don't support WKWebView will likely not work with this example. + +`cargo run --example browser --features webview` + +## Webview Custom Protocol +This example showcases a custom protocol for the webview feature. Platforms that don't support WKWebView will likely not work with this example. + +`cargo run --example webview_custom_protocol --features webview` + +## iOS (Beta) +This example showcases how to build and run an iOS app in Rust. See the README in the `ios-beta` folder for instructions on how to run. diff --git a/examples/text_input.rs b/examples/text_input.rs index d6994b2..521c74c 100644 --- a/examples/text_input.rs +++ b/examples/text_input.rs @@ -5,9 +5,9 @@ use cacao::layout::{Layout, LayoutConstraint}; use cacao::input::{TextField, TextFieldDelegate}; use cacao::view::View; -use cacao::macos::{App, AppDelegate}; -use cacao::macos::menu::{Menu, MenuItem}; -use cacao::macos::window::{Window, WindowConfig, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; struct BasicApp { window: Window @@ -41,7 +41,6 @@ impl AppDelegate for BasicApp { MenuItem::SelectAll ]), - // Sidebar option is 11.0+ only. Menu::new("View", vec![ MenuItem::EnterFullScreen ]), @@ -59,6 +58,10 @@ impl AppDelegate for BasicApp { App::activate(); self.window.show(); } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } } #[derive(Debug, Default)] diff --git a/examples/todos_list/add/mod.rs b/examples/todos_list/add/mod.rs index d94ac77..7ea5dd5 100644 --- a/examples/todos_list/add/mod.rs +++ b/examples/todos_list/add/mod.rs @@ -1,6 +1,6 @@ //! Implements a window for adding a new Todo. -use cacao::macos::window::{Window, WindowDelegate}; +use cacao::appkit::window::{Window, WindowDelegate}; use cacao::view::ViewController; use crate::storage::{dispatch_ui, Message}; diff --git a/examples/todos_list/add/view.rs b/examples/todos_list/add/view.rs index f6ef23b..d4712cf 100644 --- a/examples/todos_list/add/view.rs +++ b/examples/todos_list/add/view.rs @@ -49,6 +49,7 @@ impl ViewDelegate for AddNewTodoContentView { let input = TextField::new(); let mut button = Button::new("Add"); + button.set_key_equivalent("\r"); button.set_action(|| dispatch_ui(Message::ProcessNewTodo)); view.add_subview(&instructions); @@ -65,7 +66,8 @@ impl ViewDelegate for AddNewTodoContentView { input.trailing.constraint_equal_to(&view.trailing).offset(-16.), button.top.constraint_equal_to(&input.bottom).offset(8.), - button.trailing.constraint_equal_to(&view.trailing).offset(-16.) + button.trailing.constraint_equal_to(&view.trailing).offset(-16.), + button.bottom.constraint_equal_to(&view.bottom).offset(-16.) ]); self.view = Some(view); diff --git a/examples/todos_list/app.rs b/examples/todos_list/app.rs index 19c41a2..67dfff5 100644 --- a/examples/todos_list/app.rs +++ b/examples/todos_list/app.rs @@ -1,7 +1,7 @@ //! Implements the start of the App lifecycle. Handles creating the required menu and window //! components and message dispatching. -use cacao::macos::{App, AppDelegate}; +use cacao::appkit::{App, AppDelegate}; use cacao::notification_center::Dispatcher; use crate::menu::menu; diff --git a/examples/todos_list/main.rs b/examples/todos_list/main.rs index b7fa587..726c962 100644 --- a/examples/todos_list/main.rs +++ b/examples/todos_list/main.rs @@ -3,7 +3,7 @@ //! //! This may get extracted into a different repo some day in the future. -use cacao::macos::App; +use cacao::appkit::App; mod add; mod app; diff --git a/examples/todos_list/menu.rs b/examples/todos_list/menu.rs index 9e9ddd6..c1f288e 100644 --- a/examples/todos_list/menu.rs +++ b/examples/todos_list/menu.rs @@ -4,7 +4,7 @@ //! Correctly functioning menus are a key part of what makes a macOS app feel right, though, so //! this is here for those who might want to use this todos example as a starting point. -use cacao::macos::menu::{Menu, MenuItem}; +use cacao::appkit::menu::{Menu, MenuItem}; use crate::storage::{dispatch_ui, Message}; diff --git a/examples/todos_list/preferences/mod.rs b/examples/todos_list/preferences/mod.rs index 5af1dc9..4774f1a 100644 --- a/examples/todos_list/preferences/mod.rs +++ b/examples/todos_list/preferences/mod.rs @@ -1,7 +1,7 @@ //! Implements a stock-ish Preferences window. -use cacao::macos::window::{Window, WindowDelegate}; -use cacao::macos::toolbar::Toolbar; +use cacao::appkit::window::{Window, WindowDelegate}; +use cacao::appkit::toolbar::Toolbar; use cacao::view::ViewController; use crate::storage::Message; diff --git a/examples/todos_list/preferences/toolbar.rs b/examples/todos_list/preferences/toolbar.rs index ee47414..201998a 100644 --- a/examples/todos_list/preferences/toolbar.rs +++ b/examples/todos_list/preferences/toolbar.rs @@ -1,7 +1,7 @@ //! Implements an example toolbar for a Preferences app. Could be cleaner, probably worth cleaning //! up at some point. -use cacao::macos::toolbar::{Toolbar, ToolbarDelegate, ToolbarItem, ItemIdentifier}; +use cacao::appkit::toolbar::{Toolbar, ToolbarDelegate, ToolbarItem, ItemIdentifier}; use cacao::image::{Image, MacSystemIcon}; use crate::storage::{dispatch_ui, Message}; @@ -15,7 +15,7 @@ impl Default for PreferencesToolbar { let mut item = ToolbarItem::new("general"); item.set_title("General"); - let icon = Image::system_icon(MacSystemIcon::PreferencesGeneral, "General"); + let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "General"); item.set_image(icon); item.set_action(|| { @@ -27,7 +27,7 @@ impl Default for PreferencesToolbar { let mut item = ToolbarItem::new("advanced"); item.set_title("Advanced"); - let icon = Image::system_icon(MacSystemIcon::PreferencesAdvanced, "Advanced"); + let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "Advanced"); item.set_image(icon); item.set_action(|| { diff --git a/examples/todos_list/storage/mod.rs b/examples/todos_list/storage/mod.rs index 0513221..ed6ecca 100644 --- a/examples/todos_list/storage/mod.rs +++ b/examples/todos_list/storage/mod.rs @@ -1,7 +1,7 @@ //! Messages that we used to thread control throughout the application. //! If you come from React/Redux, you can liken it to that world. -use cacao::macos::App; +use cacao::appkit::App; use crate::app::TodosApp; diff --git a/examples/todos_list/todos/list/mod.rs b/examples/todos_list/todos/list/mod.rs index e54925b..5984878 100644 --- a/examples/todos_list/todos/list/mod.rs +++ b/examples/todos_list/todos/list/mod.rs @@ -64,6 +64,7 @@ impl ListViewDelegate for TodosListView { fn did_load(&mut self, view: ListView) { view.register(TODO_ROW, TodoViewRow::default); view.set_uses_alternating_backgrounds(true); + view.set_row_height(64.); self.view = Some(view); } diff --git a/examples/todos_list/todos/mod.rs b/examples/todos_list/todos/mod.rs index eb88166..aaacb5f 100644 --- a/examples/todos_list/todos/mod.rs +++ b/examples/todos_list/todos/mod.rs @@ -1,7 +1,7 @@ //! The main Todos window. -use cacao::macos::window::{Window, WindowDelegate}; -use cacao::macos::toolbar::Toolbar; +use cacao::appkit::window::{Window, WindowDelegate}; +use cacao::appkit::toolbar::Toolbar; use cacao::view::ViewController; use crate::storage::Message; diff --git a/examples/todos_list/todos/toolbar.rs b/examples/todos_list/todos/toolbar.rs index ec2edeb..447389b 100644 --- a/examples/todos_list/todos/toolbar.rs +++ b/examples/todos_list/todos/toolbar.rs @@ -1,7 +1,7 @@ //! The main Todos window toolbar. Contains a button to enable adding a new task. use cacao::button::Button; -use cacao::macos::toolbar::{ +use cacao::appkit::toolbar::{ Toolbar, ToolbarDelegate, ToolbarItem, ToolbarDisplayMode, ItemIdentifier }; diff --git a/examples/todos_list/windows.rs b/examples/todos_list/windows.rs index ecc0389..4e7692a 100644 --- a/examples/todos_list/windows.rs +++ b/examples/todos_list/windows.rs @@ -5,7 +5,7 @@ use std::sync::RwLock; -use cacao::macos::window::{Window, WindowConfig, WindowStyle, WindowDelegate, WindowToolbarStyle}; +use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowDelegate, WindowToolbarStyle}; use cacao::notification_center::Dispatcher; use crate::storage::Message; diff --git a/examples/window.rs b/examples/window.rs index 92e6248..719fee3 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,7 +1,8 @@ //! This example showcases setting up a basic application and window. -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::Window; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::Window; #[derive(Default)] struct BasicApp { @@ -10,12 +11,43 @@ struct BasicApp { impl AppDelegate for BasicApp { fn did_finish_launching(&self) { + App::set_menu(vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ]); + App::activate(); self.window.set_minimum_content_size(400., 400.); self.window.set_title("A Basic Window"); self.window.show(); } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } } fn main() { diff --git a/examples/window_controller.rs b/examples/window_controller.rs index d7239f9..8f5e54e 100644 --- a/examples/window_controller.rs +++ b/examples/window_controller.rs @@ -4,8 +4,9 @@ //! //! If you're not using that, you can probably get by fine with a standard `NSWindow`. -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::{Window, WindowConfig, WindowController, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowController, WindowDelegate}; struct BasicApp { window: WindowController @@ -13,10 +14,41 @@ struct BasicApp { impl AppDelegate for BasicApp { fn did_finish_launching(&self) { + App::set_menu(vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ]); + App::activate(); self.window.show(); } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } } #[derive(Default)] diff --git a/examples/window_delegate.rs b/examples/window_delegate.rs index 267c4a0..9e40b9b 100644 --- a/examples/window_delegate.rs +++ b/examples/window_delegate.rs @@ -1,8 +1,9 @@ //! This example showcases setting up a basic application and window delegate. //! Window Delegate's give you lifecycle methods that you can respond to. -use cacao::macos::{App, AppDelegate}; -use cacao::macos::window::{Window, WindowConfig, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; +use cacao::appkit::menu::{Menu, MenuItem}; +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; struct BasicApp { window: Window @@ -10,9 +11,41 @@ struct BasicApp { impl AppDelegate for BasicApp { fn did_finish_launching(&self) { + App::set_menu(vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ]); + App::activate(); + self.window.show(); } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } } #[derive(Default)] diff --git a/src/macos/alert.rs b/src/appkit/alert.rs similarity index 92% rename from src/macos/alert.rs rename to src/appkit/alert.rs index ea12ec9..7e886f2 100644 --- a/src/macos/alert.rs +++ b/src/appkit/alert.rs @@ -1,6 +1,6 @@ //! A wrapper for `NSAlert`. //! -//! This is housed inside `macos` as it's a useful tool for a few cases, but it doesn't match the +//! This is housed inside `appkit` as it's a useful tool for a few cases, but it doesn't match the //! iOS API, so we make no guarantees about it being a universal control. In general this also //! doesn't produce an amazing user experience, and you may want to shy away from using it. //! @@ -8,7 +8,7 @@ //! `Sheet`. //! //! ```rust -//! use cacao::macos::{App, AppDelegate, Alert}; +//! use cacao::appkit::{App, AppDelegate, Alert}; //! //! #[derive(Default)] //! struct ExampleApp; diff --git a/src/appkit/animation.rs b/src/appkit/animation.rs new file mode 100644 index 0000000..5bc312d --- /dev/null +++ b/src/appkit/animation.rs @@ -0,0 +1,73 @@ +use block::ConcreteBlock; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::foundation::id; + +/// A very, very basic wrapper around NSAnimationContext. 100% subject to change. +#[derive(Debug)] +pub struct AnimationContext(id); + +impl AnimationContext { + /// Wraps an NSAnimationContext pointer. + pub fn new(ctx: id) -> Self { + Self(ctx) + } + + /// Sets the animation duration. + pub fn set_duration(&mut self, duration: f64) { + unsafe { + let _: () = msg_send![self.0, setDuration:duration]; + } + } + + /// Pass it a block, and the changes in that block will be animated, provided they're + /// properties that support animation. + /// + /// [https://developer.apple.com/documentation/appkit/nsanimationcontext?language=objc] + /// + /// For more information, you should consult the documentation for NSAnimationContext, then skim + /// the supported methods here. + pub fn run(animation: F) + where + F: Fn(&mut AnimationContext) + Send + Sync + 'static + { + let block = ConcreteBlock::new(move |ctx| { + let mut context = AnimationContext(ctx); + animation(&mut context); + }); + let block = block.copy(); + + unsafe { + //let context: id = msg_send![class!(NSAnimationContext), currentContext]; + let _: () = msg_send![class!(NSAnimationContext), runAnimationGroup:block]; + } + } + + /// Pass it a block, and the changes in that block will be animated, provided they're + /// properties that support animation. + /// + /// [https://developer.apple.com/documentation/appkit/nsanimationcontext?language=objc] + /// + /// For more information, you should consult the documentation for NSAnimationContext, then skim + /// the supported methods here. + pub fn run_with_completion_handler(animation: F, completion_handler: C) + where + F: Fn(&mut AnimationContext) + Send + Sync + 'static, + C: Fn() + Send + Sync + 'static + { + let block = ConcreteBlock::new(move |ctx| { + let mut context = AnimationContext(ctx); + animation(&mut context); + }); + let block = block.copy(); + + let completion_block = ConcreteBlock::new(completion_handler); + let completion_block = completion_block.copy(); + + unsafe { + //let context: id = msg_send![class!(NSAnimationContext), currentContext]; + let _: () = msg_send![class!(NSAnimationContext), runAnimationGroup:block + completionHandler:completion_block]; + } + } +} diff --git a/src/macos/app/class.rs b/src/appkit/app/class.rs similarity index 100% rename from src/macos/app/class.rs rename to src/appkit/app/class.rs diff --git a/src/macos/app/delegate.rs b/src/appkit/app/delegate.rs similarity index 99% rename from src/macos/app/delegate.rs rename to src/appkit/app/delegate.rs index 9703b4d..4e7c6c5 100644 --- a/src/macos/app/delegate.rs +++ b/src/appkit/app/delegate.rs @@ -15,8 +15,8 @@ use url::Url; use crate::error::Error; use crate::foundation::{id, nil, to_bool, BOOL, YES, NO, NSUInteger, NSArray, NSString}; -use crate::macos::app::{APP_PTR, AppDelegate}; -use crate::macos::printing::PrintSettings; +use crate::appkit::app::{APP_PTR, AppDelegate}; +use crate::appkit::printing::PrintSettings; use crate::user_activity::UserActivity; #[cfg(feature = "cloudkit")] diff --git a/src/macos/app/enums.rs b/src/appkit/app/enums.rs similarity index 100% rename from src/macos/app/enums.rs rename to src/appkit/app/enums.rs diff --git a/src/macos/app/mod.rs b/src/appkit/app/mod.rs similarity index 98% rename from src/macos/app/mod.rs rename to src/appkit/app/mod.rs index 001d7a5..366ce08 100644 --- a/src/macos/app/mod.rs +++ b/src/appkit/app/mod.rs @@ -45,10 +45,12 @@ use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSUInteger, AutoReleasePool}; use crate::invoker::TargetActionHandler; -use crate::macos::menu::Menu; +use crate::appkit::menu::Menu; use crate::notification_center::Dispatcher; use crate::utils::activate_cocoa_multithreading; +//use crate::bundle::set_bundle_id; + mod class; use class::register_app_class; @@ -70,7 +72,7 @@ fn shared_application(handler: F) { handler(app); } -/// A wrapper for `NSApplication` on macOS, and `UIApplication` on iOS. +/// A wrapper for `NSApplication` in AppKit/Cocoa, and `UIApplication` in UIKit/Cocoa Touch. /// /// It holds (retains) a pointer to the Objective-C runtime shared application object, as well as /// handles setting up a few necessary pieces: @@ -131,7 +133,7 @@ impl App where T: AppDelegate + 'static { /// policies), injects an `NSObject` delegate wrapper, and retains everything on the /// Objective-C side of things. pub fn new(_bundle_id: &str, delegate: T) -> Self { - // set_bundle_id(bundle_id); + //set_bundle_id(bundle_id); activate_cocoa_multithreading(); diff --git a/src/macos/app/traits.rs b/src/appkit/app/traits.rs similarity index 98% rename from src/macos/app/traits.rs rename to src/appkit/app/traits.rs index 1735aa6..20f1b57 100644 --- a/src/macos/app/traits.rs +++ b/src/appkit/app/traits.rs @@ -6,10 +6,10 @@ use url::Url; use crate::error::Error; use crate::user_activity::UserActivity; -use crate::macos::app::enums::TerminateResponse; -use crate::macos::menu::Menu; -use crate::macos::printing::enums::PrintResponse; -use crate::macos::printing::settings::PrintSettings; +use crate::appkit::app::enums::TerminateResponse; +use crate::appkit::menu::Menu; +use crate::appkit::printing::enums::PrintResponse; +use crate::appkit::printing::settings::PrintSettings; #[cfg(feature = "cloudkit")] use crate::cloudkit::share::CKShareMetaData; diff --git a/src/macos/cursor.rs b/src/appkit/cursor.rs similarity index 100% rename from src/macos/cursor.rs rename to src/appkit/cursor.rs diff --git a/src/macos/enums.rs b/src/appkit/enums.rs similarity index 94% rename from src/macos/enums.rs rename to src/appkit/enums.rs index c60515d..f93f554 100644 --- a/src/macos/enums.rs +++ b/src/appkit/enums.rs @@ -3,7 +3,6 @@ use crate::foundation::{NSUInteger}; /// Used to set whether and/or how a view or cell draws a focus ring. -#[cfg(target_os = "macos")] #[derive(Debug)] pub enum FocusRingType { /// Whatever the default is. @@ -20,7 +19,6 @@ pub enum FocusRingType { Unknown(NSUInteger) } -#[cfg(target_os = "macos")] impl From for NSUInteger { fn from(ring_type: FocusRingType) -> Self { match ring_type { diff --git a/src/macos/event/mod.rs b/src/appkit/event/mod.rs similarity index 98% rename from src/macos/event/mod.rs rename to src/appkit/event/mod.rs index 3ec77f7..dbbf7d1 100644 --- a/src/macos/event/mod.rs +++ b/src/appkit/event/mod.rs @@ -32,7 +32,7 @@ impl Event { // @TODO: Check here if key event, invalid otherwise. // @TODO: Figure out if we can just return &str here, since the Objective-C side // should... make it work, I think. - let characters = NSString::from_retained(unsafe { + let characters = NSString::retain(unsafe { msg_send![&*self.0, characters] }); diff --git a/src/macos/menu/item.rs b/src/appkit/menu/item.rs similarity index 100% rename from src/macos/menu/item.rs rename to src/appkit/menu/item.rs diff --git a/src/macos/menu/menu.rs b/src/appkit/menu/menu.rs similarity index 67% rename from src/macos/menu/menu.rs rename to src/appkit/menu/menu.rs index ee24b0f..f5e3bfc 100644 --- a/src/macos/menu/menu.rs +++ b/src/appkit/menu/menu.rs @@ -7,7 +7,7 @@ use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, NSInteger, NSString}; -use crate::macos::menu::item::MenuItem; +use crate::appkit::menu::item::MenuItem; /// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting /// them throughout the application lifecycle. @@ -70,4 +70,46 @@ impl Menu { menu } + + /// Convenience method for the bare-minimum NSMenu structure that "just works" for all + /// applications, as expected. + pub fn standard() -> Vec { + vec![ + Menu::new("", vec![ + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit + ]), + + Menu::new("File", vec![ + MenuItem::CloseWindow + ]), + + Menu::new("Edit", vec![ + MenuItem::Undo, + MenuItem::Redo, + MenuItem::Separator, + MenuItem::Cut, + MenuItem::Copy, + MenuItem::Paste, + MenuItem::Separator, + MenuItem::SelectAll + ]), + + Menu::new("View", vec![ + MenuItem::EnterFullScreen + ]), + + Menu::new("Window", vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front") + ]) + ] + } } diff --git a/src/macos/menu/mod.rs b/src/appkit/menu/mod.rs similarity index 100% rename from src/macos/menu/mod.rs rename to src/appkit/menu/mod.rs diff --git a/src/macos/mod.rs b/src/appkit/mod.rs similarity index 92% rename from src/macos/mod.rs rename to src/appkit/mod.rs index f4ec45a..c96c132 100644 --- a/src/macos/mod.rs +++ b/src/appkit/mod.rs @@ -8,6 +8,9 @@ mod alert; pub use alert::Alert; +mod animation; +pub use animation::AnimationContext; + mod app; pub use app::*; diff --git a/src/macos/printing/enums.rs b/src/appkit/printing/enums.rs similarity index 100% rename from src/macos/printing/enums.rs rename to src/appkit/printing/enums.rs diff --git a/src/macos/printing/mod.rs b/src/appkit/printing/mod.rs similarity index 100% rename from src/macos/printing/mod.rs rename to src/appkit/printing/mod.rs diff --git a/src/macos/printing/settings.rs b/src/appkit/printing/settings.rs similarity index 100% rename from src/macos/printing/settings.rs rename to src/appkit/printing/settings.rs diff --git a/src/macos/toolbar/class.rs b/src/appkit/toolbar/class.rs similarity index 98% rename from src/macos/toolbar/class.rs rename to src/appkit/toolbar/class.rs index 741afe6..693f5e5 100644 --- a/src/macos/toolbar/class.rs +++ b/src/appkit/toolbar/class.rs @@ -7,7 +7,7 @@ use objc::runtime::{Class, Object, Sel}; use objc::{class, sel, sel_impl, msg_send}; use crate::foundation::{load_or_register_class, id, BOOL, NSArray, NSString}; -use crate::macos::toolbar::{TOOLBAR_PTR, ToolbarDelegate}; +use crate::appkit::toolbar::{TOOLBAR_PTR, ToolbarDelegate}; use crate::utils::load; /// Retrieves and passes the allowed item identifiers for this toolbar. diff --git a/src/macos/toolbar/enums.rs b/src/appkit/toolbar/enums.rs similarity index 100% rename from src/macos/toolbar/enums.rs rename to src/appkit/toolbar/enums.rs diff --git a/src/macos/toolbar/item.rs b/src/appkit/toolbar/item.rs similarity index 97% rename from src/macos/toolbar/item.rs rename to src/appkit/toolbar/item.rs index 3c952e8..884c65c 100644 --- a/src/macos/toolbar/item.rs +++ b/src/appkit/toolbar/item.rs @@ -1,4 +1,4 @@ -//! Implements an NSToolbar wrapper, which is one of those macOS niceties +//! Implements an NSToolbar wrapper, which is one of those AppKit niceties //! that makes it feel... "proper". //! //! UNFORTUNATELY, this is a very old and janky API. So... yeah. diff --git a/src/macos/toolbar/mod.rs b/src/appkit/toolbar/mod.rs similarity index 97% rename from src/macos/toolbar/mod.rs rename to src/appkit/toolbar/mod.rs index 62d857a..c4b07f7 100644 --- a/src/macos/toolbar/mod.rs +++ b/src/appkit/toolbar/mod.rs @@ -23,7 +23,7 @@ pub use traits::ToolbarDelegate; mod enums; pub use enums::{ToolbarDisplayMode, ToolbarSizeMode, ItemIdentifier}; -pub(crate) static TOOLBAR_PTR: &str = "rstToolbarPtr"; +pub(crate) static TOOLBAR_PTR: &str = "cacaoToolbarPtr"; /// A wrapper for `NSToolbar`. Holds (retains) pointers for the Objective-C runtime /// where our `NSToolbar` and associated delegate live. @@ -62,7 +62,7 @@ impl Toolbar where T: ToolbarDelegate + 'static { (ShareId::from_ptr(toolbar), ShareId::from_ptr(objc_delegate)) }; - &mut delegate.did_load(Toolbar { + let _ret = &mut delegate.did_load(Toolbar { objc: objc.clone(), objc_delegate: objc_delegate.clone(), identifier: identifier.clone(), diff --git a/src/macos/toolbar/traits.rs b/src/appkit/toolbar/traits.rs similarity index 96% rename from src/macos/toolbar/traits.rs rename to src/appkit/toolbar/traits.rs index 22a559b..aa8355d 100644 --- a/src/macos/toolbar/traits.rs +++ b/src/appkit/toolbar/traits.rs @@ -2,7 +2,7 @@ //! go. Currently a bit incomplete in that we don't support the customizing workflow, but feel free //! to pull request it. -use crate::macos::toolbar::{Toolbar, ToolbarItem, ItemIdentifier}; +use crate::appkit::toolbar::{Toolbar, ToolbarItem, ItemIdentifier}; /// A trait that you can implement to have your struct/etc act as an `NSToolbarDelegate`. pub trait ToolbarDelegate { diff --git a/src/macos/window/class.rs b/src/appkit/window/class.rs similarity index 99% rename from src/macos/window/class.rs rename to src/appkit/window/class.rs index 0569a35..dee8917 100644 --- a/src/macos/window/class.rs +++ b/src/appkit/window/class.rs @@ -11,7 +11,7 @@ use objc::{class, sel, sel_impl}; use crate::foundation::{load_or_register_class, id, BOOL, YES, NO, NSUInteger}; use crate::utils::{load, CGSize}; -use crate::macos::window::{WindowDelegate, WINDOW_DELEGATE_PTR}; +use crate::appkit::window::{WindowDelegate, WINDOW_DELEGATE_PTR}; /// Called when an `NSWindowDelegate` receives a `windowWillClose:` event. /// Good place to clean up memory and what not. diff --git a/src/macos/window/config.rs b/src/appkit/window/config.rs similarity index 97% rename from src/macos/window/config.rs rename to src/appkit/window/config.rs index 2cc0a18..3400000 100644 --- a/src/macos/window/config.rs +++ b/src/appkit/window/config.rs @@ -4,7 +4,7 @@ use crate::foundation::NSUInteger; use crate::geometry::Rect; -use crate::macos::window::enums::{WindowStyle, WindowToolbarStyle}; +use crate::appkit::window::enums::{WindowStyle, WindowToolbarStyle}; #[derive(Debug)] pub struct WindowConfig { diff --git a/src/macos/window/controller/class.rs b/src/appkit/window/controller/class.rs similarity index 93% rename from src/macos/window/controller/class.rs rename to src/appkit/window/controller/class.rs index c597316..05b997e 100644 --- a/src/macos/window/controller/class.rs +++ b/src/appkit/window/controller/class.rs @@ -7,7 +7,7 @@ use objc::declare::ClassDecl; use objc::runtime::Class; use objc::class; -use crate::macos::window::{WindowDelegate, WINDOW_DELEGATE_PTR}; +use crate::appkit::window::{WindowDelegate, WINDOW_DELEGATE_PTR}; /// Injects an `NSWindowController` subclass, with some callback and pointer ivars for what we /// need to do. diff --git a/src/macos/window/controller/mod.rs b/src/appkit/window/controller/mod.rs similarity index 87% rename from src/macos/window/controller/mod.rs rename to src/appkit/window/controller/mod.rs index fda9cbe..25a35f4 100644 --- a/src/macos/window/controller/mod.rs +++ b/src/appkit/window/controller/mod.rs @@ -12,8 +12,8 @@ //! # How to use //! //! ```rust,no_run -//! use cacao::macos::app::AppDelegate; -//! use cacao::macos::window::{WindowController, WindowDelegate}; +//! use cacao::appkit::app::AppDelegate; +//! use cacao::appkit::window::{WindowController, WindowDelegate}; //! //! #[derive(Default)] //! struct MyWindow; @@ -35,7 +35,7 @@ use objc_id::Id; use crate::foundation::{id, nil}; use crate::utils::Controller; -use crate::macos::window::{Window, WindowConfig, WindowDelegate, WINDOW_DELEGATE_PTR}; +use crate::appkit::window::{Window, WindowConfig, WindowDelegate, WINDOW_DELEGATE_PTR}; mod class; use class::register_window_controller_class; @@ -54,7 +54,7 @@ impl WindowController where T: WindowDelegate + 'static { /// Allocates and configures an `NSWindowController` in the Objective-C/Cocoa runtime that maps over /// to your supplied delegate. pub fn with(config: WindowConfig, delegate: T) -> Self { - let mut window = Window::with(config, delegate); + let window = Window::with(config, delegate); let objc = unsafe { let window_controller_class = register_window_controller_class::(); @@ -69,13 +69,6 @@ impl WindowController where T: WindowDelegate + 'static { Id::from_ptr(controller) }; - if let Some(delegate) = &mut window.delegate { - (*delegate).did_load(Window { - delegate: None, - objc: window.objc.clone() - }); - } - WindowController { objc, window } } diff --git a/src/macos/window/enums.rs b/src/appkit/window/enums.rs similarity index 100% rename from src/macos/window/enums.rs rename to src/appkit/window/enums.rs diff --git a/src/macos/window/mod.rs b/src/appkit/window/mod.rs similarity index 98% rename from src/macos/window/mod.rs rename to src/appkit/window/mod.rs index fb40f9f..161baad 100644 --- a/src/macos/window/mod.rs +++ b/src/appkit/window/mod.rs @@ -17,10 +17,11 @@ use objc::{msg_send, sel, sel_impl, class}; use objc::runtime::Object; use objc_id::ShareId; +use crate::appkit::toolbar::{Toolbar, ToolbarDelegate}; use crate::color::Color; use crate::foundation::{id, nil, to_bool, YES, NO, NSString, NSInteger, NSUInteger}; -use crate::layout::traits::Layout; -use crate::macos::toolbar::{Toolbar, ToolbarDelegate}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::{os, Controller}; mod class; @@ -289,7 +290,7 @@ impl Window { /// Given a view, sets it as the content view for this window. pub fn set_content_view(&self, view: &L) { - view.with_backing_node(|backing_node| unsafe { + view.with_backing_obj_mut(|backing_node| unsafe { let _: () = msg_send![&*self.objc, setContentView:&*backing_node]; }); } @@ -446,6 +447,7 @@ impl Window { } } + /// Sets the separator style for this window. pub fn set_titlebar_separator_style(&self, style: crate::foundation::NSInteger) { unsafe { let _: () = msg_send![&*self.objc, setTitlebarSeparatorStyle:style]; diff --git a/src/macos/window/traits.rs b/src/appkit/window/traits.rs similarity index 98% rename from src/macos/window/traits.rs rename to src/appkit/window/traits.rs index eb0ff62..fd6fae2 100644 --- a/src/macos/window/traits.rs +++ b/src/appkit/window/traits.rs @@ -2,8 +2,8 @@ //! module. There's a few different ones, and it's just... cleaner, if //! it's organized here. -use crate::macos::app::PresentationOption; -use crate::macos::window::Window; +use crate::appkit::app::PresentationOption; +use crate::appkit::window::Window; /// Lifecycle events for anything that `impl Window`'s. These map to the standard Cocoa /// lifecycle methods, but mix in a few extra things to handle offering configuration tools diff --git a/src/bundle.rs b/src/bundle.rs index aa6ff2c..611ab9d 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -66,9 +66,7 @@ extern fn get_bundle_id(this: &Object, s: Sel, v: id) -> id { let url: id = msg_send![main_bundle, bundleURL]; let x: id = msg_send![url, absoluteString]; println!("Got here? {:?}", x); - unsafe { - NSString::alloc(nil).init_str("com.secretkeys.subatomic") - } + NSString::new("com.test.user_notifications").into() } else { msg_send![this, __bundleIdentifier] } diff --git a/src/button/enums.rs b/src/button/enums.rs index bd3396a..0a033f8 100644 --- a/src/button/enums.rs +++ b/src/button/enums.rs @@ -2,7 +2,7 @@ use crate::foundation::NSUInteger; /// Represents a bezel style for a button. This is a macOS-specific control, and has no effect /// under iOS or tvOS. -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] #[derive(Debug)] pub enum BezelStyle { /// A standard circular button. @@ -49,7 +49,7 @@ pub enum BezelStyle { Unknown(NSUInteger) } -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] impl From for NSUInteger { fn from(style: BezelStyle) -> Self { match style { @@ -71,7 +71,7 @@ impl From for NSUInteger { } } -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] impl From for BezelStyle { fn from(i: NSUInteger) -> Self { match i { diff --git a/src/button/mod.rs b/src/button/mod.rs index 5df9739..7749bc2 100644 --- a/src/button/mod.rs +++ b/src/button/mod.rs @@ -1,4 +1,4 @@ -//! Wraps `NSButton` on macOS, and `UIButton` on iOS and tvOS. +//! 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. @@ -29,20 +29,26 @@ use objc::runtime::{Class, Object, Sel}; use objc::{class, msg_send, sel, sel_impl}; use crate::color::Color; +use crate::control::Control; use crate::image::Image; use crate::foundation::{id, nil, BOOL, YES, NO, NSString, NSUInteger}; use crate::invoker::TargetActionHandler; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +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(target_os = "macos")] -use crate::macos::FocusRingType; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; + +#[cfg(feature = "appkit")] +use crate::appkit::FocusRingType; mod enums; pub use enums::*; -/// Wraps `NSButton` on macOS, and `UIButton` on iOS and tvOS. +/// 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. @@ -71,33 +77,43 @@ pub struct Button { 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 } @@ -114,23 +130,47 @@ impl Button { ]; let _: () = msg_send![button, setWantsLayer:YES]; + + #[cfg(feature = "autolayout")] let _: () = msg_send![button, setTranslatesAutoresizingMaskIntoConstraints:NO]; + button }; Button { handler: None, image: None, + + #[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), } } @@ -144,8 +184,8 @@ impl Button { self.image = Some(image); } - /// Sets the bezel style for this button. Only supported on macOS. - #[cfg(target_os = "macos")] + /// Sets the bezel style for this button. Only supported on appkit. + #[cfg(feature = "appkit")] pub fn set_bezel_style(&self, bezel_style: BezelStyle) { let style: NSUInteger = bezel_style.into(); @@ -167,7 +207,7 @@ impl Button { pub fn set_background_color>(&self, color: C) { let color: id = color.as_ref().into(); - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.objc.with_mut(|obj| unsafe { let cell: id = msg_send![obj, cell]; let _: () = msg_send![cell, setBackgroundColor:color]; @@ -176,19 +216,29 @@ impl Button { /// Set a key to be bound to this button. When the key is pressed, the action coupled to this /// button will fire. - pub fn set_key_equivalent(&self, key: &str) { - let key = NSString::new(key); + pub fn set_key_equivalent<'a, K>(&self, key: K) + where + K: Into> + { + let key: Key<'a> = key.into(); - self.objc.with_mut(|obj| unsafe { - let _: () = msg_send![obj, setKeyEquivalent:&*key]; + self.objc.with_mut(|obj| { + let keychar = match key { + Key::Char(s) => NSString::new(s), + Key::Delete => NSString::new("\u{08}") + }; + + unsafe { + let _: () = msg_send![obj, setKeyEquivalent:&*keychar]; + } }); } /// Sets the text color for this button. /// - /// On macOS, this is done by way of an `AttributedString` under the hood. + /// On appkit, this is done by way of an `AttributedString` under the hood. pub fn set_text_color>(&self, color: C) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.objc.with_mut(move |obj| unsafe { let text: id = msg_send![obj, attributedTitle]; let len: isize = msg_send![text, length]; @@ -201,8 +251,8 @@ impl Button { } // @TODO: Figure out how to handle oddities like this. - /// For buttons on macOS, one might need to disable the border. This does that. - #[cfg(target_os = "macos")] + /// 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 { @@ -223,8 +273,8 @@ impl Button { /// Sets how the control should draw a focus ring when a user is focused on it. /// - /// This is a macOS-only method. - #[cfg(target_os = "macos")] + /// 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(); @@ -244,26 +294,32 @@ impl Button { } } -impl Layout for Button { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for Button { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } -impl Layout for &Button { - fn with_backing_node(&self, handler: F) { +impl Layout for Button {} +impl Control for Button {} + +impl ObjcAccess for &Button { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for &Button {} +impl Control for &Button {} + impl Drop for Button { /// Nils out references on the Objective-C side and removes this from the backing view. // Just to be sure, let's... nil these out. They should be weak references, diff --git a/src/color/macos_dynamic_color.rs b/src/color/appkit_dynamic_color.rs similarity index 97% rename from src/color/macos_dynamic_color.rs rename to src/color/appkit_dynamic_color.rs index cee8521..45fcc6e 100644 --- a/src/color/macos_dynamic_color.rs +++ b/src/color/appkit_dynamic_color.rs @@ -27,6 +27,9 @@ pub(crate) const AQUA_LIGHT_COLOR_HIGH_CONTRAST: &'static str = "AQUA_LIGHT_COLO pub(crate) const AQUA_DARK_COLOR_NORMAL_CONTRAST: &'static str = "AQUA_DARK_COLOR_NORMAL_CONTRAST"; pub(crate) const AQUA_DARK_COLOR_HIGH_CONTRAST: &'static str = "AQUA_DARK_COLOR_HIGH_CONTRAST"; +// Certain platforms we're interested in supporting (airyx) might not have these yet, so this +// will just force the Aqua appearance on that platform. +#[cfg(target_os = "macos")] extern "C" { static NSAppearanceNameAqua: id; static NSAppearanceNameAccessibilityHighContrastAqua: id; @@ -39,8 +42,11 @@ extern "C" { /// contrast checking on systems prior to 10.14: it's not that it couldn't be supported, but the /// ongoing question of how far back to support makes this not worth bothering with right now. /// +/// On non-Apple systems, this returns the light aqua color at all times. +/// /// Pull requests to implement that check would be welcome. fn get_effective_color(this: &Object) -> id { + #[cfg(target_os = "macos")] if os::is_minimum_semversion(10, 14, 0) { unsafe { let mut appearance: id = msg_send![class!(NSAppearance), currentAppearance]; diff --git a/src/color/mod.rs b/src/color/mod.rs index 7453277..be87818 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -26,11 +26,11 @@ use objc_id::Id; use crate::foundation::id; use crate::utils::os; -#[cfg(target_os = "macos")] -mod macos_dynamic_color; +#[cfg(feature = "appkit")] +mod appkit_dynamic_color; -#[cfg(target_os = "macos")] -use macos_dynamic_color::{ +#[cfg(feature = "appkit")] +use appkit_dynamic_color::{ AQUA_LIGHT_COLOR_NORMAL_CONTRAST, AQUA_LIGHT_COLOR_HIGH_CONTRAST, AQUA_DARK_COLOR_NORMAL_CONTRAST, AQUA_DARK_COLOR_HIGH_CONTRAST }; @@ -41,7 +41,7 @@ use macos_dynamic_color::{ /// default to the `Light` theme. #[derive(Copy, Clone, Debug)] pub enum Theme { - /// The "default" theme on a platform. On macOS, this is Aqua. + /// The "default" theme on a platform. On macOS/Airyx, this is Aqua. /// On iOS and tvOS, this is whatever you call the system defined theme. Light, @@ -217,6 +217,7 @@ pub enum Color { /// The default color to use for thin separators/lines that /// do not allow content underneath to be visible. /// This value automatically switches to the correct variant depending on light or dark mode. + #[cfg(feature = "uikit")] OpaqueSeparator, /// The default color to use for rendering links. @@ -230,11 +231,11 @@ pub enum Color { LightText, /// The background color for a given window in the system theme. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] MacOSWindowBackgroundColor, /// The background color that should appear under a page per the system theme. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] MacOSUnderPageBackgroundColor } @@ -248,10 +249,10 @@ impl Color { let a = alpha as CGFloat / 255.0; Color::Custom(Arc::new(RwLock::new(unsafe { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { Id::from_ptr(msg_send![class!(NSColor), colorWithCalibratedRed:r green:g blue:b alpha:a]) } - #[cfg(target_os = "ios")] + #[cfg(feature = "uikit")] { Id::from_ptr(msg_send![class!(UIColor), colorWithRed:r green:g blue:b alpha:a]) } }))) } @@ -271,10 +272,10 @@ impl Color { let a = alpha as CGFloat / 255.0; Color::Custom(Arc::new(RwLock::new(unsafe { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { Id::from_ptr(msg_send![class!(NSColor), colorWithCalibratedHue:h saturation:s brightness:b alpha:a]) } - #[cfg(target_os = "ios")] + #[cfg(feature = "uikit")] { Id::from_ptr(msg_send![class!(UIColor), colorWithHue:h saturation:s brightness:b alpha:a]) } }))) } @@ -289,10 +290,10 @@ impl Color { /// specified alpha. pub fn white_alpha(level: CGFloat, alpha: CGFloat) -> Self { Color::Custom(Arc::new(RwLock::new(unsafe { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { Id::from_ptr(msg_send![class!(NSColor), colorWithCalibratedWhite:level alpha:alpha]) } - #[cfg(target_os = "ios")] + #[cfg(feature = "uikit")] { Id::from_ptr(msg_send![class!(UIColor), colorWithWhite:level alpha:alpha]) } }))) } @@ -319,6 +320,7 @@ impl Color { Color::hexa(hex, 255) } + // @TODO: This is currently appkit-only but should be for uikit as well. /// Creates and returns a dynamic color, which stores a handler and enables returning specific /// colors at appearance time based on device traits (i.e, dark mode vs light mode, contrast /// settings, etc). @@ -327,7 +329,7 @@ impl Color { /// "default" or "light" color. /// /// Returning a dynamic color in your handler is unsupported and may panic. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] pub fn dynamic(handler: F) -> Self where F: Fn(Style) -> Color + 'static @@ -337,9 +339,8 @@ impl Color { // not entirely clear on how expensive the dynamic allocation would be pre-10.15/11.0 and // am happy to do this for now and let someone who needs true dynamic allocation look into // it and PR it. - #[cfg(target_os = "macos")] Color::Custom(Arc::new(RwLock::new(unsafe { - let color: id = msg_send![macos_dynamic_color::register_class(), new]; + let color: id = msg_send![appkit_dynamic_color::register_class(), new]; (&mut *color).set_ivar(AQUA_LIGHT_COLOR_NORMAL_CONTRAST, { let color: id = handler(Style { @@ -421,7 +422,7 @@ impl From<&Color> for id { /// Handles color fallback for system-provided colors. macro_rules! system_color_with_fallback { ($class:ident, $color:ident, $fallback:ident) => ({ - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { #[cfg(feature = "color-fallbacks")] if os::minimum_semversion(10, 10, 0) { @@ -434,7 +435,7 @@ macro_rules! system_color_with_fallback { msg_send![$class, $color] } - #[cfg(target_os = "ios")] + #[cfg(feature = "uikit")] { msg_send![$class, $color] } @@ -450,10 +451,10 @@ macro_rules! system_color_with_fallback { /// The goal here is to make sure that this can't reasonably break on OS's, as `Color` is kind of /// an important piece. It's not on the framework to make your app look good, though. unsafe fn to_objc(obj: &Color) -> id { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let color = class!(NSColor); - #[cfg(target_os = "ios")] + #[cfg(feature = "uikit")] let color = class!(UIColor); match obj { @@ -495,15 +496,18 @@ unsafe fn to_objc(obj: &Color) -> id { Color::SystemBackgroundSecondary => system_color_with_fallback!(color, secondarySystemBackgroundColor, clearColor), Color::SystemBackgroundTertiary => system_color_with_fallback!(color, tertiarySystemBackgroundColor, clearColor), Color::Separator => system_color_with_fallback!(color, separatorColor, lightGrayColor), + + #[cfg(feature = "uikit")] Color::OpaqueSeparator => system_color_with_fallback!(color, opaqueSeparatorColor, darkGrayColor), + Color::Link => system_color_with_fallback!(color, linkColor, blueColor), Color::DarkText => system_color_with_fallback!(color, darkTextColor, blackColor), Color::LightText => system_color_with_fallback!(color, lightTextColor, whiteColor), - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] Color::MacOSWindowBackgroundColor => system_color_with_fallback!(color, windowBackgroundColor, clearColor), - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] Color::MacOSUnderPageBackgroundColor => system_color_with_fallback!(color, underPageBackgroundColor, clearColor), } } diff --git a/src/control/mod.rs b/src/control/mod.rs new file mode 100644 index 0000000..664f850 --- /dev/null +++ b/src/control/mod.rs @@ -0,0 +1,56 @@ + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; + +use crate::foundation::{id, YES, NO, NSUInteger}; +use crate::objc_access::ObjcAccess; + +/// Use this enum for specifying NSControl size types. +#[derive(Copy, Clone, Debug)] +pub enum ControlSize { + /// The smallest control size. + Mini, + + /// A smaller control size. + Small, + + /// The default, regular, size. + Regular, + + /// A large control. Only available on macOS 11.0+. + /// If you pass this to the `set_control_size` method on the `Control` trait, it will + /// transparently map to `Regular` on 10.15 and below. + Large +} + +/// A trait that view wrappers must conform to. Enables managing the subview tree. +#[allow(unused_variables)] +pub trait Control: ObjcAccess { + /// Whether this control is enabled or not. + fn set_enabled(&self, is_enabled: bool) { + self.with_backing_obj_mut(|obj| unsafe { + let _: () = msg_send![obj, setEnabled:match is_enabled { + true => YES, + false => NO + }]; + }); + } + + /// Sets the underlying control size. + fn set_control_size(&self, size: ControlSize) { + let control_size: NSUInteger = match size { + ControlSize::Mini => 2, + ControlSize::Small => 1, + ControlSize::Regular => 0, + + ControlSize::Large => match crate::utils::os::is_minimum_version(11) { + true => 3, + false => 0 + } + }; + + self.with_backing_obj_mut(|obj| unsafe { + let _: () = msg_send![obj, setControlSize:control_size]; + }); + } +} diff --git a/src/filesystem/select.rs b/src/filesystem/select.rs index f6a7c35..f057991 100644 --- a/src/filesystem/select.rs +++ b/src/filesystem/select.rs @@ -10,10 +10,11 @@ use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::ShareId; -use crate::foundation::{id, YES, NO, NSInteger, NSString}; +use crate::foundation::{id, nil, YES, NO, NSInteger, NSString, NSURL}; use crate::filesystem::enums::ModalResponse; -use crate::macos::window::{Window, WindowDelegate}; +#[cfg(feature = "appkit")] +use crate::appkit::window::{Window, WindowDelegate}; #[derive(Debug)] pub struct FileSelectPanel { @@ -126,8 +127,7 @@ impl FileSelectPanel { self.allows_multiple_selection = allows; } - /// Shows the panel as a modal. Currently sheets are not supported, but you're free (and able - /// to) thread the Objective C calls yourself by using the panel field on this struct. + /// Shows the panel as a modal. /// /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as /// the system runs and manages that in another process, and we're still abiding by the general @@ -137,7 +137,7 @@ impl FileSelectPanel { /// script) or can't easily pass one to use as a sheet. pub fn show(&self, handler: F) where - F: Fn(Vec) + 'static + F: Fn(Vec) + 'static { let panel = self.panel.clone(); let completion = ConcreteBlock::new(move |result: NSInteger| { @@ -154,6 +154,16 @@ impl FileSelectPanel { } } + /// As panels descend behind the scenes from `NSWindow`, we can call through to close it. + /// + /// You should really prefer to utilize sheets to display panels; this is offered as a + /// convenience for rare cases where you might need to retain a panel and close it later on. + pub fn close(&self) { + unsafe { + let _: () = msg_send![&*self.panel, close]; + } + } + /// Shows the panel as a modal. Currently, this method accepts `Window`s which use a delegate. /// If you're using a `Window` without a delegate, you may need to opt to use the `show()` /// method. @@ -163,7 +173,7 @@ impl FileSelectPanel { /// retain/ownership rules here. pub fn begin_sheet(&self, window: &Window, handler: F) where - F: Fn(Vec) + 'static + F: Fn(Vec) + 'static { let panel = self.panel.clone(); let completion = ConcreteBlock::new(move |result: NSInteger| { @@ -184,15 +194,16 @@ impl FileSelectPanel { /// Retrieves the selected URLs from the provided panel. /// This is currently a bit ugly, but it's also not something that needs to be the best thing in /// the world as it (ideally) shouldn't be called repeatedly in hot spots. -pub fn get_urls(panel: &Object) -> Vec { +/// +/// (We mostly do this to find the sweet spot between Rust constructs and necessary Foundation +/// interaction patterns) +fn get_urls(panel: &Object) -> Vec { unsafe { let urls: id = msg_send![&*panel, URLs]; let count: usize = msg_send![urls, count]; (0..count).map(|index| { - let url: id = msg_send![urls, objectAtIndex:index]; - let path = NSString::retain(msg_send![url, path]).to_string(); - path.into() + NSURL::retain(msg_send![urls, objectAtIndex:index]) }).collect() } } diff --git a/src/foundation/data.rs b/src/foundation/data.rs index 422d9c9..1135ccb 100644 --- a/src/foundation/data.rs +++ b/src/foundation/data.rs @@ -49,6 +49,21 @@ impl NSData { NSData(Id::from_ptr(obj)) } } + + /// Given a slice of bytes, creates, retains, and returns a wrapped `NSData`. + /// + /// This method is borrowed straight out of [objc-foundation](objc-foundation) by the amazing + /// Steven Sheldon, and just tweaked slightly to fit the desired API semantics here. + /// + /// [objc-foundation]: https://crates.io/crates/objc-foundation + pub fn with_slice(bytes: &[u8]) -> Self { + let bytes_ptr = bytes.as_ptr() as *mut c_void; + + unsafe { + let obj: id = msg_send![class!(NSData), dataWithBytes:bytes_ptr length:bytes.len()]; + NSData(Id::from_ptr(obj)) + } + } /// Given a (presumably) `NSData`, wraps and retains it. pub fn retain(data: id) -> Self { diff --git a/src/foundation/mod.rs b/src/foundation/mod.rs index 592c07a..d8bdfdc 100644 --- a/src/foundation/mod.rs +++ b/src/foundation/mod.rs @@ -42,6 +42,10 @@ pub use number::NSNumber; mod string; pub use string::NSString; +// Separate named module to not conflict with the `url` crate. Go figure. +mod urls; +pub use urls::{NSURL, NSURLBookmarkCreationOption, NSURLBookmarkResolutionOption}; + /// Bool mapping types differ between ARM and x64. There's a number of places that we need to check /// against BOOL results throughout the framework, and this just simplifies some mismatches. #[inline(always)] diff --git a/src/foundation/urls/bookmark_options.rs b/src/foundation/urls/bookmark_options.rs new file mode 100644 index 0000000..4b98434 --- /dev/null +++ b/src/foundation/urls/bookmark_options.rs @@ -0,0 +1,66 @@ +use crate::foundation::NSUInteger; + +/// Options used when creating bookmark data. +#[derive(Copy, Clone, Debug)] +pub enum NSURLBookmarkCreationOption { + /// Specifies that a bookmark created with this option should be created with minimal information. + Minimal, + + /// Specifies that the bookmark data should include properties required to create Finder alias files. + SuitableForBookmarkFile, + + /// Specifies that you want to create a security-scoped bookmark that, when resolved, provides a + /// security-scoped URL allowing read/write access to a file-system resource. + SecurityScoped, + + /// When combined with the NSURLBookmarkCreationOptions::SecurityScoped option, specifies that you + /// want to create a security-scoped bookmark that, when resolved, provides a security-scoped URL allowing + /// read-only access to a file-system resource. + SecurityScopedReadOnly +} + +impl From for NSUInteger { + fn from(flag: NSURLBookmarkCreationOption) -> NSUInteger { + match flag { + NSURLBookmarkCreationOption::Minimal => 1u64 << 9, + NSURLBookmarkCreationOption::SuitableForBookmarkFile => 1u64 << 10, + NSURLBookmarkCreationOption::SecurityScoped => 1 << 11, + NSURLBookmarkCreationOption::SecurityScopedReadOnly => 1 << 12 + } + } +} + +impl From<&NSURLBookmarkCreationOption> for NSUInteger { + fn from(flag: &NSURLBookmarkCreationOption) -> NSUInteger { + match flag { + NSURLBookmarkCreationOption::Minimal => 1u64 << 9, + NSURLBookmarkCreationOption::SuitableForBookmarkFile => 1u64 << 10, + NSURLBookmarkCreationOption::SecurityScoped => 1 << 11, + NSURLBookmarkCreationOption::SecurityScopedReadOnly => 1 << 12 + } + } +} + +/// Options used when resolving bookmark data. +#[derive(Debug)] +pub enum NSURLBookmarkResolutionOption { + /// Specifies that no UI feedback should accompany resolution of the bookmark data. + WithoutUI, + + /// Specifies that no volume should be mounted during resolution of the bookmark data. + WithoutMounting, + + /// Specifies that the security scope, applied to the bookmark when it was created, should + /// be used during resolution of the bookmark data. + SecurityScoped +} + +impl From for NSUInteger { + fn from(flag: NSURLBookmarkResolutionOption) -> NSUInteger { + match flag { + NSURLBookmarkResolutionOption::WithoutUI => 1u64 << 8, + NSURLBookmarkResolutionOption::WithoutMounting => 1u64 << 9, + NSURLBookmarkResolutionOption::SecurityScoped => 1 << 10 + } + } +} diff --git a/src/foundation/urls/mod.rs b/src/foundation/urls/mod.rs new file mode 100644 index 0000000..e76c30e --- /dev/null +++ b/src/foundation/urls/mod.rs @@ -0,0 +1,181 @@ +use std::error::Error; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; +use objc_id::ShareId; + +use crate::foundation::{id, nil, NSData, NSString, NSUInteger}; + +mod bookmark_options; +pub use bookmark_options::{NSURLBookmarkCreationOption, NSURLBookmarkResolutionOption}; + +mod resource_keys; +pub use resource_keys::{NSURLResourceKey, NSURLFileResource, NSUbiquitousItemDownloadingStatus}; + +/// Wraps `NSURL` for use throughout the framework. +/// +/// This type may also be returned to users in some callbacks (e.g, file manager/selectors) as it's +/// a core part of the macOS/iOS experience and bridging around it is arguably blocking people from +/// being able to actually build useful things. +/// +/// For pure-Rust developers who have no interest in the Objective-C underpinnings, there's a +/// `pathbuf()` method that returns an `std::path::PathBuf` for working with. Note that this will +/// prove less useful in sandboxed applications, and if the underlying file that the PathBuf points +/// to moves, you'll be responsible for figuring out exactly what you do there. +/// +/// Otherwise, this struct bridges enough of NSURL to be useful (loading, using, and bookmarks). +/// Pull requests for additional functionality are welcome. +#[derive(Clone, Debug)] +pub struct NSURL<'a> { + /// A reference to the backing `NSURL`. + pub objc: ShareId, + phantom: PhantomData<&'a ()> +} + +impl<'a> NSURL<'a> { + /// In cases where we're vended an `NSURL` by the system, this can be used to wrap and + /// retain it. + pub fn retain(object: id) -> Self { + NSURL { + objc: unsafe { ShareId::from_ptr(object) }, + phantom: PhantomData + } + } + + /// In some cases, we want to wrap a system-provided NSURL without retaining it. + pub fn from_retained(object: id) -> Self { + NSURL { + objc: unsafe { ShareId::from_retained_ptr(object) }, + phantom: PhantomData + } + } + + /// Creates and returns a URL object by calling through to `[NSURL URLWithString]`. + pub fn with_str(url: &str) -> Self { + let url = NSString::new(url); + + Self { + objc: unsafe { + ShareId::from_ptr(msg_send![class!(NSURL), URLWithString:&*url]) + }, + + phantom: PhantomData + } + } + + /// Returns the absolute string path that this URL points to. + /// + /// Note that if the underlying file moved, this won't be accurate - you likely want to + /// research URL bookmarks. + pub fn absolute_string(&self) -> String { + let abs_str = NSString::retain(unsafe { + msg_send![&*self.objc, absoluteString] + }); + + abs_str.to_string() + } + + /// Creates and returns a Rust `PathBuf`, for users who don't need the extra pieces of NSURL + /// and just want to write Rust code. + pub fn pathbuf(&self) -> PathBuf { + let path = NSString::retain(unsafe { + msg_send![&*self.objc, path] + }); + + path.to_str().into() + } + + /// Returns bookmark data for this URL. Will error if the underlying API errors. + /// + /// Bookmarks are useful for sandboxed applications, as well as situations where you might want + /// to later resolve the true location of a file (e.g, if the user moved it between when you + /// got the URL and when you need to use it). + pub fn bookmark_data( + &self, + options: &[NSURLBookmarkCreationOption], + resource_value_keys: &[NSURLResourceKey], + relative_to_url: Option + ) -> Result> { + let mut opts: NSUInteger = 0; + for mask in options { + let i: NSUInteger = mask.into(); + opts = opts | i; + } + + // Build NSArray of resource keys + let resource_keys = nil; + + // Mutability woes mean we just go through a match here to satisfy message passing needs. + let bookmark_data = NSData::retain(match relative_to_url { + Some(relative_url) => unsafe { + msg_send![&*self.objc, bookmarkDataWithOptions:opts + includingResourceValuesForKeys:resource_keys + relativeToURL:relative_url + error:nil + ] + }, + + None => unsafe { + msg_send![&*self.objc, bookmarkDataWithOptions:opts + includingResourceValuesForKeys:resource_keys + relativeToURL:nil + error:nil + ] + } + }); + + // Check for errors... + //Err("LOL".into()) + + Ok(bookmark_data) + } + + /// Converts bookmark data into a URL. + pub fn from_bookmark_data( + data: NSData, + options: &[NSURLBookmarkResolutionOption], + relative_to_url: Option, + data_is_stale: bool + ) -> Result> { + Err("LOL".into()) + } + + /// In an app that has adopted App Sandbox, makes the resource pointed to by a security-scoped URL available to the app. + /// + /// More information can be found at: + /// [https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc] + pub fn start_accessing_security_scoped_resource(&self) { + unsafe { + let _: () = msg_send![&*self.objc, startAccessingSecurityScopedResource]; + } + } + + /// In an app that adopts App Sandbox, revokes access to the resource pointed to by a security-scoped URL. + /// + /// More information can be found at: + /// [https://developer.apple.com/documentation/foundation/nsurl/1413736-stopaccessingsecurityscopedresou?language=objc] + pub fn stop_accessing_security_scoped_resource(&self) { + unsafe { + let _: () = msg_send![&*self.objc, stopAccessingSecurityScopedResource]; + } + } +} + +/*impl From> for id { + /// Consumes and returns the pointer to the underlying NSString instance. + fn from(mut string: NSString) -> Self { + &mut *string.objc + } +}*/ + +impl Deref for NSURL<'_> { + type Target = Object; + + /// Derefs to the underlying Objective-C Object. + fn deref(&self) -> &Object { + &*self.objc + } +} diff --git a/src/foundation/urls/resource_keys.rs b/src/foundation/urls/resource_keys.rs new file mode 100644 index 0000000..3f14f88 --- /dev/null +++ b/src/foundation/urls/resource_keys.rs @@ -0,0 +1,170 @@ +use crate::foundation::id; + +/// Possible values for the `NSURLResourceKey::FileResourceType` key. +#[derive(Debug)] +pub enum NSURLFileResource { + /// The resource is a named pipe. + NamedPipe, + + /// The resource is a character special file. + CharacterSpecial, + + /// The resource is a directory. + Directory, + + /// The resource is a block special file. + BlockSpecial, + + /// The resource is a regular file. + Regular, + + /// The resource is a symbolic link. + SymbolicLink, + + /// The resource is a socket. + Socket, + + /// The resource’s type is unknown. + Unknown +} + +/// Values that describe the iCloud storage state of a file. +#[derive(Debug)] +pub enum NSUbiquitousItemDownloadingStatus { + /// A local copy of this item exists and is the most up-to-date version known to the device. + Current, + + /// A local copy of this item exists, but it is stale. The most recent version will be downloaded as soon as possible. + Downloaded, + + /// This item has not been downloaded yet. Initiate a download. + NotDownloaded +} + +#[derive(Debug)] +pub enum NSURLResourceKey { + IsApplication, + IsScriptable, + IsDirectory, + ParentDirectoryURL, + FileAllocatedSize, + FileProtection, + FileProtectionType, + FileResourceIdentifier, + FileResourceType(NSURLFileResource), + FileSecurity, + FileSize, + IsAliasFile, + IsPackage, + IsRegularFile, + PreferredIOBlockSize, + TotalFileAllocatedSize, + TotalFileSize, + + VolumeAvailableCapacity, + VolumeAvailableCapacityForImportantUsage, + VolumeAvailableCapacityForOpportunisticUsage, + VolumeTotalCapacity, + VolumeIsAutomounted, + VolumeIsBrowsable, + VolumeIsEjectable, + VolumeIsEncrypted, + VolumeIsInternal, + VolumeIsJournaling, + VolumeIsLocal, + VolumeIsReadOnly, + VolumeIsRemovable, + VolumeIsRootFileSystem, + + IsMountTrigger, + IsVolume, + VolumeCreationDate, + VolumeIdentifier, + VolumeLocalizedFormatDescription, + VolumeLocalizedName, + VolumeMaximumFileSize, + VolumeName, + VolumeResourceCount, + VolumeSupportsAccessPermissions, + VolumeSupportsAdvisoryFileLocking, + VolumeSupportsCasePreservedNames, + VolumeSupportsCaseSensitiveNames, + VolumeSupportsCompression, + VolumeSupportsExclusiveRenaming, + VolumeSupportsExtendedSecurity, + VolumeSupportsFileCloning, + VolumeSupportsHardLinks, + VolumeSupportsImmutableFiles, + VolumeSupportsJournaling, + VolumeSupportsPersistentIDs, + VolumeSupportsRenaming, + VolumeSupportsRootDirectoryDates, + VolumeSupportsSparseFiles, + VolumeSupportsSwapRenaming, + VolumeSupportsSymbolicLinks, + VolumeSupportsVolumeSizes, + VolumeSupportsZeroRuns, + VolumeURLForRemounting, + VolumeURL, + VolumeUUIDString, + + IsUbiquitousItem, + UbiquitousSharedItemMostRecentEditorNameComponents, + UbiquitousItemDownloadRequested, + UbiquitousItemIsDownloading, + UbiquitousItemDownloadingError, + UbiquitousItemDownloadingStatus(NSUbiquitousItemDownloadingStatus), + UbiquitousItemIsUploaded, + UbiquitousItemIsUploading, + UbiquitousItemUploadingError, + UbiquitousItemHasUnresolvedConflicts, + UbiquitousItemContainerDisplayName, + UbiquitousSharedItemOwnerNameComponents, + UbiquitousSharedItemCurrentUserPermissions, + UbiquitousSharedItemCurrentUserRole, + UbiquitousItemIsShared, + UbiquitousSharedItemRole, + UbiquitousSharedItemPermissions, + + ThumbnailDictionaryItem, + + KeysOfUnsetValues, + QuarantineProperties, + AddedToDirectoryDate, + AttributeModificationDate, + ContentAccessDate, + ContentModificationDate, + CreationDate, + CustomIcon, + DocumentIdentifier, + EffectiveIcon, + GenerationIdentifier, + HasHiddenExtension, + IsExcludedFromBackup, + IsExecutable, + IsHidden, + IsReadable, + IsSymbolicLink, + IsSystemImmutable, + IsUserImmutable, + IsWritable, + LabelColor, + LabelNumber, + LinkCount, + LocalizedLabel, + LocalizedName, + LocalizedTypeDescription, + Name, + Path, + CanonicalPath, + TagNames, + ContentType, + + FileContentIdentifier, + IsPurgeable, + IsSparse, + MayHaveExtendedAttributes, + MayShareFileContent, + UbiquitousItemIsExcludedFromSync, + VolumeSupportsFileProtection +} diff --git a/src/geometry.rs b/src/geometry.rs index cd8d529..547202a 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -39,7 +39,7 @@ impl Rect { impl From for CGRect { fn from(rect: Rect) -> CGRect { CGRect::new( - &CGPoint::new(rect.top, rect.left), + &CGPoint::new(rect.left, rect.top), &CGSize::new(rect.width, rect.height) ) } diff --git a/src/image/macos.rs b/src/image/appkit.rs similarity index 100% rename from src/image/macos.rs rename to src/image/appkit.rs diff --git a/src/image/icons.rs b/src/image/icons.rs index b1decca..7cd3cbd 100644 --- a/src/image/icons.rs +++ b/src/image/icons.rs @@ -1,3 +1,4 @@ +use crate::foundation::id; /// These icons are system-provided icons that are guaranteed to exist in all versions of macOS /// that Cacao supports. These will use SFSymbols on Big Sur and onwards (11.0+), and the correct @@ -23,51 +24,86 @@ pub enum MacSystemIcon { /// Returns a stock "+" icon that's common to the system. Use this for buttons that need the /// symbol. - Add + Add, + + /// A stock "-" icon that's common to the system. Use this for buttons that need the symbol. + Remove, + + /// Returns a Folder icon. + Folder +} + +extern "C" { + static NSImageNamePreferencesGeneral: id; + static NSImageNameAdvanced: id; + static NSImageNameUserAccounts: id; + static NSImageNameAddTemplate: id; + static NSImageNameFolder: id; + static NSImageNameRemoveTemplate: id; } #[cfg(target_os = "macos")] impl MacSystemIcon { /// Maps system icons to their pre-11.0 framework identifiers. - pub fn to_str(&self) -> &'static str { - match self { - MacSystemIcon::PreferencesGeneral => "NSPreferencesGeneral", - MacSystemIcon::PreferencesAdvanced => "NSAdvanced", - MacSystemIcon::PreferencesUserAccounts => "NSUserAccounts", - MacSystemIcon::Add => "NSImageNameAddTemplate" + pub fn to_id(&self) -> id { + unsafe { + match self { + MacSystemIcon::PreferencesGeneral => NSImageNamePreferencesGeneral, + MacSystemIcon::PreferencesAdvanced => NSImageNameAdvanced, + MacSystemIcon::PreferencesUserAccounts => NSImageNameUserAccounts, + MacSystemIcon::Add => NSImageNameAddTemplate, + MacSystemIcon::Remove => NSImageNameRemoveTemplate, + MacSystemIcon::Folder => NSImageNameFolder + } } } /// Maps system icons to their SFSymbols-counterparts for use on 11.0+. pub fn to_sfsymbol_str(&self) -> &'static str { match self { - MacSystemIcon::PreferencesGeneral => "gearshape", - MacSystemIcon::PreferencesAdvanced => "slider.vertical.3", - MacSystemIcon::PreferencesUserAccounts => "at", - MacSystemIcon::Add => "plus" + MacSystemIcon::PreferencesGeneral => SFSymbol::GearShape.to_str(), + MacSystemIcon::PreferencesAdvanced => SFSymbol::SliderVertical3.to_str(), + MacSystemIcon::PreferencesUserAccounts => SFSymbol::AtSymbol.to_str(), + MacSystemIcon::Add => SFSymbol::Plus.to_str(), + MacSystemIcon::Remove => SFSymbol::Minus.to_str(), + MacSystemIcon::Folder => SFSymbol::FolderFilled.to_str() } } } #[derive(Debug)] pub enum SFSymbol { + AtSymbol, + GearShape, + FolderFilled, PaperPlane, PaperPlaneFilled, + Plus, + Minus, + SliderVertical3, SquareAndArrowUpOnSquare, SquareAndArrowUpOnSquareFill, SquareAndArrowDownOnSquare, - SquareAndArrowDownOnSquareFill + SquareAndArrowDownOnSquareFill, + SquareDashed } impl SFSymbol { - pub fn to_str(&self) -> &str { + pub fn to_str(&self) -> &'static str { match self { + Self::AtSymbol => "at", + Self::GearShape => "gearshape", + Self::FolderFilled => "folder.fill", Self::PaperPlane => "paperplane", Self::PaperPlaneFilled => "paperplane.fill", + Self::Plus => "plus", + Self::Minus => "minus", + Self::SliderVertical3 => "slider.vertical.3", Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square", Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill", Self::SquareAndArrowDownOnSquare => "square.and.arrow.down.on.square", - Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill" + Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill", + Self::SquareDashed => "square.dashed" } } } diff --git a/src/image/image.rs b/src/image/image.rs index 974bbd8..455e437 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -11,7 +11,7 @@ use core_graphics::{ }; use core_graphics::context::{CGContext, CGContextRef}; -use crate::foundation::{id, YES, NO, NSString}; +use crate::foundation::{id, YES, NO, NSString, NSData}; use crate::utils::os; use super::icons::*; @@ -118,7 +118,7 @@ pub struct DrawConfig { pub resize: ResizeBehavior } -/// Wraps `NSImage` on macOS, and `UIImage` on iOS and tvOS. Can be used to display images, icons, +/// Wraps `NSImage` under AppKit, and `UIImage` on under UIKit (iOS and tvOS). Can be used to display images, icons, /// and so on. #[derive(Clone, Debug)] pub struct Image(pub ShareId); @@ -131,10 +131,53 @@ impl Image { }) } + /// Loads an image from the specified path. + pub fn with_contents_of_file(path: &str) -> Self { + let file_path = NSString::new(path); + + Image(unsafe { + let alloc: id = msg_send![class!(NSImage), alloc]; + ShareId::from_ptr(msg_send![alloc, initWithContentsOfFile:file_path]) + }) + } + + /// Given a Vec of data, will transform it into an Image by passing it through NSData. + /// This can be useful for when you need to include_bytes!() something into your binary. + pub fn with_data(data: &[u8]) -> Self { + let data = NSData::with_slice(data); + + Image(unsafe { + let alloc: id = msg_send![class!(NSImage), alloc]; + ShareId::from_ptr(msg_send![alloc, initWithData:data]) + }) + } + + // @TODO: for Airyx, unsure if this is supported - and it's somewhat modern macOS-specific, so + // let's keep the os flag here for now. /// Returns a stock system icon. These are guaranteed to exist across all versions of macOS /// supported. #[cfg(target_os = "macos")] - pub fn system_icon(icon: MacSystemIcon, accessibility_description: &str) -> Self { + pub fn system_icon(icon: MacSystemIcon) -> Self { + Image(unsafe { + ShareId::from_ptr({ + let icon = icon.to_id(); + msg_send![class!(NSImage), imageNamed:icon] + }) + }) + } + + // @TODO: for Airyx, unsure if this is supported - and it's somewhat modern macOS-specific, so + // let's keep the os flag here for now. + /// The name here can be confusing, I know. + /// + /// A system symbol will swap an SFSymbol in for macOS 11.0+, but return the correct + /// MacSystemIcon image type for versions prior to that. This is mostly helpful in situations + /// like Preferences windows toolbars, where you want to have the correct modern styling for newer OS + /// versions. + /// + /// However, if you need the correct "folder" icon for instance, you probably want `system_icon`. + #[cfg(target_os = "macos")] + pub fn toolbar_icon(icon: MacSystemIcon, accessibility_description: &str) -> Self { Image(unsafe { ShareId::from_ptr(match os::is_minimum_version(11) { true => { @@ -145,8 +188,8 @@ impl Image { }, false => { - let icon = NSString::new(icon.to_str()); - msg_send![class!(NSImage), imageNamed:&*icon] + let icon = icon.to_id(); + msg_send![class!(NSImage), imageNamed:icon] } }) }) diff --git a/src/image/mod.rs b/src/image/mod.rs index 313ad3f..8b393a4 100644 --- a/src/image/mod.rs +++ b/src/image/mod.rs @@ -4,20 +4,24 @@ use objc::{msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; -#[cfg(target_os = "macos")] -mod macos; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; -#[cfg(target_os = "macos")] -use macos::register_image_view_class; +#[cfg(feature = "appkit")] +mod appkit; -#[cfg(target_os = "ios")] -mod ios; +#[cfg(feature = "appkit")] +use appkit::register_image_view_class; -#[cfg(target_os = "ios")] -use ios::register_image_view_class; +//#[cfg(feature = "uikit")] +//mod uikit; + +//#[cfg(feature = "uikit")] +//use uikit::register_image_view_class; mod image; pub use image::{Image, DrawConfig, ResizeBehavior}; @@ -29,9 +33,11 @@ pub use icons::*; fn allocate_view(registration_fn: fn() -> *const Class) -> id { unsafe { let view: id = msg_send![registration_fn(), new]; + + #[cfg(feature = "autolayout")] let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let _: () = msg_send![view, setWantsLayer:YES]; view @@ -47,33 +53,43 @@ pub struct ImageView { pub objc: ObjcProperty, /// 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 } @@ -89,16 +105,36 @@ impl ImageView { let view = allocate_view(register_image_view_class); ImageView { + #[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), } } @@ -112,23 +148,34 @@ impl ImageView { }); } + /// Given an image reference, sets it on the image view. You're currently responsible for + /// retaining this yourself. pub fn set_image(&self, image: &Image) { self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setImage:&*image.0]; }); } + + /*pub fn set_image_scaling(&self, scaling_type: ImageScale) { + self.objc.with_mut(|obj| unsafe { + + let _: () = msg_send![obj, setImageScaling: + }); + }*/ } -impl Layout for ImageView { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ImageView { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ImageView {} + impl Drop for ImageView { /// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if diff --git a/src/image/ios.rs b/src/image/uikit.rs similarity index 100% rename from src/image/ios.rs rename to src/image/uikit.rs diff --git a/src/input/macos.rs b/src/input/appkit.rs similarity index 100% rename from src/input/macos.rs rename to src/input/appkit.rs diff --git a/src/input/mod.rs b/src/input/mod.rs index f1230d7..bd99d3c 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -45,22 +45,27 @@ use objc::{msg_send, sel, sel_impl}; use objc_id::ShareId; use crate::color::Color; +use crate::control::Control; use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NO, YES}; -use crate::layout::{Layout, LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::text::{Font, TextAlign}; use crate::utils::properties::ObjcProperty; -#[cfg(target_os = "macos")] -mod macos; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY}; -#[cfg(target_os = "macos")] -use macos::{register_view_class, register_view_class_with_delegate}; +#[cfg(feature = "appkit")] +mod appkit; -#[cfg(target_os = "ios")] -mod ios; +#[cfg(feature = "appkit")] +use appkit::{register_view_class, register_view_class_with_delegate}; -#[cfg(target_os = "ios")] -use ios::{register_view_class, register_view_class_with_delegate}; +//#[cfg(feature = "uikit")] +//mod uikit; + +//#[cfg(feature = "uikit")] +//use uikit::{register_view_class, register_view_class_with_delegate}; mod traits; pub use traits::TextFieldDelegate; @@ -72,9 +77,10 @@ fn common_init(class: *const Class) -> id { unsafe { let view: id = msg_send![class, new]; + #[cfg(feature = "autolayout")] let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints: NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let _: () = msg_send![view, setWantsLayer: YES]; view @@ -92,33 +98,43 @@ pub struct TextField { pub delegate: 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, } @@ -136,17 +152,37 @@ impl TextField { TextField { delegate: None, - top: LayoutAnchorY::top(view), - left: LayoutAnchorX::left(view), - leading: LayoutAnchorX::leading(view), - right: LayoutAnchorX::right(view), - trailing: LayoutAnchorX::trailing(view), - bottom: LayoutAnchorY::bottom(view), - width: LayoutAnchorDimension::width(view), - height: LayoutAnchorDimension::height(view), - center_x: LayoutAnchorX::center(view), - center_y: LayoutAnchorY::center(view), objc: ObjcProperty::retain(view), + + #[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) } } } @@ -169,17 +205,37 @@ where let mut label = TextField { delegate: None, - top: LayoutAnchorY::top(label), - left: LayoutAnchorX::left(label), - leading: LayoutAnchorX::leading(label), - right: LayoutAnchorX::right(label), - trailing: LayoutAnchorX::trailing(label), - bottom: LayoutAnchorY::bottom(label), - width: LayoutAnchorDimension::width(label), - height: LayoutAnchorDimension::height(label), - center_x: LayoutAnchorX::center(label), - center_y: LayoutAnchorY::center(label), objc: ObjcProperty::retain(label), + + #[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), }; (&mut delegate).did_load(label.clone_as_handle()); @@ -196,17 +252,37 @@ impl TextField { pub(crate) fn clone_as_handle(&self) -> TextField { TextField { delegate: None, - top: self.top.clone(), - leading: self.leading.clone(), - left: self.left.clone(), - trailing: self.trailing.clone(), - right: self.right.clone(), - bottom: self.bottom.clone(), - width: self.width.clone(), - height: self.height.clone(), - center_x: self.center_x.clone(), - center_y: self.center_y.clone(), objc: self.objc.clone(), + + #[cfg(feature = "autolayout")] + top: self.top.clone(), + + #[cfg(feature = "autolayout")] + leading: self.leading.clone(), + + #[cfg(feature = "autolayout")] + left: self.left.clone(), + + #[cfg(feature = "autolayout")] + trailing: self.trailing.clone(), + + #[cfg(feature = "autolayout")] + right: self.right.clone(), + + #[cfg(feature = "autolayout")] + bottom: self.bottom.clone(), + + #[cfg(feature = "autolayout")] + width: self.width.clone(), + + #[cfg(feature = "autolayout")] + height: self.height.clone(), + + #[cfg(feature = "autolayout")] + center_x: self.center_x.clone(), + + #[cfg(feature = "autolayout")] + center_y: self.center_y.clone(), } } @@ -235,6 +311,15 @@ impl TextField { }); } + /// Call this to set the text for the label. + pub fn set_placeholder_text(&self, text: &str) { + let s = NSString::new(text); + + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setPlaceholderString:&*s]; + }); + } + /// The the text alignment style for this control. pub fn set_text_alignment(&self, alignment: TextAlign) { self.objc.with_mut(|obj| unsafe { @@ -243,6 +328,35 @@ impl TextField { }); } + /// Set whether this field operates in single-line mode. + pub fn set_uses_single_line(&self, uses_single_line: bool) { + self.objc.with_mut(|obj| unsafe { + let cell: id = msg_send![obj, cell]; + let _: () = msg_send![cell, setUsesSingleLineMode:match uses_single_line { + true => YES, + false => NO + }]; + }); + } + + /// Set whether this field operates in single-line mode. + pub fn set_wraps(&self, uses_single_line: bool) { + self.objc.with_mut(|obj| unsafe { + let cell: id = msg_send![obj, cell]; + let _: () = msg_send![cell, setWraps:match uses_single_line { + true => YES, + false => NO + }]; + }); + } + + /// Sets the maximum number of lines. + pub fn set_max_number_of_lines(&self, num: NSInteger) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, setMaximumNumberOfLines:num]; + }); + } + /// Sets the font for this input. pub fn set_font>(&self, font: F) { let font = font.as_ref().clone(); @@ -253,16 +367,20 @@ impl TextField { } } -impl Layout for TextField { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for TextField { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for TextField {} + +impl Control for TextField {} + impl Drop for TextField { /// A bit of extra cleanup for delegate callback pointers. If the originating `TextField` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..4dfc71f --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,18 @@ +//! This provides some basic mapping for providing Key characters to controls. It's mostly meant as +//! a wrapper to stop magic symbols all over the place. + +/// Represents a Key character. +#[derive(Debug)] +pub enum Key<'a> { + /// Behind the scenes, this translates to NSDeleteCharacter (for AppKit). + Delete, + + /// Whatever character you want. + Char(&'a str) +} + +impl<'a> From<&'a str> for Key<'a> { + fn from(s: &'a str) -> Self { + Key::Char(s) + } +} diff --git a/src/layout/animator.rs b/src/layout/animator.rs new file mode 100644 index 0000000..5d1cc78 --- /dev/null +++ b/src/layout/animator.rs @@ -0,0 +1,27 @@ +use core_graphics::base::CGFloat; + +use objc::{msg_send, sel, sel_impl}; +use objc::runtime::{Class, Object}; +use objc_id::ShareId; + +use crate::foundation::id; + +/// A wrapper for an animation proxy object in Cocoa that supports basic animations. +#[derive(Clone, Debug)] +pub struct LayoutConstraintAnimatorProxy(pub ShareId); + +impl LayoutConstraintAnimatorProxy { + /// Wraps and returns a proxy for animation of layout constraint values. + pub fn new(proxy: id) -> Self { + Self(unsafe { + ShareId::from_ptr(msg_send![proxy, animator]) + }) + } + + /// Sets the constant (usually referred to as `offset` in Cacao) value for the constraint being animated. + pub fn set_offset(&self, value: CGFloat) { + unsafe { + let _: () = msg_send![&*self.0, setConstant:value]; + } + } +} diff --git a/src/layout/constraint.rs b/src/layout/constraint.rs index b5e0fdf..ae00671 100644 --- a/src/layout/constraint.rs +++ b/src/layout/constraint.rs @@ -10,6 +10,8 @@ use objc_id::ShareId; use crate::foundation::{id, YES, NO}; +use super::LayoutConstraintAnimatorProxy; + /// A wrapper for `NSLayoutConstraint`. This both acts as a central path through which to activate /// constraints, as well as a wrapper for layout constraints that are not axis bound (e.g, width or /// height). @@ -27,16 +29,20 @@ pub struct LayoutConstraint { /// The priority used in computing this constraint. pub priority: f64, + + /// An animator proxy that can be used inside animation contexts. + pub animator: LayoutConstraintAnimatorProxy } impl LayoutConstraint { /// An internal method for wrapping existing constraints. pub(crate) fn new(object: id) -> Self { LayoutConstraint { + animator: LayoutConstraintAnimatorProxy::new(object), constraint: unsafe { ShareId::from_ptr(object) }, offset: 0.0, multiplier: 0.0, - priority: 0.0 + priority: 0.0, } } @@ -50,6 +56,7 @@ impl LayoutConstraint { LayoutConstraint { + animator: self.animator, constraint: self.constraint, offset: offset, multiplier: self.multiplier, @@ -57,6 +64,7 @@ impl LayoutConstraint { } } + /// Sets the offset of a borrowed constraint. pub fn set_offset>(&self, offset: F) { let offset: f64 = offset.into(); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 36afe80..082c222 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,26 +1,46 @@ -//! A wrapper for `NSLayoutConstraint`, enabling AutoLayout across views. This does a few things -//! that might seem weird, but are generally good and rely on the idea that this is all written -//! once and used often. -//! -//! Notably: there are 3 structs for wrapping layout constraints; in practice, you likely don't need to -//! care. This is because we want to detect at compile time invalid layout items - i.e, you should -//! not be able to attach a left-axis to a top-axis. In Rust this is a bit tricky, but by using -//! some `impl Trait`'s in the right places we can mostly hide this detail away. +//! This module contains traits and helpers for layout. By default, standard frame-based layouts +//! are supported via the `Layout` trait, which all widgets implement. If you opt in to the +//! `AutoLayout` feature, each widget will default to using AutoLayout, which can be beneficial in +//! more complicated views that need to deal with differing screen sizes. -pub mod attributes; -pub use attributes::*; - -pub mod traits; +mod traits; pub use traits::Layout; -pub mod constraint; +mod animator; +pub use animator::LayoutConstraintAnimatorProxy; + +#[cfg(feature = "autolayout")] +mod attributes; + +#[cfg(feature = "autolayout")] +pub use attributes::*; + +#[cfg(feature = "autolayout")] +mod constraint; + +#[cfg(feature = "autolayout")] pub use constraint::LayoutConstraint; -pub mod dimension; +#[cfg(feature = "autolayout")] +mod dimension; + +#[cfg(feature = "autolayout")] pub use dimension::LayoutAnchorDimension; -pub mod horizontal; +#[cfg(feature = "autolayout")] +mod horizontal; + +#[cfg(feature = "autolayout")] pub use horizontal::LayoutAnchorX; -pub mod vertical; +#[cfg(feature = "autolayout")] +mod vertical; + +#[cfg(feature = "autolayout")] pub use vertical::LayoutAnchorY; + +#[cfg(feature = "autolayout")] +mod safe_guide; + +#[cfg(feature = "autolayout")] +pub use safe_guide::SafeAreaLayoutGuide; diff --git a/src/layout/safe_guide.rs b/src/layout/safe_guide.rs new file mode 100644 index 0000000..a8315e8 --- /dev/null +++ b/src/layout/safe_guide.rs @@ -0,0 +1,67 @@ +use objc::{msg_send, sel, sel_impl}; + +use crate::foundation::id; +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::utils::os; + +/// A SafeAreaLayoutGuide should exist on all view types, and ensures that there are anchor points +/// that work within the system constraints. On macOS 11+, this will ensure you work around system +/// padding transprently - on macOS 10.15 and under, this will transparently map to the normal +/// edges, as the underlying properties were not supported there. +#[derive(Clone, Debug)] +pub struct SafeAreaLayoutGuide { + /// A pointer to the Objective-C runtime top layout constraint. + pub top: LayoutAnchorY, + + /// A pointer to the Objective-C runtime leading layout constraint. + pub leading: LayoutAnchorX, + + /// A pointer to the Objective-C runtime left layout constraint. + pub left: LayoutAnchorX, + + /// A pointer to the Objective-C runtime trailing layout constraint. + pub trailing: LayoutAnchorX, + + /// A pointer to the Objective-C runtime right layout constraint. + pub right: LayoutAnchorX, + + /// A pointer to the Objective-C runtime bottom layout constraint. + pub bottom: LayoutAnchorY, + + /// A pointer to the Objective-C runtime width layout constraint. + pub width: LayoutAnchorDimension, + + /// A pointer to the Objective-C runtime height layout constraint. + pub height: LayoutAnchorDimension, + + /// A pointer to the Objective-C runtime center X layout constraint. + pub center_x: LayoutAnchorX, + + /// A pointer to the Objective-C runtime center Y layout constraint. + pub center_y: LayoutAnchorY +} + +impl SafeAreaLayoutGuide { + /// Given a view pointer, will extract the safe area layout guide properties and return a + /// `SafeAreaLayoutGuide` composed of them. + pub fn new(view: id) -> Self { + // For versions prior to Big Sur, we'll just use the default view anchors in place. + let guide: id = match os::is_minimum_version(11) { + true => unsafe { msg_send![view, layoutMarginsGuide] }, + false => view + }; + + Self { + top: LayoutAnchorY::top(guide), + left: LayoutAnchorX::left(guide), + leading: LayoutAnchorX::leading(guide), + right: LayoutAnchorX::right(guide), + trailing: LayoutAnchorX::trailing(guide), + bottom: LayoutAnchorY::bottom(guide), + width: LayoutAnchorDimension::width(guide), + height: LayoutAnchorDimension::height(guide), + center_x: LayoutAnchorX::center(guide), + center_y: LayoutAnchorY::center(guide) + } + } +} diff --git a/src/layout/traits.rs b/src/layout/traits.rs index c2f8815..16df281 100644 --- a/src/layout/traits.rs +++ b/src/layout/traits.rs @@ -1,6 +1,7 @@ //! Various traits related to controllers opting in to autolayout routines and support for view //! heirarchies. +use core_graphics::base::CGFloat; use core_graphics::geometry::{CGRect, CGPoint, CGSize}; use objc::{msg_send, sel, sel_impl}; @@ -9,27 +10,21 @@ use objc_id::ShareId; use crate::foundation::{id, nil, to_bool, YES, NO, NSArray, NSString}; use crate::geometry::Rect; +use crate::objc_access::ObjcAccess; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] use crate::pasteboard::PasteboardType; /// A trait that view wrappers must conform to. Enables managing the subview tree. #[allow(unused_variables)] -pub trait Layout { - /// Used for mutably interacting with the underlying Objective-C instance. - fn with_backing_node(&self, handler: F); - - /// Used for checking backing properties of the underlying Objective-C instance, without - /// needing a mutable borrow. - fn get_from_backing_node R, R>(&self, handler: F) -> R; - +pub trait Layout: ObjcAccess { /// Sets whether this needs to be redrawn before being displayed. /// /// If you're updating data that dynamically impacts this view, mark this as true - the next /// pass from the system will redraw it accordingly, and set the underlying value back to /// `false`. fn set_needs_display(&self, needs_display: bool) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, setNeedsDisplay:match needs_display { true => YES, false => NO @@ -39,8 +34,8 @@ pub trait Layout { /// Adds another Layout-backed control or view as a subview of this view. fn add_subview(&self, view: &V) { - self.with_backing_node(|backing_node| { - view.with_backing_node(|subview_node| unsafe { + self.with_backing_obj_mut(|backing_node| { + view.with_backing_obj_mut(|subview_node| unsafe { let _: () = msg_send![backing_node, addSubview:subview_node]; }); }); @@ -48,7 +43,7 @@ pub trait Layout { /// Removes a control or view from the superview. fn remove_from_superview(&self) { - self.with_backing_node(|backing_node| unsafe { + self.with_backing_obj_mut(|backing_node| unsafe { let _: () = msg_send![backing_node, removeFromSuperview]; }); } @@ -61,7 +56,7 @@ pub trait Layout { fn set_frame>(&self, rect: R) { let frame: CGRect = rect.into(); - self.with_backing_node(move |backing_node| unsafe { + self.with_backing_obj_mut(move |backing_node| unsafe { let _: () = msg_send![backing_node, setFrame:frame]; }); } @@ -71,8 +66,9 @@ pub trait Layout { /// /// Cacao defaults this to `false`; if you need to set frame-based layout pieces, /// then you should set this to `true` (or use an appropriate initializer that does it for you). + #[cfg(feature = "autolayout")] fn set_translates_autoresizing_mask_into_constraints(&self, translates: bool) { - self.with_backing_node(|backing_node| unsafe { + self.with_backing_obj_mut(|backing_node| unsafe { let _: () = msg_send![backing_node, setTranslatesAutoresizingMaskIntoConstraints:match translates { true => YES, false => NO @@ -84,7 +80,7 @@ pub trait Layout { /// /// When hidden, widgets don't receive events and is not visible. fn set_hidden(&self, hide: bool) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, setHidden:match hide { true => YES, false => NO @@ -97,7 +93,7 @@ pub trait Layout { /// Note that this can report `false` if an ancestor widget is hidden, thus hiding this - to check in /// that case, you may want `is_hidden_or_ancestor_is_hidden()`. fn is_hidden(&self) -> bool { - self.get_from_backing_node(|obj| { + self.get_from_backing_obj(|obj| { to_bool(unsafe { msg_send![obj, isHidden] }) @@ -105,9 +101,9 @@ pub trait Layout { } /// Returns whether this is hidden, *or* whether an ancestor view is hidden. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] fn is_hidden_or_ancestor_is_hidden(&self) -> bool { - self.get_from_backing_node(|obj| { + self.get_from_backing_obj(|obj| { to_bool(unsafe { msg_send![obj, isHiddenOrHasHiddenAncestor] }) @@ -115,22 +111,28 @@ pub trait Layout { } /// Register this view for drag and drop operations. - #[cfg(target_os = "macos")] + /// + /// This should be supported under UIKit as well, but is featured gated under AppKit + /// currently to avoid compile issues. + #[cfg(feature = "appkit")] fn register_for_dragged_types(&self, types: &[PasteboardType]) { let types: NSArray = types.into_iter().map(|t| { let x: NSString = (*t).into(); x.into() }).collect::>().into(); - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, registerForDraggedTypes:&*types]; }); } /// Unregisters this as a target for drag and drop operations. - #[cfg(target_os = "macos")] + /// + /// This should be supported under UIKit as well, but is featured gated under AppKit + /// currently to avoid compile issues. + #[cfg(feature = "appkit")] fn unregister_dragged_types(&self) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, unregisterDraggedTypes]; }); } @@ -139,9 +141,9 @@ pub trait Layout { /// /// If you have a high performance tableview or collectionview that has issues, disabling these /// can be helpful - but always test! - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] fn set_posts_frame_change_notifications(&self, posts: bool) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, setPostsFrameChangedNotifications:match posts { true => YES, false => NO @@ -153,13 +155,24 @@ pub trait Layout { /// /// If you have a high performance tableview or collectionview that has issues, disabling these /// can be helpful - but always test! - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] fn set_posts_bounds_change_notifications(&self, posts: bool) { - self.with_backing_node(|obj| unsafe { + self.with_backing_obj_mut(|obj| unsafe { let _: () = msg_send![obj, setPostsBoundsChangedNotifications:match posts { true => YES, false => NO }]; }); } + + /// Theoretically this belongs elsewhere, but we want to enable this on all view layers, since + /// it's common enough anyway. + #[cfg(feature = "appkit")] + fn set_alpha(&self, value: f64) { + let value: CGFloat = value.into(); + + self.with_backing_obj_mut(|obj| unsafe { + let _: () = msg_send![obj, setAlphaValue:value]; + }); + } } diff --git a/src/lib.rs b/src/lib.rs index dddef19..1ea157c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! # Cacao //! -//! This library provides safe Rust bindings for `AppKit` on macOS and (eventually) `UIKit` on iOS and tvOS. +//! This library provides safe Rust bindings for `AppKit` on appkit and (eventually) `UIKit` on uikit and tvOS. //! It tries to do so in a way that, if you've done programming for the framework before (in Swift or //! Objective-C), will feel familiar. This is tricky in Rust due to the ownership model, but some //! creative coding and assumptions can get us pretty far. @@ -28,8 +28,8 @@ //! # Hello World //! //! ```rust,no_run -//! use cacao::macos::app::{App, AppDelegate}; -//! use cacao::macos::window::Window; +//! use cacao::appkit::app::{App, AppDelegate}; +//! use cacao::appkit::window::Window; //! //! #[derive(Default)] //! struct BasicApp { @@ -59,9 +59,8 @@ //! Note that, in order for this framework to be useful, you must always elect one of the following //! features: //! -//! - `macos`: Implements macOS-specific APIs (menus, toolbars, windowing, and so on). -//! - `ios`: Implements iOS-specific APIs (scenes, navigation controllers, and so on). -//! - `tvos`: Implements tvOS-specific APIs. Currently barely implemented. +//! - `appkit`: Implements appkit-specific APIs (menus, toolbars, windowing, and so on). +//! - `uikit`: Implements uikit-specific APIs (scenes, navigation controllers, and so on). //! //! The rest of the features in this framework attempt to expose a common API across the three //! aforementioned feature platforms; if you need something else, you can often implement it @@ -72,6 +71,8 @@ //! //! The following are a list of [Cargo features][cargo-features] that can be enabled or disabled. //! +//! - `autolayout`: Enables the use of AutoLayout across all widget types. This is a default +//! feature, but is gated to enable platforms that might shim AppKit without AutoLayout support. //! - `cloudkit`: Links `CloudKit.framework` and provides some wrappers around CloudKit //! functionality. Currently not feature complete. //! - `color_fallbacks`: Provides fallback colors for older systems where `systemColor` types don't @@ -79,13 +80,13 @@ //! - `quicklook`: Links `QuickLook.framework` and offers methods for generating preview images for //! files. //! - `user-notifications`: Links `UserNotifications.framework` and provides functionality for -//! emitting notifications on macOS and iOS. Note that this _requires_ your application be +//! emitting notifications on appkit and uikit. Note that this _requires_ your application be //! code-signed, and will not work without it. //! - `webview`: Links `WebKit.framework` and provides a `WebView` control backed by `WKWebView`. -//! This feature is not supported on tvOS, as the platform has no webview control. +//! This feature will not be supported on tvOS, as the platform has no webview control. //! - `webview-downloading-macos`: Enables downloading files from the `WebView` via a private //! interface. This is not an App-Store-safe feature, so be aware of that before enabling. This -//! feature is not supported on iOS (a user would handle downloads very differently) or tvOS +//! feature is not supported on uikit (a user would handle downloads very differently) or tvOS //! (there's no web browser there at all). //! //! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section @@ -96,15 +97,20 @@ pub use objc; pub use url; pub use lazy_static; -#[cfg(target_os = "macos")] -#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] -pub mod macos; +//#[cfg(all(feature = "appkit", feature = "uikit", not(feature = "doc_cfg")))] +//compile_error!("The \"appkit\" and \"uikit\" features cannot be enabled together. Pick one. :)"); -#[cfg(target_os = "ios")] -#[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] -pub mod ios; +#[cfg(feature = "appkit")] +#[cfg_attr(docsrs, doc(cfg(feature = "appkit")))] +pub mod appkit; -#[cfg(target_os = "macos")] +//pub mod bundle; + +#[cfg(feature = "uikit")] +#[cfg_attr(docsrs, doc(cfg(feature = "uikit")))] +pub mod uikit; + +#[cfg(feature = "appkit")] pub mod button; #[cfg(any(feature = "cloudkit", doc))] @@ -113,50 +119,59 @@ pub mod cloudkit; pub mod color; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] +pub mod control; + +#[cfg(feature = "appkit")] pub mod dragdrop; pub mod error; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod events; pub mod defaults; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod filesystem; pub mod foundation; pub mod geometry; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod image; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod input; +pub(crate) mod invoker; + +pub mod keys; pub mod layer; -pub(crate) mod invoker; pub mod layout; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod listview; pub mod networking; +pub(crate) mod objc_access; pub mod notification_center; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod pasteboard; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod progress; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod scrollview; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] pub mod switch; -#[cfg(target_os = "macos")] +#[cfg(feature = "appkit")] +pub mod select; + +#[cfg(feature = "appkit")] pub mod text; #[cfg(feature = "quicklook")] diff --git a/src/listview/macos.rs b/src/listview/appkit.rs similarity index 88% rename from src/listview/macos.rs rename to src/listview/appkit.rs index a6c6010..8ed658e 100644 --- a/src/listview/macos.rs +++ b/src/listview/appkit.rs @@ -14,7 +14,7 @@ use objc::runtime::{Class, Object, Sel, BOOL}; use objc::{class, sel, sel_impl, msg_send}; use objc_id::Id; -use crate::macos::menu::{Menu, MenuItem}; +use crate::appkit::menu::{Menu, MenuItem}; use crate::foundation::{load_or_register_class, id, nil, YES, NO, NSArray, NSInteger, NSUInteger}; use crate::dragdrop::DragInfo; use crate::listview::{ @@ -37,9 +37,16 @@ extern fn view_for_column( this: &Object, _: Sel, _table_view: id, - _: id, + _table_column: id, item: NSInteger ) -> id { + /*use core_graphics::geometry::CGRect; + unsafe { + //let superview: id = msg_send![table_view, superview]; + let frame: CGRect = msg_send![table_view, frame]; + let _: () = msg_send![table_column, setWidth:frame.size.width]; + }*/ + let view = load::(this, LISTVIEW_DELEGATE_PTR); let item = view.item_for(item as usize); @@ -79,7 +86,7 @@ extern fn menu_needs_update( let _ = Menu::append(menu, items); } -/// NSTableView requires listening to an observer to detect row selection changes, but that is... +/*/// NSTableView requires listening to an observer to detect row selection changes, but that is... /// even clunkier than what we do in this framework. /// /// The other less obvious way is to subclass and override the `shouldSelectRow:` method; here, we @@ -94,6 +101,24 @@ extern fn select_row( let view = load::(this, LISTVIEW_DELEGATE_PTR); view.item_selected(item as usize); YES +}*/ + +extern fn selection_did_change( + this: &Object, + _: Sel, + notification: id +) { + let selected_row: NSInteger = unsafe { + let tableview: id = msg_send![notification, object]; + msg_send![tableview, selectedRow] + }; + + let view = load::(this, LISTVIEW_DELEGATE_PTR); + if selected_row == -1 { + view.item_selected(None); + } else { + view.item_selected(Some(selected_row as usize)); + } } extern fn row_actions_for_row( @@ -203,7 +228,7 @@ pub(crate) fn register_listview_class_with_delegate(instanc decl.add_method(sel!(numberOfRowsInTableView:), number_of_items:: as extern fn(&Object, _, id) -> NSInteger); decl.add_method(sel!(tableView:willDisplayCell:forTableColumn:row:), will_display_cell:: as extern fn(&Object, _, id, id, id, NSInteger)); decl.add_method(sel!(tableView:viewForTableColumn:row:), view_for_column:: as extern fn(&Object, _, id, id, NSInteger) -> id); - decl.add_method(sel!(tableView:shouldSelectRow:), select_row:: as extern fn(&Object, _, id, NSInteger) -> BOOL); + decl.add_method(sel!(tableViewSelectionDidChange:), selection_did_change:: as extern fn(&Object, _, id)); decl.add_method(sel!(tableView:rowActionsForRow:edge:), row_actions_for_row:: as extern fn(&Object, _, id, NSInteger, NSInteger) -> id); // A slot for some menu handling; we just let it be done here for now rather than do the diff --git a/src/listview/mod.rs b/src/listview/mod.rs index 16fa1d6..75b8b26 100644 --- a/src/listview/mod.rs +++ b/src/listview/mod.rs @@ -50,26 +50,31 @@ use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString, NSInteger, NSUInteger}; use crate::color::Color; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::layout::Layout; + +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; + +use crate::objc_access::ObjcAccess; use crate::scrollview::ScrollView; use crate::utils::{os, CellFactory, CGSize}; use crate::utils::properties::{ObjcProperty, PropertyNullable}; -use crate::view::ViewDelegate; +use crate::view::{ViewAnimatorProxy, ViewDelegate}; -#[cfg(target_os = "macos")] -use crate::macos::menu::MenuItem; +#[cfg(feature = "appkit")] +use crate::appkit::menu::MenuItem; -#[cfg(target_os = "macos")] -mod macos; +#[cfg(feature = "appkit")] +mod appkit; -#[cfg(target_os = "macos")] -use macos::{register_listview_class, register_listview_class_with_delegate}; +#[cfg(feature = "appkit")] +use appkit::{register_listview_class, register_listview_class_with_delegate}; -#[cfg(target_os = "ios")] -mod ios; +//#[cfg(target_os = "ios")] +//mod ios; -#[cfg(target_os = "ios")] -use ios::{register_view_class, register_view_class_with_delegate}; +//#[cfg(target_os = "ios")] +//use ios::{register_view_class, register_view_class_with_delegate}; mod enums; pub use enums::{RowAnimation, RowEdge}; @@ -94,11 +99,12 @@ use std::cell::RefCell; /// A helper method for instantiating view classes and applying default settings to them. fn common_init(class: *const Class) -> id { unsafe { + // Note: we do *not* enable AutoLayout here as we're by default placing this in a scroll + // view, and we want it to just do its thing. let tableview: id = msg_send![class, new]; - let _: () = msg_send![tableview, setTranslatesAutoresizingMaskIntoConstraints:NO]; // Let's... make NSTableView into UITableView-ish. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { // @TODO: Clean this up in a dealloc method. let menu: id = msg_send![class!(NSMenu), new]; @@ -137,42 +143,56 @@ pub struct ListView { /// A pointer to the Objective-C runtime view controller. pub objc: ObjcProperty, - /// On macOS, we need to manage the NSScrollView ourselves. It's a bit + /// An object that supports limited animations. Can be cloned into animation closures. + pub animator: ViewAnimatorProxy, + + /// In AppKit, we need to manage the NSScrollView ourselves. It's a bit /// more old school like that... - #[cfg(target_os = "macos")] + /// + /// In iOS, this is a pointer to the UITableView-owned UIScrollView. pub scrollview: ScrollView, /// A pointer to the delegate for this view. pub delegate: 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 } @@ -188,7 +208,7 @@ impl ListView { let class = register_listview_class(); let view = common_init(class); - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let scrollview = { let sview = ScrollView::new(); @@ -199,34 +219,59 @@ impl ListView { sview }; - // For macOS, we need to use the NSScrollView anchor points, not the NSTableView. + // For AppKit, we need to use the NSScrollView anchor points, not the NSTableView. // @TODO: Fix this with proper mutable access. - #[cfg(target_os = "macos")] + #[cfg(all(feature = "appkit", feature = "autolayout"))] let anchor_view: id = scrollview.objc.get(|obj| unsafe { msg_send![obj, self] }); - #[cfg(target_os = "ios")] - let anchor_view: id = view; + //#[cfg(all(feature = "uikit", feature = "autolayout"))] + //let anchor_view: id = view; ListView { cell_factory: CellFactory::new(), menu: PropertyNullable::default(), delegate: None, + + #[cfg(feature = "autolayout")] top: LayoutAnchorY::top(anchor_view), + + #[cfg(feature = "autolayout")] left: LayoutAnchorX::left(anchor_view), + + #[cfg(feature = "autolayout")] leading: LayoutAnchorX::leading(anchor_view), + + #[cfg(feature = "autolayout")] right: LayoutAnchorX::right(anchor_view), + + #[cfg(feature = "autolayout")] trailing: LayoutAnchorX::trailing(anchor_view), + + #[cfg(feature = "autolayout")] bottom: LayoutAnchorY::bottom(anchor_view), + + #[cfg(feature = "autolayout")] width: LayoutAnchorDimension::width(anchor_view), + + #[cfg(feature = "autolayout")] height: LayoutAnchorDimension::height(anchor_view), + + #[cfg(feature = "autolayout")] center_x: LayoutAnchorX::center(anchor_view), + + #[cfg(feature = "autolayout")] center_y: LayoutAnchorY::center(anchor_view), + + // Note that AppKit needs this to be the ScrollView! + // @TODO: Figure out if there's a use case for exposing the inner tableview animator + // property... + animator: ViewAnimatorProxy::new(anchor_view), + objc: ObjcProperty::retain(view), - #[cfg(target_os = "macos")] - scrollview: scrollview + scrollview } } } @@ -241,15 +286,13 @@ impl ListView where T: ListViewDelegate + 'static { let cell = CellFactory::new(); unsafe { - //let view: id = msg_send![register_view_class_with_delegate::(), new]; - //let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; let delegate_ptr: *const T = &*delegate; (&mut *view).set_ivar(LISTVIEW_DELEGATE_PTR, delegate_ptr as usize); let _: () = msg_send![view, setDelegate:view]; let _: () = msg_send![view, setDataSource:view]; }; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let scrollview = { let sview = ScrollView::new(); @@ -260,33 +303,53 @@ impl ListView where T: ListViewDelegate + 'static { sview }; - // For macOS, we need to use the NSScrollView anchor points, not the NSTableView. - #[cfg(target_os = "macos")] + // For AppKit, we need to use the NSScrollView anchor points, not the NSTableView. + #[cfg(all(feature = "appkit", feature = "autolayout"))] let anchor_view: id = scrollview.objc.get(|obj| unsafe { msg_send![obj, self] }); - #[cfg(target_os = "ios")] - let anchor_view = view; + //#[cfg(feature = "uikit")] + //let anchor_view = view; let mut view = ListView { cell_factory: cell, menu: PropertyNullable::default(), delegate: None, - top: LayoutAnchorY::top(anchor_view), - left: LayoutAnchorX::left(anchor_view), - leading: LayoutAnchorX::leading(anchor_view), - right: LayoutAnchorX::right(anchor_view), - trailing: LayoutAnchorX::trailing(anchor_view), - bottom: LayoutAnchorY::bottom(anchor_view), - width: LayoutAnchorDimension::width(anchor_view), - height: LayoutAnchorDimension::height(anchor_view), - center_x: LayoutAnchorX::center(anchor_view), - center_y: LayoutAnchorY::center(anchor_view), objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(anchor_view), + + #[cfg(feature = "autolayout")] + top: LayoutAnchorY::top(anchor_view), - #[cfg(target_os = "macos")] - scrollview: scrollview + #[cfg(feature = "autolayout")] + left: LayoutAnchorX::left(anchor_view), + + #[cfg(feature = "autolayout")] + leading: LayoutAnchorX::leading(anchor_view), + + #[cfg(feature = "autolayout")] + right: LayoutAnchorX::right(anchor_view), + + #[cfg(feature = "autolayout")] + trailing: LayoutAnchorX::trailing(anchor_view), + + #[cfg(feature = "autolayout")] + bottom: LayoutAnchorY::bottom(anchor_view), + + #[cfg(feature = "autolayout")] + width: LayoutAnchorDimension::width(anchor_view), + + #[cfg(feature = "autolayout")] + height: LayoutAnchorDimension::height(anchor_view), + + #[cfg(feature = "autolayout")] + center_x: LayoutAnchorX::center(anchor_view), + + #[cfg(feature = "autolayout")] + center_y: LayoutAnchorY::center(anchor_view), + + scrollview }; (&mut delegate).did_load(view.clone_as_handle()); @@ -300,24 +363,44 @@ impl ListView { /// callback pointer. We use this in calling `did_load()` - implementing delegates get a way to /// reference, customize and use the view but without the trickery of holding pieces of the /// delegate - the `View` is the only true holder of those. - pub(crate) fn clone_as_handle(&self) -> ListView { + pub fn clone_as_handle(&self) -> ListView { ListView { cell_factory: CellFactory::new(), menu: self.menu.clone(), delegate: None, - top: self.top.clone(), - leading: self.leading.clone(), - left: self.left.clone(), - trailing: self.trailing.clone(), - right: self.right.clone(), - bottom: self.bottom.clone(), - width: self.width.clone(), - height: self.height.clone(), - center_x: self.center_x.clone(), - center_y: self.center_y.clone(), objc: self.objc.clone(), + animator: self.animator.clone(), + + #[cfg(feature = "autolayout")] + top: self.top.clone(), + + #[cfg(feature = "autolayout")] + leading: self.leading.clone(), + + #[cfg(feature = "autolayout")] + left: self.left.clone(), + + #[cfg(feature = "autolayout")] + trailing: self.trailing.clone(), + + #[cfg(feature = "autolayout")] + right: self.right.clone(), + + #[cfg(feature = "autolayout")] + bottom: self.bottom.clone(), + + #[cfg(feature = "autolayout")] + width: self.width.clone(), + + #[cfg(feature = "autolayout")] + height: self.height.clone(), + + #[cfg(feature = "autolayout")] + center_x: self.center_x.clone(), + + #[cfg(feature = "autolayout")] + center_y: self.center_y.clone(), - #[cfg(target_os = "macos")] scrollview: self.scrollview.clone_as_handle() } } @@ -334,7 +417,7 @@ impl ListView { /// Dequeue a reusable cell. If one is not in the queue, will create and cache one for reuse. pub fn dequeue(&self, identifier: &'static str) -> ListViewRow { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { let key = NSString::new(identifier); let cell: id = self.objc.get(|obj| unsafe { @@ -364,8 +447,11 @@ impl ListView { /// Sets the style for the underlying NSTableView. This property is only supported on macOS /// 11.0+, and will always be `FullWidth` on anything older. - #[cfg(target_os = "macos")] + /// + /// On non-macOS platforms, this method is a noop. + #[cfg(feature = "appkit")] pub fn set_style(&self, style: crate::foundation::NSInteger) { + #[cfg(target_os = "macos")] if os::is_minimum_version(11) { self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setStyle:style]; @@ -375,10 +461,10 @@ impl ListView { /// Set whether this control can appear with no row selected. /// - /// This defaults to `true`, but some macOS pieces (e.g, a sidebar) may want this set to + /// This defaults to `true`, but some AppKit pieces (e.g, a sidebar) may want this set to /// `false`. This can be particularly useful when implementing a Source List style sidebar /// view for navigation purposes. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] pub fn set_allows_empty_selection(&self, allows: bool) { self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setAllowsEmptySelection:match allows { @@ -413,6 +499,34 @@ impl ListView { } } + /// This hack exists to avoid a bug with how Rust's model isn't really friendly with more + /// old-school GUI models. The tl;dr is that we unfortunately have to cheat a bit to gracefully + /// handle two conditions. + /// + /// The gist of it is that there are two situations (`perform_batch_updates` and `insert_rows`) + /// where we call over to the list view to, well, perform updates. This causes the internal + /// machinery of AppKit to call to the delegate, and the delegate then - rightfully - calls to + /// dequeue a cell. + /// + /// The problem is then that dequeue'ing a cell requires borrowing the underlying cell handler, + /// per Rust's model. We haven't been able to drop our existing lock though! Thus it winds up + /// panic'ing and all hell breaks loose. + /// + /// For now, we just drop to Objective-C and message pass directly to avoid a + /// double-locking-attempt on the Rust side of things. This is explicitly not ideal, and if + /// you're reading this and rightfully going "WTF?", I encourage you to contribute a solution + /// if you can come up with one. + /// + /// In practice, this hack isn't that bad - at least, no worse than existing Objective-C code. + /// The behavior is relatively well understood and documented in the above paragraph, so I'm + /// comfortable with the hack for now. + /// + /// To be ultra-clear: the hack is that we don't `borrow_mut` before sending a message. It just + /// feels dirty, hence the novel. ;P + fn hack_avoid_dequeue_loop(&self, handler: F) { + self.objc.get(handler); + } + /// This method should be used when inserting or removing multiple rows at once. Under the /// hood, it batches the changes and tries to ensure things are done properly. The provided /// `ListView` for the handler is your `ListView`, and you can call `insert_rows`, @@ -425,7 +539,7 @@ impl ListView { /// ``` pub fn perform_batch_updates(&self, update: F) { // Note that we need to thread the `with_mut` calls carefully, to avoid deadlocking. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { self.objc.get(|obj| unsafe { let _: () = msg_send![obj, beginUpdates]; @@ -434,14 +548,9 @@ impl ListView { let handle = self.clone_as_handle(); update(handle); - // This is cheating, but there's no good way around it at the moment. If we (mutably) lock in - // Rust here, firing this call will loop back around into `dequeue`, which will then - // hit a double lock. - // - // Personally, I can live with this - `endUpdates` is effectively just flushing the - // already added updates, so with this small hack here we're able to keep the mutable - // borrow structure everywhere else, which feels "correct". - self.objc.get(|obj| unsafe { + // This is done for a very explicit reason; see the comments on the method itself for + // an explanation. + self.hack_avoid_dequeue_loop(|obj| unsafe { let _: () = msg_send![obj, endUpdates]; }); } @@ -453,7 +562,7 @@ impl ListView { /// rows at once, you should also run this inside a `perform_batch_updates` call, as that will /// optimize things accordingly. pub fn insert_rows(&self, indexes: &[usize], animation: RowAnimation) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -468,7 +577,9 @@ impl ListView { // has also retained it. let x = ShareId::from_ptr(index_set); - self.objc.with_mut(|obj| { + // This is done for a very explicit reason; see the comments on the method itself for + // an explanation. + self.hack_avoid_dequeue_loop(|obj| { let _: () = msg_send![obj, insertRowsAtIndexes:&*x withAnimation:animation_options]; }); } @@ -476,7 +587,7 @@ impl ListView { /// Reload the rows at the specified indexes. pub fn reload_rows(&self, indexes: &[usize]) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -503,7 +614,7 @@ impl ListView { /// rows at once, you should also run this inside a `perform_batch_updates` call, as that will /// optimize things accordingly. pub fn remove_rows(&self, indexes: &[usize], animations: RowAnimation) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] unsafe { let index_set: id = msg_send![class!(NSMutableIndexSet), new]; @@ -537,7 +648,7 @@ impl ListView { /// /// It can make some scrolling situations much smoother. pub fn set_uses_automatic_row_heights(&self, uses: bool) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setUsesAutomaticRowHeights:match uses { true => YES, @@ -546,11 +657,11 @@ impl ListView { }); } - /// On macOS, this will instruct the underlying NSTableView to alternate + /// In AppKit, this will instruct the underlying NSTableView to alternate /// background colors automatically. If you set this, you possibly want /// to hard-set a row height as well. pub fn set_uses_alternating_backgrounds(&self, uses: bool) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setUsesAlternatingRowBackgroundColors:match uses { true => YES, @@ -561,7 +672,7 @@ impl ListView { /// End actions for a row. API subject to change. pub fn set_row_actions_visible(&self, visible: bool) { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.objc.with_mut(|obj| unsafe { let _: () = msg_send![obj, setRowActionsVisible:match visible { true => YES, @@ -570,6 +681,15 @@ impl ListView { }); } + /// Makes this table view the first responder. + #[cfg(feature = "appkit")] + pub fn make_first_responder(&self) { + self.objc.with_mut(|obj| unsafe { + let window: id = msg_send![&*obj, window]; + let _: () = msg_send![window, makeFirstResponder:&*obj]; + }); + } + /// Reloads the underlying ListView. This is more expensive than handling insert/reload/remove /// calls yourself, but often easier to implement. /// @@ -586,7 +706,7 @@ impl ListView { self.objc.get(|obj| unsafe { msg_send![obj, selectedRow] }) } - /// Returns the currently clicked row. This is macOS-specific, and is generally used in context + /// Returns the currently clicked row. This is AppKit-specific, and is generally used in context /// menu generation to determine what item the context menu should be for. If the clicked area /// is not an actual row, this will return `-1`. /// @@ -612,25 +732,27 @@ impl ListView { } } -impl Layout for ListView { - fn with_backing_node(&self, handler: F) { - // On macOS, we need to provide the scrollview for layout purposes - iOS and tvOS will know +impl ObjcAccess for ListView { + fn with_backing_obj_mut(&self, handler: F) { + // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // what to do normally. - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.scrollview.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { - // On macOS, we need to provide the scrollview for layout purposes - iOS and tvOS will know + fn get_from_backing_obj R, R>(&self, handler: F) -> R { + // In AppKit, we need to provide the scrollview for layout purposes - iOS and tvOS will know // what to do normally. // // @TODO: Review this, as property access isn't really used in the same place as layout // stuff... hmm... - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] self.scrollview.objc.get(handler) } } +impl Layout for ListView {} + impl Drop for ListView { /// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if diff --git a/src/listview/row/macos.rs b/src/listview/row/appkit.rs similarity index 100% rename from src/listview/row/macos.rs rename to src/listview/row/appkit.rs diff --git a/src/listview/row/mod.rs b/src/listview/row/mod.rs index 935ed58..2cb8204 100644 --- a/src/listview/row/mod.rs +++ b/src/listview/row/mod.rs @@ -51,32 +51,38 @@ use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; use crate::layer::Layer; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; -use crate::view::ViewDelegate; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; +use crate::view::{ViewAnimatorProxy, ViewDelegate}; use crate::utils::properties::ObjcProperty; -#[cfg(target_os = "macos")] -mod macos; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension, SafeAreaLayoutGuide}; -#[cfg(target_os = "macos")] -use macos::{register_listview_row_class, register_listview_row_class_with_delegate}; +#[cfg(feature = "appkit")] +mod appkit; -#[cfg(target_os = "ios")] -mod ios; +#[cfg(feature = "appkit")] +use appkit::{register_listview_row_class, register_listview_row_class_with_delegate}; -#[cfg(target_os = "ios")] -use ios::{register_listview_row_view_class, register_listview_row_class_with_delegate}; +//#[cfg(feature = "uikit")] +//mod ios; -pub(crate) static BACKGROUND_COLOR: &str = "alchemyBackgroundColor"; -pub(crate) static LISTVIEW_ROW_DELEGATE_PTR: &str = "rstListViewRowDelegatePtr"; +//#[cfg(feature = "uikit")] +//use ios::{register_listview_row_view_class, register_listview_row_class_with_delegate}; + +pub(crate) static BACKGROUND_COLOR: &str = "cacaoBackgroundColor"; +pub(crate) static LISTVIEW_ROW_DELEGATE_PTR: &str = "cacaoListViewRowDelegatePtr"; /// A helper method for instantiating view classes and applying default settings to them. fn allocate_view(registration_fn: fn() -> *const Class) -> id { unsafe { let view: id = msg_send![registration_fn(), new]; + + #[cfg(feature = "autolayout")] let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let _: () = msg_send![view, setWantsLayer:YES]; view @@ -88,40 +94,57 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id { /// side anyway. #[derive(Debug)] pub struct ListViewRow { + /// An object that supports limited animations. Can be cloned into animation closures. + pub animator: ViewAnimatorProxy, + /// A pointer to the Objective-C runtime view controller. pub objc: ObjcProperty, /// A pointer to the delegate for this view. pub delegate: Option>, + + /// A safe layout guide property. + #[cfg(feature = "autolayout")] + pub safe_layout_guide: SafeAreaLayoutGuide, /// A pointer to the Objective-C runtime top layout constraint. + #[cfg(feature = "autolayout")] 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 } @@ -138,17 +161,41 @@ impl ListViewRow { ListViewRow { delegate: None, - top: LayoutAnchorY::top(view), - left: LayoutAnchorX::left(view), - leading: LayoutAnchorX::leading(view), - right: LayoutAnchorX::right(view), - trailing: LayoutAnchorX::trailing(view), - bottom: LayoutAnchorY::bottom(view), - width: LayoutAnchorDimension::width(view), - height: LayoutAnchorDimension::height(view), - center_x: LayoutAnchorX::center(view), - center_y: LayoutAnchorY::center(view), objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), + + #[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), } } } @@ -175,17 +222,41 @@ impl ListViewRow where T: ViewDelegate + 'static { let view = ListViewRow { delegate: Some(delegate), - top: LayoutAnchorY::top(view), - left: LayoutAnchorX::left(view), - leading: LayoutAnchorX::leading(view), - right: LayoutAnchorX::right(view), - trailing: LayoutAnchorX::trailing(view), - bottom: LayoutAnchorY::bottom(view), - width: LayoutAnchorDimension::width(view), - height: LayoutAnchorDimension::height(view), - center_x: LayoutAnchorX::center(view), - center_y: LayoutAnchorY::center(view), objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), + + #[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), }; view @@ -201,25 +272,47 @@ impl ListViewRow where T: ViewDelegate + 'static { pub fn with_boxed(mut delegate: Box) -> ListViewRow { let view = allocate_view(register_listview_row_class_with_delegate::); unsafe { - //let view: id = msg_send![register_view_class_with_delegate::(), new]; - //let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; let ptr: *const T = &*delegate; (&mut *view).set_ivar(LISTVIEW_ROW_DELEGATE_PTR, ptr as usize); }; let mut view = ListViewRow { delegate: None, - top: LayoutAnchorY::top(view), - left: LayoutAnchorX::left(view), - leading: LayoutAnchorX::leading(view), - right: LayoutAnchorX::right(view), - trailing: LayoutAnchorX::trailing(view), - bottom: LayoutAnchorY::bottom(view), - width: LayoutAnchorDimension::width(view), - height: LayoutAnchorDimension::height(view), - center_x: LayoutAnchorX::center(view), - center_y: LayoutAnchorY::center(view), objc: ObjcProperty::retain(view), + animator: ViewAnimatorProxy::new(view), + + #[cfg(feature = "autolayout")] + safe_layout_guide: SafeAreaLayoutGuide::new(view), + + #[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), }; (&mut delegate).did_load(view.clone_as_handle()); @@ -237,17 +330,41 @@ impl ListViewRow where T: ViewDelegate + 'static { ListViewRow { delegate: None, + objc: self.objc.clone(), + animator: self.animator.clone(), + + #[cfg(feature = "autolayout")] + safe_layout_guide: self.safe_layout_guide.clone(), + + #[cfg(feature = "autolayout")] top: self.top.clone(), + + #[cfg(feature = "autolayout")] leading: self.leading.clone(), + + #[cfg(feature = "autolayout")] left: self.left.clone(), + + #[cfg(feature = "autolayout")] trailing: self.trailing.clone(), + + #[cfg(feature = "autolayout")] right: self.right.clone(), + + #[cfg(feature = "autolayout")] bottom: self.bottom.clone(), + + #[cfg(feature = "autolayout")] width: self.width.clone(), + + #[cfg(feature = "autolayout")] height: self.height.clone(), + + #[cfg(feature = "autolayout")] center_x: self.center_x.clone(), + + #[cfg(feature = "autolayout")] center_y: self.center_y.clone(), - objc: self.objc.clone() } } } @@ -261,18 +378,42 @@ impl ListViewRow { crate::view::View { delegate: None, is_handle: true, - layer: Layer::new(), + layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row. + objc: self.objc.clone(), + animator: self.animator.clone(), + + #[cfg(feature = "autolayout")] + safe_layout_guide: self.safe_layout_guide.clone(), + + #[cfg(feature = "autolayout")] top: self.top.clone(), + + #[cfg(feature = "autolayout")] leading: self.leading.clone(), + + #[cfg(feature = "autolayout")] left: self.left.clone(), + + #[cfg(feature = "autolayout")] trailing: self.trailing.clone(), + + #[cfg(feature = "autolayout")] right: self.right.clone(), + + #[cfg(feature = "autolayout")] bottom: self.bottom.clone(), + + #[cfg(feature = "autolayout")] width: self.width.clone(), + + #[cfg(feature = "autolayout")] height: self.height.clone(), + + #[cfg(feature = "autolayout")] center_x: self.center_x.clone(), + + #[cfg(feature = "autolayout")] center_y: self.center_y.clone(), - objc: self.objc.clone() } } @@ -295,16 +436,18 @@ impl ListViewRow { } } -impl Layout for ListViewRow { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ListViewRow { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ListViewRow {} + impl Drop for ListViewRow { fn drop(&mut self) { } diff --git a/src/listview/row/ios.rs b/src/listview/row/uikit.rs similarity index 100% rename from src/listview/row/ios.rs rename to src/listview/row/uikit.rs diff --git a/src/listview/traits.rs b/src/listview/traits.rs index 1590ffa..5ad01c1 100644 --- a/src/listview/traits.rs +++ b/src/listview/traits.rs @@ -1,6 +1,6 @@ //! Various traits used for Views. -use crate::macos::menu::MenuItem; +use crate::appkit::menu::MenuItem; use crate::dragdrop::{DragInfo, DragOperation}; use crate::listview::{ListView, ListViewRow, RowAction, RowEdge}; use crate::layout::Layout; @@ -36,8 +36,9 @@ pub trait ListViewDelegate { /// had time to sit down and figure them out properly yet. fn item_for(&self, row: usize) -> ListViewRow; - /// Called when an item has been selected (clicked/tapped on). - fn item_selected(&self, row: usize) {} + /// Called when an item has been selected (clicked/tapped on). If the selection was cleared, + /// then this will be called with `None`. + fn item_selected(&self, row: Option) {} /// Called when the menu for the tableview is about to be shown. You can update the menu here /// depending on, say, what the user has context-clicked on. You should avoid any expensive diff --git a/src/objc_access.rs b/src/objc_access.rs new file mode 100644 index 0000000..a24055e --- /dev/null +++ b/src/objc_access.rs @@ -0,0 +1,23 @@ +//! Implements a parent trait for the various sub-traits we use throughout Cacao. The methods +//! defined on here provide access handlers for common properties that the sub-traits need to +//! enable modifying. + +use objc::runtime::Object; + +use crate::foundation::id; + +/// Types that implement this should provide access to their underlying root node type (e.g, the +/// view or control). Traits that have this as their super-trait can rely on this to ensure access +/// without needing to derive or do extra work elsewhere. +#[allow(unused_variables)] +pub trait ObjcAccess { + /// Used for mutably interacting with the underlying Objective-C instance. + /// Setters should use this. + fn with_backing_obj_mut(&self, handler: F); + + /// Used for checking backing properties of the underlying Objective-C instance, without + /// needing a mutable borrow. + /// + /// Getters should use this. + fn get_from_backing_obj R, R>(&self, handler: F) -> R; +} diff --git a/src/pasteboard/mod.rs b/src/pasteboard/mod.rs index 3b6ffa9..56f87cb 100644 --- a/src/pasteboard/mod.rs +++ b/src/pasteboard/mod.rs @@ -20,7 +20,7 @@ use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; use url::Url; -use crate::foundation::{id, nil, NSString, NSArray}; +use crate::foundation::{id, nil, NSString, NSArray, NSURL}; use crate::error::Error; mod types; @@ -93,7 +93,7 @@ impl Pasteboard { /// _Note that this method returns a list of `Url` entities, in an attempt to be closer to how /// Cocoa & co operate. This method may go away in the future if it's determined that people /// wind up just using `get_file_paths()`._ - pub fn get_file_urls(&self) -> Result, Box> { + pub fn get_file_urls(&self) -> Result, Box> { unsafe { let class: id = msg_send![class!(NSURL), class]; let classes = NSArray::new(&[class]); @@ -113,40 +113,7 @@ impl Pasteboard { } let urls = NSArray::retain(contents).map(|url| { - let path = NSString::retain(msg_send![url, path]); - Url::parse(&format!("file://{}", path.to_str())) - }).into_iter().filter_map(|r| r.ok()).collect(); - - Ok(urls) - } - } - - /// Looks inside the pasteboard contents and extracts what FileURLs are there, if any. - /// - /// Note that this method operates on file paths, as opposed to URLs, and returns a list of - /// results in a format more Rust-y. - pub fn get_file_paths(&self) -> Result, Box> { - unsafe { - let class: id = msg_send![class!(NSURL), class]; - let classes = NSArray::new(&[class]); - let contents: id = msg_send![&*self.0, readObjectsForClasses:classes options:nil]; - - // This can happen if the Pasteboard server has an error in returning items. - // In our case, we'll bubble up an error by checking the pasteboard. - if contents == nil { - // This error is not necessarily "correct", but in the event of an error in - // Pasteboard server retrieval I'm not sure where to check... and this stuff is - // kinda ancient and has conflicting docs in places. ;P - return Err(Box::new(Error { - code: 666, - domain: "com.cacao-rs.pasteboard".to_string(), - description: "Pasteboard server returned no data.".to_string() - })); - } - - let urls = NSArray::retain(contents).map(|url| { - let path = NSString::retain(msg_send![url, path]).to_str().to_string(); - PathBuf::from(path) + NSURL::retain(url) }).into_iter().collect(); Ok(urls) diff --git a/src/progress/ios.rs b/src/progress/ios.rs deleted file mode 100644 index 1e835ed..0000000 --- a/src/progress/ios.rs +++ /dev/null @@ -1,45 +0,0 @@ -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, YES, NO, NSUInteger}; -use crate::dragdrop::DragInfo; -use crate::view::{VIEW_DELEGATE_PTR, ViewDelegate}; -use crate::utils::load; - -/// Injects an `NSView` 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!(UIView); - let mut decl = ClassDecl::new("RSTView", 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_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("RSTViewWithDelegate", superclass).unwrap(); - decl.add_ivar::(VIEW_DELEGATE_PTR); - VIEW_CLASS = decl.register(); - }); - - unsafe { - VIEW_CLASS - } -} diff --git a/src/progress/mod.rs b/src/progress/mod.rs index 83ef309..9b76559 100644 --- a/src/progress/mod.rs +++ b/src/progress/mod.rs @@ -1,7 +1,7 @@ //! A progress indicator widget. //! -//! This control wraps `NSProgressIndicator` on macOS, and -//! `UIProgressView+UIActivityIndicatorView` on iOS and tvOS. It operates in two modes: determinate +//! This control wraps `NSProgressIndicator` in AppKit, and +//! `UIProgressView+UIActivityIndicatorView` in iOS/tvOS. It operates in two modes: determinate //! (where you have a fixed start and end) and indeterminate (infinite; it will go and go until you //! tell it to stop). //! @@ -19,14 +19,12 @@ use objc::{class, msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSUInteger}; use crate::color::Color; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::utils::properties::ObjcProperty; -#[cfg(target_os = "ios")] -mod ios; - -#[cfg(target_os = "ios")] -use ios::register_progress_indicator_class; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; mod enums; pub use enums::ProgressIndicatorStyle; @@ -38,33 +36,43 @@ pub struct ProgressIndicator { pub objc: ObjcProperty, /// 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 } @@ -79,27 +87,49 @@ impl ProgressIndicator { /// need it to stay around. pub fn new() -> Self { let view = unsafe { - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let view: id = msg_send![class!(NSProgressIndicator), new]; + + #[cfg(feature = "autolayout")] let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] let _: () = msg_send![view, setWantsLayer:YES]; view }; ProgressIndicator { + #[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), } } @@ -172,16 +202,18 @@ impl ProgressIndicator { } } -impl Layout for ProgressIndicator { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ProgressIndicator { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ProgressIndicator {} + impl Drop for ProgressIndicator { /// A bit of extra cleanup for delegate callback pointers. /// If the originating `ProgressIndicator` is being diff --git a/src/quicklook/config.rs b/src/quicklook/config.rs index c4dc512..5d2577a 100644 --- a/src/quicklook/config.rs +++ b/src/quicklook/config.rs @@ -83,7 +83,7 @@ impl Default for ThumbnailConfig { // #TODO: Should query the current screen size maybe? 2x is fairly safe // for most moderns Macs right now. - scale: 2., + scale: 1., minimum_dimension: 0., diff --git a/src/quicklook/mod.rs b/src/quicklook/mod.rs index 2866d4f..049d96e 100644 --- a/src/quicklook/mod.rs +++ b/src/quicklook/mod.rs @@ -1,6 +1,6 @@ use std::path::Path; -use objc::runtime::{Object}; +use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::ShareId; @@ -17,13 +17,19 @@ pub use config::{ThumbnailConfig, ThumbnailQuality}; pub struct ThumbnailGenerator(pub ShareId); impl ThumbnailGenerator { + /// Returns the global shared, wrapped, QLThumbnailGenerator. pub fn shared() -> Self { ThumbnailGenerator(unsafe { ShareId::from_ptr(msg_send![class!(QLThumbnailGenerator), sharedGenerator]) }) } - pub fn generate(&self, path: &Path, config: ThumbnailConfig, callback: F) + /// Given a path and config, will generate a preview image, calling back on the provided + /// callback closure. + /// + /// Note that this callback can come back on a separate thread, so react accordingly to get to + /// the main thread if you need to. + pub fn generate_from_path(&self, path: &Path, config: ThumbnailConfig, callback: F) where F: Fn(Result<(Image, ThumbnailQuality), Error>) + Send + Sync + 'static { diff --git a/src/scrollview/macos.rs b/src/scrollview/appkit.rs similarity index 100% rename from src/scrollview/macos.rs rename to src/scrollview/appkit.rs diff --git a/src/scrollview/mod.rs b/src/scrollview/mod.rs index 535609e..a66a977 100644 --- a/src/scrollview/mod.rs +++ b/src/scrollview/mod.rs @@ -47,21 +47,25 @@ use objc::{msg_send, sel, sel_impl}; use crate::foundation::{id, nil, YES, NO, NSArray, NSString}; use crate::color::Color; -use crate::layout::{Layout, LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; use crate::pasteboard::PasteboardType; use crate::utils::properties::ObjcProperty; -#[cfg(target_os = "macos")] -mod macos; +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; -#[cfg(target_os = "macos")] -use macos::{register_scrollview_class, register_scrollview_class_with_delegate}; +#[cfg(feature = "appkit")] +mod appkit; -#[cfg(target_os = "ios")] -mod ios; +#[cfg(feature = "appkit")] +use appkit::{register_scrollview_class, register_scrollview_class_with_delegate}; -#[cfg(target_os = "ios")] -use ios::{register_view_class, register_view_class_with_delegate}; +//#[cfg(feature = "uikit")] +//mod ios; + +//#[cfg(feature = "uikit")] +//use ios::{register_view_class, register_view_class_with_delegate}; mod traits; pub use traits::ScrollViewDelegate; @@ -72,9 +76,11 @@ pub(crate) static SCROLLVIEW_DELEGATE_PTR: &str = "rstScrollViewDelegatePtr"; fn allocate_view(registration_fn: fn() -> *const Class) -> id { unsafe { let view: id = msg_send![registration_fn(), new]; + + #[cfg(feature = "autolayout")] let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO]; - #[cfg(target_os = "macos")] + #[cfg(feature = "appkit")] { let _: () = msg_send![view, setDrawsBackground:NO]; let _: () = msg_send![view, setWantsLayer:YES]; @@ -97,33 +103,43 @@ pub struct ScrollView { pub delegate: 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 } @@ -140,16 +156,37 @@ impl ScrollView { ScrollView { delegate: None, + + #[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), } } @@ -169,16 +206,37 @@ impl ScrollView where T: ScrollViewDelegate + 'static { let mut view = ScrollView { delegate: None, + + #[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), }; @@ -196,16 +254,37 @@ impl ScrollView { pub(crate) fn clone_as_handle(&self) -> ScrollView { ScrollView { delegate: None, + + #[cfg(feature = "autolayout")] top: self.top.clone(), + + #[cfg(feature = "autolayout")] leading: self.leading.clone(), + + #[cfg(feature = "autolayout")] left: self.left.clone(), + + #[cfg(feature = "autolayout")] trailing: self.trailing.clone(), + + #[cfg(feature = "autolayout")] right: self.right.clone(), + + #[cfg(feature = "autolayout")] bottom: self.bottom.clone(), + + #[cfg(feature = "autolayout")] width: self.width.clone(), + + #[cfg(feature = "autolayout")] height: self.height.clone(), + + #[cfg(feature = "autolayout")] center_x: self.center_x.clone(), + + #[cfg(feature = "autolayout")] center_y: self.center_y.clone(), + objc: self.objc.clone() } } @@ -221,16 +300,18 @@ impl ScrollView { } } -impl Layout for ScrollView { - fn with_backing_node(&self, handler: F) { +impl ObjcAccess for ScrollView { + fn with_backing_obj_mut(&self, handler: F) { self.objc.with_mut(handler); } - fn get_from_backing_node R, R>(&self, handler: F) -> R { + fn get_from_backing_obj R, R>(&self, handler: F) -> R { self.objc.get(handler) } } +impl Layout for ScrollView {} + impl Drop for ScrollView { /// A bit of extra cleanup for delegate callback pointers. If the originating `View` is being /// dropped, we do some logic to clean it all up (e.g, we go ahead and check to see if diff --git a/src/select/mod.rs b/src/select/mod.rs new file mode 100644 index 0000000..13ed692 --- /dev/null +++ b/src/select/mod.rs @@ -0,0 +1,272 @@ +//! Implements a Select-style dropdown. By default this uses NSPopupSelect on macOS. + +use std::sync::Once; + +use core_graphics::geometry::CGRect; + +use objc_id::ShareId; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::control::Control; +use crate::foundation::{id, nil, YES, NO, NSString, NSInteger}; +use crate::invoker::TargetActionHandler; +use crate::geometry::Rect; +use crate::layout::Layout; +use crate::objc_access::ObjcAccess; +use crate::utils::properties::ObjcProperty; + +#[cfg(feature = "autolayout")] +use crate::layout::{LayoutAnchorX, LayoutAnchorY, LayoutAnchorDimension}; + +/// Wraps `NSPopUpSelect` on AppKit. Not currently implemented for iOS. +/// +/// Acts like a `