diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..82bd996 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + fmt: + name: Check formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: rustfmt + override: true + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0738c40..ee3b68a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,9 +1,9 @@ # Cacao Architecture -Cacao is a library to interface with AppKit (macOS) or UIKit (iOS/iPadOS/tvOS). It uses the Objective-C runtime to +Cacao is a library to interface with AppKit (macOS) or UIKit (iOS/iPadOS/tvOS). It uses the Objective-C runtime to handle calling into these frameworks. -Said frameworks typically use an Object Oriented style of programming (subclasses, etc), which can be tricky to -handle with the way that Rust works with regards to ownership. Thankfully, AppKit & UIKit often also use a +Said frameworks typically use an Object Oriented style of programming (subclasses, etc), which can be tricky to +handle with the way that Rust works with regards to ownership. Thankfully, AppKit & UIKit often also use a delegate pattern - objects registered to receive callbacks. With some creative assumptions, we can get somewhat close to expected conventions. @@ -208,7 +208,7 @@ impl View { height: LayoutAnchorDimension::height(view), center_x: LayoutAnchorX::center(view), center_y: LayoutAnchorY::center(view), - + layer: Layer::wrap(unsafe { msg_send![view, layer] }), @@ -236,7 +236,7 @@ impl View where T: ViewDelegate + 'static { pub fn with(delegate: T) -> View { let class = register_view_class_with_delegate(&delegate); let mut delegate = Box::new(delegate); - + let view = unsafe { let view: id = msg_send![class, new]; let ptr = Box::into_raw(delegate); @@ -246,7 +246,7 @@ impl View where T: ViewDelegate + 'static { }; let mut view = View::init(view); - (&mut delegate).did_load(view.clone_as_handle()); + (&mut delegate).did_load(view.clone_as_handle()); view.delegate = Some(delegate); view } @@ -360,10 +360,10 @@ pub(crate) fn register_view_class() -> *const Class { let superclass = class!(NSView); let mut decl = ClassDecl::new("RSTView", superclass).unwrap(); - decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL); + decl.add_method(sel!(isFlipped), enforce_normalcy as extern "C" fn(&Object, _) -> BOOL); decl.add_ivar::(BACKGROUND_COLOR); - + VIEW_CLASS = decl.register(); }); @@ -371,7 +371,7 @@ pub(crate) fn register_view_class() -> *const Class { } ``` -This function (called inside `View::new()`) creates one reusable `View` subclass, and returns the type on subsequent calls. We're able to add methods to it (`add_method`) which match +This function (called inside `View::new()`) creates one reusable `View` subclass, and returns the type on subsequent calls. We're able to add methods to it (`add_method`) which match Objective-C method signatures, as well as provision space for variable storage (`add_ivar`). For our _delegate_ types, we need a different class creation method - one that creates a subclass per-unique-type: @@ -384,12 +384,12 @@ pub(crate) fn register_view_class_with_delegate(instance: &T) - decl.add_method( sel!(isFlipped), - enforce_normalcy as extern fn(&Object, _) -> BOOL + enforce_normalcy as extern "C" fn(&Object, _) -> BOOL ); decl.add_method( sel!(draggingEntered:), - dragging_entered:: as extern fn (&mut Object, _, _) -> NSUInteger + dragging_entered:: as extern "C" fn (&mut Object, _, _) -> NSUInteger ); }) } @@ -401,7 +401,7 @@ to the Rust `ViewDelegate` implementation. The methods we're setting up can range from simple to complex - take `isFlipped`: ``` rust -extern fn is_flipped(_: &Object, _: Sel) -> BOOL { +extern "C" fn is_flipped(_: &Object, _: Sel) -> BOOL { return YES; } ``` @@ -409,7 +409,7 @@ extern fn is_flipped(_: &Object, _: Sel) -> BOOL { Here, we just want to tell `NSView` to use top,left as the origin point, so we need to respond `YES` in this subclass method. ``` rust -extern fn dragging_entered(this: &mut Object, _: Sel, info: id) -> NSUInteger { +extern "C" fn dragging_entered(this: &mut Object, _: Sel, info: id) -> NSUInteger { let view = utils::load::(this, VIEW_DELEGATE_PTR); view.dragging_entered(DragInfo { info: unsafe { Id::from_ptr(info) } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ae7e6d..98b2ca2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,5 +99,5 @@ Check out [their README](https://github.com/rust-lang-nursery/rustfmt) for detai ### Notes -This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's +This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's worthwhile to ensure that it scans well. It also maps well to existing Cocoa/cacao idioms and is generally preferred. diff --git a/README.md b/README.md index e7c06f1..5ff8cab 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ inform development. That said, this library is currently early stages and may ha 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 +> **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 -rampant for wrapped controls. This does **not** mean you can't assess, review, or question unsafe -usage - just know it's happening, and in large part it's not going away. Issues pertaining to the mere +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 +rampant for wrapped controls. This does **not** mean you can't assess, review, or question unsafe +usage - just know it's happening, and in large part it's not going away. 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 @@ -108,17 +108,17 @@ The following are a list of [Cargo features][cargo-features] that can be enabled [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section ## General Notes -**Why not extend the existing cocoa-rs crate?** +**Why not extend the existing cocoa-rs crate?** A good question. At the end of the day, that crate (I believe, and someone can correct me if I'm wrong) is somewhat tied to Servo, and I wanted to experiment with what the best approach for representing the Cocoa UI model in Rust was. This crate doesn't ignore their work entirely, either - `core_foundation` and `core_graphics` are used internally and re-exported for general use. -**Why should I write in Rust, rather than X language?** +**Why should I write in Rust, rather than X language?** In _my_ case, I want to be able to write native applications for my devices (and the platform I like to build products for) without being locked in to writing in Apple-specific languages... and without writing in C/C++ or JavaScript (note: the _toolchain_, not the language - ES6/Typescript are fine). I want to do this because I'm tired of hitting a mountain of work when I want to port my applications to other ecosystems. I think that Rust offers a (growing, but significant) viable model for sharing code across platforms and ecosystems without sacrificing performance. _(This is the part where the internet lights up and rants about some combination of Electron, Qt, and so on - we're not bothering here as it's beaten to death elsewhere)_ This crate is useful for people who don't need to go all-in on the Apple ecosystem, but want to port their work there with some relative ease. It's not expected that everyone will suddenly want to rewrite their macOS/iOS/tvOS apps in Rust. -**Isn't Objective-C dead?** +**Isn't Objective-C dead?** Yes, and no. It's true that Apple definitely favors Swift, and for good reason (and I say this as an unabashed lover of Objective-C). With that said, I would be surprised if we didn't have another ~5+ years of support; Apple is quick to deprecate, but removing the Objective-C runtime would require a ton of time and effort. Maybe SwiftUI kills it, who knows. A wrapper around this stuff should conceivably make it easier to swap out the underlying UI backend whenever it comes time. @@ -133,21 +133,21 @@ Some might also decry Objective-C as slow. To that, I'd note the following: **tl;dr** it's probably fine, and you have Rust for your performance needs. -**Why not just wrap UIKit, and then rely on Catalyst?** +**Why not just wrap UIKit, and then rely on Catalyst?** I have yet to see a single application where Catalyst felt good. The goal is good, though, and if it got to a point where that just seemed like the way forward (e.g, Apple just kills AppKit) then it's certainly an option. -**You can't possibly wrap all platform-specific behavior here...** +**You can't possibly wrap all platform-specific behavior here...** Correct! Each UI control contains a `objc` field, which you can use as an escape hatch - if the control doesn't support something, you're free to drop to the Objective-C runtime yourself and handle it. -**Why don't you use bindings to automatically generate this stuff?** +**Why don't you use bindings to automatically generate this stuff?** For initial exploration purposes I've done most of this by hand, as I wanted to find an approach that fit well in the Rust model before committing to binding generation. This is something I'll likely focus on next now that I've got things "working" well enough. -**Is this related to Cacao, the Swift project?** +**Is this related to Cacao, the Swift project?** No. The project referred to in this question aimed to map portions of Cocoa and UIKit over to run on Linux, but hasn't seen activity in some time (it was really cool, too!). Open source project naming in 2020 is like trying to buy a `.com` domain: everything good is taken. Luckily, multiple projects can share a name... so that's what's going to happen here. -**Isn't this kind of cheating the Rust object model?** +**Isn't this kind of cheating the Rust object model?** Depends on how you look at it. I personally don't care too much - the GUI layer for these platforms is a hard requirement to support for certain classes of products, and giving them up also means giving up battle-tested tools for things like Accessibility and deeper OS integration. With that said, internally there are efforts to try and make things respect Rust's model of how things should work. You can think of this as similar to gtk-rs. If you want to support or try a more _pure_ model, go check out Druid or something. :) diff --git a/build.rs b/build.rs index 83ec90c..f4358fd 100644 --- a/build.rs +++ b/build.rs @@ -2,10 +2,10 @@ fn main() { println!("cargo:rustc-link-lib=framework=Foundation"); - + #[cfg(feature = "appkit")] println!("cargo:rustc-link-lib=framework=AppKit"); - + #[cfg(feature = "uikit")] println!("cargo:rustc-link-lib=framework=UIKit"); @@ -15,13 +15,13 @@ fn main() { #[cfg(feature = "webview")] println!("cargo:rustc-link-lib=framework=WebKit"); - + #[cfg(feature = "cloudkit")] println!("cargo:rustc-link-lib=framework=CloudKit"); #[cfg(feature = "user-notifications")] println!("cargo:rustc-link-lib=framework=UserNotifications"); - + #[cfg(feature = "quicklook")] println!("cargo:rustc-link-lib=framework=QuickLook"); } diff --git a/examples/animation.rs b/examples/animation.rs index 9df26fb..8d9de6c 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -1,4 +1,4 @@ -//! This example builds on the AutoLayout example, but adds in animation +//! 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`. //! @@ -9,10 +9,10 @@ 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}; +use cacao::appkit::{AnimationContext, App, AppDelegate}; +use cacao::appkit::{Event, EventMask, EventMonitor}; struct BasicApp { window: Window @@ -54,33 +54,26 @@ const ANIMATIONS: [[[f64; 5]; 4]; 3] = [ [44., 16., 100., 100., 1.], [128., 84., 144., 124., 1.], [32., 32., 44., 44., 0.7], - [328., 157., 200., 200., 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], + [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.], + [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] { +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); @@ -117,22 +110,26 @@ impl WindowDelegate for AppWindow { 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 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); + 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::>>(); + 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 + // 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(); @@ -175,5 +172,6 @@ impl WindowDelegate for AppWindow { fn main() { App::new("com.test.window", BasicApp { window: Window::with(WindowConfig::default(), AppWindow::default()) - }).run(); + }) + .run(); } diff --git a/examples/autolayout.rs b/examples/autolayout.rs index 880b3bf..6bcdecd 100644 --- a/examples/autolayout.rs +++ b/examples/autolayout.rs @@ -5,9 +5,9 @@ use cacao::color::{Color, Theme}; use cacao::layout::{Layout, LayoutConstraint}; use cacao::view::View; -use cacao::appkit::{App, AppDelegate}; use cacao::appkit::menu::{Menu, MenuItem}; use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; struct BasicApp { window: Window @@ -23,23 +23,16 @@ impl AppDelegate for BasicApp { MenuItem::HideOthers, MenuItem::ShowAll, MenuItem::Separator, - MenuItem::Quit + MenuItem::Quit, ]), - - Menu::new("File", vec![ - MenuItem::CloseWindow - ]), - - Menu::new("View", vec![ - MenuItem::EnterFullScreen - ]), - + 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") - ]) + MenuItem::new("Bring All to Front"), + ]), ]); App::activate(); @@ -89,16 +82,14 @@ impl WindowDelegate for AppWindow { self.blue.leading.constraint_equal_to(&self.content.leading).offset(16.), self.blue.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), self.blue.width.constraint_equal_to_constant(100.), - self.red.top.constraint_equal_to(&self.content.top).offset(46.), self.red.leading.constraint_equal_to(&self.blue.trailing).offset(16.), self.red.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), - self.green.top.constraint_equal_to(&self.content.top).offset(46.), self.green.leading.constraint_equal_to(&self.red.trailing).offset(16.), self.green.trailing.constraint_equal_to(&self.content.trailing).offset(-16.), self.green.bottom.constraint_equal_to(&self.content.bottom).offset(-16.), - self.green.width.constraint_equal_to_constant(100.), + self.green.width.constraint_equal_to_constant(100.) ]); } } @@ -106,5 +97,6 @@ impl WindowDelegate for AppWindow { fn main() { App::new("com.test.window", BasicApp { window: Window::with(WindowConfig::default(), AppWindow::default()) - }).run(); + }) + .run(); } diff --git a/examples/browser/main.rs b/examples/browser/main.rs index 5b8fdf1..e6433fc 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::appkit::{App, AppDelegate}; use cacao::appkit::menu::{Menu, MenuItem}; use cacao::appkit::toolbar::Toolbar; use cacao::appkit::window::{Window, WindowConfig, WindowDelegate, WindowToolbarStyle}; +use cacao::appkit::{App, AppDelegate}; mod toolbar; use toolbar::BrowserToolbar; @@ -39,13 +39,9 @@ impl AppDelegate for BasicApp { MenuItem::HideOthers, MenuItem::ShowAll, MenuItem::Separator, - MenuItem::Quit + MenuItem::Quit, ]), - - Menu::new("File", vec![ - MenuItem::CloseWindow - ]), - + Menu::new("File", vec![MenuItem::CloseWindow]), Menu::new("Edit", vec![ MenuItem::Undo, MenuItem::Redo, @@ -54,21 +50,16 @@ impl AppDelegate for BasicApp { MenuItem::Copy, MenuItem::Paste, MenuItem::Separator, - MenuItem::SelectAll + MenuItem::SelectAll, ]), - - Menu::new("View", vec![ - MenuItem::EnterFullScreen - ]), - + Menu::new("View", vec![MenuItem::EnterFullScreen]), Menu::new("Window", vec![ MenuItem::Minimize, MenuItem::Zoom, MenuItem::Separator, - MenuItem::new("Bring All to Front") + MenuItem::new("Bring All to Front"), ]), - - Menu::new("Help", vec![]) + Menu::new("Help", vec![]), ]); App::activate(); @@ -84,9 +75,15 @@ impl Dispatcher for BasicApp { let webview = &window.content; match message { - Action::Back => { webview.go_back(); }, - Action::Forwards => { webview.go_forward(); }, - Action::Load(url) => { window.load_url(&url); } + Action::Back => { + webview.go_back(); + }, + Action::Forwards => { + webview.go_forward(); + }, + Action::Load(url) => { + window.load_url(&url); + } } } } @@ -132,13 +129,17 @@ impl WindowDelegate for AppWindow { fn main() { App::new("com.test.window", BasicApp { - window: Window::with({ - let mut config = WindowConfig::default(); + window: Window::with( + { + let mut config = WindowConfig::default(); - // This flag is necessary for Big Sur to use the correct toolbar style. - config.toolbar_style = WindowToolbarStyle::Expanded; + // This flag is necessary for Big Sur to use the correct toolbar style. + config.toolbar_style = WindowToolbarStyle::Expanded; - config - }, AppWindow::new()) - }).run(); + config + }, + AppWindow::new() + ) + }) + .run(); } diff --git a/examples/browser/toolbar.rs b/examples/browser/toolbar.rs index b27628d..ecdbb6c 100644 --- a/examples/browser/toolbar.rs +++ b/examples/browser/toolbar.rs @@ -1,10 +1,9 @@ - use cacao::objc::{msg_send, sel, sel_impl}; use cacao::button::Button; use cacao::input::{TextField, TextFieldDelegate}; -use cacao::appkit::toolbar::{Toolbar, ToolbarDisplayMode, ToolbarItem, ItemIdentifier, ToolbarDelegate}; +use cacao::appkit::toolbar::{ItemIdentifier, Toolbar, ToolbarDelegate, ToolbarDisplayMode, ToolbarItem}; use super::Action; @@ -45,8 +44,8 @@ impl BrowserToolbar { let url_bar = TextField::with(URLBar); let url_bar_item = ToolbarItem::new(URL_BAR); - - // We cheat for now to link these, as there's no API for Toolbar yet + + // We cheat for now to link these, as there's no API for Toolbar yet // to support arbitrary view types. The framework is designed to support this kind of // cheating, though: it's not outlandish to need to just manage things yourself when it // comes to Objective-C/AppKit sometimes. @@ -74,7 +73,7 @@ impl BrowserToolbar { ItemIdentifier::Custom(FWDS_BUTTON), ItemIdentifier::Space, ItemIdentifier::Custom(URL_BAR), - ItemIdentifier::Space + ItemIdentifier::Space, ] } } @@ -99,7 +98,9 @@ impl ToolbarDelegate for BrowserToolbar { BACK_BUTTON => &self.back_item, FWDS_BUTTON => &self.forwards_item, URL_BAR => &self.url_bar_item, - _ => { std::unreachable!(); } + _ => { + std::unreachable!(); + } } } } diff --git a/examples/calculator/button_row.rs b/examples/calculator/button_row.rs index 04166fb..053c9bb 100644 --- a/examples/calculator/button_row.rs +++ b/examples/calculator/button_row.rs @@ -1,10 +1,10 @@ -use cacao::layout::{LayoutConstraint, Layout}; use cacao::button::Button; use cacao::color::Color; +use cacao::layout::{Layout, LayoutConstraint}; use cacao::view::View; use crate::calculator::Msg; -use crate::content_view::{button, BUTTON_WIDTH, BUTTON_HEIGHT}; +use crate::content_view::{button, BUTTON_HEIGHT, BUTTON_WIDTH}; pub struct ButtonRow { pub view: View, @@ -15,31 +15,36 @@ impl ButtonRow { pub fn new(x: [Msg; 4], color: Color, action_color: Color) -> Self { let view = View::new(); - let buttons: Vec