Initial commit
This commit is contained in:
commit
963015b37a
103
CONTRIBUTING.md
Normal file
103
CONTRIBUTING.md
Normal file
|
@ -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.
|
21
LICENSE-MIT.md
Normal file
21
LICENSE-MIT.md
Normal file
|
@ -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.
|
355
LICENSE-MPL.md
Normal file
355
LICENSE-MPL.md
Normal file
|
@ -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.
|
19
README.md
Normal file
19
README.md
Normal file
|
@ -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.
|
20
appkit-derive/Cargo.toml
Normal file
20
appkit-derive/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "appkit-derive"
|
||||||
|
description = "A crate containing macros for appkit."
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
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"
|
5
appkit-derive/README.md
Normal file
5
appkit-derive/README.md
Normal file
|
@ -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/).
|
32
appkit-derive/src/lib.rs
Normal file
32
appkit-derive/src/lib.rs
Normal file
|
@ -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)
|
||||||
|
}
|
18
appkit/Cargo.toml
Normal file
18
appkit/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "appkit"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
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"] }
|
12
appkit/build.rs
Normal file
12
appkit/build.rs
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
46
appkit/src/alert.rs
Normal file
46
appkit/src/alert.rs
Normal file
|
@ -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<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
appkit/src/app/events.rs
Normal file
39
appkit/src/app/events.rs
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
169
appkit/src/app/mod.rs
Normal file
169
appkit/src/app/mod.rs
Normal file
|
@ -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<T = (), M = ()> {
|
||||||
|
pub inner: Id<Object>,
|
||||||
|
pub objc_delegate: Id<Object>,
|
||||||
|
pub delegate: Box<T>,
|
||||||
|
_t: std::marker::PhantomData<M>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Menu>) {
|
||||||
|
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<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate<Message = M> {
|
||||||
|
/// 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::<T>();
|
||||||
|
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<D: AppDelegate>(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<D: AppDelegate>(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<D: AppDelegate>() -> *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::<usize>(APP_PTR);
|
||||||
|
|
||||||
|
// Add callback methods
|
||||||
|
decl.add_method(sel!(applicationDidFinishLaunching:), did_finish_launching::<D> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationDidBecomeActive:), did_become_active::<D> as extern fn(&Object, _, _));
|
||||||
|
|
||||||
|
DELEGATE_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
DELEGATE_CLASS
|
||||||
|
}
|
||||||
|
}
|
99
appkit/src/bundle.rs
Normal file
99
appkit/src/bundle.rs
Normal file
|
@ -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<F>(bundle_id: &str, func: F) where F: MethodImplementation<Callee=Object> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
56
appkit/src/button.rs
Normal file
56
appkit/src/button.rs
Normal file
|
@ -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<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
27
appkit/src/events.rs
Normal file
27
appkit/src/events.rs
Normal file
|
@ -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;
|
||||||
|
}
|
30
appkit/src/file_panel/enums.rs
Normal file
30
appkit/src/file_panel/enums.rs
Normal file
|
@ -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<NSInteger> 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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
appkit/src/file_panel/mod.rs
Normal file
13
appkit/src/file_panel/mod.rs
Normal file
|
@ -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;
|
111
appkit/src/file_panel/save.rs
Normal file
111
appkit/src/file_panel/save.rs
Normal file
|
@ -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<Object>,
|
||||||
|
|
||||||
|
/// The internal `NSObject` that routes delegate callbacks around.
|
||||||
|
pub delegate: ShareId<Object>,
|
||||||
|
|
||||||
|
/// 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<F: Fn(Option<String>) + '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<String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
appkit/src/file_panel/select.rs
Normal file
167
appkit/src/file_panel/select.rs
Normal file
|
@ -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<Object>,
|
||||||
|
|
||||||
|
/// The internal `NSObject` that routes delegate callbacks around.
|
||||||
|
pub delegate: ShareId<Object>,
|
||||||
|
|
||||||
|
/// 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<F: Fn(Vec<String>) + '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<String> {
|
||||||
|
let mut paths: Vec<String> = 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
|
||||||
|
}
|
21
appkit/src/file_panel/traits.rs
Normal file
21
appkit/src/file_panel/traits.rs
Normal file
|
@ -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 }
|
||||||
|
}
|
0
appkit/src/layout/mod.rs
Normal file
0
appkit/src/layout/mod.rs
Normal file
67
appkit/src/lib.rs
Normal file
67
appkit/src/lib.rs
Normal file
|
@ -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<ShareId<Object>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
189
appkit/src/menu/item.rs
Normal file
189
appkit/src/menu/item.rs
Normal file
|
@ -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<Sel>, modifier: Option<NSUInteger>) -> 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<Object>),
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
54
appkit/src/menu/menu.rs
Normal file
54
appkit/src/menu/menu.rs
Normal file
|
@ -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<Object>,
|
||||||
|
pub items: Vec<MenuItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<MenuItem>) -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
appkit/src/menu/mod.rs
Normal file
7
appkit/src/menu/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
//! Module hoisting.
|
||||||
|
|
||||||
|
pub mod menu;
|
||||||
|
pub use menu::Menu;
|
||||||
|
|
||||||
|
pub mod item;
|
||||||
|
pub use item::MenuItem;
|
30
appkit/src/networking/mod.rs
Normal file
30
appkit/src/networking/mod.rs
Normal file
|
@ -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<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
appkit/src/notifications/center.rs
Normal file
62
appkit/src/notifications/center.rs
Normal file
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
appkit/src/notifications/mod.rs
Normal file
7
appkit/src/notifications/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
//! Hoisting.
|
||||||
|
|
||||||
|
pub mod center;
|
||||||
|
pub use center::*;
|
||||||
|
|
||||||
|
pub mod notifications;
|
||||||
|
pub use notifications::*;
|
36
appkit/src/notifications/notifications.rs
Normal file
36
appkit/src/notifications/notifications.rs
Normal file
|
@ -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<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
appkit/src/toolbar/item.rs
Normal file
72
appkit/src/toolbar/item.rs
Normal file
|
@ -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<Object>,
|
||||||
|
pub button: Option<Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToolbarItem<'a> {
|
||||||
|
/// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance),
|
||||||
|
/// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime
|
||||||
|
/// pointers.
|
||||||
|
pub fn new(identifier: &'a str) -> Self {
|
||||||
|
let inner = unsafe {
|
||||||
|
let identifier = NSString::alloc(nil).init_str(identifier);
|
||||||
|
let alloc: id = msg_send![class!(NSToolbarItem), alloc];
|
||||||
|
let item: id = msg_send![alloc, initWithItemIdentifier:identifier];
|
||||||
|
Id::from_ptr(item)
|
||||||
|
};
|
||||||
|
|
||||||
|
ToolbarItem {
|
||||||
|
identifier: identifier,
|
||||||
|
inner: inner,
|
||||||
|
button: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_title(&mut self, title: &str) {
|
||||||
|
unsafe {
|
||||||
|
let title = NSString::alloc(nil).init_str(title);
|
||||||
|
let _: () = msg_send![&*self.inner, setTitle:title];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_button(&mut self, button: Button) {
|
||||||
|
button.set_bezel_style(11);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![&*self.inner, setView:&*button.inner];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.button = Some(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_min_size(&mut self, width: f64, height: f64) {
|
||||||
|
unsafe {
|
||||||
|
let size = NSSize::new(width.into(), height.into());
|
||||||
|
let _: () = msg_send![&*self.inner, setMinSize:size];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_size(&mut self, width: f64, height: f64) {
|
||||||
|
unsafe {
|
||||||
|
let size = NSSize::new(width.into(), height.into());
|
||||||
|
let _: () = msg_send![&*self.inner, setMaxSize:size];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
appkit/src/toolbar/mod.rs
Normal file
7
appkit/src/toolbar/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
//! Module hoisting.
|
||||||
|
|
||||||
|
pub mod toolbar;
|
||||||
|
pub use toolbar::{Toolbar, ToolbarDelegate};
|
||||||
|
|
||||||
|
pub mod item;
|
||||||
|
pub use item::ToolbarItem;
|
130
appkit/src/toolbar/toolbar.rs
Normal file
130
appkit/src/toolbar/toolbar.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
//! Implements an NSToolbar, which is one of those macOS niceties
|
||||||
|
//! that makes it feel... "proper".
|
||||||
|
//!
|
||||||
|
//! UNFORTUNATELY, this is a very old and janky API. So... yeah.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil};
|
||||||
|
use cocoa::foundation::{NSArray, NSString};
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel};
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::toolbar::item::ToolbarItem;
|
||||||
|
use crate::utils::str_from;
|
||||||
|
|
||||||
|
static TOOLBAR_PTR: &str = "rstToolbarPtr";
|
||||||
|
|
||||||
|
/// A trait that you can implement to have your struct/etc act as an `NSToolbarDelegate`.
|
||||||
|
pub trait ToolbarDelegate {
|
||||||
|
/// What items are allowed in this toolbar.
|
||||||
|
fn allowed_item_identifiers(&self) -> Vec<&'static str>;
|
||||||
|
|
||||||
|
/// The default items in this toolbar.
|
||||||
|
fn default_item_identifiers(&self) -> Vec<&'static str>;
|
||||||
|
|
||||||
|
/// For a given `identifier`, return the `ToolbarItem` that should be displayed.
|
||||||
|
fn item_for(&self, _identifier: &str) -> ToolbarItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
|
||||||
|
/// where our `NSWindow` and associated delegate live.
|
||||||
|
pub struct Toolbar {
|
||||||
|
pub inner: Id<Object>,
|
||||||
|
pub objc_delegate: Id<Object>,
|
||||||
|
pub delegate: Box<dyn ToolbarDelegate>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Toolbar {
|
||||||
|
/// Creates a new `NSToolbar` instance, configures it appropriately, injects an `NSObject`
|
||||||
|
/// delegate wrapper, and retains the necessary Objective-C runtime pointers.
|
||||||
|
pub fn new<D: ToolbarDelegate + 'static>(identifier: &str, delegate: D) -> Self {
|
||||||
|
let inner = unsafe {
|
||||||
|
let identifier = NSString::alloc(nil).init_str(identifier);
|
||||||
|
let alloc: id = msg_send![Class::get("NSToolbar").unwrap(), alloc];
|
||||||
|
let toolbar: id = msg_send![alloc, initWithIdentifier:identifier];
|
||||||
|
Id::from_ptr(toolbar)
|
||||||
|
};
|
||||||
|
|
||||||
|
let toolbar_delegate = Box::new(delegate);
|
||||||
|
|
||||||
|
let objc_delegate = unsafe {
|
||||||
|
let delegate_class = register_delegate_class::<D>();
|
||||||
|
let objc_delegate: id = msg_send![delegate_class, new];
|
||||||
|
let delegate_ptr: *const D = &*toolbar_delegate;
|
||||||
|
(&mut *objc_delegate).set_ivar(TOOLBAR_PTR, delegate_ptr as usize);
|
||||||
|
let _: () = msg_send![&*inner, setDelegate:objc_delegate];
|
||||||
|
Id::from_ptr(objc_delegate)
|
||||||
|
};
|
||||||
|
|
||||||
|
Toolbar {
|
||||||
|
inner: inner,
|
||||||
|
objc_delegate: objc_delegate,
|
||||||
|
delegate: toolbar_delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loops back to the delegate.
|
||||||
|
extern fn allowed_item_identifiers<D: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id {
|
||||||
|
unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(TOOLBAR_PTR);
|
||||||
|
let toolbar = ptr as *mut D;
|
||||||
|
let identifiers = (*toolbar).allowed_item_identifiers().iter().map(|identifier| {
|
||||||
|
NSString::alloc(nil).init_str(identifier)
|
||||||
|
}).collect::<Vec<id>>();
|
||||||
|
|
||||||
|
NSArray::arrayWithObjects(nil, &identifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loops back to the delegate.
|
||||||
|
extern fn default_item_identifiers<D: ToolbarDelegate>(this: &Object, _: Sel, _: id) -> id {
|
||||||
|
unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(TOOLBAR_PTR);
|
||||||
|
let toolbar = ptr as *mut D;
|
||||||
|
let identifiers = (*toolbar).default_item_identifiers().iter().map(|identifier| {
|
||||||
|
NSString::alloc(nil).init_str(identifier)
|
||||||
|
}).collect::<Vec<id>>();
|
||||||
|
|
||||||
|
NSArray::arrayWithObjects(nil, &identifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loops back to the delegate.
|
||||||
|
extern fn item_for_identifier<D: ToolbarDelegate>(this: &Object, _: Sel, _: id, identifier: id, _: id) -> id {
|
||||||
|
unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(TOOLBAR_PTR);
|
||||||
|
let toolbar = ptr as *mut D;
|
||||||
|
let identifier = str_from(identifier);
|
||||||
|
let mut item = (*toolbar).item_for(identifier);
|
||||||
|
&mut *item.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers an `NSObject` subclass, and configures it to hold some ivars for various things we need
|
||||||
|
/// to store.
|
||||||
|
fn register_delegate_class<D: ToolbarDelegate>() -> *const Class {
|
||||||
|
static mut TOOLBAR_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("RSTToolbarDelegate", superclass).unwrap();
|
||||||
|
|
||||||
|
// For callbacks
|
||||||
|
decl.add_ivar::<usize>(TOOLBAR_PTR);
|
||||||
|
|
||||||
|
// Add callback methods
|
||||||
|
decl.add_method(sel!(toolbarAllowedItemIdentifiers:), allowed_item_identifiers::<D> as extern fn(&Object, _, _) -> id);
|
||||||
|
decl.add_method(sel!(toolbarDefaultItemIdentifiers:), default_item_identifiers::<D> as extern fn(&Object, _, _) -> id);
|
||||||
|
decl.add_method(sel!(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:), item_for_identifier::<D> as extern fn(&Object, _, _, _, _) -> id);
|
||||||
|
|
||||||
|
TOOLBAR_DELEGATE_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { TOOLBAR_DELEGATE_CLASS }
|
||||||
|
}
|
25
appkit/src/utils.rs
Normal file
25
appkit/src/utils.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
//! Utils is a dumping ground for various methods that don't really have a particular module they
|
||||||
|
//! belong to. These are typically internal, and if you rely on them... well, don't be surprised if
|
||||||
|
//! they go away one day.
|
||||||
|
|
||||||
|
use std::{slice, str};
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
|
use cocoa::base::id;
|
||||||
|
use cocoa::foundation::NSString;
|
||||||
|
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
/// A utility method for taking an `NSString` and bridging it to a Rust `&str`.
|
||||||
|
pub fn str_from(nsstring: id) -> &'static str {
|
||||||
|
unsafe {
|
||||||
|
let bytes = {
|
||||||
|
let bytes: *const c_char = msg_send![nsstring, UTF8String];
|
||||||
|
bytes as *const u8
|
||||||
|
};
|
||||||
|
|
||||||
|
let len = nsstring.len();
|
||||||
|
let bytes = slice::from_raw_parts(bytes, len);
|
||||||
|
str::from_utf8(bytes).unwrap()
|
||||||
|
}
|
||||||
|
}
|
55
appkit/src/view/class.rs
Normal file
55
appkit/src/view/class.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
|
||||||
|
//! modern era.
|
||||||
|
//!
|
||||||
|
//! I kid, I kid.
|
||||||
|
//!
|
||||||
|
//! It just enforces that coordinates are judged from the top-left, which is what most people look
|
||||||
|
//! for in the modern era. It also implements a few helpers for things like setting a background
|
||||||
|
//! color, and enforcing layer backing by default.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
|
||||||
|
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn update_layer(this: &Object, _: Sel) {
|
||||||
|
unsafe {
|
||||||
|
let background_color: id = msg_send![class!(NSColor), redColor];
|
||||||
|
if background_color != nil {
|
||||||
|
let layer: id = msg_send![this, layer];
|
||||||
|
let cg: id = msg_send![background_color, CGColor];
|
||||||
|
let _: () = msg_send![layer, setBackgroundColor:cg];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
|
||||||
|
/// need to do.
|
||||||
|
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::get("NSView").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("RSTView", superclass).unwrap();
|
||||||
|
|
||||||
|
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
decl.add_method(sel!(requiresConstraintBasedLayout), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
|
||||||
|
|
||||||
|
VIEW_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
VIEW_CLASS
|
||||||
|
}
|
||||||
|
}
|
72
appkit/src/view/controller.rs
Normal file
72
appkit/src/view/controller.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
//! Hoists a basic NSView. In our current particular use case,
|
||||||
|
//! this is primarily used as the ContentView for a window. From there,
|
||||||
|
//! we configure an NSToolbar and WKWebview on top of them.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
use cocoa::base::{id, YES};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize};
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
/// A trait for handling the view lifecycle.
|
||||||
|
pub trait View {
|
||||||
|
fn did_load(&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
|
||||||
|
/// where our `NSWindow` and associated delegate live.
|
||||||
|
pub struct View {
|
||||||
|
pub inner: Id<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
/// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance),
|
||||||
|
/// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime
|
||||||
|
/// pointers.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let inner = unsafe {
|
||||||
|
let rect_zero = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
|
||||||
|
let alloc: id = msg_send![register_class(), alloc];
|
||||||
|
let view: id = msg_send![alloc, initWithFrame:rect_zero];
|
||||||
|
let _: () = msg_send![view, setWantsLayer:YES];
|
||||||
|
let _: () = msg_send![view, setLayerContentsRedrawPolicy:1];
|
||||||
|
Id::from_ptr(view)
|
||||||
|
};
|
||||||
|
|
||||||
|
View {
|
||||||
|
inner: inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is used for some specific calls, where macOS NSView needs to be
|
||||||
|
/// forcefully dragged into the modern age (e.g, position coordinates from top left...).
|
||||||
|
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers an `NSView` 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("NSView").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("SBAView", superclass).unwrap();
|
||||||
|
|
||||||
|
// Force NSView to render from the top-left, not bottom-left
|
||||||
|
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
|
||||||
|
// Request optimized backing layers
|
||||||
|
decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
|
||||||
|
VIEW_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { VIEW_CLASS }
|
||||||
|
}
|
2
appkit/src/view/mod.rs
Normal file
2
appkit/src/view/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
pub(crate) mod class;
|
127
appkit/src/webview/action.rs
Normal file
127
appkit/src/webview/action.rs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
//! Implements wrappers around `WKNavigationAction` and `WKNavigationActionPolicy`.
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::NSInteger;
|
||||||
|
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::networking::URLRequest;
|
||||||
|
|
||||||
|
pub enum NavigationType {
|
||||||
|
LinkActivated,
|
||||||
|
FormSubmitted,
|
||||||
|
BackForward,
|
||||||
|
Reload,
|
||||||
|
FormResubmitted,
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NSInteger> for NavigationType {
|
||||||
|
fn from(i: NSInteger) -> Self {
|
||||||
|
match i {
|
||||||
|
-1 => NavigationType::Other,
|
||||||
|
0 => NavigationType::LinkActivated,
|
||||||
|
1 => NavigationType::FormSubmitted,
|
||||||
|
2 => NavigationType::BackForward,
|
||||||
|
3 => NavigationType::Reload,
|
||||||
|
4 => NavigationType::FormResubmitted,
|
||||||
|
e => { panic!("Unsupported navigation type: {}", e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NavigationAction {
|
||||||
|
pub navigation_type: NavigationType,
|
||||||
|
pub request: URLRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigationAction {
|
||||||
|
pub fn new(action: id) -> Self {
|
||||||
|
NavigationAction {
|
||||||
|
navigation_type: unsafe {
|
||||||
|
let nav_type: NSInteger = msg_send![action, navigationType];
|
||||||
|
nav_type.into()
|
||||||
|
},
|
||||||
|
|
||||||
|
request: URLRequest::with(unsafe {
|
||||||
|
msg_send![action, request]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NavigationPolicy {
|
||||||
|
Cancel,
|
||||||
|
Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<NSInteger> for NavigationPolicy {
|
||||||
|
fn into(self) -> NSInteger {
|
||||||
|
match self {
|
||||||
|
NavigationPolicy::Cancel => 0,
|
||||||
|
NavigationPolicy::Allow => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NavigationResponse {
|
||||||
|
pub can_show_mime_type: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavigationResponse {
|
||||||
|
pub fn new(response: id) -> Self {
|
||||||
|
NavigationResponse {
|
||||||
|
can_show_mime_type: unsafe {
|
||||||
|
let canShow: BOOL = msg_send![response, canShowMIMEType];
|
||||||
|
if canShow == YES { true } else { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NavigationResponsePolicy {
|
||||||
|
Cancel = 0,
|
||||||
|
Allow = 1,
|
||||||
|
|
||||||
|
// This is a private API!
|
||||||
|
BecomeDownload = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<NSInteger> for NavigationResponsePolicy {
|
||||||
|
fn into(self) -> NSInteger {
|
||||||
|
match self {
|
||||||
|
NavigationResponsePolicy::Cancel => 0,
|
||||||
|
NavigationResponsePolicy::Allow => 1,
|
||||||
|
NavigationResponsePolicy::BecomeDownload => 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct OpenPanelParameters {
|
||||||
|
pub allows_directories: bool,
|
||||||
|
pub allows_multiple_selection: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<id> for OpenPanelParameters {
|
||||||
|
fn from(params: id) -> Self {
|
||||||
|
OpenPanelParameters {
|
||||||
|
allows_directories: unsafe {
|
||||||
|
match msg_send![params, allowsDirectories] {
|
||||||
|
YES => true,
|
||||||
|
NO => false,
|
||||||
|
_ => { panic!("Invalid value from WKOpenPanelParameters:allowsDirectories"); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
allows_multiple_selection: unsafe {
|
||||||
|
match msg_send![params, allowsMultipleSelection] {
|
||||||
|
YES => true,
|
||||||
|
NO => false,
|
||||||
|
_ => { panic!("Invalid value from WKOpenPanelParameters:allowsMultipleSelection"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
appkit/src/webview/config.rs
Normal file
46
appkit/src/webview/config.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
//! A wrapper for `WKWebViewConfiguration`. It aims to (mostly) cover
|
||||||
|
//! the important pieces of configuring and updating a WebView configuration.
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::NSString;
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::runtime::Object;
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::webview::process_pool::register_process_pool;
|
||||||
|
|
||||||
|
/// Whether a script should be injected at the start or end of the document load.
|
||||||
|
pub enum InjectAt {
|
||||||
|
Start = 0,
|
||||||
|
End = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper for `WKWebViewConfiguration`. Holds (retains) pointers for the Objective-C runtime
|
||||||
|
/// where everything lives.
|
||||||
|
pub struct WebViewConfig {
|
||||||
|
pub inner: Id<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WebViewConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
let inner = unsafe {
|
||||||
|
let cls = class!(WKWebViewConfiguration);
|
||||||
|
let inner: id = msg_send![cls, new];
|
||||||
|
|
||||||
|
// For debug builds, we want to enable this as it allows the inspector to be used.
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let key = NSString::alloc(nil).init_str("developerExtrasEnabled");
|
||||||
|
let yes: id = msg_send![class!(NSNumber), numberWithBool:YES];
|
||||||
|
let preferences: id = msg_send![inner, preferences];
|
||||||
|
let _: () = msg_send![preferences, setValue:yes forKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
Id::from_ptr(inner)
|
||||||
|
};
|
||||||
|
|
||||||
|
WebViewConfig {
|
||||||
|
inner: inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
245
appkit/src/webview/controller.rs
Normal file
245
appkit/src/webview/controller.rs
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
//! Hoists a basic NSView. In our current particular use case,
|
||||||
|
//! this is primarily used as the ContentView for a window. From there,
|
||||||
|
//! we configure an NSToolbar and WKWebview on top of them.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use block::Block;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSArray, NSInteger};
|
||||||
|
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::ViewController;
|
||||||
|
use crate::view::class::register_view_class;
|
||||||
|
use crate::webview::action::{NavigationAction, NavigationResponse, OpenPanelParameters};
|
||||||
|
use crate::webview::{WEBVIEW_VAR, WEBVIEW_CONFIG_VAR, WEBVIEW_CONTROLLER_PTR};
|
||||||
|
use crate::webview::traits::WebViewController;
|
||||||
|
use crate::utils::str_from;
|
||||||
|
|
||||||
|
/// Loads and configures ye old WKWebView/View for this controller.
|
||||||
|
extern fn load_view<T: ViewController + WebViewController>(this: &mut Object, _: Sel) {
|
||||||
|
unsafe {
|
||||||
|
let configuration: id = *this.get_ivar(WEBVIEW_CONFIG_VAR);
|
||||||
|
|
||||||
|
// Technically private!
|
||||||
|
let process_pool: id = msg_send![configuration, processPool];
|
||||||
|
let _: () = msg_send![process_pool, _setDownloadDelegate:&*this];
|
||||||
|
|
||||||
|
let zero = NSRect::new(NSPoint::new(0., 0.), NSSize::new(1000., 600.));
|
||||||
|
let webview_alloc: id = msg_send![class!(WKWebView), alloc];
|
||||||
|
let webview: id = msg_send![webview_alloc, initWithFrame:zero configuration:configuration];
|
||||||
|
let _: () = msg_send![webview, setWantsLayer:YES];
|
||||||
|
let _: () = msg_send![webview, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||||
|
|
||||||
|
// Provide an easy way to grab this later
|
||||||
|
(*this).set_ivar(WEBVIEW_VAR, webview);
|
||||||
|
|
||||||
|
// Clean this up to be safe, as WKWebView makes a copy and we don't need it anymore.
|
||||||
|
(*this).set_ivar(WEBVIEW_CONFIG_VAR, nil);
|
||||||
|
|
||||||
|
// Note that we put this in a backing NSView to handle an edge case - if someone sets the
|
||||||
|
// WKWebView as the content view of a window, and the inspector is able to be activated,
|
||||||
|
// it'll try to (at first) add the inspector to the parent view of the WKWebView... which
|
||||||
|
// is undefined behavior.
|
||||||
|
let view: id = msg_send![register_view_class(), new];
|
||||||
|
let _: () = msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||||
|
let _: () = msg_send![view, setFrame:zero];
|
||||||
|
let _: () = msg_send![view, addSubview:webview];
|
||||||
|
|
||||||
|
let constraint = class!(NSLayoutConstraint);
|
||||||
|
let constraints = NSArray::arrayWithObjects(nil, &vec![
|
||||||
|
msg_send![constraint, constraintWithItem:webview attribute:7 relatedBy:0 toItem:view attribute:7 multiplier:1.0 constant:0.0],
|
||||||
|
msg_send![constraint, constraintWithItem:webview attribute:8 relatedBy:0 toItem:view attribute:8 multiplier:1.0 constant:0.0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let _: () = msg_send![constraint, activateConstraints:constraints];
|
||||||
|
let _: () = msg_send![this, setView:view];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to connect delegates - doing this in `loadView` can be... bug-inducing.
|
||||||
|
extern fn view_did_load<T: ViewController + WebViewController>(this: &Object, _: Sel) {
|
||||||
|
unsafe {
|
||||||
|
let webview: id = *this.get_ivar(WEBVIEW_VAR);
|
||||||
|
let _: () = msg_send![webview, setNavigationDelegate:&*this];
|
||||||
|
let _: () = msg_send![webview, setUIDelegate:&*this];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when an `alert()` from the underlying `WKWebView` is fired. Will call over to your
|
||||||
|
/// `WebViewController`, where you should handle the event.
|
||||||
|
extern fn alert<T: WebViewController + 'static>(_: &Object, _: Sel, _: id, s: id, _: id, complete: id) {
|
||||||
|
let alert = str_from(s);
|
||||||
|
println!("Alert: {}", alert);
|
||||||
|
|
||||||
|
// @TODO: This is technically (I think?) a private method, and there's some other dance that
|
||||||
|
// needs to be done here involving taking the pointer/invoke/casting... but this is fine for
|
||||||
|
// now as it's being exposed purely for debugging.
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![complete, invoke];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
(*webview).alert(alert);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*let queue = dispatch::Queue::main();
|
||||||
|
queue.exec_async(move || {
|
||||||
|
let a = Alert::new("Subatomic", message);
|
||||||
|
a.show();
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when a message has been passed from the underlying `WKWebView`.
|
||||||
|
extern fn on_message<T: WebViewController + 'static>(this: &Object, _: Sel, _: id, script_message: id) {
|
||||||
|
unsafe {
|
||||||
|
let name = str_from(msg_send![script_message, name]);
|
||||||
|
let body = str_from(msg_send![script_message, body]);
|
||||||
|
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
(*webview).on_message(name, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when deciding a navigation policy - i.e, should something be allowed or not.
|
||||||
|
extern fn decide_policy_for_action<T: WebViewController + 'static>(this: &Object, _: Sel, _: id, action: id, handler: usize) {
|
||||||
|
let webview = unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
&*webview
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = NavigationAction::new(action);
|
||||||
|
webview.policy_for_navigation_action(action, |policy| {
|
||||||
|
// This is very sketch and should be heavily checked. :|
|
||||||
|
unsafe {
|
||||||
|
let handler = handler as *const Block<(NSInteger,), c_void>;
|
||||||
|
(*handler).call((policy.into(),));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when deciding a navigation policy - i.e, should something be allowed or not.
|
||||||
|
extern fn decide_policy_for_response<T: WebViewController + 'static>(this: &Object, _: Sel, _: id, response: id, handler: usize) {
|
||||||
|
let webview = unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
&*webview
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = NavigationResponse::new(response);
|
||||||
|
webview.policy_for_navigation_response(response, |policy| {
|
||||||
|
// This is very sketch and should be heavily checked. :|
|
||||||
|
unsafe {
|
||||||
|
let handler = handler as *const Block<(NSInteger,), c_void>;
|
||||||
|
(*handler).call((policy.into(),));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when deciding a navigation policy - i.e, should something be allowed or not.
|
||||||
|
extern fn run_open_panel<T: WebViewController + 'static>(this: &Object, _: Sel, _: id, params: id, _: id, handler: usize) {
|
||||||
|
let webview = unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
&*webview
|
||||||
|
};
|
||||||
|
|
||||||
|
webview.run_open_panel(params.into(), move |urls| {
|
||||||
|
// This is very sketch and should be heavily checked. :|
|
||||||
|
unsafe {
|
||||||
|
let handler = handler as *const Block<(id,), c_void>;
|
||||||
|
|
||||||
|
match urls {
|
||||||
|
Some(u) => {
|
||||||
|
let nsurls: Vec<id> = u.iter().map(|s| {
|
||||||
|
let s = NSString::alloc(nil).init_str(&s);
|
||||||
|
msg_send![class!(NSURL), URLWithString:s]
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let array = NSArray::arrayWithObjects(nil, &nsurls);
|
||||||
|
(*handler).call((array,));
|
||||||
|
},
|
||||||
|
|
||||||
|
None => { (*handler).call((nil,)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn handle_download<T: WebViewController + 'static>(this: &Object, _: Sel, download: id, suggested_filename: id, handler: usize) {
|
||||||
|
let webview = unsafe {
|
||||||
|
let ptr: usize = *this.get_ivar(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
let webview = ptr as *const T;
|
||||||
|
&*webview
|
||||||
|
};
|
||||||
|
|
||||||
|
let handler = handler as *const Block<(BOOL, id), c_void>;
|
||||||
|
let filename = str_from(suggested_filename);
|
||||||
|
|
||||||
|
webview.run_save_panel(filename, move |can_overwrite, path| unsafe {
|
||||||
|
if path.is_none() {
|
||||||
|
let _: () = msg_send![download, cancel];
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Saving to Path: {:?}", path);
|
||||||
|
let path = NSString::alloc(nil).init_str(&path.unwrap());
|
||||||
|
|
||||||
|
(*handler).call((match can_overwrite {
|
||||||
|
true => YES,
|
||||||
|
false => NO
|
||||||
|
}, path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Registers an `NSViewController` that we effectively turn into a `WebViewController`. Acts as
|
||||||
|
/// both a subclass of `NSViewController` and a delegate of the held `WKWebView` (for the various
|
||||||
|
/// varieties of delegates needed there).
|
||||||
|
pub fn register_controller_class<
|
||||||
|
T: ViewController + WebViewController + 'static,
|
||||||
|
>() -> *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("NSViewController").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("RSTWebViewController", superclass).unwrap();
|
||||||
|
|
||||||
|
decl.add_ivar::<id>(WEBVIEW_CONFIG_VAR);
|
||||||
|
decl.add_ivar::<id>(WEBVIEW_VAR);
|
||||||
|
decl.add_ivar::<usize>(WEBVIEW_CONTROLLER_PTR);
|
||||||
|
|
||||||
|
// NSViewController
|
||||||
|
decl.add_method(sel!(loadView), load_view::<T> as extern fn(&mut Object, _));
|
||||||
|
decl.add_method(sel!(viewDidLoad), view_did_load::<T> as extern fn(&Object, _));
|
||||||
|
|
||||||
|
// WKNavigationDelegate
|
||||||
|
decl.add_method(sel!(webView:decidePolicyForNavigationAction:decisionHandler:), decide_policy_for_action::<T> as extern fn(&Object, _, _, id, usize));
|
||||||
|
decl.add_method(sel!(webView:decidePolicyForNavigationResponse:decisionHandler:), decide_policy_for_response::<T> as extern fn(&Object, _, _, id, usize));
|
||||||
|
|
||||||
|
// WKScriptMessageHandler
|
||||||
|
decl.add_method(sel!(userContentController:didReceiveScriptMessage:), on_message::<T> as extern fn(&Object, _, _, id));
|
||||||
|
|
||||||
|
// WKUIDelegate
|
||||||
|
decl.add_method(sel!(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:), alert::<T> as extern fn(&Object, _, _, id, _, _));
|
||||||
|
decl.add_method(sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), run_open_panel::<T> as extern fn(&Object, _, _, id, _, usize));
|
||||||
|
|
||||||
|
// WKDownloadDelegate is a private class on macOS that handles downloading (saving) files.
|
||||||
|
// It's absurd that this is still private in 2020. This probably couldn't get into the app
|
||||||
|
// store, so... screw it, fine for now.
|
||||||
|
decl.add_method(sel!(_download:decideDestinationWithSuggestedFilename:completionHandler:), handle_download::<T> as extern fn(&Object, _, id, id, usize));
|
||||||
|
|
||||||
|
VIEW_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { VIEW_CLASS }
|
||||||
|
}
|
19
appkit/src/webview/mod.rs
Normal file
19
appkit/src/webview/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
//! A wrapper for WKWebview and associated configurations and properties.
|
||||||
|
|
||||||
|
pub(crate) static WEBVIEW_CONFIG_VAR: &str = "rstWebViewConfig";
|
||||||
|
pub(crate) static WEBVIEW_VAR: &str = "rstWebView";
|
||||||
|
pub(crate) static WEBVIEW_CONTROLLER_PTR: &str = "rstWebViewControllerPtr";
|
||||||
|
|
||||||
|
pub mod action;
|
||||||
|
|
||||||
|
pub(crate) mod controller;
|
||||||
|
pub(crate) mod process_pool;
|
||||||
|
|
||||||
|
pub mod traits;
|
||||||
|
pub use traits::{WebViewController};
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub use config::{WebViewConfig, InjectAt};
|
||||||
|
|
||||||
|
pub mod webview;
|
||||||
|
pub use webview::WebView;
|
44
appkit/src/webview/process_pool.rs
Normal file
44
appkit/src/webview/process_pool.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
//! Implements a shared `WKProcessPool`, so that multiple webviews (should they be needed) properly
|
||||||
|
//! share cookies and the like. It also, if you opt in to the feature flag, enables a download
|
||||||
|
//! delegate that's sadly a private API... in 2020.
|
||||||
|
//!
|
||||||
|
//! If you use that feature, there are no guarantees you'll be accepted into the App Store.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use block::Block;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSArray, NSInteger};
|
||||||
|
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::webview::traits::WebViewController;
|
||||||
|
|
||||||
|
extern fn download_delegate(this: &Object, _: Sel) -> id {
|
||||||
|
println!("YO!");
|
||||||
|
unsafe {
|
||||||
|
NSString::alloc(nil).init_str("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_process_pool() -> *const Object {
|
||||||
|
static mut PROCESS_POOL: *const Object = 0 as *const Object;
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
|
INIT.call_once(|| unsafe {
|
||||||
|
let superclass = Class::get("WKProcessPool").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("RSTWebViewProcessPool", superclass).unwrap();
|
||||||
|
|
||||||
|
//decl.add_ivar::<id>(DOWNLOAD_DELEGATE_PTR);
|
||||||
|
decl.add_method(sel!(_downloadDelegate), download_delegate as extern fn(&Object, _) -> id);
|
||||||
|
|
||||||
|
//PROCESS_POOL = decl.register();
|
||||||
|
PROCESS_POOL = msg_send![decl.register(), new];
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { PROCESS_POOL }
|
||||||
|
}
|
22
appkit/src/webview/traits.rs
Normal file
22
appkit/src/webview/traits.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
use crate::webview::action::{NavigationAction, NavigationPolicy, NavigationResponse, NavigationResponsePolicy, OpenPanelParameters};
|
||||||
|
|
||||||
|
pub trait WebViewController {
|
||||||
|
fn on_message(&self, name: &str, body: &str) {}
|
||||||
|
|
||||||
|
fn policy_for_navigation_action<F: Fn(NavigationPolicy)>(&self, action: NavigationAction, handler: F) {
|
||||||
|
handler(NavigationPolicy::Allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn policy_for_navigation_response<F: Fn(NavigationResponsePolicy)>(&self, response: NavigationResponse, handler: F) {
|
||||||
|
handler(NavigationResponsePolicy::Allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_open_panel<F: Fn(Option<Vec<String>>) + 'static>(&self, parameters: OpenPanelParameters, handler: F) {
|
||||||
|
handler(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_save_panel<F: Fn(bool, Option<String>) + 'static>(&self, suggested_filename: &str, handler: F) {
|
||||||
|
handler(false, None);
|
||||||
|
}
|
||||||
|
}
|
129
appkit/src/webview/webview.rs
Normal file
129
appkit/src/webview/webview.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
//! Implements a WebView, which wraps a number of different classes/delegates/controllers into one
|
||||||
|
//! useful interface. This encompasses...
|
||||||
|
//!
|
||||||
|
//! - `WKWebView`
|
||||||
|
//! - `WKUIDelegate`
|
||||||
|
//! - `WKScriptMessageHandler`
|
||||||
|
//! - `NSViewController`
|
||||||
|
//!
|
||||||
|
//! ...yeah.
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString};
|
||||||
|
|
||||||
|
use objc_id::ShareId;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::ViewController;
|
||||||
|
use crate::webview::{WEBVIEW_VAR, WEBVIEW_CONFIG_VAR, WEBVIEW_CONTROLLER_PTR};
|
||||||
|
use crate::webview::WebViewController;
|
||||||
|
use crate::webview::controller::register_controller_class;
|
||||||
|
|
||||||
|
use crate::webview::config::{WebViewConfig, InjectAt};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WebViewInner {
|
||||||
|
pub config: WebViewConfig,
|
||||||
|
pub controller: Option<ShareId<Object>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebViewInner {
|
||||||
|
pub fn configure<T: ViewController + WebViewController + 'static>(&mut self, controller: &T) {
|
||||||
|
self.controller = Some(unsafe {
|
||||||
|
let view_controller: id = msg_send![register_controller_class::<T>(), new];
|
||||||
|
(&mut *view_controller).set_ivar(WEBVIEW_CONFIG_VAR, &*self.config.inner);
|
||||||
|
(&mut *view_controller).set_ivar(WEBVIEW_CONTROLLER_PTR, controller as *const T as usize);
|
||||||
|
ShareId::from_ptr(view_controller)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder pattern?
|
||||||
|
// let webview: id = msg_send![view_controller, view];
|
||||||
|
// let _: () = msg_send![webview, setUIDelegate:view_controller];
|
||||||
|
|
||||||
|
pub fn add_user_script(&self, script: &str, inject_at: InjectAt, main_frame_only: bool) {
|
||||||
|
unsafe {
|
||||||
|
let source = NSString::alloc(nil).init_str(script);
|
||||||
|
|
||||||
|
let cls = class!(WKUserScript);
|
||||||
|
let alloc: id = msg_send![cls, alloc];
|
||||||
|
let user_script: id = msg_send![alloc, initWithSource:source injectionTime:inject_at forMainFrameOnly:match main_frame_only {
|
||||||
|
true => YES,
|
||||||
|
false => NO
|
||||||
|
}];
|
||||||
|
|
||||||
|
let content_controller: id = msg_send![&*self.config.inner, userContentController];
|
||||||
|
let _: () = msg_send![content_controller, addUserScript:user_script];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_handler(&self, name: &str) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let name = NSString::alloc(nil).init_str(name);
|
||||||
|
let content_controller: id = msg_send![&*self.config.inner, userContentController];
|
||||||
|
let _: () = msg_send![content_controller, addScriptMessageHandler:controller.clone() name:name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_url(&self, url: &str) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
// This is weird, I know, but it has to be done due to a lifecycle "quirk" in AppKit.
|
||||||
|
// In short: `loadView` isn't called unless the view is actually accessed, and you
|
||||||
|
// could theoretically call this without having had it done. We use the `loadView`
|
||||||
|
// method because we *want* the lazy loading aspect, but for this call to work we
|
||||||
|
// need things to be done.
|
||||||
|
//
|
||||||
|
// We can't create the `WKWebView` before `loadView` as it copies
|
||||||
|
// `WKWebViewConfiguration` on initialization, and we defer that for API reasons.
|
||||||
|
let _view: id = msg_send![*controller, view];
|
||||||
|
|
||||||
|
let url_string = NSString::alloc(nil).init_str(url);
|
||||||
|
let u: id = msg_send![class!(NSURL), URLWithString:url_string];
|
||||||
|
let request: id = msg_send![class!(NSURLRequest), requestWithURL:u];
|
||||||
|
let webview: id = *controller.get_ivar(WEBVIEW_VAR);
|
||||||
|
let _: () = msg_send![webview, loadRequest:request];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WebView(Rc<RefCell<WebViewInner>>);
|
||||||
|
|
||||||
|
impl WebView {
|
||||||
|
pub fn configure<T: ViewController + WebViewController + 'static>(&self, controller: &T) {
|
||||||
|
{
|
||||||
|
let mut webview = self.0.borrow_mut();
|
||||||
|
webview.configure(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.did_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_handle(&self) -> Option<ShareId<Object>> {
|
||||||
|
let webview = self.0.borrow();
|
||||||
|
webview.controller.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_url(&self, url: &str) {
|
||||||
|
let webview = self.0.borrow();
|
||||||
|
webview.load_url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_user_script(&self, script: &str, inject_at: InjectAt, main_frame_only: bool) {
|
||||||
|
let webview = self.0.borrow();
|
||||||
|
webview.add_user_script(script, inject_at, main_frame_only);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_handler(&self, name: &str) {
|
||||||
|
let webview = self.0.borrow();
|
||||||
|
webview.add_handler(name);
|
||||||
|
}
|
||||||
|
}
|
54
appkit/src/window/config.rs
Normal file
54
appkit/src/window/config.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
//! Implements an `NSWindow` wrapper for MacOS, backed by
|
||||||
|
//! Cocoa and associated widgets. This also handles looping back
|
||||||
|
//! lifecycle events, such as window resizing or close events.
|
||||||
|
|
||||||
|
use cocoa::base::{id, YES, NO};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSUInteger};
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::runtime::Object;
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals, non_snake_case)]
|
||||||
|
pub mod WindowStyle {
|
||||||
|
use cocoa::foundation::NSUInteger;
|
||||||
|
|
||||||
|
pub const Borderless: NSUInteger = 0;
|
||||||
|
pub const Titled: NSUInteger = 1 << 0;
|
||||||
|
pub const Closable: NSUInteger = 1 << 1;
|
||||||
|
pub const Miniaturizable: NSUInteger = 1 << 2;
|
||||||
|
pub const Resizable: NSUInteger = 1 << 3;
|
||||||
|
pub const UnifiedTitleAndToolbar: NSUInteger = 1 << 12;
|
||||||
|
pub const FullScreen: NSUInteger = 1 << 14;
|
||||||
|
pub const FullSizeContentView: NSUInteger = 1 << 15;
|
||||||
|
pub const Utility: NSUInteger = 1 << 4;
|
||||||
|
pub const DocModalWindow: NSUInteger = 1 << 6;
|
||||||
|
pub const NonActivatingPanel: NSUInteger = 1 << 7;
|
||||||
|
pub const HUDWindow: NSUInteger = 1 << 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WindowConfig(pub Id<Object>);
|
||||||
|
|
||||||
|
impl Default for WindowConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
WindowConfig(unsafe {
|
||||||
|
let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(800., 600.));
|
||||||
|
|
||||||
|
let style = WindowStyle::Resizable | WindowStyle::Miniaturizable | WindowStyle::UnifiedTitleAndToolbar |
|
||||||
|
WindowStyle::Closable | WindowStyle::Titled;
|
||||||
|
|
||||||
|
let alloc: id = msg_send![class!(NSWindow), alloc];
|
||||||
|
let window: id = msg_send![alloc, initWithContentRect:dimensions styleMask:style backing:2 as NSUInteger defer:YES];
|
||||||
|
let _: () = msg_send![window, autorelease];
|
||||||
|
|
||||||
|
let _: () = msg_send![window, setTitlebarAppearsTransparent:NO];
|
||||||
|
|
||||||
|
// This is very important! NSWindow is an old class and has some behavior that we need
|
||||||
|
// to disable, like... this. If we don't set this, we'll segfault entirely because the
|
||||||
|
// Objective-C runtime gets out of sync.
|
||||||
|
let _: () = msg_send![window, setReleasedWhenClosed:NO];
|
||||||
|
|
||||||
|
Id::from_ptr(window)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
49
appkit/src/window/controller.rs
Normal file
49
appkit/src/window/controller.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
//! Everything useful for the `WindowController`. Handles injecting an `NSWindowController` subclass
|
||||||
|
//! into the Objective C runtime, which loops back to give us lifecycle methods.
|
||||||
|
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel};
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::window::WindowController;
|
||||||
|
|
||||||
|
static WINDOW_CONTROLLER_PTR: &str = "rstWindowController";
|
||||||
|
|
||||||
|
/// Called when an `NSWindow` receives a `windowWillClose:` event.
|
||||||
|
/// Good place to clean up memory and what not.
|
||||||
|
extern fn will_close<T: WindowController>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let window_ptr: usize = *this.get_ivar(WINDOW_CONTROLLER_PTR);
|
||||||
|
let window = window_ptr as *const T;
|
||||||
|
(*window).will_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects an `NSWindowController` subclass, with some callback and pointer ivars for what we
|
||||||
|
/// need to do.
|
||||||
|
pub(crate) fn register_window_controller_class<T: WindowController + 'static>() -> *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("NSWindowController").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("RSTWindowController", superclass).unwrap();
|
||||||
|
|
||||||
|
decl.add_ivar::<usize>(WINDOW_CONTROLLER_PTR);
|
||||||
|
|
||||||
|
// Subclassed methods
|
||||||
|
|
||||||
|
// NSWindowDelegate methods
|
||||||
|
decl.add_method(sel!(windowWillClose:), will_close::<T> as extern fn(&Object, _, _));
|
||||||
|
|
||||||
|
DELEGATE_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
DELEGATE_CLASS
|
||||||
|
}
|
||||||
|
}
|
12
appkit/src/window/mod.rs
Normal file
12
appkit/src/window/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
//! Implements wrappers and traits for `NSWindowController` and associated types.
|
||||||
|
|
||||||
|
pub mod traits;
|
||||||
|
pub use traits::{WindowController, WindowWrapper};
|
||||||
|
|
||||||
|
mod controller;
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub use config::{WindowConfig, WindowStyle};
|
||||||
|
|
||||||
|
pub mod window;
|
||||||
|
pub use window::{Window, WindowTitleVisibility};
|
130
appkit/src/window/traits.rs
Normal file
130
appkit/src/window/traits.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
//! A module to house the traits used throughout this window
|
||||||
|
//! module. There's a few different ones, and it's just... cleaner, if
|
||||||
|
//! it's organized here.
|
||||||
|
|
||||||
|
use crate::window::WindowConfig;
|
||||||
|
|
||||||
|
/// `WindowController` is a trait that handles providing higher level methods
|
||||||
|
/// that map into platform specific methods. Typically, you won't want to (or at least, won't need
|
||||||
|
/// to) implement this yourself - simply derive `WindowWrapper` and it'll work for you.
|
||||||
|
///
|
||||||
|
/// By deriving or implementing this, you get usable methods on your struct - for example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use appkit::{AppDelegate, Window, WindowController, WindowWrapper};
|
||||||
|
///
|
||||||
|
/// #[derive(Default, WindowWrapper)]
|
||||||
|
/// struct MyWindow {
|
||||||
|
/// window: Window
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl WindowController for MyWindow {
|
||||||
|
/// // The default implementation is actually okay!
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Default)]
|
||||||
|
/// struct MyApp {
|
||||||
|
/// window: MyWindow
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl AppDelegate for MyApp {
|
||||||
|
/// fn did_finish_launching(&mut self) {
|
||||||
|
/// window.show();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let app = App::new("com.myapp.lol", MyApp::default());
|
||||||
|
/// app.run();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait WindowWrapper {
|
||||||
|
/// Sets the title for the underlying window.
|
||||||
|
fn set_title(&self, title: &str);
|
||||||
|
|
||||||
|
/// Calls through to the NSWindow show method (technically, `[NSWindowController showWindow:]`.
|
||||||
|
/// Notable, this handles passing the implementing entity as the delegate, ensuring that
|
||||||
|
/// callbacks work appropriately.
|
||||||
|
///
|
||||||
|
/// We're technically setting the delegate later than is ideal, but in practice it works fine
|
||||||
|
/// in most cases due to the underlying implementation of `NSWindow` deferring things until
|
||||||
|
/// needed.
|
||||||
|
fn show(&self);
|
||||||
|
|
||||||
|
/// Calls through to the native NSwindow close implementation.
|
||||||
|
fn close(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// in lieu of subclasses.
|
||||||
|
pub trait WindowController {
|
||||||
|
/// `NSWindow` has a lovely usability feature wherein it'll cache the position in
|
||||||
|
/// `UserDefaults` when a window closes. This is generally nice for a lot of cases (e.g,
|
||||||
|
/// documents) but needs a key to work with. A blank key, the default, will not cache - so
|
||||||
|
/// you can implement this and return your own key per window delegate to cache accordingly.
|
||||||
|
fn autosave_name(&self) -> &str { "" }
|
||||||
|
|
||||||
|
/// The framework offers a standard, modern `NSWindow` by default - but sometimes you want
|
||||||
|
/// something else. Implement this and return your desired Window configuration.
|
||||||
|
fn config(&self) -> WindowConfig { WindowConfig::default() }
|
||||||
|
|
||||||
|
/// Fires when this window has loaded in memory, and is about to display. This is a good point
|
||||||
|
/// to set up your views and what not.
|
||||||
|
///
|
||||||
|
/// If you're coming from the web, you can think of this as `DOMContentLoaded`.
|
||||||
|
fn did_load(&self) {}
|
||||||
|
|
||||||
|
/// Fires when a window is going to close. You might opt to, say, clean up things here -
|
||||||
|
/// perhaps you have a long running task, or something that should be removed.
|
||||||
|
fn will_close(&self) {}
|
||||||
|
|
||||||
|
/// Fired when the window is about to move.
|
||||||
|
fn will_move(&self) {}
|
||||||
|
|
||||||
|
/// Fired after the window has moved.
|
||||||
|
fn did_move(&self) {}
|
||||||
|
|
||||||
|
/// Fired when the window changes screens - you might find this useful for certain scenarios,
|
||||||
|
/// such as rendering in retina vs non-retina environments.
|
||||||
|
fn did_change_screen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to become the key window.
|
||||||
|
fn did_become_key(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to resign key window status.
|
||||||
|
fn did_resign_key(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to become the main window.
|
||||||
|
fn did_become_main(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to resign main status.
|
||||||
|
fn did_resign_main(&self) {}
|
||||||
|
|
||||||
|
/// Fires when the window is about to miniaturize (e.g, to the Dock).
|
||||||
|
fn will_miniaturize(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window miniaturized (e.g, to the Dock).
|
||||||
|
fn did_miniaturize(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window de-miniaturized (e.g, from the Dock).
|
||||||
|
fn did_deminiaturize(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to go full screen.
|
||||||
|
fn will_enter_fullscreen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window entered full screen.
|
||||||
|
fn did_enter_fullscreen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window is about to exit full screen.
|
||||||
|
fn will_exit_fullscreen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window exited full screen.
|
||||||
|
fn did_exit_fullscreen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window failed to enter full screen.
|
||||||
|
fn did_fail_to_enter_fullscreen(&self) {}
|
||||||
|
|
||||||
|
/// Fires when this window failed to exit full screen.
|
||||||
|
fn did_fail_to_exit_fullscreen(&self) {}
|
||||||
|
}
|
261
appkit/src/window/window.rs
Normal file
261
appkit/src/window/window.rs
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
//! Implements an `NSWindow` wrapper for MacOS, backed by Cocoa and associated widgets. This also handles looping back
|
||||||
|
//! lifecycle events, such as window resizing or close events.
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::foundation::NSString;
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::runtime::Object;
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use crate::{ViewController, ViewWrapper};
|
||||||
|
use crate::toolbar::{Toolbar, ToolbarDelegate};
|
||||||
|
use crate::window::WindowController;
|
||||||
|
use crate::window::controller::{register_window_controller_class};
|
||||||
|
|
||||||
|
static WINDOW_CONTROLLER_PTR: &str = "rstWindowController";
|
||||||
|
|
||||||
|
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
|
||||||
|
/// where our `NSWindow` and associated delegate live.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WindowInner {
|
||||||
|
pub controller: Option<Id<Object>>,
|
||||||
|
pub toolbar: Option<Toolbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod WindowTitleVisibility {
|
||||||
|
pub const Visible: usize = 0;
|
||||||
|
pub const Hidden: usize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowInner {
|
||||||
|
/// Configures the `NSWindow` to know about our delegate.
|
||||||
|
///
|
||||||
|
/// Now, you may look at this and go "hey, the hell is going on here - why don't you make the
|
||||||
|
/// `NSWindow` in `[NSWindowController loadWindow]`?
|
||||||
|
///
|
||||||
|
/// This is a great question. It's because NSWindowController is... well, broken or buggy -
|
||||||
|
/// pick a term, either works. It's optimized for loading from xib/nib files, and attempting to
|
||||||
|
/// get loadWindow to fire properly is a pain in the rear (you're fighting a black box).
|
||||||
|
///
|
||||||
|
/// This is why we do this work here, but for things subclassing `NSViewController`, we go with
|
||||||
|
/// the route of implementing `loadView`.
|
||||||
|
///
|
||||||
|
/// APPKIT!
|
||||||
|
pub fn configure<T: WindowController + 'static>(&mut self, window_controller: &T) {
|
||||||
|
let autosave_name = window_controller.autosave_name();
|
||||||
|
|
||||||
|
let window = window_controller.config().0;
|
||||||
|
|
||||||
|
self.controller = Some(unsafe {
|
||||||
|
let window_controller_class = register_window_controller_class::<T>();
|
||||||
|
let controller_alloc: id = msg_send![window_controller_class, alloc];
|
||||||
|
let controller: id = msg_send![controller_alloc, initWithWindow:window];
|
||||||
|
(&mut *controller).set_ivar(WINDOW_CONTROLLER_PTR, window_controller as *const T as usize);
|
||||||
|
|
||||||
|
let window: id = msg_send![controller, window];
|
||||||
|
let _: () = msg_send![window, setDelegate:controller];
|
||||||
|
|
||||||
|
// Now we need to make sure to re-apply the NSAutoSaveName, as initWithWindow
|
||||||
|
// strips it... for some reason. We want it applied as it does nice things like
|
||||||
|
// save the window position in the Defaults database, which is what users expect.
|
||||||
|
let autosave = NSString::alloc(nil).init_str(autosave_name);
|
||||||
|
let _: () = msg_send![window, setFrameAutosaveName:autosave];
|
||||||
|
|
||||||
|
Id::from_ptr(controller)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles setting the title on the underlying window. Allocates and passes an `NSString` over
|
||||||
|
/// to the Objective C runtime.
|
||||||
|
pub fn set_title(&mut self, title: &str) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let title = NSString::alloc(nil).init_str(title);
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setTitle:title];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the title visibility for the underlying window.
|
||||||
|
pub fn set_title_visibility(&mut self, visibility: usize) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setTitleVisibility:visibility];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for configuring whether the window is movable via the background.
|
||||||
|
pub fn set_movable_by_background(&self, movable: bool) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setMovableByWindowBackground:match movable {
|
||||||
|
true => YES,
|
||||||
|
false => NO
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for setting whether this titlebar appears transparent.
|
||||||
|
pub fn set_titlebar_appears_transparent(&self, transparent: bool) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setTitlebarAppearsTransparent:match transparent {
|
||||||
|
true => YES,
|
||||||
|
false => NO
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for setting a toolbar on this window. Note that this takes ownership of whatever
|
||||||
|
/// `ToolbarDelegate` you pass! The underlying `NSToolbar` is a bit... old, and it's just
|
||||||
|
/// easier to do things this way.
|
||||||
|
///
|
||||||
|
/// If you find yourself in a position where you need your toolbar after the fact, you
|
||||||
|
/// probably have bigger issues.
|
||||||
|
pub fn set_toolbar<T: ToolbarDelegate + 'static>(&mut self, identifier: &str, toolbar: T) {
|
||||||
|
let toolbar = Toolbar::new(identifier, toolbar);
|
||||||
|
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setToolbar:&*toolbar.inner];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toolbar = Some(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used for setting the content view controller for this window.
|
||||||
|
pub fn set_content_view<T: ViewController + ViewWrapper + 'static>(&mut self, view_controller: &T) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
if let Some(vc) = view_controller.get_handle() {
|
||||||
|
let _: () = msg_send![*controller, setContentViewController:&*vc];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On macOS, calling `show()` is equivalent to calling `makeKeyAndOrderFront`. This is the
|
||||||
|
/// most common use case, hence why this method was chosen - if you want or need something
|
||||||
|
/// else, feel free to open an issue to discuss.
|
||||||
|
///
|
||||||
|
/// You should never be calling this yourself, mind you - Alchemy core handles this for you.
|
||||||
|
pub fn show(&self) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![*controller, showWindow:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the
|
||||||
|
/// window.
|
||||||
|
///
|
||||||
|
/// I dunno what else to say here, lol.
|
||||||
|
pub fn close(&self) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![*controller, close];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WindowInner {
|
||||||
|
/// When a Window is dropped on the Rust side, we want to ensure that we break the delegate
|
||||||
|
/// link on the Objective-C side. While this shouldn't actually be an issue, I'd rather be
|
||||||
|
/// safer than sorry.
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(controller) = &self.controller {
|
||||||
|
unsafe {
|
||||||
|
let window: id = msg_send![*controller, window];
|
||||||
|
let _: () = msg_send![window, setDelegate:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Window wraps `NSWindowController`, using interior mutability to handle configuration and calling
|
||||||
|
/// through to it.
|
||||||
|
///
|
||||||
|
/// Why `NSWindowController` and not `NSWindow`, you ask? The former has lifecycle events we're
|
||||||
|
/// interested in, the latter is... well, just the window.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Window(Rc<RefCell<WindowInner>>);
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
/// Sets the window title.
|
||||||
|
pub fn set_title(&self, title: &str) {
|
||||||
|
let mut window = self.0.borrow_mut();
|
||||||
|
window.set_title(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the window title visibility.
|
||||||
|
pub fn set_title_visibility(&self, visibility: usize) {
|
||||||
|
let mut window = self.0.borrow_mut();
|
||||||
|
window.set_title_visibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the window is movable by the background or not.
|
||||||
|
pub fn set_movable_by_background(&self, movable: bool) {
|
||||||
|
let window = self.0.borrow();
|
||||||
|
window.set_movable_by_background(movable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the titlebar appears transparent or not.
|
||||||
|
pub fn set_titlebar_appears_transparent(&self, transparent: bool) {
|
||||||
|
let window = self.0.borrow();
|
||||||
|
window.set_titlebar_appears_transparent(transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the Toolbar for this window. Note that this takes ownership of the toolbar!
|
||||||
|
pub fn set_toolbar<T: ToolbarDelegate + 'static>(&self, identifier: &str, toolbar: T) {
|
||||||
|
let mut window = self.0.borrow_mut();
|
||||||
|
window.set_toolbar(identifier, toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the content view controller for the window.
|
||||||
|
pub fn set_content_view<T: ViewController + ViewWrapper + 'static>(&self, view: &T) {
|
||||||
|
let mut window = self.0.borrow_mut();
|
||||||
|
window.set_content_view(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the window, running a configuration pass if necessary.
|
||||||
|
pub fn show<T: WindowController + 'static>(&self, controller: &T) {
|
||||||
|
let did_load = {
|
||||||
|
let mut window = self.0.borrow_mut();
|
||||||
|
|
||||||
|
if window.controller.is_none() {
|
||||||
|
window.configure(controller);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if did_load {
|
||||||
|
controller.did_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = self.0.borrow();
|
||||||
|
window.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the window.
|
||||||
|
pub fn close(&self) {
|
||||||
|
let window = self.0.borrow();
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
6
clippy.toml
Normal file
6
clippy.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
cyclomatic-complexity-threshold = 30
|
||||||
|
doc-valid-idents = [
|
||||||
|
"MiB", "GiB", "TiB", "PiB", "EiB",
|
||||||
|
"DirectX", "OpenGL", "TrueType",
|
||||||
|
"GitHub"
|
||||||
|
]
|
78
code_of_conduct.md
Normal file
78
code_of_conduct.md
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Contributor Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity and
|
||||||
|
orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting a project maintainer at:
|
||||||
|
|
||||||
|
* Ryan McGrath <ryan@rymc.io>
|
||||||
|
|
||||||
|
All complaints will be reviewed and investigated and will result in a response
|
||||||
|
that is deemed necessary and appropriate to the circumstances. The project team
|
||||||
|
is obligated to maintain confidentiality with regard to the reporter of an
|
||||||
|
incident. Further details of specific enforcement policies may be posted
|
||||||
|
separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at [https://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[homepage]: https://contributor-covenant.org
|
||||||
|
[version]: https://contributor-covenant.org/version/1/4/
|
Loading…
Reference in a new issue