commit 963015b37a4ced7e8e2471bc5c333a84500b3c68 Author: Ryan McGrath Date: Thu Feb 27 18:34:34 2020 -0800 Initial commit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5649a86 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing + +Thanks for your interest in contributing to this project! Suggestions, bug reports, and pull requests and so on are cool, but keep in mind this is open source - there's currently no guarantee this project does much. + +*Note:* Anyone who interacts with this project in any space, including but not +limited to this GitHub repository, must follow the [code of +conduct](https://github.com/ryanmcgrath/appkit/blob/trunk/code_of_conduct.md). + + +## Submitting bug reports + +Have a look at the [issue tracker](https://github.com/ryanmcgrath/appkit/issues). If you can't find an issue (open or closed) +describing your problem (or a very similar one) there, please open a new issue with +the following details: + +- Which versions of Rust and Appkit (and macOS build) are you using? +- Which feature flags are you using? +- What are you trying to accomplish? +- What is the full error you are seeing? +- How can this be reproduced? + - Please quote as much of your code as needed to reproduce (best link to a + public repository or [Gist]) + - Please post as much of your database schema as is relevant to your error + +[issue tracker]: https://github.com/ryanmcgrath/appkit/issues +[Gist]: https://gist.github.com + +Thank you! + + +## Submitting feature requests + +If you can't find an issue (open or closed) describing your idea on the [issue +tracker], open an issue. Adding answers to the following +questions in your description is +1: + +- What do you want to do, and how do you expect Alchemy to support you with that? +- How might this be added to Alchemy? +- What are possible alternatives? +- Are there any disadvantages? + +Thank you! + + +## Contribute code to Alchemy + +### Setting up Appkit locally + +1. Install Rust. Stable should be fine. +2. Clone this repository and open it in your favorite editor. +3. `cargo build`, or link it via your `Cargo.toml` to mess with it. + +### Coding Style + +Generally follow the [Rust Style Guide](https://github.com/rust-lang-nursery/fmt-rfcs/blob/master/guide/guide.md), enforced using [rustfmt](https://github.com/rust-lang-nursery/rustfmt). +In a few cases, though, it's fine to deviate - a good example is branching match trees. + +To run rustfmt tests locally: + +1. Use rustup to set rust toolchain to the version specified in the + [rust-toolchain file](./rust-toolchain). + +2. Install the rustfmt and clippy by running + ``` + rustup component add rustfmt-preview + rustup component add clippy-preview + ``` + +3. Run clippy using cargo from the root of your alchemy repo. + ``` + cargo clippy + ``` + Each PR needs to compile without warning. + +4. Run rustfmt using cargo from the root of your alchemy repo. + + To see changes that need to be made, run + + ``` + cargo fmt --all -- --check + ``` + + If all code is properly formatted (e.g. if you have not made any changes), + this should run without error or output. + If your code needs to be reformatted, + you will see a diff between your code and properly formatted code. + If you see code here that you didn't make any changes to + then you are probably running the wrong version of rustfmt. + Once you are ready to apply the formatting changes, run + + ``` + cargo fmt --all + ``` + + You won't see any output, but all your files will be corrected. + +You can also use rustfmt to make corrections or highlight issues in your editor. +Check out [their README](https://github.com/rust-lang-nursery/rustfmt) for details. + + +### Notes +This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's +worthwhile to ensure that it scans well. It also maps well to existing Cocoa/Appkit idioms and is generally preferred. diff --git a/LICENSE-MIT.md b/LICENSE-MIT.md new file mode 100644 index 0000000..e0daa30 --- /dev/null +++ b/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ryan McGrath. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-MPL.md b/LICENSE-MPL.md new file mode 100644 index 0000000..cd44203 --- /dev/null +++ b/LICENSE-MPL.md @@ -0,0 +1,355 @@ +Mozilla Public License Version 2.0 +================================== + +### 1. Definitions + +**1.1. “Contributor”** + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +**1.2. “Contributor Version”** + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +**1.3. “Contribution”** + means Covered Software of a particular Contributor. + +**1.4. “Covered Software”** + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +**1.5. “Incompatible With Secondary Licenses”** + means + +* **(a)** that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or +* **(b)** that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +**1.6. “Executable Form”** + means any form of the work other than Source Code Form. + +**1.7. “Larger Work”** + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +**1.8. “License”** + means this document. + +**1.9. “Licensable”** + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +**1.10. “Modifications”** + means any of the following: + +* **(a)** any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or +* **(b)** any new file in Source Code Form that contains any Covered + Software. + +**1.11. “Patent Claims” of a Contributor** + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +**1.12. “Secondary License”** + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +**1.13. “Source Code Form”** + means the form of the work preferred for making modifications. + +**1.14. “You” (or “Your”)** + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, “control” means **(a)** the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or **(b)** ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + + +### 2. License Grants and Conditions + +#### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +* **(a)** under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and +* **(b)** under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +#### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +#### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +* **(a)** for any code that a Contributor has removed from Covered Software; + or +* **(b)** for infringements caused by: **(i)** Your and any other third party's + modifications of Covered Software, or **(ii)** the combination of its + Contributions with other software (except as part of its Contributor + Version); or +* **(c)** under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +#### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +#### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +#### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +#### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + + +### 3. Responsibilities + +#### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +#### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +* **(a)** such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +* **(b)** You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +#### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +#### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +#### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + + +### 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: **(a)** comply with +the terms of this License to the maximum extent possible; and **(b)** +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + + +### 5. Termination + +**5.1.** The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated **(a)** provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and **(b)** on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +**5.2.** If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + + +### 6. Disclaimer of Warranty + +> Covered Software is provided under this License on an “as is” +> basis, without warranty of any kind, either expressed, implied, or +> statutory, including, without limitation, warranties that the +> Covered Software is free of defects, merchantable, fit for a +> particular purpose or non-infringing. The entire risk as to the +> quality and performance of the Covered Software is with You. +> Should any Covered Software prove defective in any respect, You +> (not any Contributor) assume the cost of any necessary servicing, +> repair, or correction. This disclaimer of warranty constitutes an +> essential part of this License. No use of any Covered Software is +> authorized under this License except under this disclaimer. + +### 7. Limitation of Liability + +> Under no circumstances and under no legal theory, whether tort +> (including negligence), contract, or otherwise, shall any +> Contributor, or anyone who distributes Covered Software as +> permitted above, be liable to You for any direct, indirect, +> special, incidental, or consequential damages of any character +> including, without limitation, damages for lost profits, loss of +> goodwill, work stoppage, computer failure or malfunction, or any +> and all other commercial damages or losses, even if such party +> shall have been informed of the possibility of such damages. This +> limitation of liability shall not apply to liability for death or +> personal injury resulting from such party's negligence to the +> extent applicable law prohibits such limitation. Some +> jurisdictions do not allow the exclusion or limitation of +> incidental or consequential damages, so this exclusion and +> limitation may not apply to You. + + +### 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + + +### 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + + +### 10. Versions of the License + +#### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +#### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +#### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e57987e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Rust Bindings for AppKit on macOS +This repository contains some _very_ exploratory, experimental bindings for `AppKit` on macOS. They aim to enable writing native macOS applications in pure Rust. There are currently no guarantees for anything, but you're welcome to clone and tinker. + +- It relies on the Objective C runtime, so you should consider this a bridge and not "the way forward". With that said, something like this needs to exist to jumpstart things. Down the road I could totally see this being superseded. It could also be cool to see something like this used as a layer for rendering in other frameworks (e.g, [Bodil's vgtk](https://docs.rs/vgtk/0.2.1/vgtk/) or something). +- It attempts to mimic how you'd write things in native ObjC/Swift, by providing very clear hooks for lifecycle events. +- As it runs via the ObjC runtime, there are many `unsafe` blocks. You can question them, and feel free to suggest better ways to do it, but this library will never have no `unsafe` usage. Issues pertaining to total removal will be closed without question. If you want a Rust UI framework for the future, then follow what's happening over in Druid or something. If you'd like to do things now, natively, then feel free to consider tinkering with this. + +I look at it like this: you realistically, for an app with a proper GUI, can't write 100% "safe" code today. You can get close, though - pick your poison. + +## Can I use this now? +For now, you can clone this repository and link it into your `Cargo.toml` by path. I'm squatting the names on `crates.io`, as I (in time) will throw this up there, but only when it's at a point where there's reasonable expectation that things won't be changing around much. + +If you're interested in seeing this in use in a shipping app, head on over to [subatomic](https://github.com/ryanmcgrath/subatomic/). + +## Gotchas +Note that this framework expects that you're participating in code signing. Certain linked frameworks (`UserNotifications.framework`, etc) will not work if you're not. + +## Etc +I assume I'll produce a better README at some point, but who knows. You can follow me over on [twitter](https://twitter.com/ryanmcgrath/) or [email me](mailto:ryan@rymc.io) with questions. Dual licensed MPL 2.0 and MIT. diff --git a/appkit-derive/Cargo.toml b/appkit-derive/Cargo.toml new file mode 100644 index 0000000..3b1430e --- /dev/null +++ b/appkit-derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "appkit-derive" +description = "A crate containing macros for appkit." +version = "0.1.0" +authors = ["Ryan McGrath "] +edition = "2018" +license = "MPL-2.0+" +repository = "https://github.com/ryanmcgrath/appkit-rs" +categories = ["gui", "rendering::engine", "multimedia"] +keywords = ["gui", "ui"] + +[lib] +proc-macro = true + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +syn = "1.0.14" +quote = "1.0.2" diff --git a/appkit-derive/README.md b/appkit-derive/README.md new file mode 100644 index 0000000..8b9e0a9 --- /dev/null +++ b/appkit-derive/README.md @@ -0,0 +1,5 @@ +# Alchemy-Macros +This crate holds macros for `ShinkWrap`-esque wrappers for UI controlers, which enable a nicer programming experience - e.g, if you have a `ViewController`, and you want to (a level up in the stack) set a background color, it'd be as easy as calling `vc.set_background_color(...)`. + +## Questions, Comments? +Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/). diff --git a/appkit-derive/src/lib.rs b/appkit-derive/src/lib.rs new file mode 100644 index 0000000..8c548fd --- /dev/null +++ b/appkit-derive/src/lib.rs @@ -0,0 +1,32 @@ +//! Macros used for `appkit-rs`. Mostly acting as `ShinkWrap`-esque forwarders. +//! Note that most of this is experimental! + +extern crate proc_macro; + +use crate::proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, parse_macro_input}; + +/// Derivces an `appkit::prelude::WinWrapper` block, which implements forwarding methods for things +/// like setting the window title, or showing and closing it. It currently expects that the wrapped +/// struct has `window` as the field holding the `Window` from `appkit-rs`. +/// +/// Note that this expects that pointers to Window(s) should not move once created. +#[proc_macro_derive(WindowWrapper)] +pub fn impl_window_controller(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics appkit::prelude::WinWrapper for #name #ty_generics #where_clause { + fn set_title(&self, title: &str) { self.window.set_title(title); } + fn show(&self) { self.window.show(self); } + fn close(&self) { self.window.close(); } + } + }; + + TokenStream::from(expanded) +} diff --git a/appkit/Cargo.toml b/appkit/Cargo.toml new file mode 100644 index 0000000..b2edcf6 --- /dev/null +++ b/appkit/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "appkit" +version = "0.1.0" +authors = ["Ryan McGrath "] +edition = "2018" +build = "build.rs" + +[dependencies] +appkit-derive = { path = "../appkit-derive" } +block = "0.1.6" +cocoa = "0.20.0" +core-foundation = "0.7" +core-graphics = "0.19.0" +dispatch = "0.2.0" +lazy_static = "1" +objc = "0.2.7" +objc_id = "0.1.1" +uuid = { version = "0.8", features = ["v4"] } diff --git a/appkit/build.rs b/appkit/build.rs new file mode 100644 index 0000000..7a563d7 --- /dev/null +++ b/appkit/build.rs @@ -0,0 +1,12 @@ +//! Specifies various frameworks to link against. Note that this is something where you probably +//! only want to be compiling this project on macOS. ;P +//! +//! (it checks to see if it's macOS before emitting anything, but still) + +fn main() { + if std::env::var("TARGET").unwrap().contains("-apple") { + println!("cargo:rustc-link-lib=framework=Security"); + println!("cargo:rustc-link-lib=framework=WebKit"); + println!("cargo:rustc-link-lib=framework=UserNotifications"); + } +} diff --git a/appkit/src/alert.rs b/appkit/src/alert.rs new file mode 100644 index 0000000..afbf392 --- /dev/null +++ b/appkit/src/alert.rs @@ -0,0 +1,46 @@ +//! A wrapper for `NSAlert`. Currently doesn't cover everything possible for this class, as it was +//! built primarily for debugging uses. Feel free to extend via pull requests or something. + +use cocoa::base::{id, nil}; +use cocoa::foundation::NSString; + +use objc_id::Id; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +/// Represents an `NSAlert`. Has no information other than the retained pointer to the Objective C +/// side, so... don't bother inspecting this. +pub struct Alert { + pub inner: Id +} + +impl Alert { + /// Creates a basic `NSAlert`, storing a pointer to it in the Objective C runtime. + /// You can show this alert by calling `show()`. + pub fn new(title: &str, message: &str) -> Self { + Alert { + inner: unsafe { + let cls = class!(NSAlert); + let alert: id = msg_send![cls, new]; + + let title = NSString::alloc(nil).init_str(title); + let _: () = msg_send![alert, setMessageText:title]; + + let message = NSString::alloc(nil).init_str(message); + let _: () = msg_send![alert, setInformativeText:message]; + + let x = NSString::alloc(nil).init_str("OK"); + let _: () = msg_send![alert, addButtonWithTitle:x]; + + Id::from_ptr(alert) + } + } + } + + /// Shows this alert as a modal. + pub fn show(&self) { + unsafe { + let _: () = msg_send![&*self.inner, runModal]; + } + } +} diff --git a/appkit/src/app/events.rs b/appkit/src/app/events.rs new file mode 100644 index 0000000..a6a43ea --- /dev/null +++ b/appkit/src/app/events.rs @@ -0,0 +1,39 @@ +//! This module handles providing a special subclass of `NSApplication`. +//! +//! Now, I know what you're thinking: this is dumb. +//! +//! And sure, maybe. But if you've ever opened Xcode and wondered why the hell +//! you have a xib/nib in your macOS project, it's (partly) because *that* handles +//! the NSMenu architecture for you... an architecture that, supposedly, is one of the +//! last Carbon pieces still laying around. +//! +//! And I gotta be honest, I ain't about the xib/nib life. SwiftUI will hopefully clear +//! that mess up one day, but in the meantime, we'll do this. +//! +//! Now, what we're *actually* doing here is relatively plain - on certain key events, +//! we want to make sure cut/copy/paste/etc are sent down the event chain. Usually, the +//! xib/nib stuff handles this for you... but this'll mostly do the same. + +use std::sync::Once; + +use cocoa::base::{id, nil}; + +use objc::declare::ClassDecl; +use objc::runtime::Class; +use objc::{class, msg_send, sel, sel_impl}; + +/// Used for injecting a custom NSApplication. Currently does nothing. +pub(crate) fn register_app_class() -> *const Class { + static mut DELEGATE_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = Class::get("NSApplication").unwrap(); + let decl = ClassDecl::new("RSTApplication", superclass).unwrap(); + DELEGATE_CLASS = decl.register(); + }); + + unsafe { + DELEGATE_CLASS + } +} diff --git a/appkit/src/app/mod.rs b/appkit/src/app/mod.rs new file mode 100644 index 0000000..8ebf406 --- /dev/null +++ b/appkit/src/app/mod.rs @@ -0,0 +1,169 @@ +//! A wrapper for `NSApplicationDelegate` on macOS. Handles looping back events and providing a very janky +//! messaging architecture. + +use std::sync::Once; + +use cocoa::base::{id, nil}; +use cocoa::foundation::NSString; +use cocoa::appkit::{NSRunningApplication}; + +use objc_id::Id; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel}; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::menu::Menu; + +mod events; +use events::register_app_class; + +static APP_PTR: &str = "rstAppPtr"; + +pub trait AppDelegate { + type Message: Send + Sync; + + fn did_finish_launching(&self) {} + fn did_become_active(&self) {} + + fn on_message(&self, message: Self::Message) {} +} + +/// A wrapper for `NSApplication`. It holds (retains) pointers for the Objective-C runtime, +/// which is where our application instance lives. It also injects an `NSObject` subclass, +/// which acts as the Delegate, looping back into our Vaulthund shared application. +pub struct App { + pub inner: Id, + pub objc_delegate: Id, + pub delegate: Box, + _t: std::marker::PhantomData +} + +impl App { + /// Sets a set of `Menu`'s as the top level Menu for the current application. Note that behind + /// the scenes, Cocoa/AppKit make a copy of the menu you pass in - so we don't retain it, and + /// you shouldn't bother to either. + pub fn set_menu(menus: Vec) { + unsafe { + let menu_cls = class!(NSMenu); + let main_menu: id = msg_send![menu_cls, new]; + + let item_cls = class!(NSMenuItem); + for menu in menus.iter() { + let item: id = msg_send![item_cls, new]; + let _: () = msg_send![item, setSubmenu:&*menu.inner]; + let _: () = msg_send![main_menu, addItem:item]; + } + + let cls = class!(RSTApplication); + let shared_app: id = msg_send![cls, sharedApplication]; + let _: () = msg_send![shared_app, setMainMenu:main_menu]; + } + } +} + +impl App where M: Send + Sync + 'static, T: AppDelegate { + /// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate, + /// and passing back through there. All messages are currently dispatched on the main thread. + pub fn dispatch(message: M) { + let queue = dispatch::Queue::main(); + + queue.exec_async(move || unsafe { + let app: id = msg_send![register_app_class(), sharedApplication]; + let app_delegate: id = msg_send![app, delegate]; + let delegate_ptr: usize = *(*app_delegate).get_ivar(APP_PTR); + let delegate = delegate_ptr as *const T; + (&*delegate).on_message(message); + }); + } + + /// Creates an NSAutoReleasePool, configures various NSApplication properties (e.g, activation + /// 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); + + let _pool = unsafe { + //msg_send![class!( + cocoa::foundation::NSAutoreleasePool::new(nil) + }; + + let inner = unsafe { + let app: id = msg_send![register_app_class(), sharedApplication]; + let _: () = msg_send![app, setActivationPolicy:0]; + //app.setActivationPolicy_(cocoa::appkit::NSApplicationActivationPolicyRegular); + Id::from_ptr(app) + }; + + let app_delegate = Box::new(delegate); + + let objc_delegate = unsafe { + let delegate_class = register_delegate_class::(); + let delegate: id = msg_send![delegate_class, new]; + let delegate_ptr: *const T = &*app_delegate; + (&mut *delegate).set_ivar(APP_PTR, delegate_ptr as usize); + let _: () = msg_send![&*inner, setDelegate:delegate]; + Id::from_ptr(delegate) + }; + + App { + objc_delegate: objc_delegate, + inner: inner, + delegate: app_delegate, + _t: std::marker::PhantomData + } + } + + /// Kicks off the NSRunLoop for the NSApplication instance. This blocks when called. + /// If you're wondering where to go from here... you need an `AppDelegate` that implements + /// `did_finish_launching`. :) + pub fn run(&self) { + unsafe { + let current_app = cocoa::appkit::NSRunningApplication::currentApplication(nil); + current_app.activateWithOptions_(cocoa::appkit::NSApplicationActivateIgnoringOtherApps); + let shared_app: id = msg_send![class!(RSTApplication), sharedApplication]; + let _: () = msg_send![shared_app, run]; + } + } +} + +/// Fires when the Application Delegate receives a `applicationDidFinishLaunching` notification. +extern fn did_finish_launching(this: &Object, _: Sel, _: id) { + unsafe { + let app_ptr: usize = *this.get_ivar(APP_PTR); + let app = app_ptr as *const D; + (*app).did_finish_launching(); + }; +} + +/// Fires when the Application Delegate receives a `applicationDidBecomeActive` notification. +extern fn did_become_active(this: &Object, _: Sel, _: id) { + unsafe { + let app_ptr: usize = *this.get_ivar(APP_PTR); + let app = app_ptr as *const D; + (*app).did_become_active(); + } +} + +/// Registers an `NSObject` application delegate, and configures it for the various callbacks and +/// pointers we need to have. +fn register_delegate_class() -> *const Class { + static mut DELEGATE_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = Class::get("NSObject").unwrap(); + let mut decl = ClassDecl::new("RSTAppDelegate", superclass).unwrap(); + + decl.add_ivar::(APP_PTR); + + // Add callback methods + decl.add_method(sel!(applicationDidFinishLaunching:), did_finish_launching:: as extern fn(&Object, _, _)); + decl.add_method(sel!(applicationDidBecomeActive:), did_become_active:: as extern fn(&Object, _, _)); + + DELEGATE_CLASS = decl.register(); + }); + + unsafe { + DELEGATE_CLASS + } +} diff --git a/appkit/src/bundle.rs b/appkit/src/bundle.rs new file mode 100644 index 0000000..fe27304 --- /dev/null +++ b/appkit/src/bundle.rs @@ -0,0 +1,99 @@ +//! Implements some stuff to handle dynamically setting the `NSBundle` identifier. +//! This is not currently in use, but does have places where it's useful... and to be honest I'm +//! kinda happy this is done as a swizzling implementation in pure Rust, which I couldn't find +//! examples of anywhere else. +//! +//! Disregard until you can't, I guess. + +use std::ffi::CString; +use std::mem; + +use cocoa::foundation::{NSString}; +use cocoa::base::{id, nil, BOOL, YES};//, NO}; +use objc::{class, msg_send, sel, sel_impl, Encode, Encoding, EncodeArguments, Message}; +use objc::runtime::{Class, Sel, Method, Object, Imp}; +use objc::runtime::{ + objc_getClass, + class_addMethod, + class_getInstanceMethod, + method_exchangeImplementations +}; + +/// Types that can be used as the implementation of an Objective-C method. +pub trait MethodImplementation { + /// The callee type of the method. + type Callee: Message; + /// The return type of the method. + type Ret: Encode; + /// The argument types of the method. + type Args: EncodeArguments; + + /// Returns self as an `Imp` of a method. + fn imp(self) -> Imp; +} + +macro_rules! method_decl_impl { + (-$s:ident, $r:ident, $f:ty, $($t:ident),*) => ( + impl<$s, $r $(, $t)*> MethodImplementation for $f + where $s: Message, $r: Encode $(, $t: Encode)* { + type Callee = $s; + type Ret = $r; + type Args = ($($t,)*); + + fn imp(self) -> Imp { + unsafe { mem::transmute(self) } + } + } + ); + ($($t:ident),*) => ( + method_decl_impl!(-T, R, extern fn(&T, Sel $(, $t)*) -> R, $($t),*); + method_decl_impl!(-T, R, extern fn(&mut T, Sel $(, $t)*) -> R, $($t),*); + ); +} + +method_decl_impl!(); +method_decl_impl!(A); + +extern fn get_bundle_id(this: &Object, s: Sel, v: id) -> id { + unsafe { + let bundle = class!(NSBundle); + let main_bundle: id = msg_send![bundle, mainBundle]; + let e: BOOL = msg_send![this, isEqual:main_bundle]; + if e == YES { + 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") + } + } else { + msg_send![this, __bundleIdentifier] + } + } +} + +unsafe fn swizzle_bundle_id(bundle_id: &str, func: F) where F: MethodImplementation { + let name = CString::new("NSBundle").unwrap(); + let cls = objc_getClass(name.as_ptr()); + + // let mut cls = class!(NSBundle) as *mut Class; + // Class::get("NSBundle").unwrap(); + // let types = format!("{}{}{}", Encoding::String, <*mut Object>::ENCODING, Sel::ENCODING); + + let added = class_addMethod( + cls as *mut Class, + sel!(__bundleIdentifier), + func.imp(), + CString::new("*@:").unwrap().as_ptr() + ); + + let method1 = class_getInstanceMethod(cls, sel!(bundleIdentifier)) as *mut Method; + let method2 = class_getInstanceMethod(cls, sel!(__bundleIdentifier)) as *mut Method; + method_exchangeImplementations(method1, method2); +} + +pub fn set_bundle_id(bundle_id: &str) { + unsafe { + swizzle_bundle_id(bundle_id, get_bundle_id as extern fn(&Object, _, _) -> id); + } +} diff --git a/appkit/src/button.rs b/appkit/src/button.rs new file mode 100644 index 0000000..44a34ad --- /dev/null +++ b/appkit/src/button.rs @@ -0,0 +1,56 @@ +//! A wrapper for NSButton. Currently the epitome of jank - if you're poking around here, expect +//! that this will change at some point. + +use std::sync::Once; + +use cocoa::base::{id, nil}; +use cocoa::foundation::{NSString}; + +use objc_id::Id; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object}; +use objc::{msg_send, sel, sel_impl}; + +/// A wrapper for `NSButton`. Holds (retains) pointers for the Objective-C runtime +/// where our `NSButton` lives. +pub struct Button { + pub inner: Id +} + +impl Button { + /// Creates a new `NSButton` instance, configures it appropriately, + /// and retains the necessary Objective-C runtime pointer. + pub fn new(text: &str) -> Self { + let inner = unsafe { + let title = NSString::alloc(nil).init_str(text); + let button: id = msg_send![register_class(), buttonWithTitle:title target:nil action:nil]; + Id::from_ptr(button) + }; + + Button { + inner: inner + } + } + + /// Sets the bezel style for this button. + pub fn set_bezel_style(&self, bezel_style: i32) { + unsafe { + let _: () = msg_send![&*self.inner, setBezelStyle:bezel_style]; + } + } +} + +/// Registers an `NSButton` subclass, and configures it to hold some ivars for various things we need +/// to store. +fn register_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = Class::get("NSButton").unwrap(); + let decl = ClassDecl::new("RSTButton", superclass).unwrap(); + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/appkit/src/events.rs b/appkit/src/events.rs new file mode 100644 index 0000000..27fa0c0 --- /dev/null +++ b/appkit/src/events.rs @@ -0,0 +1,27 @@ +//! Hoists some type definitions in a way that I personally find cleaner than what's in the Servo +//! code. + +#[allow(non_upper_case_globals, non_snake_case)] +pub mod NSEventModifierFlag { + use cocoa::foundation::NSUInteger; + + /// Indicates the Caps Lock key has been pressed. + pub const CapsLock: NSUInteger = 1 << 16; + + /// Indicates the Control key has been pressed. + pub const Control: NSUInteger = 1 << 18; + + /// Indicates the Option key has been pressed. + pub const Option: NSUInteger = 1 << 19; + + /// Indicates the Command key has been pressed. + pub const Command: NSUInteger = 1 << 20; + + /// Indicates device-independent modifier flags are in play. + pub const DeviceIndependentFlagsMask: NSUInteger = 0xffff0000; +} + +#[allow(non_upper_case_globals, non_snake_case)] +mod NSEventType { + pub const KeyDown: usize = 10; +} diff --git a/appkit/src/file_panel/enums.rs b/appkit/src/file_panel/enums.rs new file mode 100644 index 0000000..0cf5c0e --- /dev/null +++ b/appkit/src/file_panel/enums.rs @@ -0,0 +1,30 @@ +//! Certain enums that are useful (response types, etc). + +use cocoa::foundation::{NSInteger}; + +pub enum ModalResponse { + Ok, + Continue, + Canceled, + Stopped, + Aborted, + FirstButtonReturned, + SecondButtonReturned, + ThirdButtonReturned +} + +impl From for ModalResponse { + fn from(i: NSInteger) -> Self { + match i { + 1 => ModalResponse::Ok, + 0 => ModalResponse::Canceled, + 1000 => ModalResponse::FirstButtonReturned, + 1001 => ModalResponse::SecondButtonReturned, + 1002 => ModalResponse::ThirdButtonReturned, + -1000 => ModalResponse::Stopped, + -1001 => ModalResponse::Aborted, + -1002 => ModalResponse::Continue, + e => { panic!("Unknown NSModalResponse sent back! {}", e); } + } + } +} diff --git a/appkit/src/file_panel/mod.rs b/appkit/src/file_panel/mod.rs new file mode 100644 index 0000000..e745a7c --- /dev/null +++ b/appkit/src/file_panel/mod.rs @@ -0,0 +1,13 @@ + + +pub mod enums; +pub use enums::*; + +pub mod traits; +pub use traits::OpenSaveController; + +pub mod save; +pub use save::FileSavePanel; + +pub mod select; +pub use select::FileSelectPanel; diff --git a/appkit/src/file_panel/save.rs b/appkit/src/file_panel/save.rs new file mode 100644 index 0000000..6466f26 --- /dev/null +++ b/appkit/src/file_panel/save.rs @@ -0,0 +1,111 @@ +//! Implements `FileSavePanel`, which allows the user to select where a file should be saved. +//! It currently doesn't implement _everything_ necessary, but it's functional +//! enough for general use. + +use block::ConcreteBlock; + +use cocoa::base::{id, nil, YES, NO, BOOL}; +use cocoa::foundation::{NSInteger, NSUInteger, NSString}; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; +use objc_id::ShareId; + +use crate::file_panel::enums::ModalResponse; +use crate::utils::str_from; + +#[derive(Debug)] +pub struct FileSavePanel { + /// The internal Objective C `NSOpenPanel` instance. + pub panel: ShareId, + + /// The internal `NSObject` that routes delegate callbacks around. + pub delegate: ShareId, + + /// Whether the user can choose files. Defaults to `true`. + pub can_create_directories: bool +} + +impl Default for FileSavePanel { + fn default() -> Self { + FileSavePanel::new() + } +} + +impl FileSavePanel { + /// Creates and returns a `FileSavePanel`, which holds pointers to the Objective C runtime for + /// instrumenting the dialog. + pub fn new() -> Self { + FileSavePanel { + panel: unsafe { + let cls = class!(NSSavePanel); + let x: id = msg_send![cls, savePanel]; + ShareId::from_ptr(x) + }, + + delegate: unsafe { + ShareId::from_ptr(msg_send![class!(NSObject), new]) + }, + + can_create_directories: true + } + } + + pub fn set_delegate(&mut self) {} + + pub fn set_suggested_filename(&mut self, suggested_filename: &str) { + unsafe { + let filename = NSString::alloc(nil).init_str(suggested_filename); + let _: () = msg_send![&*self.panel, setNameFieldStringValue:filename]; + } + } + + /// Sets whether directories can be created by the user. + pub fn set_can_create_directories(&mut self, can_create: bool) { + unsafe { + let _: () = msg_send![&*self.panel, setCanCreateDirectories:match can_create { + true => YES, + false => NO + }]; + } + + self.can_create_directories = can_create; + } + + /// 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. + /// + /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as + /// the system runs and manages that in another process, and we're still abiding by the general + /// retain/ownership rules here. + pub fn show) + 'static>(&self, handler: F) { + let panel = self.panel.clone(); + let completion = ConcreteBlock::new(move |_result: NSInteger| { + //let response: ModalResponse = result.into(); + handler(get_url(&panel)); + }); + let completion = completion.copy(); + + unsafe { + let _: () = msg_send![&*self.panel, runModal]; + completion.call((1,)); + //beginWithCompletionHandler:completion.copy()]; + //let _: () = msg_send![&*self.panel, beginWithCompletionHandler:completion.copy()]; + } + } +} + +/// 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_url(panel: &Object) -> Option { + unsafe { + let url: id = msg_send![&*panel, URL]; + if url == nil { + None + } else { + let path: id = msg_send![url, path]; + Some(str_from(path).to_string()) + } + } +} diff --git a/appkit/src/file_panel/select.rs b/appkit/src/file_panel/select.rs new file mode 100644 index 0000000..92700bb --- /dev/null +++ b/appkit/src/file_panel/select.rs @@ -0,0 +1,167 @@ +//! Implements `FileSelectPanel`, which allows the user to select files for processing and hands you +//! urls to work with. It currently doesn't implement _everything_ necessary, but it's functional +//! enough for general use. + +use block::ConcreteBlock; + +use cocoa::base::{id, nil, YES, NO, BOOL}; +use cocoa::foundation::{NSInteger, NSUInteger}; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; +use objc_id::ShareId; + +use crate::file_panel::enums::ModalResponse; +use crate::utils::str_from; + +#[derive(Debug)] +pub struct FileSelectPanel { + /// The internal Objective C `NSOpenPanel` instance. + pub panel: ShareId, + + /// The internal `NSObject` that routes delegate callbacks around. + pub delegate: ShareId, + + /// Whether the user can choose files. Defaults to `true`. + pub can_choose_files: bool, + + /// Whether the user can choose directories. Defaults to `false`. + pub can_choose_directories: bool, + + /// When the value of this property is true, dropping an alias on the panel or asking + /// for filenames or URLs returns the resolved aliases. The default value of this property + /// is true. When this value is false, selecting an alias returns the alias instead of the + /// file or directory it represents. + pub resolves_aliases: bool, + + /// When the value of this property is true, the user may select multiple items from the + /// browser. Defaults to `false`. + pub allows_multiple_selection: bool +} + +impl Default for FileSelectPanel { + fn default() -> Self { + FileSelectPanel::new() + } +} + +impl FileSelectPanel { + /// Creates and returns a `FileSelectPanel`, which holds pointers to the Objective C runtime for + /// instrumenting the dialog. + pub fn new() -> Self { + FileSelectPanel { + panel: unsafe { + let cls = class!(NSOpenPanel); + let x: id = msg_send![cls, openPanel]; + ShareId::from_ptr(x) + }, + + delegate: unsafe { + ShareId::from_ptr(msg_send![class!(NSObject), new]) + }, + + can_choose_files: true, + can_choose_directories: false, + resolves_aliases: true, + allows_multiple_selection: true + } + } + + pub fn set_delegate(&mut self) {} + + /// Sets whether files can be chosen by the user. + pub fn set_can_choose_files(&mut self, can_choose: bool) { + unsafe { + let _: () = msg_send![&*self.panel, setCanChooseFiles:match can_choose { + true => YES, + false => NO + }]; + } + + self.can_choose_files = can_choose; + } + + /// Sets whether the user can choose directories. + pub fn set_can_choose_directories(&mut self, can_choose: bool) { + unsafe { + let _: () = msg_send![&*self.panel, setCanChooseDirectories:match can_choose { + true => YES, + false => NO + }]; + } + + self.can_choose_directories = can_choose; + } + + /// Sets whether the panel resolves aliases. + pub fn set_resolves_aliases(&mut self, resolves: bool) { + unsafe { + let _: () = msg_send![&*self.panel, setResolvesAliases:match resolves { + true => YES, + false => NO + }]; + } + + self.resolves_aliases = resolves; + } + + /// Sets whether the panel allows multiple selections. + pub fn set_allows_multiple_selection(&mut self, allows: bool) { + unsafe { + let _: () = msg_send![&*self.panel, setAllowsMultipleSelection:match allows { + true => YES, + false => NO + }]; + } + + 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. + /// + /// Note that this clones the underlying `NSOpenPanel` pointer. This is theoretically safe as + /// the system runs and manages that in another process, and we're still abiding by the general + /// retain/ownership rules here. + pub fn show) + 'static>(&self, handler: F) { + let panel = self.panel.clone(); + let completion = ConcreteBlock::new(move |result: NSInteger| { + let response: ModalResponse = result.into(); + + handler(match response { + ModalResponse::Ok => get_urls(&panel), + _ => Vec::new() + }); + }); + + unsafe { + let _: () = msg_send![&*self.panel, beginWithCompletionHandler:completion.copy()]; + } + } +} + +/// 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 { + let mut paths: Vec = vec![]; + + unsafe { + let urls: id = msg_send![&*panel, URLs]; + let mut count: usize = msg_send![urls, count]; + + loop { + if count == 0 { + break; + } + + let url: id = msg_send![urls, objectAtIndex:count-1]; + let path: id = msg_send![url, absoluteString]; + paths.push(str_from(path).to_string()); + count -= 1; + } + } + + paths.reverse(); + paths +} diff --git a/appkit/src/file_panel/traits.rs b/appkit/src/file_panel/traits.rs new file mode 100644 index 0000000..a080497 --- /dev/null +++ b/appkit/src/file_panel/traits.rs @@ -0,0 +1,21 @@ +//! A trait that you can implement to handle open and save file dialogs. This more or less maps +//! over to `NSOpenPanel` and `NSSavePanel` handling. + +pub trait OpenSaveController { + /// Called when the user has entered a filename (typically, during saving). `confirmed` + /// indicates whether or not they hit the save button. + fn user_entered_filename(&self, filename: &str, confirmed: bool) {} + + /// Notifies you that the panel selection changed. + fn panel_selection_did_change(&self) {} + + /// Notifies you that the user changed directories. + fn did_change_to_directory(&self, url: &str) {} + + /// Notifies you that the Save panel is about to expand or collapse because the user + /// clicked the disclosure triangle that displays or hides the file browser. + fn will_expand(&self, expanding: bool) {} + + /// Determine whether the specified URL should be enabled in the Open panel. + fn should_enable_url(&self, url: &str) -> bool { true } +} diff --git a/appkit/src/layout/mod.rs b/appkit/src/layout/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/appkit/src/lib.rs b/appkit/src/lib.rs new file mode 100644 index 0000000..ee41206 --- /dev/null +++ b/appkit/src/lib.rs @@ -0,0 +1,67 @@ +//! This crate provides pieces necessary for interfacing with `AppKit` (`Cocoa`, on macOS). 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. +//! +//! 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, 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. +//! +//! It's best to look at this crate as a bridge to the future: you can write your own (safe) Rust +//! code, and have it intermix in the (existing, unsafe) world. +//! +//! This crate is also, currently, _very_ early stage and may have bugs. Your usage of it is at +//! your own risk. With that said, provided you follow the rules (regarding memory/ownership) it's +//! already fine for some apps. Check the README for more info! + +pub use objc_id::ShareId; +pub use objc::runtime::Object; +pub use cocoa::base::id; + +pub trait ViewWrapper { + fn get_handle(&self) -> Option>; +} + +pub trait ViewController { + fn did_load(&self); +} + +pub mod alert; +pub mod app; +pub mod events; +pub mod menu; +pub mod button; +pub mod file_panel; +pub mod toolbar; +pub mod notifications; +pub mod webview; +pub mod view; +pub mod window; +pub mod networking; +pub mod utils; + +pub mod prelude { + pub use crate::app::{App, AppDelegate}; + + pub use crate::menu::{Menu, MenuItem}; + pub use crate::notifications::{Notification, NotificationCenter, NotificationAuthOption}; + pub use crate::toolbar::{ToolbarDelegate}; + + pub use crate::networking::URLRequest; + + pub use crate::window::{ + Window, WindowWrapper as WinWrapper, WindowController + }; + + pub use crate::webview::{ + WebView, WebViewConfig, WebViewController + }; + + pub use crate::{ViewController, ViewWrapper}; + + pub use appkit_derive::{ + WindowWrapper + }; +} diff --git a/appkit/src/menu/item.rs b/appkit/src/menu/item.rs new file mode 100644 index 0000000..824f355 --- /dev/null +++ b/appkit/src/menu/item.rs @@ -0,0 +1,189 @@ +//! A wrapper for NSMenuItem. Currently only supports menus going +//! one level deep; this could change in the future but is fine for +//! now. + +use cocoa::base::{id, nil}; +use cocoa::foundation::{NSString, NSUInteger}; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::{Object, Sel}; +use objc_id::ShareId; + +use crate::events::NSEventModifierFlag; + +/// Internal method (shorthand) for generating `NSMenuItem` holders. +fn make_menu_item(title: &str, key: Option<&str>, action: Option, modifier: Option) -> MenuItem { + unsafe { + let cls = class!(NSMenuItem); + let alloc: id = msg_send![cls, alloc]; + let title = NSString::alloc(nil).init_str(title); + + // Note that AppKit requires a blank string if nil, not nil. + let key = NSString::alloc(nil).init_str(match key { + Some(s) => s, + None => "" + }); + + let item = ShareId::from_ptr(match action { + Some(a) => msg_send![alloc, initWithTitle:title action:a keyEquivalent:key], + None => msg_send![alloc, initWithTitle:title action:nil keyEquivalent:key] + }); + + if let Some(modifier) = modifier { + let _: () = msg_send![&*item, setKeyEquivalentModifierMask:modifier]; + }; + + MenuItem::Action(item) + } +} + +/// Represents varying `NSMenuItem` types - e.g, a separator vs an action. +#[derive(Debug)] +pub enum MenuItem { + /// Represents a Menu item that's not a separator - for all intents and purposes, you can consider + /// this the real `NSMenuItem`. + Action(ShareId), + + /// Represents a Separator. You can't do anything with this, but it's useful nonetheless for + /// separating out pieces of the `NSMenu` structure. + Separator +} + +impl MenuItem { + /// Creates and returns a `MenuItem::Action` with the specified title. + pub fn action(title: &str) -> Self { + make_menu_item(title, None, None, None) + } + + /// Configures the menu item, if it's not a separator, to support a key equivalent. + pub fn key(self, key: &str) -> Self { + match self { + MenuItem::Separator => MenuItem::Separator, + + MenuItem::Action(item) => { + unsafe { + let key = NSString::alloc(nil).init_str(key); + let _: () = msg_send![&*item, setKeyEquivalent:key]; + } + + MenuItem::Action(item) + } + } + } + + /// Returns a standard "About" item. + pub fn about(name: &str) -> Self { + let title = format!("About {}", name); + make_menu_item(&title, None, Some(sel!(orderFrontStandardAboutPanel:)), None) + } + + /// Returns a standard "Hide" item. + pub fn hide() -> Self { + make_menu_item("Hide", Some("h"), Some(sel!(hide:)), None) + } + + /// Returns the standard "Services" item. This one does some extra work to link in the default + /// Services submenu. + pub fn services() -> Self { + match make_menu_item("Services", None, None, None) { + // Link in the services menu, which is part of NSApp + MenuItem::Action(item) => { + unsafe { + let app: id = msg_send![class!(RSTApplication), sharedApplication]; + let services: id = msg_send![app, servicesMenu]; + let _: () = msg_send![&*item, setSubmenu:services]; + } + + MenuItem::Action(item) + }, + + // Should never be hit + MenuItem::Separator => MenuItem::Separator + } + } + + /// Returns a standard "Hide" item. + pub fn hide_others() -> Self { + make_menu_item( + "Hide Others", + Some("h"), + Some(sel!(hide:)), + Some(NSEventModifierFlag::Command | NSEventModifierFlag::Option) + ) + } + + /// Returns a standard "Hide" item. + pub fn show_all() -> Self { + make_menu_item("Show All", None, Some(sel!(unhideAllApplications:)), None) + } + + /// Returns a standard "Close Window" item. + pub fn close_window() -> Self { + make_menu_item("Close Window", Some("w"), Some(sel!(performClose:)), None) + } + + /// Returns a standard "Quit" item. + pub fn quit() -> Self { + make_menu_item("Quit", Some("q"), Some(sel!(terminate:)), None) + } + + /// Returns a standard "Copy" item. + pub fn copy() -> Self { + make_menu_item("Copy", Some("c"), Some(sel!(copy:)), None) + } + + /// Returns a standard "Undo" item. + pub fn undo() -> Self { + make_menu_item("Undo", Some("z"), Some(sel!(undo:)), None) + } + + /// Returns a standard "Enter Full Screen" item + pub fn enter_full_screen() -> Self { + make_menu_item( + "Enter Full Screen", + Some("f"), + Some(sel!(toggleFullScreen:)), + Some(NSEventModifierFlag::Command | NSEventModifierFlag::Control) + ) + } + + /// Returns a standard "Miniaturize" item + pub fn minimize() -> Self { + make_menu_item( + "Minimize", + Some("m"), + Some(sel!(performMiniaturize:)), + None + ) + } + + /// Returns a standard "Zoom" item + pub fn zoom() -> Self { + make_menu_item( + "Zoom", + None, + Some(sel!(performZoom:)), + None + ) + } + + /// Returns a standard "Redo" item. + pub fn redo() -> Self { + make_menu_item("Redo", Some("Z"), Some(sel!(redo:)), None) + } + + /// Returns a standard "Cut" item. + pub fn cut() -> Self { + make_menu_item("Cut", Some("x"), Some(sel!(cut:)), None) + } + + /// Returns a standard "Select All" item. + pub fn select_all() -> Self { + make_menu_item("Select All", Some("a"), Some(sel!(selectAll:)), None) + } + + /// Returns a standard "Paste" item. + pub fn paste() -> Self { + make_menu_item("Paste", Some("v"), Some(sel!(paste:)), None) + } +} diff --git a/appkit/src/menu/menu.rs b/appkit/src/menu/menu.rs new file mode 100644 index 0000000..eabd1c0 --- /dev/null +++ b/appkit/src/menu/menu.rs @@ -0,0 +1,54 @@ +//! Wraps NSMenu and handles instrumenting necessary delegate pieces. + +use cocoa::base::{id, nil, YES}; +use cocoa::foundation::NSString; + +use objc_id::Id; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::menu::item::MenuItem; + +/// A struct that represents an `NSMenu`. It takes ownership of items, and handles instrumenting +/// them throughout the application lifecycle. +#[derive(Debug)] +pub struct Menu { + pub inner: Id, + pub items: Vec +} + +impl Menu { + /// Creates a new `Menu` with the given title, and uses the passed items as submenu items. + pub fn new(title: &str, items: Vec) -> Self { + let inner = unsafe { + let cls = class!(NSMenu); + let alloc: id = msg_send![cls, alloc]; + let title = NSString::alloc(nil).init_str(title); + let inner: id = msg_send![alloc, initWithTitle:title]; + Id::from_ptr(inner) + }; + + for item in items.iter() { + match item { + MenuItem::Action(item) => { + unsafe { + let _: () = msg_send![&*inner, addItem:item.clone()]; + } + }, + + MenuItem::Separator => { + unsafe { + let cls = class!(NSMenuItem); + let separator: id = msg_send![cls, separatorItem]; + let _: () = msg_send![&*inner, addItem:separator]; + } + } + } + } + + Menu { + inner: inner, + items: items + } + } +} diff --git a/appkit/src/menu/mod.rs b/appkit/src/menu/mod.rs new file mode 100644 index 0000000..21b719d --- /dev/null +++ b/appkit/src/menu/mod.rs @@ -0,0 +1,7 @@ +//! Module hoisting. + +pub mod menu; +pub use menu::Menu; + +pub mod item; +pub use item::MenuItem; diff --git a/appkit/src/networking/mod.rs b/appkit/src/networking/mod.rs new file mode 100644 index 0000000..85d38af --- /dev/null +++ b/appkit/src/networking/mod.rs @@ -0,0 +1,30 @@ +//! A lightweight wrapper over some networking components, like `NSURLRequest` and co. +//! This is currently not meant to be exhaustive. + +use cocoa::base::id; +use objc_id::Id; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; + +use crate::utils::str_from; + +pub struct URLRequest { + pub inner: Id +} + +impl URLRequest { + pub fn with(inner: id) -> Self { + URLRequest { + inner: unsafe { Id::from_ptr(inner) } + } + } + + pub fn url(&self) -> &'static str { + unsafe { + let url: id = msg_send![&*self.inner, URL]; + let path: id = msg_send![url, absoluteString]; + str_from(path) + } + } +} diff --git a/appkit/src/notifications/center.rs b/appkit/src/notifications/center.rs new file mode 100644 index 0000000..73439a9 --- /dev/null +++ b/appkit/src/notifications/center.rs @@ -0,0 +1,62 @@ +//! Wraps UNUserNotificationCenter for macOS. Note that this uses the newer +//! `UserNotifications.framework` API, which requires that your application be properly signed. + +use block::ConcreteBlock; + +use cocoa::base::{id, nil}; +use cocoa::foundation::NSString; + +use objc::{class, msg_send, sel, sel_impl}; + +use crate::notifications::Notification; +use crate::utils::str_from; + +#[allow(non_upper_case_globals, non_snake_case)] +pub mod NotificationAuthOption { + pub const Badge: i32 = 1 << 0; + pub const Sound: i32 = 1 << 1; + pub const Alert: i32 = 1 << 2; +} + +/// Acts as a central interface to the Notification Center on macOS. +pub struct NotificationCenter; + +impl NotificationCenter { + /// Requests authorization from the user to send them notifications. + pub fn request_authorization(options: i32) { + unsafe { + let block = ConcreteBlock::new(|_: id, error: id| { + let msg: id = msg_send![error, localizedDescription]; + + let localized_description = str_from(msg); + if localized_description != "" { + println!("{:?}", localized_description); + } + }); + + let center: id = msg_send![class!(UNUserNotificationCenter), currentNotificationCenter]; + let _: () = msg_send![center, requestAuthorizationWithOptions:options completionHandler:block.copy()]; + } + } + + /// Queues up a `Notification` to be displayed to the user. + pub fn notify(notification: Notification) { + let uuidentifier = format!("{}", uuid::Uuid::new_v4()); + + unsafe { + let identifier = NSString::alloc(nil).init_str(&uuidentifier); + let request: id = msg_send![class!(UNNotificationRequest), requestWithIdentifier:identifier content:&*notification.inner trigger:nil]; + + let center: id = msg_send![class!(UNUserNotificationCenter), currentNotificationCenter]; + let _: () = msg_send![center, addNotificationRequest:request]; + } + } + + /// Removes all notifications that have been delivered (e.g, in the notification center). + pub fn remove_all_delivered_notifications() { + unsafe { + let center: id = msg_send![class!(UNUserNotificationCenter), currentNotificationCenter]; + let _: () = msg_send![center, removeAllDeliveredNotifications]; + } + } +} diff --git a/appkit/src/notifications/mod.rs b/appkit/src/notifications/mod.rs new file mode 100644 index 0000000..d6e90bd --- /dev/null +++ b/appkit/src/notifications/mod.rs @@ -0,0 +1,7 @@ +//! Hoisting. + +pub mod center; +pub use center::*; + +pub mod notifications; +pub use notifications::*; diff --git a/appkit/src/notifications/notifications.rs b/appkit/src/notifications/notifications.rs new file mode 100644 index 0000000..4ee464f --- /dev/null +++ b/appkit/src/notifications/notifications.rs @@ -0,0 +1,36 @@ +//! Acts as a (currently dumb) wrapper for `UNMutableNotificationContent`, which is what you mostly +//! need to pass to the notification center for things to work. + +use cocoa::base::{id, nil}; +use cocoa::foundation::NSString; + +use objc_id::Id; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +/// A wrapper for `UNMutableNotificationContent`. Retains the pointer from the Objective C side, +/// and is ultimately dropped upon sending. +pub struct Notification { + pub inner: Id +} + +impl Notification { + /// Constructs a new `Notification`. This allocates `NSString`'s, as it has to do so for the + /// Objective C runtime - be aware if you're slaming this (you shouldn't be slamming this). + pub fn new(title: &str, body: &str) -> Self { + Notification { + inner: unsafe { + let cls = class!(UNMutableNotificationContent); + let content: id = msg_send![cls, new]; + + let title = NSString::alloc(nil).init_str(title); + let _: () = msg_send![content, setTitle:title]; + + let body = NSString::alloc(nil).init_str(body); + let _: () = msg_send![content, setBody:body]; + + Id::from_ptr(content) + } + } + } +} diff --git a/appkit/src/toolbar/item.rs b/appkit/src/toolbar/item.rs new file mode 100644 index 0000000..305f1ca --- /dev/null +++ b/appkit/src/toolbar/item.rs @@ -0,0 +1,72 @@ +//! Implements an NSToolbar wrapper, which is one of those macOS niceties +//! that makes it feel... "proper". +//! +//! UNFORTUNATELY, this is a very old and janky API. So... yeah. + +use cocoa::base::{id, nil}; +use cocoa::foundation::{NSSize, NSString}; + +use objc_id::Id; +use objc::runtime::Object; +use objc::{class, msg_send, sel, sel_impl}; + +use crate::button::Button; + +/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime +/// where our `NSWindow` and associated delegate live. +pub struct ToolbarItem<'a> { + pub identifier: &'a str, + pub inner: Id, + pub button: Option