Guess I should put this under version control LOL
This commit is contained in:
commit
2035318460
73 changed files with 8836 additions and 0 deletions
24
.editorconfig
Normal file
24
.editorconfig
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.toml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
45
.github/ISSUE_TEMPLATE.md
vendored
Normal file
45
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!--
|
||||||
|
If you want to report a bug, we added some points below you can fill out. If you want to request a feature, feel free to remove all the irrelevant text.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
|
||||||
|
- **Rust:**
|
||||||
|
- **Alchemy:**
|
||||||
|
- **Operating System (or native layer in use)**
|
||||||
|
|
||||||
|
### Feature Flags
|
||||||
|
|
||||||
|
|
||||||
|
## Problem Description
|
||||||
|
|
||||||
|
|
||||||
|
### What are you trying to accomplish?
|
||||||
|
|
||||||
|
|
||||||
|
### What is the expected output?
|
||||||
|
|
||||||
|
|
||||||
|
### What is the actual output?
|
||||||
|
|
||||||
|
|
||||||
|
### Are you seeing any additional errors?
|
||||||
|
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please include as much of your codebase as needed to reproduce the error. If the relevant files are large, please consider linking to a public repository or a [Gist](https://gist.github.com/).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have already looked over the [issue tracker](https://github.com/ryanmcgrath/alchemy/issues) for similar issues.
|
||||||
|
- [ ] This issue can be reproduced on Rust's stable channel. (Your issue will be
|
||||||
|
closed if this is not the case)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for your submission! You're helping make an amazing GUI framework.
|
||||||
|
-->
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
**/*.rs.bk
|
||||||
|
target
|
||||||
|
Cargo.lock
|
||||||
111
CONTRIBUTING.md
Normal file
111
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thanks for your interest in contributing to Alchemy! We very much look forward to
|
||||||
|
your suggestions, bug reports, and pull requests and so on.
|
||||||
|
|
||||||
|
*Note:* Anyone who interacts with Alchemy in any space, including but not
|
||||||
|
limited to this GitHub repository, must follow our [code of
|
||||||
|
conduct](https://github.com/ryanmcgrath/alchemy/blob/master/code_of_conduct.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Submitting bug reports
|
||||||
|
|
||||||
|
Have a look at our [issue tracker](https://github.com/ryanmcgrath/alchemy/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 Alchemy 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 we reproduce this?
|
||||||
|
- 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/alchemy/issues
|
||||||
|
[Gist]: https://gist.github.com
|
||||||
|
|
||||||
|
Thank you! We'll try to respond as quickly as possible.
|
||||||
|
|
||||||
|
|
||||||
|
## Submitting feature requests
|
||||||
|
|
||||||
|
If you can't find an issue (open or closed) describing your idea on our [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! We'll try to respond as quickly as possible.
|
||||||
|
|
||||||
|
|
||||||
|
## Contribute code to Alchemy
|
||||||
|
|
||||||
|
### Setting up Alchemy locally
|
||||||
|
|
||||||
|
1. Install Rust using [rustup], which allows you to easily switch between Rust
|
||||||
|
versions. Alchemy currently supports Rust Stable, but it's worthwhile in case
|
||||||
|
we want to test against beta versions.
|
||||||
|
|
||||||
|
2. Clone this repository and open it in your favorite editor.
|
||||||
|
3. Now, try running the test suite to confirm everything works for you locally
|
||||||
|
by executing `bin/test`. (Initially, this will take a while to compile
|
||||||
|
everything.)
|
||||||
|
|
||||||
|
[rustup]: https://www.rustup.rs
|
||||||
|
|
||||||
|
### Coding Style
|
||||||
|
|
||||||
|
We 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, like the CSS parsing.
|
||||||
|
|
||||||
|
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.
|
||||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"styles",
|
||||||
|
"macros",
|
||||||
|
"lifecycle",
|
||||||
|
"alchemy",
|
||||||
|
"examples/layout"
|
||||||
|
]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
panic = "abort"
|
||||||
21
LICENSE-MIT.md
Normal file
21
LICENSE-MIT.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 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.
|
||||||
130
README.md
Normal file
130
README.md
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
[](https://alchemy.rs)
|
||||||
|
|
||||||
|
A Rust GUI Framework
|
||||||
|
==========================================================
|
||||||
|
|
||||||
|
[](https://gitter.im/alchemy-rs/alchemy)
|
||||||
|
[](https://crates.io/crates/alchemy)
|
||||||
|
|
||||||
|
API Documentation: [latest release](https://docs.rs/alchemy) – [trunk branch](https://docs.alchemy.rs)
|
||||||
|
|
||||||
|
[Homepage](https://alchemy.rs)
|
||||||
|
|
||||||
|
Alchemy is a Rust GUI Framework, backed by native widgets on each platform it supports, with an API that's a blend of those found in AppKit, UIKit, and React Native. It aims to provide an API that feels at home in Rust, while striving to provide a visual appearance that's easy to scan and parse. It does not, and will never, require nightly. It's still early stages, but feedback and contributions are welcome.
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
Alchemy will, ideally, support the platforms listed below. At the moment, the `Cocoa` backend is the most complete, as I develop on a Mac and know the framework more than I'd care to admit. This list will be updated as more frameworks are added.
|
||||||
|
|
||||||
|
- `cocoa`, which provides backing widgets, windows and assorted frameworks for `macOS`.
|
||||||
|
- `cocoa-touch`, which provides backing widgets, windows and assorted frameworks for `iOS`.
|
||||||
|
- `gtk`, which affords a `GTK` layer. This is mostly intended for GNOME users; if you'd like to run it elsewhere, you're on your own.
|
||||||
|
- `qt`, which affords a `Qt` layer. This is mostly indended for `KDE` users; if you'd like to run it elsewhere, you're on your own.
|
||||||
|
- `uwp`, which affords a `"UWP"` layer for Microsoft platforms that support it. This is a bit of a hack, provided by linking into the [microsoft/WinObjC](https://github.com/Microsoft/WinObjC/) framework, originally inteded for porting `iOS` applications to `UWP`. Down the road, if or when a proper `UWP` library for Rust surfaces, I'd be happy to look at replacing this.
|
||||||
|
|
||||||
|
Support for more platforms is desired - for example, I think an [`OrbTk`](https://gitlab.redox-os.org/redox-os/orbtk) or [`Piston`](https://www.piston.rs) backend could be cool to see. A [`winapi-rs`](https://github.com/retep998/winapi-rs) backend could be cool, too!
|
||||||
|
|
||||||
|
## What Currently Works...?
|
||||||
|
At the moment, the following is implemented:
|
||||||
|
|
||||||
|
- A basic `cocoa` API, which implements the `Application` and `Window` lifecycles. `View`s are supported as well.
|
||||||
|
- A basic `reconciliation` module, which handles computing changes to the widget tree and applying them as necessary. It currently follows a design similar to React pre-16; I'm open to changing this if someone wants to collaborate.
|
||||||
|
- A CSS parser, based on the work done over in [servo/servo](https://github.com/servo/servo). It doesn't support cascading, and follows an API closer to that of React Native's. This is intentional.
|
||||||
|
- An RSX system, based on work done in [bodil/typed-html](https://github.com/bodil/typed-html) by Bodil Stokke. This was actually the project that made me circle back to the entire thing, too.
|
||||||
|
- Macros for easy UI construction - `rsx! {}`, which transforms JSX-ish syntax into element trees for the reconciler to work with, and `styles! {}`, which pre-process CSS into their styles.
|
||||||
|
- A CSS layout system, based off the work done over in [vislyhq/stretch](https://github.com/vislyhq/stretch). At the moment, this project includes a fork with a newer underlying API by [msiglreith](https://github.com/msiglreith/stretch/tree/index). Once the API is merged upstream, it's likely the dependency would change to `stretch` proper.
|
||||||
|
|
||||||
|
## What's it look like?
|
||||||
|
``` rust
|
||||||
|
use alchemy::{AppDelegate, Error, RSX, rsx, styles, View, Window, WindowDelegate};
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
window: Window
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDelegate for AppState {
|
||||||
|
fn did_finish_launching(&mut self) {
|
||||||
|
self.window.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WindowState;
|
||||||
|
|
||||||
|
impl WindowDelegate for WindowState {
|
||||||
|
fn render(&self) -> Result<RSX, Error> {
|
||||||
|
Ok(rsx! {
|
||||||
|
<View styles=["box"]>
|
||||||
|
<View styles=["innerbox"] />
|
||||||
|
</View>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = alchemy::shared_app();
|
||||||
|
|
||||||
|
app.register_styles("default", styles! {
|
||||||
|
box {
|
||||||
|
background-color: #307ace;
|
||||||
|
width: 300;
|
||||||
|
height: 300;
|
||||||
|
margin: 10;
|
||||||
|
padding: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
innerbox {
|
||||||
|
background-color: #003366;
|
||||||
|
width: 200;
|
||||||
|
height: 200;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alchemy::shared_app().run(AppState {
|
||||||
|
window: Window::new("Le Appy App", (0., 0., 600., 600.), WindowState {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
``
|
||||||
|
|
||||||
|
## Does it support custom Components?
|
||||||
|
Alchemy implements the React component lifecycle. It does not (currently) implement Hooks, and may or may not implement them in the future. The class-based lifecycle maps fairly well to Rust idioms already, as you really never wanted to subclass in React anyway.
|
||||||
|
|
||||||
|
A custom component would look like the following:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
use alchemy::{Component, Error, rsx, RSX};
|
||||||
|
|
||||||
|
pub struct MySpecialWidget {
|
||||||
|
your_special_value_or_whatever: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for MySpecialWidget {
|
||||||
|
fn component_did_mount(&mut self, props: &Props) {
|
||||||
|
// Do whatever you want. Fire a network request or something, I dunno.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, props: &Props) -> Result<RSX, Error> {
|
||||||
|
Ok(RSX::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rust allows the lifecycle to have a few cool guarantees that you can't really get in JavaScript - for instance, props don't actually belong to you... but it was a weird aspect of class-based components in JavaScript where you'd be able to arbitrarily call `this.props.whatever`. Function based components actually communicated it better, in that they were passed in - with Rust, it's very clear that you just get a reference.
|
||||||
|
|
||||||
|
Alchemy follows [this diagram of React's lifecycle methods](https://twitter.com/dan_abramov/status/981712092611989509) to a T for the most part. What's cool is that methods that shouldn't have side effects, we can call as straight up borrows... and the ones that are allowed to have mutable side effects, we can call them as `&mut self`. You can, of course, still incur side effects by doing something else, but being able to imply the intention directly in the API is kind of cool.
|
||||||
|
|
||||||
|
## License
|
||||||
|
I'm dual licensing this, due to the licenses that some of the projects it depends on being that. If there's some other (more appropriate) way to do this, please feel free to open an issue.
|
||||||
|
|
||||||
|
* Mozilla Public License, Version 2.0, ([LICENSE-MPL](LICENSE-MPL.md) or https://www.mozilla.org/en-US/MPL/)
|
||||||
|
* MIT License ([LICENSE-MIT](LICENSE-MIT.md) or https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
Before contributing, please read the [contributors guide](https://github.com/ryanmcgrath/alchemy/blob/master/CONTRIBUTING.md)
|
||||||
|
for useful information about setting up Alchemy locally, coding style and common abbreviations.
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution you intentionally submit
|
||||||
|
for inclusion in the work, should be dual-licensed as above, without any additional terms or conditions.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Major thanks to [David McNeil](https://github.com/davidMcneil) for graciously allowing me to take the `alchemy` name on crates.io. Hot take, if we had user namespacing, this wouldn't be an issue!
|
||||||
|
- Cheers to [diesel-rs/diesel](https://github.com/diesel-rs/diesel), who have a very well laid out repository that a bunch of this structure was cribbed from.
|
||||||
|
- Questions or comments that you don't think warrant an issue? Feel free to [poke me over on Twitter](https://twitter.com/ryanmcgrath/) or email me ([ryan@rymc.io](mailto:ryan@rymc.io)).
|
||||||
35
alchemy/Cargo.toml
Normal file
35
alchemy/Cargo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "alchemy"
|
||||||
|
description = "A cross-platform GUI framework written in Rust. Adapts to native view-layers on each platform. UIKit/React inspired."
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2018"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
license = "MPL-2.0+"
|
||||||
|
repository = "https://github.com/ryanmcgrath/alchemy"
|
||||||
|
categories = ["gui", "rendering::engine", "multimedia"]
|
||||||
|
keywords = ["gui", "css", "styles", "layout", "react"]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cocoa = ["alchemy-cocoa", "alchemy-lifecycle/cocoa"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alchemy-cocoa = { version = "0.1", path = "../cocoa", optional = true }
|
||||||
|
alchemy-lifecycle = { version = "0.1", path = "../lifecycle" }
|
||||||
|
alchemy-macros = { version = "0.1", path = "../macros" }
|
||||||
|
alchemy-styles = { version = "0.1", path = "../styles", features = ["parser"] }
|
||||||
|
mime = "0.3.13"
|
||||||
|
htmlescape = "0.3.1"
|
||||||
|
language-tags = "0.2.2"
|
||||||
|
lazy_static = "1.3"
|
||||||
|
matches = "0.1"
|
||||||
|
phf = "0.7"
|
||||||
|
proc-macro-hack = "0.5.4"
|
||||||
|
proc-macro-nested = "0.1.3"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
strum = "0.15.0"
|
||||||
|
strum_macros = "0.15.0"
|
||||||
|
toml = "0.5"
|
||||||
16
alchemy/README.md
Normal file
16
alchemy/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Alchemy Core
|
||||||
|
This crate implements the core Alchemy application, which is what users ultimately import. Applications are a singleton; some might not like this, but it enables a design pattern that meshes a bit better with existing GUI framework systems and patterns.
|
||||||
|
|
||||||
|
The general pattern for developing with Alchemy is as follows:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
[Alchemy API] -> [Inner Mutability] -> [Platform Bridge (implemented in other crates)]
|
||||||
|
|
|
||||||
|
|
|
||||||
|
|- [Delegate]
|
||||||
|
```
|
||||||
|
|
||||||
|
The delegate pattern is cribbed from AppKit/UIKit, where it tends to work quite nicely as a way to respond to system level events.
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
148
alchemy/src/app.rs
Normal file
148
alchemy/src/app.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
//! This module implements the Application structure and associated
|
||||||
|
//! lifecycle methods. You typically never create this struct yourself;
|
||||||
|
//! in Alchemy, there's a global `shared_app` that you should use to work
|
||||||
|
//! with the `App` struct.
|
||||||
|
//!
|
||||||
|
//! This ensures that you can respond to application lifecycles, and so
|
||||||
|
//! routing things around works correctly.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::AppDelegate;
|
||||||
|
|
||||||
|
use crate::theme::{ThemeEngine, StyleSheet};
|
||||||
|
use crate::window::WindowManager;
|
||||||
|
|
||||||
|
#[cfg(feature = "cocoa")]
|
||||||
|
pub use alchemy_cocoa::app::{App as PlatformAppBridge};
|
||||||
|
|
||||||
|
/// A default delegate that is mostly used for creating the initial struct,
|
||||||
|
/// without requiring the actual `AppDelegate` from the user. Will ideally
|
||||||
|
/// never see the light of day.
|
||||||
|
struct DefaultAppDelegate;
|
||||||
|
impl AppDelegate for DefaultAppDelegate {}
|
||||||
|
|
||||||
|
/// The Application structure itself. It holds a Mutex'd platform bridge, to
|
||||||
|
/// handle communicating with the platform-specific app instance, along with a
|
||||||
|
/// delegate to forward events to. The `ThemeEngine` and `WindowManager` are
|
||||||
|
/// also stored here for easy access.
|
||||||
|
pub struct App {
|
||||||
|
pub(crate) bridge: Mutex<Option<PlatformAppBridge>>,
|
||||||
|
pub delegate: Mutex<Box<AppDelegate>>,
|
||||||
|
pub themes: ThemeEngine,
|
||||||
|
pub windows: WindowManager
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Creates a new app, allocated on the heap. Provides a pointer to
|
||||||
|
/// said allocated instance so that the platform-specific app instances
|
||||||
|
/// can loop events back around.
|
||||||
|
pub(crate) fn new() -> Arc<App> {
|
||||||
|
let app = Arc::new(App {
|
||||||
|
bridge: Mutex::new(None),
|
||||||
|
delegate: Mutex::new(Box::new(DefaultAppDelegate {})),
|
||||||
|
themes: ThemeEngine::new(),
|
||||||
|
windows: WindowManager::new()
|
||||||
|
});
|
||||||
|
|
||||||
|
let app_ptr: *const App = &*app;
|
||||||
|
app.configure_bridge(app_ptr);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles providing the app pointer to the inner bridge.
|
||||||
|
pub(crate) fn configure_bridge(&self, ptr: *const App) {
|
||||||
|
let mut bridge = self.bridge.lock().unwrap();
|
||||||
|
*bridge = Some(PlatformAppBridge::new(ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience method for registering one-off styles. Typically, you would want
|
||||||
|
/// to store your stylesheets as separate files, to enable hot-reloading - but it's
|
||||||
|
/// conceivable that you might want to just have them in your app, too, and this enables
|
||||||
|
/// that use case.
|
||||||
|
pub fn register_styles(&self, theme_key: &str, stylesheet: StyleSheet) {
|
||||||
|
self.themes.register_styles(theme_key, stylesheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the app instance, by setting the necessary delegate and forwarding the run call
|
||||||
|
/// to the inner backing application. This is a blocking operation; if you run this, you
|
||||||
|
/// will want to begin your app (for real) in `AppDelegate::did_finish_launching()`.
|
||||||
|
pub fn run<S: 'static + AppDelegate>(&self, state: S) {
|
||||||
|
{
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
*delegate = Box::new(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lock = self.bridge.lock().unwrap();
|
||||||
|
if let Some(bridge) = &*lock {
|
||||||
|
bridge.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementing `AppDelegate` for `App` serves two purposes - for one, we're able to
|
||||||
|
/// separate the inner implementaton from the abstract one by referring to a trait type, avoiding
|
||||||
|
/// a cyclical dependency... and two, it allows us to react to these events on the App layer for
|
||||||
|
/// our own purposes, while still forwarding them on to the delegate.
|
||||||
|
impl AppDelegate for App {
|
||||||
|
/// Called when the application will finish launching.
|
||||||
|
fn will_finish_launching(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.will_finish_launching();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application did finish launching.
|
||||||
|
fn did_finish_launching(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.did_finish_launching();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application will become active. We can use this, for instance,
|
||||||
|
/// to resume rendering cycles and so on.
|
||||||
|
fn will_become_active(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.will_become_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application did become active. We can use this, for instance,
|
||||||
|
/// to resume rendering cycles and so on.
|
||||||
|
fn did_become_active(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.did_become_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application will resigned active. We can use this, for instance,
|
||||||
|
/// to pause rendering cycles and so on.
|
||||||
|
fn will_resign_active(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.will_resign_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application has resigned active. We can use this, for instance,
|
||||||
|
/// to pause rendering cycles and so on.
|
||||||
|
fn did_resign_active(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.did_resign_active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application should terminate - we can use it
|
||||||
|
/// to avoid termination if Alchemy needs more time for something,
|
||||||
|
/// for whatever reason.
|
||||||
|
fn should_terminate(&self) -> bool {
|
||||||
|
let delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.should_terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the application is about to terminate.
|
||||||
|
fn will_terminate(&mut self) {
|
||||||
|
let mut delegate = self.delegate.lock().unwrap();
|
||||||
|
delegate.will_terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a private method, and you should not attempt to call it or
|
||||||
|
/// rely on it. It exists to enable easy loopback of Window-level events
|
||||||
|
/// on some platforms.
|
||||||
|
fn _window_will_close(&self, window_id: usize) {
|
||||||
|
self.windows.will_close(window_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
alchemy/src/components/fragment.rs
Normal file
22
alchemy/src/components/fragment.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
//! A Fragment is for components that want to return or hoist multiple inner
|
||||||
|
//! child nodes. `impl IntoIterator` can't be used in trait returns right now,
|
||||||
|
//! and this API more or less matches what React presents, so I'm fine with it...
|
||||||
|
//! but as the language stabilizes even further I'd love to get rid of this and
|
||||||
|
//! just allow returning arbitrary iterators.
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::Component;
|
||||||
|
|
||||||
|
/// Fragments are special - you can do something like the following in cases where you
|
||||||
|
/// want to render some views without requiring an intermediate view.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// <Fragment>
|
||||||
|
/// <View />
|
||||||
|
/// <View />
|
||||||
|
/// <View />
|
||||||
|
/// </Fragment>
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Fragment;
|
||||||
|
|
||||||
|
impl Component for Fragment {}
|
||||||
12
alchemy/src/components/mod.rs
Normal file
12
alchemy/src/components/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//! This module implements basic core Components (View, Label, etc).
|
||||||
|
//! End-users are of course free to implement their own; the core
|
||||||
|
//! Components in this module should just be enough to build a
|
||||||
|
//! functioning app.
|
||||||
|
|
||||||
|
pub mod fragment;
|
||||||
|
pub mod view;
|
||||||
|
//pub mod text;
|
||||||
|
|
||||||
|
pub use fragment::Fragment;
|
||||||
|
pub use view::View;
|
||||||
|
//pub use text::*;
|
||||||
34
alchemy/src/components/text.rs
Normal file
34
alchemy/src/components/text.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
//! components/label.rs
|
||||||
|
//!
|
||||||
|
//! Implements a Label Component struct. Used for TextNode
|
||||||
|
//! behind the scenes on most platforms.
|
||||||
|
//!
|
||||||
|
//! @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
//! @created 03/29/2019
|
||||||
|
|
||||||
|
use crate::prelude::RSX;
|
||||||
|
use crate::components::Component;
|
||||||
|
use crate::dom::elements::FlowContent;
|
||||||
|
|
||||||
|
#[derive(RSX, Debug, Default)]
|
||||||
|
pub struct Text {}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl Component for Text {
|
||||||
|
fn create_native_backing_node(&self) -> cocoa::base::id {
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize};
|
||||||
|
use cocoa::base::id;
|
||||||
|
use crate::components::macos::objc_classes::label;
|
||||||
|
|
||||||
|
let view: cocoa::base::id;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let rect_zero = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
|
||||||
|
let alloc: id = msg_send![label::register_class(), alloc];
|
||||||
|
view = msg_send![alloc, initWithFrame:rect_zero];
|
||||||
|
}
|
||||||
|
|
||||||
|
view
|
||||||
|
}
|
||||||
|
}
|
||||||
64
alchemy/src/components/view.rs
Normal file
64
alchemy/src/components/view.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
//! Handles hoisting per-platform specific View components.
|
||||||
|
//! Each platform needs the freedom to do some specific things,
|
||||||
|
//! hence why they're all (somewhat annoyingly, but lovingly) re-implemented
|
||||||
|
//! as bridges.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use alchemy_styles::styles::{Layout, Style};
|
||||||
|
|
||||||
|
use alchemy_lifecycle::error::Error;
|
||||||
|
use alchemy_lifecycle::rsx::{Props, RSX, StylesList};
|
||||||
|
use alchemy_lifecycle::traits::{Component, PlatformSpecificNodeType};
|
||||||
|
|
||||||
|
use crate::components::Fragment;
|
||||||
|
|
||||||
|
#[cfg(feature = "cocoa")]
|
||||||
|
use alchemy_cocoa::view::{View as PlatformViewBridge};
|
||||||
|
|
||||||
|
/// Views are the most basic piece of the API. If you want to display something, you'll
|
||||||
|
/// probably be reaching for a View first and foremost.
|
||||||
|
///
|
||||||
|
/// Views accept styles and event callbacks as props. For example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// <View styles=["styleKey1", "styleKey2"] />
|
||||||
|
/// ```
|
||||||
|
pub struct View(Mutex<PlatformViewBridge>);
|
||||||
|
|
||||||
|
impl Default for View {
|
||||||
|
fn default() -> View {
|
||||||
|
View(Mutex::new(PlatformViewBridge::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for View {
|
||||||
|
fn has_native_backing_node(&self) -> bool { true }
|
||||||
|
|
||||||
|
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> {
|
||||||
|
let bridge = self.0.lock().unwrap();
|
||||||
|
Some(bridge.borrow_native_backing_node())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_child_component(&self, component: &Arc<Component>) {
|
||||||
|
if let Some(child) = component.borrow_native_backing_node() {
|
||||||
|
let mut bridge = self.0.lock().unwrap();
|
||||||
|
bridge.append_child(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_styles(&self, layout: &Layout, style: &Style) {
|
||||||
|
let mut bridge = self.0.lock().unwrap();
|
||||||
|
bridge.apply_styles(layout, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, props: &Props) -> Result<RSX, Error> {
|
||||||
|
Ok(RSX::node("Fragment", || Box::new(Fragment::default()), Props {
|
||||||
|
attributes: std::collections::HashMap::new(),
|
||||||
|
key: "".into(),
|
||||||
|
styles: StylesList::new(),
|
||||||
|
children: props.children.clone()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
49
alchemy/src/lib.rs
Normal file
49
alchemy/src/lib.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! Alchemy is a Rust GUI framework that implements the React Component lifecycle on top of a
|
||||||
|
//! delegate system inspired by those found in AppKit/UIKit. It's backed by native widgets
|
||||||
|
//! per-platform, but doesn't bind you to any one design style or visual appearance.
|
||||||
|
//!
|
||||||
|
//! CSS support (no cascading) provides a familiar syntax for developers who tend to work on
|
||||||
|
//! UI/UX projects, and the Component lifecycle is familiar enough to anyone who's touched React.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
pub use lazy_static::lazy_static;
|
||||||
|
use proc_macro_hack::proc_macro_hack;
|
||||||
|
|
||||||
|
pub use alchemy_lifecycle::traits::{
|
||||||
|
AppDelegate, Component, WindowDelegate
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use alchemy_lifecycle::error::Error;
|
||||||
|
pub use alchemy_lifecycle::rsx::{
|
||||||
|
Props, RSX, StyleKey, StylesList,
|
||||||
|
SpacedSet, VirtualNode, VirtualText
|
||||||
|
};
|
||||||
|
|
||||||
|
#[proc_macro_hack(support_nested)]
|
||||||
|
pub use alchemy_macros::rsx;
|
||||||
|
|
||||||
|
#[proc_macro_hack]
|
||||||
|
pub use alchemy_macros::styles;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
pub use components::{Fragment, View};
|
||||||
|
|
||||||
|
pub(crate) mod reconciler;
|
||||||
|
pub mod theme;
|
||||||
|
|
||||||
|
pub mod window;
|
||||||
|
pub use window::Window;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub(crate) static ref SHARED_APP: Arc<App> = App::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function supports calling the shared global application instance from anywhere in your
|
||||||
|
/// code. It's useful in cases where you need to have an escape hatch, but if you're using it as
|
||||||
|
/// such, you may want to double check your Application design to make sure you need it.
|
||||||
|
pub fn shared_app() -> Arc<App> {
|
||||||
|
SHARED_APP.clone()
|
||||||
|
}
|
||||||
388
alchemy/src/reconciler.rs
Normal file
388
alchemy/src/reconciler.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
//! render/diff.rs
|
||||||
|
//!
|
||||||
|
//! Implements tree diffing, and attempts to cache Component instances where
|
||||||
|
//! possible.
|
||||||
|
//!
|
||||||
|
//! @created 05/03/2019
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::mem::{discriminant, swap};
|
||||||
|
|
||||||
|
use alchemy_styles::Stretch;
|
||||||
|
use alchemy_styles::styles::Style;
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::Component;
|
||||||
|
use alchemy_lifecycle::rsx::{StylesList, RSX, VirtualNode};
|
||||||
|
|
||||||
|
pub fn diff_and_patch_tree(old: RSX, new: RSX, stretch: &mut Stretch, depth: usize) -> Result<RSX, Box<Error>> {
|
||||||
|
// Whether we replace or not depends on a few things. If we're working on two different node
|
||||||
|
// types (text vs node), if the node tags are different, or if the key (in some cases) is
|
||||||
|
// different.
|
||||||
|
let is_replace = match discriminant(&old) != discriminant(&new) {
|
||||||
|
true => true,
|
||||||
|
false => {
|
||||||
|
if let (RSX::VirtualNode(old_element), RSX::VirtualNode(new_element)) = (&old, &new) {
|
||||||
|
old_element.tag != new_element.tag
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match (old, new) {
|
||||||
|
(RSX::VirtualNode(mut old_element), RSX::VirtualNode(mut new_element)) => {
|
||||||
|
if is_replace {
|
||||||
|
// Do something different in here...
|
||||||
|
//let mut mounted = mount_component_tree(new_tree);
|
||||||
|
// unmount_component_tree(old_tree);
|
||||||
|
// Swap them in memory, copy any layout + etc as necessary
|
||||||
|
// append, link layout nodes, etc
|
||||||
|
return Ok(RSX::VirtualNode(new_element));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, it's an update to an existing element. This means a cached Component
|
||||||
|
// instance might exist, and we want to keep it around and reuse it if possible. Let's check
|
||||||
|
// and do some swapping action to handle it.
|
||||||
|
//
|
||||||
|
// These need to move to the new tree, since we always keep 'em. We also wanna cache a
|
||||||
|
// reference to our content view.
|
||||||
|
swap(&mut old_element.instance, &mut new_element.instance);
|
||||||
|
swap(&mut old_element.layout_node, &mut new_element.layout_node);
|
||||||
|
|
||||||
|
// For the root tag, which is usually the content view of the Window, we don't want to
|
||||||
|
// perform the whole render/component lifecycle routine. It's a special case element,
|
||||||
|
// where the Window (or other root element) patches in the output of a render method
|
||||||
|
// specific to that object. An easy way to handle this is the depth parameter - in
|
||||||
|
// fact, it's why it exists. Depth 0 should be considered special and skip the
|
||||||
|
// rendering phase.
|
||||||
|
if depth > 0 {
|
||||||
|
// diff props, set new props
|
||||||
|
// instance.get_derived_state_from_props()
|
||||||
|
|
||||||
|
if let Some(instance) = &mut new_element.instance {
|
||||||
|
// diff props, set new props
|
||||||
|
// instance.get_derived_state_from_props()
|
||||||
|
|
||||||
|
//if instance.should_component_update() {
|
||||||
|
// instance.render() { }
|
||||||
|
// instance.get_snapshot_before_update()
|
||||||
|
// apply changes
|
||||||
|
//instance.component_did_update();
|
||||||
|
//} else {
|
||||||
|
// If should_component_update() returns false, then we want to take the
|
||||||
|
// children from the old node, move them to the new node, and recurse into
|
||||||
|
// that tree instead.
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This None path should never be hit. If it does, the algorithm is doing something way
|
||||||
|
// off base.
|
||||||
|
let is_native_backed = match &new_element.instance {
|
||||||
|
Some(instance) => instance.has_native_backing_node(),
|
||||||
|
None => false
|
||||||
|
};
|
||||||
|
|
||||||
|
// There is probably a nicer way to do this that doesn't allocate as much, and I'm open
|
||||||
|
// to revisiting it. Platforms outside of Rust allocate far more than this, though, and
|
||||||
|
// in general the whole "avoid allocations" thing is fear mongering IMO. Revisit later.
|
||||||
|
let mut children: Vec<RSX> = Vec::with_capacity(new_element.children.len());
|
||||||
|
std::mem::swap(&mut children, &mut new_element.children);
|
||||||
|
|
||||||
|
old_element.children.reverse();
|
||||||
|
for new_child_tree in children {
|
||||||
|
match old_element.children.pop() {
|
||||||
|
// A matching child in the old tree means we can pass right back into the
|
||||||
|
// update phase.
|
||||||
|
Some(old_child_tree) => {
|
||||||
|
let updated = diff_and_patch_tree(old_child_tree, new_child_tree, stretch, depth + 1)?;
|
||||||
|
new_element.children.push(updated);
|
||||||
|
},
|
||||||
|
|
||||||
|
// If there's no matching child in the old tree, this is a new Component and we
|
||||||
|
// can feel free to mount/connect it.
|
||||||
|
None => {
|
||||||
|
if let RSX::VirtualNode(new_el) = new_child_tree {
|
||||||
|
let mut mounted = mount_component_tree(new_el, stretch)?;
|
||||||
|
|
||||||
|
// Link the layout nodes, handle the appending, etc.
|
||||||
|
// This happens inside mount_component_tree, but that only handles that
|
||||||
|
// specific tree. Think of this step as joining two trees in the graph.
|
||||||
|
|
||||||
|
if is_native_backed {
|
||||||
|
println!("Linking 1");
|
||||||
|
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_element.children.push(RSX::VirtualNode(mounted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the fat - more children in the old tree than the new one means we gonna be
|
||||||
|
// droppin'. We need to send unmount lifecycle calls to these, and break any links we
|
||||||
|
// have (e.g, layout, backing view tree, etc).
|
||||||
|
loop {
|
||||||
|
match old_element.children.pop() {
|
||||||
|
Some(child) => {
|
||||||
|
if let RSX::VirtualNode(mut old_child) = child {
|
||||||
|
unmount_component_tree(&mut old_child)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RSX::VirtualNode(new_element))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're comparing two text nodes. Realistically... this requires nothing from us, because
|
||||||
|
// the <Text> tag should handle it. We'll do a quick sanity check to make sure that it
|
||||||
|
// actually has a parent <Text>, though.
|
||||||
|
(RSX::VirtualText(_), RSX::VirtualText(text)) => {
|
||||||
|
//match &parent {
|
||||||
|
// RSX::VirtualText(_) => { panic!("Raw text must be surrounded by a <Text></Text> component!"); },
|
||||||
|
// _ => {}
|
||||||
|
// }
|
||||||
|
Ok(RSX::VirtualText(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are all edge cases that shouldn't get hit. In particular:
|
||||||
|
//
|
||||||
|
// - VirtualText being replaced by VirtualNode should be caught by the discriminant check
|
||||||
|
// in the beginning of this function, which registers as a replace/mount.
|
||||||
|
// - VirtualNode being replaced with VirtualText is the same scenario as above.
|
||||||
|
// - The (RSX::None, ...) checks are to shut the compiler up; we never store the RSX::None
|
||||||
|
// return value, as it's mostly a value in place for return signature usability. Thus,
|
||||||
|
// these should quite literally never register.
|
||||||
|
//
|
||||||
|
// This goes without saying, but: never ever store RSX::None lol
|
||||||
|
(RSX::VirtualText(_), RSX::VirtualNode(_)) | (RSX::VirtualNode(_), RSX::VirtualText(_)) |
|
||||||
|
(RSX::None, RSX::VirtualText(_)) | (RSX::None, RSX::VirtualNode(_)) | (RSX::None, RSX::None) |
|
||||||
|
(RSX::VirtualNode(_), RSX::None) | (RSX::VirtualText(_), RSX::None) => {
|
||||||
|
unreachable!("Unequal variant discriminants should already have been handled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a set of style keys, and a mutable style to update, this will walk the keys
|
||||||
|
/// and configure the Style node for the upcoming layout + render pass. Where appropriate,
|
||||||
|
/// it will mark the node explicitly as dirty.
|
||||||
|
///
|
||||||
|
/// This may not need to be it's own function, we'll see down the road.
|
||||||
|
fn configure_styles(style_keys: &StylesList, style: &mut Style) {
|
||||||
|
let app = crate::shared_app();
|
||||||
|
app.themes.configure_style_for_keys(style_keys, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walks the tree and applies styles. This happens after a layout computation, typically.
|
||||||
|
pub(crate) fn walk_and_apply_styles(node: &VirtualNode, layout_manager: &mut Stretch) {
|
||||||
|
if let (Some(layout_node), Some(instance)) = (node.layout_node, &node.instance) {
|
||||||
|
match (layout_manager.layout(layout_node), layout_manager.style(layout_node)) {
|
||||||
|
(Ok(layout), Ok(style)) => { instance.apply_styles(layout, style); },
|
||||||
|
(Err(e), Err(e2)) => { eprintln!("Error retrieving computed style? {:?} {:?}", e, e2); },
|
||||||
|
_ => { eprintln!("Error retrieving computed style!"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in &node.children {
|
||||||
|
println!("IN CHILD!");
|
||||||
|
if let RSX::VirtualNode(child_node) = child {
|
||||||
|
walk_and_apply_styles(child_node, layout_manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a tree, will walk the branches until it finds the next root nodes to connect.
|
||||||
|
/// While this sounds slow, in practice it rarely has to go far in any direction.
|
||||||
|
fn find_and_link_layout_nodes(parent_node: &mut VirtualNode, child_tree: &mut VirtualNode, stretch: &mut Stretch) -> Result<(), Box<Error>> {
|
||||||
|
// First, check if the tree has a layout node we can use...
|
||||||
|
if let (Some(parent_instance), Some(child_instance)) = (&mut parent_node.instance, &mut child_tree.instance) {
|
||||||
|
if let (Some(parent_layout_node), Some(child_layout_node)) = (&parent_node.layout_node, &child_tree.layout_node) {
|
||||||
|
println!("--- LINKING");
|
||||||
|
stretch.add_child(*parent_layout_node, *child_layout_node)?;
|
||||||
|
parent_instance.append_child_component(child_instance);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in child_tree.children.iter_mut() {
|
||||||
|
if let RSX::VirtualNode(child_tree) = child {
|
||||||
|
find_and_link_layout_nodes(parent_node, child_tree, stretch)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively constructs a Component tree. This entails adding it to the backing
|
||||||
|
/// view tree, firing various lifecycle methods, and ensuring that nodes for layout
|
||||||
|
/// passes are configured.
|
||||||
|
fn mount_component_tree(mut new_element: VirtualNode, stretch: &mut Stretch) -> Result<VirtualNode, Box<Error>> {
|
||||||
|
let mut instance = (new_element.create_component_fn)();
|
||||||
|
println!("> Mounting {}", new_element.tag);
|
||||||
|
// "compute" props, set on instance
|
||||||
|
// instance.get_derived_state_from_props(props)
|
||||||
|
|
||||||
|
let is_native_backed = instance.has_native_backing_node();
|
||||||
|
|
||||||
|
if is_native_backed {
|
||||||
|
let mut style = Style::default();
|
||||||
|
configure_styles(&new_element.props.styles, &mut style);
|
||||||
|
|
||||||
|
let layout_node = stretch.new_node(style, vec![])?;
|
||||||
|
new_element.layout_node = Some(layout_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x: std::sync::Arc<Component> = instance.into();
|
||||||
|
let renderer = x.clone();
|
||||||
|
new_element.instance = Some(x);
|
||||||
|
|
||||||
|
let mut children = match renderer.render(&new_element.props) {
|
||||||
|
Ok(opt) => match opt {
|
||||||
|
RSX::VirtualNode(child) => {
|
||||||
|
let mut children = vec![];
|
||||||
|
|
||||||
|
// We want to support Components being able to return arbitrary iteratable
|
||||||
|
// elements, but... well, it's not quite that simple. Thus we'll offer a <Fragment>
|
||||||
|
// tag similar to what React does, which just hoists the children out of it and
|
||||||
|
// discards the rest.
|
||||||
|
if child.tag == "Fragment" {
|
||||||
|
println!(" > In Fragment");
|
||||||
|
for child_node in child.props.children {
|
||||||
|
if let RSX::VirtualNode(node) = child_node {
|
||||||
|
let mut mounted = mount_component_tree(node, stretch)?;
|
||||||
|
|
||||||
|
println!(" > Mounted Fragment...");
|
||||||
|
if is_native_backed {
|
||||||
|
println!(" > Linking Fragment: {} {}", new_element.tag, mounted.tag);
|
||||||
|
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(RSX::VirtualNode(mounted));
|
||||||
|
} else {
|
||||||
|
println!(" > Mounting other type of node...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut mounted = mount_component_tree(child, stretch)?;
|
||||||
|
|
||||||
|
if is_native_backed {
|
||||||
|
println!("Linking Child");
|
||||||
|
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(RSX::VirtualNode(mounted));
|
||||||
|
}
|
||||||
|
|
||||||
|
children
|
||||||
|
},
|
||||||
|
|
||||||
|
// If a Component renders nothing (or this is a Text string, which we do nothing with)
|
||||||
|
// that's totally fine.
|
||||||
|
_ => vec![]
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
// return an RSX::VirtualNode(ErrorComponentView) or something?
|
||||||
|
/* instance.get_derived_state_from_error(e) */
|
||||||
|
// render error state or something I guess?
|
||||||
|
/* instance.component_did_catch(e, info) */
|
||||||
|
eprintln!("Error rendering: {}", e);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new_element.children.append(&mut children);
|
||||||
|
|
||||||
|
// instance.get_snapshot_before_update()
|
||||||
|
//renderer.component_did_mount(&new_element.props);
|
||||||
|
|
||||||
|
//let x: std::sync::Arc<Component> = instance.into();
|
||||||
|
|
||||||
|
// new_element.instance = Some(instance);
|
||||||
|
//new_element.instance = Some(x);
|
||||||
|
Ok(new_element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the tree and unmount Component instances. This means we fire the
|
||||||
|
/// `component_will_unmount` hook and remove the node(s) from their respective trees.
|
||||||
|
///
|
||||||
|
/// This fires the hooks from a recursive inward-out pattern; that is, the deepest nodes in the tree
|
||||||
|
/// are the first to go, ensuring that everything is properly cleaned up.
|
||||||
|
fn unmount_component_tree(old_element: &mut VirtualNode) -> Result<(), Box<Error>> {
|
||||||
|
// We only need to recurse on VirtualNodes. Text and so on will automagically drop
|
||||||
|
// because we don't support freeform text, it has to be inside a <Text> at all times.
|
||||||
|
for child in old_element.children.iter_mut() {
|
||||||
|
if let RSX::VirtualNode(child_element) = child {
|
||||||
|
unmount_component_tree(child_element)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire the appropriate lifecycle method and then remove the node from the underlying
|
||||||
|
// graph. Remember that a Component can actually not necessarily have a native backing
|
||||||
|
// node, hence our necessary check.
|
||||||
|
if let Some(old_component) = &mut old_element.instance {
|
||||||
|
//old_component.component_will_unmount();
|
||||||
|
|
||||||
|
/*if let Some(view) = old_component.get_native_backing_node() {
|
||||||
|
if let Some(native_view) = replace_native_view {
|
||||||
|
//replace_view(&view, &native_view);
|
||||||
|
} else {
|
||||||
|
//remove_view(&view);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rather than try to keep track of parent/child stuff for removal... just obliterate it,
|
||||||
|
// the underlying library does a good job of killing the links anyway.
|
||||||
|
if let Some(layout_node) = &mut old_element.layout_node {
|
||||||
|
//layout_node.set_children(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/*let mut add_attributes: HashMap<&str, &str> = HashMap::new();
|
||||||
|
let mut remove_attributes: Vec<&str> = vec![];
|
||||||
|
|
||||||
|
// TODO: -> split out into func
|
||||||
|
for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
|
||||||
|
match old_element.attrs.get(new_attr_name) {
|
||||||
|
Some(ref old_attr_val) => {
|
||||||
|
if old_attr_val != &new_attr_val {
|
||||||
|
add_attributes.insert(new_attr_name, new_attr_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
add_attributes.insert(new_attr_name, new_attr_val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: -> split out into func
|
||||||
|
for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
|
||||||
|
if add_attributes.get(&old_attr_name[..]).is_some() {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match new_element.attrs.get(old_attr_name) {
|
||||||
|
Some(ref new_attr_val) => {
|
||||||
|
if new_attr_val != &old_attr_val {
|
||||||
|
remove_attributes.push(old_attr_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
remove_attributes.push(old_attr_name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if add_attributes.len() > 0 {
|
||||||
|
patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes));
|
||||||
|
}
|
||||||
|
if remove_attributes.len() > 0 {
|
||||||
|
patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes));
|
||||||
|
}*/
|
||||||
122
alchemy/src/theme/mod.rs
Normal file
122
alchemy/src/theme/mod.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//! Implements a Theme loader, which scans a few places and loads any
|
||||||
|
//! CSS files that are necessary.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use toml;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use alchemy_lifecycle::rsx::StylesList;
|
||||||
|
|
||||||
|
pub use alchemy_styles::color;
|
||||||
|
pub use alchemy_styles::styles;
|
||||||
|
pub use styles::{Style, Styles};
|
||||||
|
|
||||||
|
pub mod stylesheet;
|
||||||
|
pub use stylesheet::StyleSheet;
|
||||||
|
|
||||||
|
static CONFIG_FILE_NAME: &str = "alchemy.toml";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct RawConfig<'d> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
general: Option<General<'d>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct General<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
dirs: Option<Vec<&'a str>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `ThemeEngine` controls loading themes and registering associated
|
||||||
|
/// styles.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ThemeEngine {
|
||||||
|
pub dirs: Vec<PathBuf>,
|
||||||
|
pub themes: RwLock<HashMap<String, StyleSheet>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeEngine {
|
||||||
|
/// Creates a new 'ThemeEngine` instance.
|
||||||
|
pub fn new() -> ThemeEngine {
|
||||||
|
// This env var is set by Cargo... so if this code breaks, there's
|
||||||
|
// bigger concerns, lol
|
||||||
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
|
||||||
|
let root = PathBuf::from(manifest_dir);
|
||||||
|
let default_dirs = vec![root.join("themes")];
|
||||||
|
|
||||||
|
let toml_contents = read_config_file();
|
||||||
|
let raw: RawConfig<'_> = toml::from_str(&toml_contents).expect(&format!("Invalid TOML in {}!", CONFIG_FILE_NAME));
|
||||||
|
|
||||||
|
let dirs = match raw.general {
|
||||||
|
Some(General { dirs }) => (
|
||||||
|
dirs.map_or(default_dirs, |v| {
|
||||||
|
v.into_iter().map(|dir| root.join(dir)).collect()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
None => default_dirs
|
||||||
|
};
|
||||||
|
|
||||||
|
ThemeEngine { dirs, themes: RwLock::new(HashMap::new()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a stylesheet (typically created by the `styles! {}` macro) for a given
|
||||||
|
/// theme.
|
||||||
|
pub fn register_styles(&self, key: &str, stylesheet: StyleSheet) {
|
||||||
|
let mut themes = self.themes.write().unwrap();
|
||||||
|
if !themes.contains_key(key) {
|
||||||
|
themes.insert(key.to_string(), stylesheet);
|
||||||
|
println!("REGISTERED STYLES");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if let Some(existing_stylesheet) = self.themes.get_mut(key) {
|
||||||
|
// *existing_stylesheet.merge(stylesheet);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a theme key, style keys, and a style, configures the style for layout
|
||||||
|
/// and appearance.
|
||||||
|
pub fn configure_style_for_keys_in_theme(&self, theme: &str, keys: &StylesList, style: &mut Style) {
|
||||||
|
let themes = self.themes.read().unwrap();
|
||||||
|
|
||||||
|
match themes.get(theme) {
|
||||||
|
Some(theme) => {
|
||||||
|
for key in &keys.0 {
|
||||||
|
theme.apply_styles(key, style);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
eprintln!("No styles for theme!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same logic as `configure_style_for_keys_in_theme`, but defaults to the default theme.
|
||||||
|
pub fn configure_style_for_keys(&self, keys: &StylesList, style: &mut Style) {
|
||||||
|
self.configure_style_for_keys_in_theme("default", keys, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility method for reading a config file from the `CARGO_MANIFEST_DIR`. Hat tip to
|
||||||
|
/// [askama](https://github.com/djc/askama) for this!
|
||||||
|
pub fn read_config_file() -> String {
|
||||||
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
let root = PathBuf::from(manifest_dir);
|
||||||
|
let filename = root.join(CONFIG_FILE_NAME);
|
||||||
|
|
||||||
|
if filename.exists() {
|
||||||
|
fs::read_to_string(&filename)
|
||||||
|
.expect(&format!("Unable to read {}", filename.to_str().unwrap()))
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
303
alchemy/src/theme/stylesheet.rs
Normal file
303
alchemy/src/theme/stylesheet.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
//! Implements a `StyleSheet`, which contains inner logic for
|
||||||
|
//! determining what styles should be applied to a given widget.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use alchemy_styles::styles::{Dimension, Rect, Size, Style, Styles};
|
||||||
|
|
||||||
|
/// A `StyleSheet` contains selectors and parsed `Styles` attributes.
|
||||||
|
/// It also has some logic to apply styles for n keys to a given `Style` node.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StyleSheet(HashMap<&'static str, Vec<Styles>>);
|
||||||
|
|
||||||
|
impl StyleSheet {
|
||||||
|
/// Creates a new `Stylesheet`.
|
||||||
|
pub fn new(styles: HashMap<&'static str, Vec<Styles>>) -> Self {
|
||||||
|
StyleSheet(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_styles(&self, key: &str, style: &mut Style) {
|
||||||
|
match self.0.get(key) {
|
||||||
|
Some(styles) => { reduce_styles_into_style(styles, style); },
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This takes a list of styles, and a mutable style object, and attempts to configure the
|
||||||
|
/// style object in a way that makes sense given n styles.
|
||||||
|
fn reduce_styles_into_style(styles: &Vec<Styles>, layout: &mut Style) {
|
||||||
|
for style in styles { match style {
|
||||||
|
Styles::AlignContent(val) => { layout.align_content = *val; },
|
||||||
|
Styles::AlignItems(val) => { layout.align_items = *val; },
|
||||||
|
Styles::AlignSelf(val) => { layout.align_self = *val; },
|
||||||
|
Styles::AspectRatio(val) => { layout.aspect_ratio = *val; },
|
||||||
|
Styles::BackfaceVisibility(_val) => { },
|
||||||
|
Styles::BackgroundColor(val) => { layout.background_color = *val; },
|
||||||
|
|
||||||
|
Styles::BorderColor(_val) => { },
|
||||||
|
Styles::BorderEndColor(_val) => { },
|
||||||
|
Styles::BorderBottomColor(_val) => { },
|
||||||
|
Styles::BorderLeftColor(_val) => { },
|
||||||
|
Styles::BorderRightColor(_val) => { },
|
||||||
|
Styles::BorderTopColor(_val) => { },
|
||||||
|
Styles::BorderStartColor(_val) => { },
|
||||||
|
|
||||||
|
Styles::BorderStyle(_val) => { },
|
||||||
|
Styles::BorderEndStyle(_val) => { },
|
||||||
|
Styles::BorderBottomStyle(_val) => { },
|
||||||
|
Styles::BorderLeftStyle(_val) => { },
|
||||||
|
Styles::BorderRightStyle(_val) => { },
|
||||||
|
Styles::BorderTopStyle(_val) => { },
|
||||||
|
Styles::BorderStartStyle(_val) => { },
|
||||||
|
|
||||||
|
Styles::BorderWidth(_val) => { },
|
||||||
|
Styles::BorderEndWidth(_val) => { },
|
||||||
|
Styles::BorderBottomWidth(_val) => { },
|
||||||
|
Styles::BorderLeftWidth(_val) => { },
|
||||||
|
Styles::BorderRightWidth(_val) => { },
|
||||||
|
Styles::BorderTopWidth(_val) => { },
|
||||||
|
Styles::BorderStartWidth(_val) => { },
|
||||||
|
|
||||||
|
Styles::BorderRadius(_val) => { },
|
||||||
|
Styles::BorderBottomEndRadius(_val) => { },
|
||||||
|
Styles::BorderBottomLeftRadius(_val) => { },
|
||||||
|
Styles::BorderBottomRightRadius(_val) => { },
|
||||||
|
Styles::BorderBottomStartRadius(_val) => { },
|
||||||
|
Styles::BorderTopLeftRadius(_val) => { },
|
||||||
|
Styles::BorderTopRightRadius(_val) => { },
|
||||||
|
Styles::BorderTopEndRadius(_val) => { },
|
||||||
|
Styles::BorderTopStartRadius(_val) => { },
|
||||||
|
|
||||||
|
Styles::Bottom(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: layout.position.start,
|
||||||
|
end: layout.position.end,
|
||||||
|
top: layout.position.top,
|
||||||
|
bottom: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::Direction(val) => { layout.direction = *val; },
|
||||||
|
Styles::Display(val) => { layout.display = *val; },
|
||||||
|
|
||||||
|
Styles::End(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: layout.position.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.position.top,
|
||||||
|
bottom: layout.position.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::FlexBasis(val) => { layout.flex_basis = Dimension::Points(*val); },
|
||||||
|
Styles::FlexDirection(val) => { layout.flex_direction = *val; },
|
||||||
|
Styles::FlexGrow(val) => { layout.flex_grow = *val; },
|
||||||
|
Styles::FlexShrink(val) => { layout.flex_shrink = *val; },
|
||||||
|
Styles::FlexWrap(val) => { layout.flex_wrap = *val; },
|
||||||
|
|
||||||
|
Styles::FontFamily(_val) => { },
|
||||||
|
Styles::FontLineHeight(_val) => { },
|
||||||
|
Styles::FontSize(_val) => { },
|
||||||
|
Styles::FontStyle(_val) => { },
|
||||||
|
Styles::FontWeight(_val) => { },
|
||||||
|
|
||||||
|
Styles::Height(val) => {
|
||||||
|
layout.size = Size {
|
||||||
|
width: layout.size.width,
|
||||||
|
height: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::JustifyContent(val) => { layout.justify_content = *val; },
|
||||||
|
|
||||||
|
Styles::Left(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.position.end,
|
||||||
|
top: layout.position.top,
|
||||||
|
bottom: layout.position.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginBottom(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: layout.margin.start,
|
||||||
|
end: layout.margin.end,
|
||||||
|
top: layout.margin.top,
|
||||||
|
bottom: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginEnd(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: layout.margin.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.margin.top,
|
||||||
|
bottom: layout.margin.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginLeft(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.margin.end,
|
||||||
|
top: layout.margin.top,
|
||||||
|
bottom: layout.margin.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginRight(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: layout.margin.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.margin.top,
|
||||||
|
bottom: layout.margin.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginStart(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.margin.end,
|
||||||
|
top: layout.margin.top,
|
||||||
|
bottom: layout.margin.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MarginTop(val) => {
|
||||||
|
layout.margin = Rect {
|
||||||
|
start: layout.margin.start,
|
||||||
|
end: layout.margin.end,
|
||||||
|
top: Dimension::Points(*val),
|
||||||
|
bottom: layout.margin.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MaxHeight(val) => {
|
||||||
|
layout.max_size = Size {
|
||||||
|
width: layout.max_size.width,
|
||||||
|
height: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MaxWidth(val) => {
|
||||||
|
layout.max_size = Size {
|
||||||
|
width: Dimension::Points(*val),
|
||||||
|
height: layout.max_size.height
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MinHeight(val) => {
|
||||||
|
layout.min_size = Size {
|
||||||
|
width: layout.min_size.width,
|
||||||
|
height: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::MinWidth(val) => {
|
||||||
|
layout.min_size = Size {
|
||||||
|
width: Dimension::Points(*val),
|
||||||
|
height: layout.min_size.height
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::Opacity(val) => { },
|
||||||
|
Styles::Overflow(val) => { },
|
||||||
|
|
||||||
|
Styles::PaddingBottom(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: layout.padding.start,
|
||||||
|
end: layout.padding.end,
|
||||||
|
top: layout.padding.top,
|
||||||
|
bottom: Dimension::Points(*val)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PaddingEnd(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: layout.padding.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.padding.top,
|
||||||
|
bottom: layout.padding.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PaddingLeft(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.padding.end,
|
||||||
|
top: layout.padding.top,
|
||||||
|
bottom: layout.padding.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PaddingRight(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: layout.padding.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.padding.top,
|
||||||
|
bottom: layout.padding.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PaddingStart(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.padding.end,
|
||||||
|
top: layout.padding.top,
|
||||||
|
bottom: layout.padding.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PaddingTop(val) => {
|
||||||
|
layout.padding = Rect {
|
||||||
|
start: layout.padding.start,
|
||||||
|
end: layout.padding.end,
|
||||||
|
top: Dimension::Points(*val),
|
||||||
|
bottom: layout.padding.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::PositionType(val) => { layout.position_type = *val; },
|
||||||
|
|
||||||
|
Styles::Right(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: layout.position.start,
|
||||||
|
end: Dimension::Points(*val),
|
||||||
|
top: layout.position.top,
|
||||||
|
bottom: layout.position.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::Start(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: Dimension::Points(*val),
|
||||||
|
end: layout.position.end,
|
||||||
|
top: layout.position.top,
|
||||||
|
bottom: layout.position.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::TextAlignment(val) => { },
|
||||||
|
Styles::TextColor(val) => { },
|
||||||
|
Styles::TextDecorationColor(val) => { },
|
||||||
|
Styles::TextShadowColor(val) => { },
|
||||||
|
Styles::TintColor(val) => { },
|
||||||
|
|
||||||
|
Styles::Top(val) => {
|
||||||
|
layout.position = Rect {
|
||||||
|
start: layout.position.start,
|
||||||
|
end: layout.position.end,
|
||||||
|
top: Dimension::Points(*val),
|
||||||
|
bottom: layout.position.bottom
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
Styles::Width(val) => {
|
||||||
|
layout.size = Size {
|
||||||
|
width: Dimension::Points(*val),
|
||||||
|
height: layout.size.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
60
alchemy/src/window/manager.rs
Normal file
60
alchemy/src/window/manager.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
//! Per-platform windows have their own nuances, and typically, their own windowservers.
|
||||||
|
//! We don't want to take away from that, but we do want to avoid scenarios where things get
|
||||||
|
//! a bit weird.
|
||||||
|
//!
|
||||||
|
//! Consider the following: let's say we have a `Window` instantiated in Rust, and we call
|
||||||
|
//! `.show()` on it. Then the window drops, on the Rust side. We should probably clean up our side,
|
||||||
|
//! right?
|
||||||
|
//!
|
||||||
|
//! There's also the fact that a user could opt to close a window. If that happens, we want to be
|
||||||
|
//! able to remove it from our structure... hence this manager that acts as a lightweight interface
|
||||||
|
//! for managing per-platform Window instances.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use crate::window::AppWindow;
|
||||||
|
|
||||||
|
/// A struct that provides a Window Manager, via some interior mutability magic.
|
||||||
|
pub struct WindowManager(Mutex<Vec<Arc<Mutex<AppWindow>>>>);
|
||||||
|
|
||||||
|
impl WindowManager {
|
||||||
|
/// Creates a new WindowManager instance.
|
||||||
|
pub(crate) fn new() -> WindowManager {
|
||||||
|
WindowManager(Mutex::new(Vec::with_capacity(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locks and acquires a new window ID, which our Windows use to loop back for
|
||||||
|
/// events and callbacks.
|
||||||
|
pub(crate) fn allocate_new_window_id(&self) -> usize {
|
||||||
|
let windows = self.0.lock().unwrap();
|
||||||
|
windows.len() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an `AppWindow` to this instance.
|
||||||
|
pub(crate) fn add(&self, window: Arc<Mutex<AppWindow>>) {
|
||||||
|
let mut windows = self.0.lock().unwrap();
|
||||||
|
if let None = windows.iter().position(|w| Arc::ptr_eq(&w, &window)) {
|
||||||
|
windows.push(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On a `will_close` event, our delegates will loop back here and notify that a window
|
||||||
|
/// with x id is closing, and should be removed. The `WindowDelegate` `will_close()` event
|
||||||
|
/// is fired here.
|
||||||
|
///
|
||||||
|
/// At the end of this, the window drops.
|
||||||
|
pub(crate) fn will_close(&self, window_id: usize) {
|
||||||
|
let mut windows = self.0.lock().unwrap();
|
||||||
|
if let Some(index) = windows.iter().position(|window| {
|
||||||
|
let mut w = window.lock().unwrap();
|
||||||
|
|
||||||
|
if w.id == window_id {
|
||||||
|
w.delegate.will_close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}) {
|
||||||
|
windows.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
alchemy/src/window/mod.rs
Normal file
7
alchemy/src/window/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! This module implements Windows and their associated lifecycles.
|
||||||
|
|
||||||
|
mod manager;
|
||||||
|
pub(crate) use manager::WindowManager;
|
||||||
|
|
||||||
|
pub mod window;
|
||||||
|
pub use window::{AppWindow, Window};
|
||||||
211
alchemy/src/window/window.rs
Normal file
211
alchemy/src/window/window.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
//! Implements the Window API. It attempts to provide a nice, common interface across
|
||||||
|
//! per-platform Window APIs.
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::{Component, WindowDelegate};
|
||||||
|
use alchemy_lifecycle::rsx::{Props, RSX};
|
||||||
|
|
||||||
|
use alchemy_styles::Stretch;
|
||||||
|
use alchemy_styles::number::Number;
|
||||||
|
use alchemy_styles::geometry::Size;
|
||||||
|
use alchemy_styles::styles::{Style, Dimension};
|
||||||
|
|
||||||
|
use crate::{App, SHARED_APP};
|
||||||
|
use crate::components::View;
|
||||||
|
use crate::reconciler::{diff_and_patch_tree, walk_and_apply_styles};
|
||||||
|
|
||||||
|
#[cfg(feature = "cocoa")]
|
||||||
|
use alchemy_cocoa::window::{Window as PlatformWindowBridge};
|
||||||
|
|
||||||
|
/// Utility function for creating a root_node.
|
||||||
|
fn create_root_node(instance: Option<Arc<Component>>, layout_manager: &mut Stretch) -> RSX {
|
||||||
|
let mut props = Props::default();
|
||||||
|
props.styles = "root".into();
|
||||||
|
|
||||||
|
let mut root_node = RSX::node("root", || Box::new(View::default()), props);
|
||||||
|
|
||||||
|
if let RSX::VirtualNode(root) = &mut root_node {
|
||||||
|
root.layout_node = match instance.is_some() {
|
||||||
|
true => {
|
||||||
|
let mut style = Style::default();
|
||||||
|
style.size = Size {
|
||||||
|
width: Dimension::Points(600.),
|
||||||
|
height: Dimension::Points(600.)
|
||||||
|
};
|
||||||
|
|
||||||
|
match layout_manager.new_node(style, vec![]) {
|
||||||
|
Ok(node) => Some(node),
|
||||||
|
Err(e) => { None }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
false => None
|
||||||
|
};
|
||||||
|
|
||||||
|
root.instance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
root_node
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AppWindow contains the inner details of a Window. It's guarded by a Mutex on `Window`,
|
||||||
|
/// and you shouldn't create this yourself, but it's documented here so you can understand what
|
||||||
|
/// it holds.
|
||||||
|
pub struct AppWindow {
|
||||||
|
pub id: usize,
|
||||||
|
pub title: String,
|
||||||
|
pub bridge: PlatformWindowBridge,
|
||||||
|
pub delegate: Box<WindowDelegate>,
|
||||||
|
pub root_node: RSX,
|
||||||
|
pub layout: Stretch
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppWindow {
|
||||||
|
/// Re-renders a window. This method calls `render()` on the `WindowDelegate`, patches it into
|
||||||
|
/// the root tree node, and then diffs the old (current) tree against a new tree by walking it
|
||||||
|
/// and determining what needs to be changed. This also calculates and applies layout and
|
||||||
|
/// styling.
|
||||||
|
///
|
||||||
|
/// This method is called on the `show` event, and in rare cases can be useful to call
|
||||||
|
/// directly.
|
||||||
|
pub fn render(&mut self) {
|
||||||
|
let mut new_root_node = create_root_node(None, &mut self.layout);
|
||||||
|
|
||||||
|
// For API reasons, we'll call the render for this Window, and then patch it into a new
|
||||||
|
// root node for the tree diff/patch comparison. For this we only need to go one level
|
||||||
|
// deep, the recursion in the next step will handle the rest.
|
||||||
|
match self.delegate.render() {
|
||||||
|
Ok(opt) => match opt {
|
||||||
|
RSX::VirtualNode(mut child) => {
|
||||||
|
if let RSX::VirtualNode(root) = &mut new_root_node {
|
||||||
|
if child.tag == "Fragment" {
|
||||||
|
root.children.append(&mut child.children);
|
||||||
|
} else {
|
||||||
|
root.children.push(RSX::VirtualNode(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// If it's an RSX::None, or a RSX::VirtualText, we do nothing, as... one
|
||||||
|
// requires nothing, and one isn't supported unless it's inside a <Text> tag.
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => { eprintln!("Error rendering window! {}", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taking ownership of the tree makes parts of this so much easier, so let's swap
|
||||||
|
// them out for the moment. We're going to discard the old one anyway.
|
||||||
|
let mut old_root_node = RSX::None;
|
||||||
|
std::mem::swap(&mut old_root_node, &mut self.root_node);
|
||||||
|
|
||||||
|
self.root_node = match diff_and_patch_tree(old_root_node, new_root_node, &mut self.layout, 0) {
|
||||||
|
Ok(node) => node,
|
||||||
|
Err(e) => { eprintln!("Error: {}", e); RSX::None }
|
||||||
|
};
|
||||||
|
|
||||||
|
self.configure_and_apply_styles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walks the tree again, purely concerning itself with calculating layout and applying styles.
|
||||||
|
/// This in effect creates a two-pass layout system. In the future much of this may be made
|
||||||
|
/// async, so relying on underlying behavior in here is considered... suspect.
|
||||||
|
///
|
||||||
|
/// This method is called on window resize and show events.
|
||||||
|
pub fn configure_and_apply_styles(&mut self) {
|
||||||
|
let window_size = Size {
|
||||||
|
width: Number::Defined(600.),
|
||||||
|
height: Number::Defined(600.)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RSX::VirtualNode(root_node) = &mut self.root_node {
|
||||||
|
if let Some(layout_node) = &root_node.layout_node {
|
||||||
|
match &self.layout.compute_layout(*layout_node, window_size) {
|
||||||
|
Ok(_) => { walk_and_apply_styles(&root_node, &mut self.layout); },
|
||||||
|
Err(e) => { eprintln!("Error computing layout: {:?}", e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders and calls through to the native platform window show method.
|
||||||
|
pub fn show(&mut self) {
|
||||||
|
self.render();
|
||||||
|
self.bridge.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls through to the native platform window close method.
|
||||||
|
pub fn close(&mut self) {
|
||||||
|
self.bridge.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows represented... well, a Window. When you create one, you get the Window back. When you
|
||||||
|
/// show one, a clone of the pointer is added to the window manager, and removed on close.
|
||||||
|
pub struct Window(pub(crate) Arc<Mutex<AppWindow>>);
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
/// Creates a new window.
|
||||||
|
pub fn new<S: 'static + WindowDelegate>(title: &str, dimensions: (f64, f64, f64, f64), delegate: S) -> Window {
|
||||||
|
let window_id = SHARED_APP.windows.allocate_new_window_id();
|
||||||
|
let mut layout = Stretch::new();
|
||||||
|
let view = View::default();
|
||||||
|
let shared_app_ptr: *const App = &**SHARED_APP;
|
||||||
|
let bridge = PlatformWindowBridge::new(window_id, title, dimensions, &view, shared_app_ptr);
|
||||||
|
|
||||||
|
Window(Arc::new(Mutex::new(AppWindow {
|
||||||
|
id: window_id,
|
||||||
|
title: title.into(),
|
||||||
|
bridge: bridge,
|
||||||
|
delegate: Box::new(delegate),
|
||||||
|
root_node: create_root_node(Some(Arc::new(view)), &mut layout),
|
||||||
|
layout: layout
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a window. By default, a window renders nothing - make sure you implement `render()`
|
||||||
|
/// on your `WindowDelegate`. Note that calling `.show()` implicitly calls this for you, so you
|
||||||
|
/// rarely need to call this yourself.
|
||||||
|
pub fn render(&self) {
|
||||||
|
let mut window = self.0.lock().unwrap();
|
||||||
|
window.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers this window with the window manager, renders it, and shows it.
|
||||||
|
pub fn show(&self) {
|
||||||
|
SHARED_APP.windows.add(self.0.clone());
|
||||||
|
let mut window = self.0.lock().unwrap();
|
||||||
|
window.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hides a window. On some platforms, this is minimizing... on others, like macOS, it's
|
||||||
|
/// actually hiding. On mobile, this shouldn't do anything.
|
||||||
|
pub fn hide(&self) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the window, unregistering it from the window manager in the process and ensuring the
|
||||||
|
/// necessary delegate method(s) are fired.
|
||||||
|
pub fn close(&self) {
|
||||||
|
let window_id = self.0.lock().unwrap().id;
|
||||||
|
SHARED_APP.windows.will_close(window_id);
|
||||||
|
let mut window = self.0.lock().unwrap();
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Window {
|
||||||
|
/// Clones a `Window` by cloning the inner `AppWindow`.
|
||||||
|
fn clone(&self) -> Window {
|
||||||
|
Window(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Window {
|
||||||
|
/// When a `Window` is dropped, we want to ensure that it's closed, so we'll silently call
|
||||||
|
/// `.close()` to be safe.
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
assets/README.md
Normal file
8
assets/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Alchemy Assets
|
||||||
|
This folder contains assets used for Alchemy (graphics, etc).
|
||||||
|
|
||||||
|
## The Logo
|
||||||
|
[The potion bottle graphic is a vector graphic from Vecteezy](https://www.vecteezy.com/vector-art/124561-free-magic-item-vector). Interested in contributing logo work? Please get in touch!
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
BIN
assets/alchemy_logo_250x.png
Normal file
BIN
assets/alchemy_logo_250x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
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"
|
||||||
|
]
|
||||||
23
cocoa/Cargo.toml
Normal file
23
cocoa/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "alchemy-cocoa"
|
||||||
|
description = "Cocoa bindings for Alchemy, a cross-platform GUI framework written in Rust."
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
license = "MPL-2.0+"
|
||||||
|
repository = "https://github.com/ryanmcgrath/alchemy"
|
||||||
|
categories = ["gui", "rendering::engine", "multimedia"]
|
||||||
|
keywords = ["gui", "cocoa", "macos", "appkit", "react"]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alchemy-lifecycle = { version = "0.1", path = "../lifecycle", features = ["cocoa"] }
|
||||||
|
alchemy-styles = { version = "0.1", path = "../styles" }
|
||||||
|
objc = "0.2.6"
|
||||||
|
objc_id = "0.1.1"
|
||||||
|
dispatch = "0.1.4"
|
||||||
|
cocoa = "0.18.4"
|
||||||
|
core-foundation = "0.6"
|
||||||
|
core-graphics = "0.17.1"
|
||||||
5
cocoa/README.md
Normal file
5
cocoa/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Alchemy-Cocoa
|
||||||
|
This crate implements a backend for Cocoa-based widgets, such as `NSView`, `NSTextField`, and so on. Note that while it's under development currently, the fate of AppKit is still kind of a gray area. If Apple ends up pushing Marzipan as "the" solution, it's possible this might become obsolete, or would run in tandem with the iOS crate for iOS/Marzipan.
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
153
cocoa/src/app.rs
Normal file
153
cocoa/src/app.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
//! A wrapper for `NSApplication` on macOS. If you opt in to the `cocoa` feature on
|
||||||
|
//! Alchemy, this will loop system-level application events back to your `AppDelegate`.
|
||||||
|
|
||||||
|
use std::sync::{Once, ONCE_INIT};
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil};
|
||||||
|
use cocoa::appkit::{NSApplication, NSRunningApplication};
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel};
|
||||||
|
use objc::{msg_send, class, sel, sel_impl};
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::AppDelegate;
|
||||||
|
|
||||||
|
static ALCHEMY_APP_PTR: &str = "alchemyParentAppPtr";
|
||||||
|
|
||||||
|
/// 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 Alchemy shared application.
|
||||||
|
pub struct App {
|
||||||
|
pub inner: Id<Object>,
|
||||||
|
pub delegate: Id<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// 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<T: AppDelegate>(parent_app_ptr: *const T) -> Self {
|
||||||
|
let inner = unsafe {
|
||||||
|
let _pool = cocoa::foundation::NSAutoreleasePool::new(nil);
|
||||||
|
let app = cocoa::appkit::NSApp();
|
||||||
|
app.setActivationPolicy_(cocoa::appkit::NSApplicationActivationPolicyRegular);
|
||||||
|
Id::from_ptr(app)
|
||||||
|
};
|
||||||
|
|
||||||
|
let delegate = unsafe {
|
||||||
|
let delegate_class = register_app_delegate_class::<T>();
|
||||||
|
let delegate: id = msg_send![delegate_class, new];
|
||||||
|
(&mut *delegate).set_ivar(ALCHEMY_APP_PTR, parent_app_ptr as usize);
|
||||||
|
msg_send![&*inner, setDelegate:delegate];
|
||||||
|
Id::from_ptr(delegate)
|
||||||
|
};
|
||||||
|
|
||||||
|
App {
|
||||||
|
delegate: delegate,
|
||||||
|
inner: inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kicks off the NSRunLoop for the NSApplication instance. This blocks when called.
|
||||||
|
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!(NSApplication), sharedApplication];
|
||||||
|
msg_send![shared_app, run];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationWillFinishLaunching` notification.
|
||||||
|
extern fn will_finish_launching<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).will_finish_launching();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationDidFinishLaunching` notification.
|
||||||
|
extern fn did_finish_launching<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).did_finish_launching();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationWillBecomeActive` notification.
|
||||||
|
extern fn will_become_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).will_become_active();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationDidBecomeActive` notification.
|
||||||
|
extern fn did_become_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).did_become_active();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationWillResignActive` notification.
|
||||||
|
extern fn will_resign_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).will_resign_active();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationDidResignActive` notification.
|
||||||
|
extern fn did_resign_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).did_resign_active();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fires when the Application Delegate receives a `applicationWillTerminate` notification.
|
||||||
|
extern fn will_terminate<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app).will_terminate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers an `NSObject` application delegate, and configures it for the various callbacks and
|
||||||
|
/// pointers we need to have.
|
||||||
|
fn register_app_delegate_class<T: AppDelegate>() -> *const Class {
|
||||||
|
static mut DELEGATE_CLASS: *const Class = 0 as *const Class;
|
||||||
|
static INIT: Once = ONCE_INIT;
|
||||||
|
|
||||||
|
INIT.call_once(|| unsafe {
|
||||||
|
let superclass = Class::get("NSObject").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("AlchemyAppDelegate", superclass).unwrap();
|
||||||
|
|
||||||
|
decl.add_ivar::<usize>(ALCHEMY_APP_PTR);
|
||||||
|
|
||||||
|
// Add callback methods
|
||||||
|
decl.add_method(sel!(applicationWillFinishLaunching:), will_finish_launching::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationDidFinishLaunching:), did_finish_launching::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationWillBecomeActive:), will_become_active::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationDidBecomeActive:), did_become_active::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationWillResignActive:), will_resign_active::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationDidResignActive:), did_resign_active::<T> as extern fn(&Object, _, _));
|
||||||
|
decl.add_method(sel!(applicationWillTerminate:), will_terminate::<T> as extern fn(&Object, _, _));
|
||||||
|
|
||||||
|
DELEGATE_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
DELEGATE_CLASS
|
||||||
|
}
|
||||||
|
}
|
||||||
29
cocoa/src/color.rs
Normal file
29
cocoa/src/color.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
//! Implements a conversion method for taking an `alchemy::Color` and turning it into
|
||||||
|
//! an `NSColor`.
|
||||||
|
|
||||||
|
use objc_id::Id;
|
||||||
|
use objc::runtime::Object;
|
||||||
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
use core_graphics::base::CGFloat;
|
||||||
|
|
||||||
|
use alchemy_styles::color::Color;
|
||||||
|
|
||||||
|
pub trait IntoNSColor {
|
||||||
|
fn into_nscolor(&self) -> Id<Object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoNSColor for Color {
|
||||||
|
/// This creates an NSColor, retains it, and returns it. Dropping this value will
|
||||||
|
/// call `release` on the Objective-C side.
|
||||||
|
fn into_nscolor(&self) -> Id<Object> {
|
||||||
|
let red = self.red as CGFloat / 255.0;
|
||||||
|
let green = self.green as CGFloat / 255.0;
|
||||||
|
let blue = self.blue as CGFloat / 255.0;
|
||||||
|
let alpha = self.alpha as CGFloat / 255.0;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
Id::from_ptr(msg_send![class!(NSColor), colorWithRed:red green:green blue:blue alpha:alpha])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
cocoa/src/lib.rs
Normal file
23
cocoa/src/lib.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
//! This crate provides a Cocoa backend for Alchemy, the Rust GUI framework.
|
||||||
|
//! This means that, on macOS, you'll be using native `NSView`, `NSTextField`,
|
||||||
|
//! and other assorted controls. Where possible, it attempts to opt into
|
||||||
|
//! smoother rendering paths (e.g, layer-backed views, drawing subview layers
|
||||||
|
//! together where appropriate).
|
||||||
|
//!
|
||||||
|
//! # License
|
||||||
|
//!
|
||||||
|
//! Copyright 2018 Ryan McGrath. See the license files included in the root repository
|
||||||
|
//! for more information, along with credit to applicable parties for who this project
|
||||||
|
//! would not have happened.
|
||||||
|
//!
|
||||||
|
//! # Code of Conduct
|
||||||
|
//!
|
||||||
|
//! Please note that this project is released with a [Contributor Code of
|
||||||
|
//! Conduct][coc]. By participating in this project you agree to abide by its terms.
|
||||||
|
//!
|
||||||
|
//! [coc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct
|
||||||
|
|
||||||
|
pub mod color;
|
||||||
|
pub mod app;
|
||||||
|
pub mod view;
|
||||||
|
pub mod window;
|
||||||
147
cocoa/src/view.rs
Normal file
147
cocoa/src/view.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
//! components/view/mod.rs
|
||||||
|
//!
|
||||||
|
//! Implements a View Component struct. The most common
|
||||||
|
//! basic building block of any app. Maps back to native
|
||||||
|
//! layer per-platform.
|
||||||
|
//!
|
||||||
|
//! @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
//! @created 03/29/2019
|
||||||
|
|
||||||
|
use std::sync::{Once, ONCE_INIT};
|
||||||
|
|
||||||
|
use objc_id::{Id, ShareId};
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize};
|
||||||
|
|
||||||
|
use crate::color::IntoNSColor;
|
||||||
|
|
||||||
|
use alchemy_styles::color::Color;
|
||||||
|
use alchemy_styles::styles::Style;
|
||||||
|
use alchemy_styles::result::Layout;
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::PlatformSpecificNodeType;
|
||||||
|
|
||||||
|
static ALCHEMY_DELEGATE: &str = "alchemyDelegate";
|
||||||
|
static BACKGROUND_COLOR: &str = "alchemyBackgroundColor";
|
||||||
|
|
||||||
|
/// A wrapper for `NSView`. This holds retained pointers for the Objective-C
|
||||||
|
/// runtime - namely, the view itself, and associated things such as background
|
||||||
|
/// colors and so forth.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct View {
|
||||||
|
inner_mut: Id<Object>,
|
||||||
|
inner_share: ShareId<Object>,
|
||||||
|
background_color: Id<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View {
|
||||||
|
/// Allocates a new `NSView` on the Objective-C side, ensuring that things like coordinate
|
||||||
|
/// flipping occur (macOS still uses (0,0) as lower-left by default), and opting in to layer
|
||||||
|
/// backed views for smoother scrolling.
|
||||||
|
pub fn new() -> View {
|
||||||
|
let (inner_mut, inner_share) = 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];
|
||||||
|
msg_send![view, setWantsLayer:YES];
|
||||||
|
msg_send![view, setLayerContentsRedrawPolicy:1];
|
||||||
|
let x = view.clone();
|
||||||
|
(Id::from_ptr(view), ShareId::from_ptr(x))
|
||||||
|
};
|
||||||
|
|
||||||
|
View {
|
||||||
|
inner_mut: inner_mut,
|
||||||
|
inner_share: inner_share,
|
||||||
|
background_color: Color::transparent().into_nscolor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a pointer to the underlying Objective-C view. The pointer is not mutable; however,
|
||||||
|
/// you can send messages to it (unsafely).
|
||||||
|
pub fn borrow_native_backing_node(&self) -> PlatformSpecificNodeType {
|
||||||
|
self.inner_share.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a child NSView (or subclassed type) to this view.
|
||||||
|
pub fn append_child(&mut self, child: PlatformSpecificNodeType) {
|
||||||
|
unsafe {
|
||||||
|
msg_send![&*self.inner_mut, addSubview:child];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a `&Style`, will set the frame, background color, borders and so forth. It then
|
||||||
|
/// calls `setNeedsDisplay:YES` on the Objective-C side, so that Cocoa will re-render this
|
||||||
|
/// view.
|
||||||
|
pub fn apply_styles(&mut self, layout: &Layout, style: &Style) {
|
||||||
|
unsafe {
|
||||||
|
let rect = NSRect::new(
|
||||||
|
NSPoint::new(layout.location.x.into(), layout.location.y.into()),
|
||||||
|
NSSize::new(layout.size.width.into(), layout.size.height.into())
|
||||||
|
);
|
||||||
|
|
||||||
|
self.background_color = style.background_color.into_nscolor();
|
||||||
|
self.inner_mut.set_ivar(BACKGROUND_COLOR, &*self.background_color);
|
||||||
|
|
||||||
|
msg_send![&*self.inner_mut, setFrame:rect];
|
||||||
|
msg_send![&*self.inner_mut, setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When an `NSView` has `updateLayer` called, it will get passed through here, at which point we
|
||||||
|
/// instruct the layer how it should render (e.g, background color).
|
||||||
|
extern fn update_layer(this: &Object, _: Sel) {
|
||||||
|
unsafe {
|
||||||
|
let background_color: id = *this.get_ivar(BACKGROUND_COLOR);
|
||||||
|
if background_color != nil {
|
||||||
|
let layer: id = msg_send![this, layer];
|
||||||
|
let cg: id = msg_send![background_color, CGColor];
|
||||||
|
msg_send![layer, setBackgroundColor:cg];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_INIT;
|
||||||
|
|
||||||
|
INIT.call_once(|| unsafe {
|
||||||
|
let superclass = Class::get("NSView").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("AlchemyView", 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);
|
||||||
|
|
||||||
|
// Opt-in to AutoLayout
|
||||||
|
//decl.add_method(sel!(requiresConstraintBasedLayout), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
|
||||||
|
// Request optimized backing layers
|
||||||
|
decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
|
||||||
|
decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||||
|
|
||||||
|
// Ensure mouse events and so on work
|
||||||
|
//decl.add_method(sel!(acceptsFirstResponder), update_layer as extern fn(&Object, _));
|
||||||
|
|
||||||
|
// A pointer back to our View, for forwarding mouse + etc events.
|
||||||
|
// Note that NSView's don't really have a "delegate", I'm just using it here
|
||||||
|
// for common terminology sake.
|
||||||
|
decl.add_ivar::<usize>(ALCHEMY_DELEGATE);
|
||||||
|
decl.add_ivar::<id>(BACKGROUND_COLOR);
|
||||||
|
|
||||||
|
VIEW_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { VIEW_CLASS }
|
||||||
|
}
|
||||||
148
cocoa/src/window.rs
Normal file
148
cocoa/src/window.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
//! 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::sync::{Once, ONCE_INIT};
|
||||||
|
|
||||||
|
use cocoa::base::{id, nil, YES, NO};
|
||||||
|
use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSBackingStoreType};
|
||||||
|
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSAutoreleasePool};
|
||||||
|
|
||||||
|
use objc_id::ShareId;
|
||||||
|
use objc::declare::ClassDecl;
|
||||||
|
use objc::runtime::{Class, Object, Sel};
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use alchemy_lifecycle::traits::{AppDelegate, Component};
|
||||||
|
|
||||||
|
static APP_PTR: &str = "alchemyAppPtr";
|
||||||
|
static WINDOW_MANAGER_ID: &str = "alchemyWindowManagerID";
|
||||||
|
|
||||||
|
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
|
||||||
|
/// where our `NSWindow` and associated delegate live.
|
||||||
|
pub struct Window {
|
||||||
|
pub inner: ShareId<Object>,
|
||||||
|
pub delegate: ShareId<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
/// 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<T: AppDelegate>(window_id: usize, title: &str, dimensions: (f64, f64, f64, f64), content_view: &Component, app_ptr: *const T) -> Window {
|
||||||
|
let (top, left, width, height) = dimensions;
|
||||||
|
let dimensions = NSRect::new(NSPoint::new(top, left), NSSize::new(width, height));
|
||||||
|
|
||||||
|
let style = NSWindowStyleMask::NSResizableWindowMask |
|
||||||
|
NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask |
|
||||||
|
NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask;
|
||||||
|
|
||||||
|
let inner = unsafe {
|
||||||
|
let window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_(
|
||||||
|
dimensions,
|
||||||
|
style,
|
||||||
|
NSBackingStoreType::NSBackingStoreBuffered,
|
||||||
|
NO
|
||||||
|
).autorelease();
|
||||||
|
|
||||||
|
let title = NSString::alloc(nil).init_str(title);
|
||||||
|
window.setTitle_(title);
|
||||||
|
msg_send![window, setTitlebarAppearsTransparent:YES];
|
||||||
|
msg_send![window, setTitleVisibility:1];
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
msg_send![window, setReleasedWhenClosed:NO];
|
||||||
|
|
||||||
|
if let Some(view_ptr) = content_view.borrow_native_backing_node() {
|
||||||
|
msg_send![window, setContentView:view_ptr];
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareId::from_ptr(window)
|
||||||
|
};
|
||||||
|
|
||||||
|
let delegate = unsafe {
|
||||||
|
let delegate_class = register_window_class::<T>();
|
||||||
|
let delegate: id = msg_send![delegate_class, new];
|
||||||
|
(&mut *delegate).set_ivar(APP_PTR, app_ptr as usize);
|
||||||
|
(&mut *delegate).set_ivar(WINDOW_MANAGER_ID, window_id);
|
||||||
|
msg_send![inner, setDelegate:delegate];
|
||||||
|
ShareId::from_ptr(delegate)
|
||||||
|
};
|
||||||
|
|
||||||
|
Window {
|
||||||
|
inner: inner,
|
||||||
|
delegate: delegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
unsafe {
|
||||||
|
msg_send![&*self.inner, makeKeyAndOrderFront:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the
|
||||||
|
/// window.
|
||||||
|
///
|
||||||
|
/// I dunno what else to say here, lol.
|
||||||
|
///
|
||||||
|
/// You should never be calling this yourself, mind you - Alchemy core handles this for you.
|
||||||
|
pub fn close(&self) {
|
||||||
|
unsafe {
|
||||||
|
msg_send![&*self.inner, close];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Window {
|
||||||
|
/// 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) {
|
||||||
|
// This bridging link needs to be broken on Drop.
|
||||||
|
unsafe {
|
||||||
|
msg_send![&*self.inner, setDelegate:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when a Window receives a `windowWillClose:` event. Loops back to the shared
|
||||||
|
/// Alchemy app instance, so that our window manager can act appropriately.
|
||||||
|
extern fn will_close<T: AppDelegate>(this: &Object, _: Sel, _: id) {
|
||||||
|
unsafe {
|
||||||
|
let app_ptr: usize = *this.get_ivar(APP_PTR);
|
||||||
|
let window_id: usize = *this.get_ivar(WINDOW_MANAGER_ID);
|
||||||
|
let app = app_ptr as *mut T;
|
||||||
|
(*app)._window_will_close(window_id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we
|
||||||
|
/// need to do.
|
||||||
|
fn register_window_class<T: AppDelegate>() -> *const Class {
|
||||||
|
static mut DELEGATE_CLASS: *const Class = 0 as *const Class;
|
||||||
|
static INIT: Once = ONCE_INIT;
|
||||||
|
|
||||||
|
INIT.call_once(|| unsafe {
|
||||||
|
let superclass = Class::get("NSObject").unwrap();
|
||||||
|
let mut decl = ClassDecl::new("alchemyWindowDelegateShim", superclass).unwrap();
|
||||||
|
|
||||||
|
decl.add_ivar::<usize>(APP_PTR);
|
||||||
|
decl.add_ivar::<usize>(WINDOW_MANAGER_ID);
|
||||||
|
|
||||||
|
decl.add_method(sel!(windowWillClose:), will_close::<T> as extern fn(&Object, _, _));
|
||||||
|
|
||||||
|
DELEGATE_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
DELEGATE_CLASS
|
||||||
|
}
|
||||||
|
}
|
||||||
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/
|
||||||
8
examples/layout/Cargo.toml
Normal file
8
examples/layout/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "layout"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alchemy = { path = "../../alchemy", version = "0.2.0", features = ["cocoa"] }
|
||||||
108
examples/layout/src/main.rs
Normal file
108
examples/layout/src/main.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#![recursion_limit="256"]
|
||||||
|
|
||||||
|
/// demo/main.rs
|
||||||
|
///
|
||||||
|
/// Used to sketch out application structure/feel/etc.
|
||||||
|
///
|
||||||
|
/// @author Ryan McGrath <ryan@rymc.io>
|
||||||
|
/// @created March 26th, 2019
|
||||||
|
|
||||||
|
use alchemy::{
|
||||||
|
AppDelegate, Component, Fragment, Props, Error, rsx, RSX, styles,
|
||||||
|
View, Window, WindowDelegate
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
window: Window
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDelegate for AppState {
|
||||||
|
fn did_finish_launching(&mut self) {
|
||||||
|
self.window.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Banner;
|
||||||
|
|
||||||
|
impl Component for Banner {
|
||||||
|
fn render(&self, props: &Props) -> Result<RSX, Error> {
|
||||||
|
Ok(rsx! {
|
||||||
|
<Fragment>
|
||||||
|
<View styles=["wut1"]></View>
|
||||||
|
{props.children.clone()}
|
||||||
|
</Fragment>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WindowState;
|
||||||
|
|
||||||
|
impl WindowDelegate for WindowState {
|
||||||
|
fn will_close(&mut self) {
|
||||||
|
println!("Closing!?");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> Result<RSX, Error> {
|
||||||
|
let messages = vec!["LOL"]; //, "wut", "BERT"];
|
||||||
|
|
||||||
|
Ok(rsx! {
|
||||||
|
<View styles={messages}>
|
||||||
|
<View styles=["boxxx"] />
|
||||||
|
/*{messages.iter().map(|message| rsx! {
|
||||||
|
<View>{text!("{}", message)}</View>
|
||||||
|
})}*/
|
||||||
|
<View styles=["box1"]>
|
||||||
|
//<View styles=["box1"]></View>
|
||||||
|
<Banner>
|
||||||
|
<View styles=["innermostBox"] />
|
||||||
|
</Banner>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = alchemy::shared_app();
|
||||||
|
|
||||||
|
app.register_styles("default", styles! {
|
||||||
|
LOL {
|
||||||
|
background-color: #307ace;
|
||||||
|
width: 500;
|
||||||
|
height: 230;
|
||||||
|
padding-top: 20;
|
||||||
|
padding-left: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
boxxx {
|
||||||
|
background-color: rgba(245, 217, 28, .8);
|
||||||
|
width: 100;
|
||||||
|
height: 100;
|
||||||
|
margin-top: 40;
|
||||||
|
margin-right: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
box1 {
|
||||||
|
background-color: #f51c69;
|
||||||
|
width: 250;
|
||||||
|
height: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
wut1 {
|
||||||
|
background-color: black;
|
||||||
|
width: 50;
|
||||||
|
height: 230;
|
||||||
|
}
|
||||||
|
|
||||||
|
innermostBox {
|
||||||
|
background-color: green;
|
||||||
|
width: 20;
|
||||||
|
height: 20;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run(AppState {
|
||||||
|
window: Window::new("Testing...", (0., 0., 600., 600.), WindowState {})
|
||||||
|
});
|
||||||
|
}
|
||||||
19
lifecycle/Cargo.toml
Normal file
19
lifecycle/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "alchemy-lifecycle"
|
||||||
|
description = "A crate containing traits used in Alchemy, the Rust cross-platform GUI framework."
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
license = "MPL-2.0+"
|
||||||
|
repository = "https://github.com/ryanmcgrath/alchemy"
|
||||||
|
categories = ["gui", "rendering::engine", "multimedia"]
|
||||||
|
keywords = ["gui", "css", "styles", "layout", "ui"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cocoa = ["objc", "objc_id"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alchemy-styles = { version = "0.1", path = "../styles" }
|
||||||
|
objc = { version = "0.2.6", optional = true }
|
||||||
|
objc_id = { version = "0.1.1", optional = true }
|
||||||
|
serde_json = "1"
|
||||||
5
lifecycle/README.md
Normal file
5
lifecycle/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Alchemy-Lifecycle
|
||||||
|
This crate holds traits used in Alchemy, such as `AppDelegate`, `WindowDelegate`, and `Component`. It also holds the RSX node/tag system, since it's standalone and `Component` ends up requiring it anyway.
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
7
lifecycle/src/error.rs
Normal file
7
lifecycle/src/error.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! Implements an Error type. Currently we just alias this to
|
||||||
|
//! Box<Error>, because I'm not sure how this should really look. Consider
|
||||||
|
//! it an implementation detail hook that could change down the road.
|
||||||
|
|
||||||
|
/// A generic Error type that we use. It currently just aliases to `Box<std::error::Error>`,
|
||||||
|
/// but could change in the future.
|
||||||
|
pub type Error = Box<std::error::Error>;
|
||||||
13
lifecycle/src/lib.rs
Normal file
13
lifecycle/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
//! Lifecycle aspects for Alchemy.
|
||||||
|
//!
|
||||||
|
//! What's a lifecycle? Well, it includes things like delegates (App+Window),
|
||||||
|
//! where they act as hooks for the system to inform you of events. It includes
|
||||||
|
//! things like `Component`s, which instruct your views how to exist.
|
||||||
|
//!
|
||||||
|
//! It also includes the `RSX` enum, which is what `render()` methods generally
|
||||||
|
//! return. It's common enough to multiple crates, and is intricately linked to the
|
||||||
|
//! `Component` lifecycle, so it'll live here.
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod rsx;
|
||||||
|
pub mod traits;
|
||||||
91
lifecycle/src/rsx/mod.rs
Normal file
91
lifecycle/src/rsx/mod.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
//! This module holds pieces pertaining to `RSX` element(s), which are lightweight
|
||||||
|
//! structs that represent how something should be flushed to the screen. Alchemy
|
||||||
|
//! uses these to build and alter UI; they're typically returned from `render()`
|
||||||
|
//! methods.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
mod virtual_node;
|
||||||
|
pub use virtual_node::VirtualNode;
|
||||||
|
|
||||||
|
mod virtual_text;
|
||||||
|
pub use virtual_text::VirtualText;
|
||||||
|
|
||||||
|
mod props;
|
||||||
|
pub use props::Props;
|
||||||
|
|
||||||
|
mod style_keys;
|
||||||
|
pub use self::style_keys::StyleKey;
|
||||||
|
|
||||||
|
mod spacedlist;
|
||||||
|
pub use self::spacedlist::SpacedList;
|
||||||
|
|
||||||
|
mod spacedset;
|
||||||
|
pub use self::spacedset::SpacedSet;
|
||||||
|
|
||||||
|
pub type StylesList = SpacedSet<StyleKey>;
|
||||||
|
|
||||||
|
use crate::traits::Component;
|
||||||
|
|
||||||
|
/// An enum representing the types of nodes that the
|
||||||
|
/// system can work with. `None`, `VirtualText`, or `VirtualNode`.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum RSX {
|
||||||
|
None,
|
||||||
|
VirtualText(VirtualText),
|
||||||
|
VirtualNode(VirtualNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RSX {
|
||||||
|
/// Shorthand method for creating a new `RSX::VirtualNode` instance. Rarely should you call
|
||||||
|
/// this yourself; the `rsx! {}` macro handles this for you.
|
||||||
|
pub fn node<F: Fn() -> Box<Component> + Send + Sync + 'static>(tag: &'static str, create_fn: F, props: Props) -> RSX {
|
||||||
|
RSX::VirtualNode(VirtualNode {
|
||||||
|
tag: tag,
|
||||||
|
create_component_fn: Arc::new(create_fn),
|
||||||
|
instance: None,
|
||||||
|
layout_node: None,
|
||||||
|
props: props,
|
||||||
|
children: vec![]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand method for creating a new `RSX::VirtualText` instance. Rarely should you call
|
||||||
|
/// this yourself; the `rsx! {}` and `text!()` macros handle this for you.
|
||||||
|
pub fn text(s: String) -> RSX {
|
||||||
|
RSX::VirtualText(VirtualText(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for RSX {
|
||||||
|
type Item = RSX;
|
||||||
|
type IntoIter = std::vec::IntoIter<RSX>;
|
||||||
|
|
||||||
|
/// Turn a single `RSX` node into an iterable instance.
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
vec![self].into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RSX {
|
||||||
|
/// Specialized rendering for displaying RSX nodes.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
match self {
|
||||||
|
RSX::VirtualNode(node) => { std::fmt::Display::fmt(&node, f) },
|
||||||
|
RSX::VirtualText(text) => { std::fmt::Display::fmt(&text, f) }
|
||||||
|
RSX::None => { Ok(()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for RSX {
|
||||||
|
/// Specialized rendering for debugging RSX nodes.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
RSX::VirtualNode(node) => { std::fmt::Debug::fmt(&node, f) },
|
||||||
|
RSX::VirtualText(text) => { std::fmt::Debug::fmt(&text, f) }
|
||||||
|
RSX::None => { Ok(()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lifecycle/src/rsx/props.rs
Normal file
52
lifecycle/src/rsx/props.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
//! Implements a Props struct that mostly acts as expected. For arbitrary primitive values,
|
||||||
|
//! it shadows a `serde_json::Value`.
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::rsx::{RSX, StylesList};
|
||||||
|
|
||||||
|
/// A value stored inside the `attributes` field on a `Props` instance.
|
||||||
|
/// It shadows `serde_json::Value`, but also allows for some other value
|
||||||
|
/// types common to Alchemy.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AttributeType {
|
||||||
|
Value(Value),
|
||||||
|
//RSX(RSX)
|
||||||
|
//EventHandler(Box<ComponentEventHandler>)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for AttributeType {
|
||||||
|
/// Converts a &str to a storable AttributeType.
|
||||||
|
fn from(f: &str) -> Self {
|
||||||
|
AttributeType::Value(Value::String(f.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emulates props from React, in a sense. Common keys such as `children`, `key` and `styles`
|
||||||
|
/// are extracted out for fast access, and everything else found gets put into the `attributes`
|
||||||
|
/// HashMap.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Props {
|
||||||
|
pub attributes: HashMap<&'static str, AttributeType>,
|
||||||
|
pub children: Vec<RSX>,
|
||||||
|
pub key: String,
|
||||||
|
pub styles: StylesList
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Props {
|
||||||
|
/// Returns a Vec of RSX nodes, which are really just cloned pointers for the most part.
|
||||||
|
pub fn children(&self) -> Vec<RSX> {
|
||||||
|
self.children.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Option<&AttributeType> from the `attributes` inner HashMap.
|
||||||
|
pub fn get(&self, key: &str) -> Option<&AttributeType> {
|
||||||
|
match key {
|
||||||
|
"children" => { None },
|
||||||
|
"key" => { None },
|
||||||
|
"styles" => { None },
|
||||||
|
_ => { None } //self.attributes.get(key) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
262
lifecycle/src/rsx/spacedlist.rs
Normal file
262
lifecycle/src/rsx/spacedlist.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
//! A space separated list of values.
|
||||||
|
//!
|
||||||
|
//! This type represents a list of non-unique values represented as a string of
|
||||||
|
//! values separated by spaces in HTML attributes. This is rarely used; a
|
||||||
|
//! SpacedSet of unique values is much more common.
|
||||||
|
|
||||||
|
|
||||||
|
use std::fmt::{Debug, Display, Error, Formatter};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// A space separated list of values.
|
||||||
|
///
|
||||||
|
/// This type represents a list of non-unique values represented as a string of
|
||||||
|
/// values separated by spaces in HTML attributes. This is rarely used; a
|
||||||
|
/// SpacedSet of unique values is much more common.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SpacedList<A>(Vec<A>);
|
||||||
|
|
||||||
|
impl<A> SpacedList<A> {
|
||||||
|
/// Construct an empty `SpacedList`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SpacedList(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> Default for SpacedList<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> FromIterator<A> for SpacedList<A> {
|
||||||
|
fn from_iter<I>(iter: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = A>,
|
||||||
|
{
|
||||||
|
SpacedList(iter.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: 'a + Clone> FromIterator<&'a A> for SpacedList<A> {
|
||||||
|
fn from_iter<I>(iter: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a A>,
|
||||||
|
{
|
||||||
|
SpacedList(iter.into_iter().cloned().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: FromStr> From<&'a str> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: &'a str) -> Self {
|
||||||
|
Self::from_iter(s.split_whitespace().map(|s| FromStr::from_str(s).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> Deref for SpacedList<A> {
|
||||||
|
type Target = Vec<A>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> DerefMut for SpacedList<A> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Display> Display for SpacedList<A> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||||
|
let mut it = self.0.iter().peekable();
|
||||||
|
while let Some(class) = it.next() {
|
||||||
|
Display::fmt(class, f)?;
|
||||||
|
if it.peek().is_some() {
|
||||||
|
Display::fmt(" ", f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Debug> Debug for SpacedList<A> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||||
|
f.debug_list().entries(self.0.iter()).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, A: FromStr> From<(&'a str, &'b str)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, A: FromStr> From<(&'a str, &'b str, &'c str)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, A: FromStr> From<(&'a str, &'b str, &'c str, &'d str)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.3).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, A: FromStr> From<(&'a str, &'b str, &'c str, &'d str, &'e str)>
|
||||||
|
for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.4).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, A: FromStr>
|
||||||
|
From<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.5).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A: FromStr>
|
||||||
|
From<(
|
||||||
|
&'a str,
|
||||||
|
&'b str,
|
||||||
|
&'c str,
|
||||||
|
&'d str,
|
||||||
|
&'e str,
|
||||||
|
&'f str,
|
||||||
|
&'g str,
|
||||||
|
)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.5).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.6).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A: FromStr>
|
||||||
|
From<(
|
||||||
|
&'a str,
|
||||||
|
&'b str,
|
||||||
|
&'c str,
|
||||||
|
&'d str,
|
||||||
|
&'e str,
|
||||||
|
&'f str,
|
||||||
|
&'g str,
|
||||||
|
&'h str,
|
||||||
|
)> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.push(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.5).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.6).unwrap());
|
||||||
|
list.push(FromStr::from_str(s.7).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! spacedlist_from_array {
|
||||||
|
($num:tt) => {
|
||||||
|
impl<'a, A: FromStr> From<[&'a str; $num]> for SpacedList<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: [&str; $num]) -> Self {
|
||||||
|
Self::from_iter(s.into_iter().map(|s| FromStr::from_str(*s).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spacedlist_from_array!(1);
|
||||||
|
spacedlist_from_array!(2);
|
||||||
|
spacedlist_from_array!(3);
|
||||||
|
spacedlist_from_array!(4);
|
||||||
|
spacedlist_from_array!(5);
|
||||||
|
spacedlist_from_array!(6);
|
||||||
|
spacedlist_from_array!(7);
|
||||||
|
spacedlist_from_array!(8);
|
||||||
|
spacedlist_from_array!(9);
|
||||||
|
spacedlist_from_array!(10);
|
||||||
|
spacedlist_from_array!(11);
|
||||||
|
spacedlist_from_array!(12);
|
||||||
|
spacedlist_from_array!(13);
|
||||||
|
spacedlist_from_array!(14);
|
||||||
|
spacedlist_from_array!(15);
|
||||||
|
spacedlist_from_array!(16);
|
||||||
|
spacedlist_from_array!(17);
|
||||||
|
spacedlist_from_array!(18);
|
||||||
|
spacedlist_from_array!(19);
|
||||||
|
spacedlist_from_array!(20);
|
||||||
|
spacedlist_from_array!(21);
|
||||||
|
spacedlist_from_array!(22);
|
||||||
|
spacedlist_from_array!(23);
|
||||||
|
spacedlist_from_array!(24);
|
||||||
|
spacedlist_from_array!(25);
|
||||||
|
spacedlist_from_array!(26);
|
||||||
|
spacedlist_from_array!(27);
|
||||||
|
spacedlist_from_array!(28);
|
||||||
|
spacedlist_from_array!(29);
|
||||||
|
spacedlist_from_array!(30);
|
||||||
|
spacedlist_from_array!(31);
|
||||||
|
spacedlist_from_array!(32);
|
||||||
293
lifecycle/src/rsx/spacedset.rs
Normal file
293
lifecycle/src/rsx/spacedset.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
//! A space separated set of unique values.
|
||||||
|
//!
|
||||||
|
//! This type represents a set of unique values represented as a string of
|
||||||
|
//! values separated by spaces in HTML attributes.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt::{Debug, Display, Error, Formatter};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// A space separated set of unique values.
|
||||||
|
///
|
||||||
|
/// This type represents a set of unique values represented as a string of
|
||||||
|
/// values separated by spaces in HTML attributes.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SpacedSet<A: Ord>(pub BTreeSet<A>);
|
||||||
|
|
||||||
|
impl<A: Ord> SpacedSet<A> {
|
||||||
|
/// Construct an empty `SpacedSet`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SpacedSet(BTreeSet::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a value to the `SpacedSet`.
|
||||||
|
pub fn add<T: Into<A>>(&mut self, value: T) -> bool {
|
||||||
|
self.0.insert(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord> Default for SpacedSet<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord> FromIterator<A> for SpacedSet<A> {
|
||||||
|
fn from_iter<I>(iter: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = A>,
|
||||||
|
{
|
||||||
|
SpacedSet(iter.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: 'a + Ord + Clone> FromIterator<&'a A> for SpacedSet<A> {
|
||||||
|
fn from_iter<I>(iter: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a A>,
|
||||||
|
{
|
||||||
|
SpacedSet(iter.into_iter().cloned().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: Ord + FromStr> FromStr for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
type Err = <A as FromStr>::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let result: Result<Vec<A>, Self::Err> =
|
||||||
|
s.split_whitespace().map(|s| FromStr::from_str(s)).collect();
|
||||||
|
result.map(Self::from_iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: Ord + FromStr> From<&'a str> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: &'a str) -> Self {
|
||||||
|
Self::from_iter(s.split_whitespace().map(|s| FromStr::from_str(s).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord> Deref for SpacedSet<A> {
|
||||||
|
type Target = BTreeSet<A>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord> DerefMut for SpacedSet<A> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord + Display> Display for SpacedSet<A> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||||
|
let mut it = self.0.iter().peekable();
|
||||||
|
while let Some(class) = it.next() {
|
||||||
|
Display::fmt(class, f)?;
|
||||||
|
if it.peek().is_some() {
|
||||||
|
Display::fmt(" ", f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Ord + Debug> Debug for SpacedSet<A> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||||
|
f.debug_list().entries(self.0.iter()).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, A: Ord + FromStr> From<Vec<&'a str>> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: Vec<&'a str>) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
|
||||||
|
for key in s {
|
||||||
|
list.insert(FromStr::from_str(key).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, A: Ord + FromStr> From<(&'a str, &'b str)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, A: Ord + FromStr> From<(&'a str, &'b str, &'c str)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, A: Ord + FromStr> From<(&'a str, &'b str, &'c str, &'d str)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.3).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, A: Ord + FromStr> From<(&'a str, &'b str, &'c str, &'d str, &'e str)>
|
||||||
|
for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.4).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, A: Ord + FromStr>
|
||||||
|
From<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.5).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A: Ord + FromStr>
|
||||||
|
From<(
|
||||||
|
&'a str,
|
||||||
|
&'b str,
|
||||||
|
&'c str,
|
||||||
|
&'d str,
|
||||||
|
&'e str,
|
||||||
|
&'f str,
|
||||||
|
&'g str,
|
||||||
|
)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.5).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.6).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A: Ord + FromStr>
|
||||||
|
From<(
|
||||||
|
&'a str,
|
||||||
|
&'b str,
|
||||||
|
&'c str,
|
||||||
|
&'d str,
|
||||||
|
&'e str,
|
||||||
|
&'f str,
|
||||||
|
&'g str,
|
||||||
|
&'h str,
|
||||||
|
)> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Self {
|
||||||
|
let mut list = Self::new();
|
||||||
|
list.insert(FromStr::from_str(s.0).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.1).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.2).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.3).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.4).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.5).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.6).unwrap());
|
||||||
|
list.insert(FromStr::from_str(s.7).unwrap());
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! spacedlist_from_array {
|
||||||
|
($num:tt) => {
|
||||||
|
impl<'a, A: Ord + FromStr> From<[&'a str; $num]> for SpacedSet<A>
|
||||||
|
where
|
||||||
|
<A as FromStr>::Err: Debug,
|
||||||
|
{
|
||||||
|
fn from(s: [&str; $num]) -> Self {
|
||||||
|
Self::from_iter(s.into_iter().map(|s| FromStr::from_str(*s).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spacedlist_from_array!(1);
|
||||||
|
spacedlist_from_array!(2);
|
||||||
|
spacedlist_from_array!(3);
|
||||||
|
spacedlist_from_array!(4);
|
||||||
|
spacedlist_from_array!(5);
|
||||||
|
spacedlist_from_array!(6);
|
||||||
|
spacedlist_from_array!(7);
|
||||||
|
spacedlist_from_array!(8);
|
||||||
|
spacedlist_from_array!(9);
|
||||||
|
spacedlist_from_array!(10);
|
||||||
|
spacedlist_from_array!(11);
|
||||||
|
spacedlist_from_array!(12);
|
||||||
|
spacedlist_from_array!(13);
|
||||||
|
spacedlist_from_array!(14);
|
||||||
|
spacedlist_from_array!(15);
|
||||||
|
spacedlist_from_array!(16);
|
||||||
|
spacedlist_from_array!(17);
|
||||||
|
spacedlist_from_array!(18);
|
||||||
|
spacedlist_from_array!(19);
|
||||||
|
spacedlist_from_array!(20);
|
||||||
|
spacedlist_from_array!(21);
|
||||||
|
spacedlist_from_array!(22);
|
||||||
|
spacedlist_from_array!(23);
|
||||||
|
spacedlist_from_array!(24);
|
||||||
|
spacedlist_from_array!(25);
|
||||||
|
spacedlist_from_array!(26);
|
||||||
|
spacedlist_from_array!(27);
|
||||||
|
spacedlist_from_array!(28);
|
||||||
|
spacedlist_from_array!(29);
|
||||||
|
spacedlist_from_array!(30);
|
||||||
|
spacedlist_from_array!(31);
|
||||||
|
spacedlist_from_array!(32);
|
||||||
83
lifecycle/src/rsx/style_keys.rs
Normal file
83
lifecycle/src/rsx/style_keys.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
//! A valid CSS class.
|
||||||
|
//!
|
||||||
|
//! A CSS class is a non-empty string that starts with an alphanumeric character
|
||||||
|
//! and is followed by any number of alphanumeric characters and the
|
||||||
|
//! `_`, `-` and `.` characters.
|
||||||
|
|
||||||
|
use std::fmt::{Display, Error, Formatter};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// A valid CSS class.
|
||||||
|
///
|
||||||
|
/// A CSS class is a non-empty string that starts with an alphanumeric character
|
||||||
|
/// and is followed by any number of alphanumeric characters and the
|
||||||
|
/// `_`, `-` and `.` characters.
|
||||||
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub struct StyleKey(String);
|
||||||
|
|
||||||
|
impl StyleKey {
|
||||||
|
/// Construct a new styles list from a string.
|
||||||
|
///
|
||||||
|
/// Returns `Err` if the provided string is invalid.
|
||||||
|
pub fn try_new<S: Into<String>>(id: S) -> Result<Self, &'static str> {
|
||||||
|
let id = id.into();
|
||||||
|
{
|
||||||
|
let mut chars = id.chars();
|
||||||
|
match chars.next() {
|
||||||
|
None => return Err("style keys cannot be empty"),
|
||||||
|
Some(c) if !c.is_alphabetic() => {
|
||||||
|
return Err("style keys must start with an alphabetic character")
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
for c in chars {
|
||||||
|
if !c.is_alphanumeric() && c != '-' {
|
||||||
|
return Err(
|
||||||
|
"style keys can only contain alphanumerics (dash included)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(StyleKey(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new class name from a string.
|
||||||
|
///
|
||||||
|
/// Panics if the provided string is invalid.
|
||||||
|
pub fn new<S: Into<String>>(id: S) -> Self {
|
||||||
|
let id = id.into();
|
||||||
|
Self::try_new(id.clone()).unwrap_or_else(|err| {
|
||||||
|
panic!(
|
||||||
|
"alchemy::dom::types::StyleKey: {:?} is not a valid class name: {}",
|
||||||
|
id, err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for StyleKey {
|
||||||
|
type Err = &'static str;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
StyleKey::try_new(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for StyleKey {
|
||||||
|
fn from(str: &'a str) -> Self {
|
||||||
|
StyleKey::from_str(str).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StyleKey {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||||
|
Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for StyleKey {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lifecycle/src/rsx/virtual_node.rs
Normal file
59
lifecycle/src/rsx/virtual_node.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
//! Implements the `RSX::VirtualNode` struct, which is a bit of a recursive
|
||||||
|
//! structure.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::fmt::{Display, Debug};
|
||||||
|
|
||||||
|
use alchemy_styles::node::Node;
|
||||||
|
|
||||||
|
use crate::traits::Component;
|
||||||
|
use crate::rsx::{RSX, Props};
|
||||||
|
|
||||||
|
/// A VirtualNode is akin to an `Element` in React terms. Here, we provide a way
|
||||||
|
/// for lazy `Component` instantiation, along with storage for things like layout nodes,
|
||||||
|
/// properties, children and so on.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VirtualNode {
|
||||||
|
/// Used in debugging/printing/etc.
|
||||||
|
pub tag: &'static str,
|
||||||
|
|
||||||
|
/// `Component` instances are created on-demand, if the reconciler deems it be so. This
|
||||||
|
/// is a closure that should return an instance of the correct type.
|
||||||
|
pub create_component_fn: Arc<Fn() -> Box<Component> + Send + Sync + 'static>,
|
||||||
|
|
||||||
|
/// A cached component instance, which is transferred between trees. Since `Component`
|
||||||
|
/// instances are lazily created, this is an `Option`, and defaults to `None`.
|
||||||
|
pub instance: Option<Arc<Component>>,
|
||||||
|
|
||||||
|
/// A cached `Node` for computing `Layout` with `Stretch`. Some components may not have
|
||||||
|
/// a need for layout (e.g, if they don't have a backing node), and thus this is optional.
|
||||||
|
///
|
||||||
|
/// The reconciler will handle bridging tree structures as necessary.
|
||||||
|
pub layout_node: Option<Node>,
|
||||||
|
|
||||||
|
/// `Props`, which are to be passed to this `Component` at various lifecycle methods.
|
||||||
|
pub props: Props,
|
||||||
|
|
||||||
|
/// Computed children get stored here.
|
||||||
|
pub children: Vec<RSX>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VirtualNode {
|
||||||
|
/// Special formatting for displaying nodes.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "<{}>", self.tag)?;
|
||||||
|
|
||||||
|
for child in &self.children {
|
||||||
|
write!(f, "{:?}", child)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "</{}>", self.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for VirtualNode {
|
||||||
|
/// Special formatting for debugging nodes.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "VirtualNode({})", self.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lifecycle/src/rsx/virtual_text.rs
Normal file
29
lifecycle/src/rsx/virtual_text.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
//! Implements `RSX::VirtualText`, which holds data pertaining to <Text>, primarily.
|
||||||
|
|
||||||
|
use std::fmt::{Display, Debug};
|
||||||
|
|
||||||
|
/// Currently a wrapper for `String`, but could be something else down the road. Frees
|
||||||
|
/// us from needing to change the public API later.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VirtualText(pub String);
|
||||||
|
|
||||||
|
impl VirtualText {
|
||||||
|
/// Given a `String`, returns a `VirtualText` node.
|
||||||
|
pub fn new(s: String) -> VirtualText {
|
||||||
|
VirtualText(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for VirtualText {
|
||||||
|
/// Formatting for `VirtualText` display.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for VirtualText {
|
||||||
|
/// Formatting for `VirtualText` debugging.
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "VirtualText({})", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
183
lifecycle/src/traits.rs
Normal file
183
lifecycle/src/traits.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
//! Traits that are used in Alchemy. Alchemy implements a React-based Component
|
||||||
|
//! lifecycle, coupled with a delegate pattern inspired by those found in AppKit/UIKit.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use alchemy_styles::styles::{Layout, Style};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::rsx::{RSX, Props};
|
||||||
|
|
||||||
|
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
|
||||||
|
#[cfg(feature = "cocoa")]
|
||||||
|
pub type PlatformSpecificNodeType = objc_id::ShareId<objc::runtime::Object>;
|
||||||
|
|
||||||
|
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
|
||||||
|
#[cfg(not(feature = "cocoa"))]
|
||||||
|
pub type PlatformSpecificNodeType = ();
|
||||||
|
|
||||||
|
/// Each platform tends to have their own startup routine, their own runloop, and so on.
|
||||||
|
/// Alchemy recognizes this and provides an `AppDelegate` that receives events at a system
|
||||||
|
/// level and allows the user to operate within the established framework per-system.
|
||||||
|
pub trait AppDelegate: Send + Sync {
|
||||||
|
/// Fired when an Application is about to finish launching.
|
||||||
|
fn will_finish_launching(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application has finished launching - this is a good place to, say, show your
|
||||||
|
/// window.
|
||||||
|
fn did_finish_launching(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application will become active.
|
||||||
|
fn will_become_active(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application became active.
|
||||||
|
fn did_become_active(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application will resign active. You can use this to, say, persist resources
|
||||||
|
/// or state.
|
||||||
|
fn will_resign_active(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application has resigned active.
|
||||||
|
fn did_resign_active(&mut self) {}
|
||||||
|
|
||||||
|
/// Fired when an Application is going to terminate. You can use this to, say, instruct the
|
||||||
|
/// system to "wait a minute, lemme finish".
|
||||||
|
fn should_terminate(&self) -> bool { true }
|
||||||
|
|
||||||
|
/// Fired when the Application has determined "no, you're done, stop the world".
|
||||||
|
fn will_terminate(&mut self) {}
|
||||||
|
|
||||||
|
/// A private trait method that you shouldn't call. This may change or disappear in later
|
||||||
|
/// releases. Do not rely on this.
|
||||||
|
fn _window_will_close(&self, _window_id: usize) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each platform has their own `Window` API, which Alchemy attempts to pair down to one consistent
|
||||||
|
/// API. This also acts as the bootstrapping point for a `render` tree.
|
||||||
|
pub trait WindowDelegate: Send + Sync {
|
||||||
|
/// Fired when this Window will close. You can use this to clean up or destroy resources,
|
||||||
|
/// timers, and other things.
|
||||||
|
fn will_close(&mut self) { }
|
||||||
|
|
||||||
|
/// Called as the first step in the `render` tree. Every Window contains its own content view
|
||||||
|
/// that is special, called the root. Widget trees are added to it as necessary, bootstrapped
|
||||||
|
/// from here.
|
||||||
|
fn render(&self) -> Result<RSX, Error> { Ok(RSX::None) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait State {}
|
||||||
|
|
||||||
|
/// The `Component` lifecycle, mostly inspired from React, with a few extra methods for views that
|
||||||
|
/// need to have a backing native layer. A good breakdown of the React Component lifecycle can be
|
||||||
|
/// found [in this tweet](https://twitter.com/dan_abramov/status/981712092611989509?lang=en).
|
||||||
|
///
|
||||||
|
/// Alchemy does not currently implement Hooks, and at the moment has no plans to do so (the API
|
||||||
|
/// doesn't feel comfortable in Rust, in any way I tried). If you think you have an interesting
|
||||||
|
/// proposal for this, feel free to open an issue!
|
||||||
|
pub trait Component: Send + Sync {
|
||||||
|
/// Indicates whether a Component instance carries a native backing node. If you return `true`
|
||||||
|
/// from this, the reconciler will opt-in to the native backing layer. Returns `false` by
|
||||||
|
/// default.
|
||||||
|
fn has_native_backing_node(&self) -> bool { false }
|
||||||
|
|
||||||
|
/// Returns a wrapped-per-platform pointer type that the backing framework tree can use.
|
||||||
|
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> { None }
|
||||||
|
|
||||||
|
/// If you implement a Native-backed component, you'll need to implement this. Given a
|
||||||
|
/// `component`, you need to instruct the system how to append it to the tree at your point.
|
||||||
|
fn append_child_component(&self, _component: &Arc<Component>) {}
|
||||||
|
|
||||||
|
/// If you implement a Native-backed component, you'll need to implement this. Given a
|
||||||
|
/// `component`, you need to instruct the system how to replace it in the tree at your point.
|
||||||
|
fn replace_child_component(&self, _component: Arc<Component>) {}
|
||||||
|
|
||||||
|
/// If you implement a Native-backed component, you'll need to implement this. Given a
|
||||||
|
/// `component`, you need to instruct the system how to remove it from the tree at your point.
|
||||||
|
fn remove_child_component(&self, _component: Arc<Component>) {}
|
||||||
|
|
||||||
|
/// Given a computed `layout`, and an accompanying `Style` (which holds appearance-based
|
||||||
|
/// styles, like colors), this method should transform them into appropriate calls to the
|
||||||
|
/// backing native node.
|
||||||
|
fn apply_styles(&self, _layout: &Layout, _style: &Style) {}
|
||||||
|
|
||||||
|
/// Invoked right before calling the render method, both on the initial mount and on subsequent updates.
|
||||||
|
/// It should return an object to update the state, or null to update nothing.
|
||||||
|
/// This method exists for rare use cases where the state depends on changes in props over time.
|
||||||
|
fn get_derived_state_from_props(&self, _props: Props) {}
|
||||||
|
|
||||||
|
/// Invoked right before the most recently rendered output is committed to the backing layer tree.
|
||||||
|
/// It enables your component to capture some information from the tree (e.g. scroll position) before it's
|
||||||
|
/// potentially changed. Any value returned by this lifecycle will be passed as a parameter
|
||||||
|
/// to component_did_update().
|
||||||
|
///
|
||||||
|
/// This use case is not common, but it may occur in UIs like a chat thread that need to handle scroll
|
||||||
|
/// position in a special way. A snapshot value (or None) should be returned.
|
||||||
|
fn get_snapshot_before_update(&self, _props: Props) {}
|
||||||
|
|
||||||
|
/// Invoked immediately after a component is mounted (inserted into the tree).
|
||||||
|
/// If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
|
||||||
|
/// This method is also a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe
|
||||||
|
/// in component_will_unmount().
|
||||||
|
fn component_did_mount(&mut self, _props: &Props) {}
|
||||||
|
|
||||||
|
/// Invoked immediately after updating occurs. This method is not called for the initial render.
|
||||||
|
/// This is also a good place to do network requests as long as you compare the current props to previous props
|
||||||
|
/// (e.g. a network request may not be necessary if the props have not changed).
|
||||||
|
fn component_did_update(&mut self, _props: &Props) {}
|
||||||
|
|
||||||
|
/// Invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this
|
||||||
|
/// method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that
|
||||||
|
/// were created in component_did_mount().
|
||||||
|
///
|
||||||
|
/// You should not call set state in this method because the component will never be re-rendered. Once a
|
||||||
|
/// component instance is unmounted, it will never be mounted again.
|
||||||
|
fn component_will_unmount(&mut self, _props: &Props) {}
|
||||||
|
|
||||||
|
/// Invoked after an error has been thrown by a descendant component. Called during the "commit" phase,
|
||||||
|
/// so side-effects are permitted. It should be used for things like logging errors (e.g,
|
||||||
|
/// Sentry).
|
||||||
|
fn component_did_catch(&mut self, _props: &Props/* error: */) {}
|
||||||
|
|
||||||
|
/// Use this to let Alchemy know if a component’s output is not affected by the current change in state
|
||||||
|
/// or props. The default behavior is to re-render on every state change, and in the vast majority of
|
||||||
|
/// cases you should rely on the default behavior.
|
||||||
|
///
|
||||||
|
/// This is invoked before rendering when new props or state are being received. Defaults to true. This
|
||||||
|
/// method is not called for the initial render or when force_update() is used. This method only exists
|
||||||
|
/// as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs.
|
||||||
|
fn should_component_update(&self) -> bool { true }
|
||||||
|
|
||||||
|
/// The only required method for a `Component`. Should return a Result of RSX nodes, or an
|
||||||
|
/// Error (in very rare cases, such as trying to get a key from a strange HashMap or
|
||||||
|
/// something).
|
||||||
|
///
|
||||||
|
/// The render() function should be pure, meaning that it does not modify component state, it
|
||||||
|
/// returns the same result each time it’s invoked, and it does not directly interact with the
|
||||||
|
/// backing rendering framework.
|
||||||
|
///
|
||||||
|
/// If you need to interact with the browser, perform your work in component_did_mount() or the other
|
||||||
|
/// lifecycle methods instead. Keeping `render()` pure makes components easier to think about.
|
||||||
|
///
|
||||||
|
/// This method is not called if should_component_update() returns `false`.
|
||||||
|
fn render(&self, _props: &Props) -> Result<RSX, Error> { Ok(RSX::None) }
|
||||||
|
|
||||||
|
/// This lifecycle is invoked after an error has been thrown by a descendant component. It receives
|
||||||
|
/// the error that was thrown as a parameter and should return a value to update state.
|
||||||
|
///
|
||||||
|
/// This is called during the "render" phase, so side-effects are not permitted.
|
||||||
|
/// For those use cases, use component_did_catch() instead.
|
||||||
|
fn get_derived_state_from_error(&self, _error: ()) {}
|
||||||
|
|
||||||
|
/// By default, when your component’s state or props change, your component will re-render.
|
||||||
|
/// If your `render()` method depends on some other data, you can tell Alchemy that the component
|
||||||
|
/// needs re-rendering by calling `force_update()`.
|
||||||
|
///
|
||||||
|
/// Calling `force_update()` will cause `render()` to be called on the component, skipping
|
||||||
|
/// `should_component_update()`. This will trigger the normal lifecycle methods for child components,
|
||||||
|
/// including the `should_component_update()` method of each child. Alchemy will still only update the
|
||||||
|
/// backing widget tree if the markup changes.
|
||||||
|
///
|
||||||
|
/// Normally, you should try to avoid all uses of `force_update()` and only read from `this.props`
|
||||||
|
/// and `this.state` in `render()`.
|
||||||
|
fn force_update(&self) {}
|
||||||
|
}
|
||||||
30
macros/Cargo.toml
Normal file
30
macros/Cargo.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "alchemy-macros"
|
||||||
|
description = "A crate containing macros used in Alchemy, the Rust cross-platform GUI framework."
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
build = "src/build.rs"
|
||||||
|
license = "MPL-2.0+"
|
||||||
|
repository = "https://github.com/ryanmcgrath/alchemy"
|
||||||
|
categories = ["gui", "rendering::engine", "multimedia"]
|
||||||
|
keywords = ["gui", "css", "styles", "layout", "ui"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ansi_term = "0.11.0"
|
||||||
|
lalrpop-util = "0.16.1"
|
||||||
|
proc-macro2 = { version = "0.4.24", features = ["nightly"] }
|
||||||
|
proc-macro-hack = "0.5.2"
|
||||||
|
quote = "0.6.10"
|
||||||
|
alchemy-styles = { version = "0.1", path = "../styles", features = ["parser", "tokenize"] }
|
||||||
|
syn = "0.15"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
lalrpop = "0.16.1"
|
||||||
|
version_check = "0.1.5"
|
||||||
8
macros/README.md
Normal file
8
macros/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Alchemy-Macros
|
||||||
|
This crate holds macros for two things, primarily:
|
||||||
|
|
||||||
|
- `rsx! {}`, which transforms `<View></<View>` tags into their proper `RSX` calls. Much of this is forked from the awesome work done by [Bodil Stokke in typed-html](https://github.com/bodil/typed-html).
|
||||||
|
- `styles! {}`, which transforms CSS style nodes into `Vec<Styles>`, which the rendering engine uses to theme and style nodes. This relies on the [CSS Parser from Servo](https://github.com/servo/rust-cssparser). Styles do not support cascading; this is a design decision, as inheritance is already a bit of a taboo in Rust, so to do it in styling code feels really odd and involves a mental shift the deeper you go. Opt to apply successive style keys, conditionally if need be, to achieve the same thing with a compositional approach.
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
11
macros/src/build.rs
Normal file
11
macros/src/build.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
extern crate lalrpop;
|
||||||
|
extern crate version_check;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
lalrpop::process_root().unwrap();
|
||||||
|
|
||||||
|
if version_check::is_nightly().unwrap_or(false) {
|
||||||
|
println!("cargo:rustc-cfg=can_join_spans");
|
||||||
|
println!("cargo:rustc-cfg=can_show_location_of_runtime_parse_error");
|
||||||
|
}
|
||||||
|
}
|
||||||
116
macros/src/error.rs
Normal file
116
macros/src/error.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
use ansi_term::Style;
|
||||||
|
use lalrpop_util::ParseError::*;
|
||||||
|
use crate::lexer::Token;
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::{quote, quote_spanned};
|
||||||
|
|
||||||
|
pub type ParseError = lalrpop_util::ParseError<usize, Token, HtmlParseError>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HtmlParseError {
|
||||||
|
TagMismatch { open: Ident, close: Ident },
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pprint_token(token: &str) -> &str {
|
||||||
|
match token {
|
||||||
|
"BraceGroupToken" => "code block",
|
||||||
|
"LiteralToken" => "literal",
|
||||||
|
"IdentToken" => "identifier",
|
||||||
|
a => a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pprint_tokens(tokens: &[String]) -> String {
|
||||||
|
let tokens: Vec<&str> = tokens.iter().map(|s| pprint_token(&s)).collect();
|
||||||
|
if tokens.len() > 1 {
|
||||||
|
let start = tokens[..tokens.len() - 1].join(", ");
|
||||||
|
let end = &tokens[tokens.len() - 1];
|
||||||
|
format!("{} or {}", start, end)
|
||||||
|
} else {
|
||||||
|
tokens[0].to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_in_node_position(tokens: &[String]) -> bool {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
let input: HashSet<&str> = tokens.iter().map(String::as_str).collect();
|
||||||
|
let output: HashSet<&str> = ["\"<\"", "BraceGroupToken", "LiteralToken"]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
input == output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_error(input: &[Token], error: &ParseError) -> TokenStream {
|
||||||
|
match error {
|
||||||
|
InvalidToken { location } => {
|
||||||
|
let span = input[*location].span();
|
||||||
|
quote_spanned! {span=>
|
||||||
|
compile_error! { "invalid token" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnrecognizedToken {
|
||||||
|
token: None,
|
||||||
|
expected,
|
||||||
|
} => {
|
||||||
|
let msg = format!(
|
||||||
|
"unexpected end of macro; missing {}",
|
||||||
|
pprint_tokens(&expected)
|
||||||
|
);
|
||||||
|
quote! {
|
||||||
|
compile_error! { #msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnrecognizedToken {
|
||||||
|
token: Some((_, token, _)),
|
||||||
|
expected,
|
||||||
|
} => {
|
||||||
|
let span = token.span();
|
||||||
|
let error_msg = format!("expected {}", pprint_tokens(&expected));
|
||||||
|
let error = quote_spanned! {span=>
|
||||||
|
compile_error! { #error_msg }
|
||||||
|
};
|
||||||
|
let help = if is_in_node_position(expected) && token.is_ident() {
|
||||||
|
// special case: you probably meant to quote that text
|
||||||
|
let help_msg = format!(
|
||||||
|
"text nodes need to be quoted, eg. {}",
|
||||||
|
Style::new().bold().paint("<p>\"Hello Joe!\"</p>")
|
||||||
|
);
|
||||||
|
Some(quote_spanned! {span=>
|
||||||
|
compile_error! { #help_msg }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
quote! {{
|
||||||
|
#error
|
||||||
|
#help
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
ExtraToken {
|
||||||
|
token: (_, token, _),
|
||||||
|
} => {
|
||||||
|
let span = token.span();
|
||||||
|
quote_spanned! {span=>
|
||||||
|
compile_error! { "superfluous token" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
User {
|
||||||
|
error: HtmlParseError::TagMismatch { open, close },
|
||||||
|
} => {
|
||||||
|
let close_span = close.span();
|
||||||
|
let close_msg = format!("expected closing tag '</{}>', found '</{}>'", open, close);
|
||||||
|
let close_error = quote_spanned! {close_span=>
|
||||||
|
compile_error! { #close_msg }
|
||||||
|
};
|
||||||
|
let open_span = open.span();
|
||||||
|
let open_error = quote_spanned! {open_span=>
|
||||||
|
compile_error! { "unclosed tag" }
|
||||||
|
};
|
||||||
|
quote! {{
|
||||||
|
#close_error
|
||||||
|
#open_error
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
macros/src/grammar.lalrpop
Normal file
303
macros/src/grammar.lalrpop
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
use crate::lexer::{self, Token, to_stream};
|
||||||
|
use crate::error::HtmlParseError;
|
||||||
|
use crate::rsx::{Node, Element};
|
||||||
|
//use crate::declare::Declare;
|
||||||
|
use crate::map::StringyMap;
|
||||||
|
use proc_macro2::{Delimiter, Ident, Literal, Group, TokenTree};
|
||||||
|
use lalrpop_util::ParseError;
|
||||||
|
use crate::span;
|
||||||
|
|
||||||
|
grammar;
|
||||||
|
|
||||||
|
/// Match a B separated list of zero or more A, return a list of A.
|
||||||
|
Separated<A, B>: Vec<A> = {
|
||||||
|
<v:(<A> B)*> <e:A?> => match e {
|
||||||
|
None => v,
|
||||||
|
Some(e) => {
|
||||||
|
let mut v = v;
|
||||||
|
v.push(e);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match a B separated list of one or more A, return a list of tokens, including the Bs.
|
||||||
|
/// Both A and B must resolve to a Token.
|
||||||
|
SeparatedInc<A, B>: Vec<Token> = {
|
||||||
|
<v:(A B)*> <e:A> => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (a, b) in v {
|
||||||
|
out.push(a);
|
||||||
|
out.push(b);
|
||||||
|
}
|
||||||
|
out.push(e);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ident: Ident = IdentToken => {
|
||||||
|
match <> {
|
||||||
|
Token::Ident(ident) => ident,
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Literal: Literal = LiteralToken => {
|
||||||
|
match <> {
|
||||||
|
Token::Literal(literal) => literal,
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GroupToken = {
|
||||||
|
BraceGroupToken,
|
||||||
|
BracketGroupToken,
|
||||||
|
ParenGroupToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A kebab case HTML ident, converted to a snake case ident.
|
||||||
|
HtmlIdent: Ident = {
|
||||||
|
<init:(<Ident> "-")*> <last:Ident> => {
|
||||||
|
let mut init = init;
|
||||||
|
init.push(last);
|
||||||
|
let (span, name) = init.into_iter().fold((None, String::new()), |(span, name), token| {
|
||||||
|
(
|
||||||
|
match span {
|
||||||
|
None => Some(token.span().unstable()),
|
||||||
|
Some(span) => {
|
||||||
|
#[cfg(can_join_spans)]
|
||||||
|
{
|
||||||
|
span.join(token.span().unstable())
|
||||||
|
}
|
||||||
|
#[cfg(not(can_join_spans))]
|
||||||
|
{
|
||||||
|
Some(span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
if name.is_empty() {
|
||||||
|
name + &token.to_string()
|
||||||
|
} else {
|
||||||
|
name + "_" + &token.to_string()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Ident::new(&name, span::from_unstable(span.unwrap()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// The HTML macro
|
||||||
|
|
||||||
|
/// An approximation of a Rust expression.
|
||||||
|
BareExpression: Token = "&"? (IdentToken ":" ":")* SeparatedInc<IdentToken, "."> ParenGroupToken? => {
|
||||||
|
let (reference, left, right, args) = (<>);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if let Some(reference) = reference {
|
||||||
|
out.push(reference);
|
||||||
|
}
|
||||||
|
for (ident, c1, c2) in left {
|
||||||
|
out.push(ident);
|
||||||
|
out.push(c1);
|
||||||
|
out.push(c2);
|
||||||
|
}
|
||||||
|
out.extend(right);
|
||||||
|
if let Some(args) = args {
|
||||||
|
out.push(args);
|
||||||
|
}
|
||||||
|
Group::new(Delimiter::Brace, to_stream(out)).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
AttrValue: Token = {
|
||||||
|
LiteralToken,
|
||||||
|
GroupToken,
|
||||||
|
BareExpression,
|
||||||
|
};
|
||||||
|
|
||||||
|
Attr: (Ident, Token) = <name:HtmlIdent> "=" <value:AttrValue> => (name, value);
|
||||||
|
|
||||||
|
Attrs: StringyMap<Ident, TokenTree> = Attr* => <>.into();
|
||||||
|
|
||||||
|
OpeningTag: (Ident, StringyMap<Ident, TokenTree>) = "<" <HtmlIdent> <Attrs> ">";
|
||||||
|
|
||||||
|
ClosingTag: Ident = "<" "/" <HtmlIdent> ">";
|
||||||
|
|
||||||
|
SingleTag: Element = "<" <name:HtmlIdent> <attributes:Attrs> "/" ">" => {
|
||||||
|
Element {
|
||||||
|
name,
|
||||||
|
attributes,
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ParentTag: Element = <opening:OpeningTag> <children:Node*> <closing:ClosingTag> =>? {
|
||||||
|
let (name, attributes) = opening;
|
||||||
|
let closing_name = closing.to_string();
|
||||||
|
if closing_name == name.to_string() {
|
||||||
|
Ok(Element {
|
||||||
|
name,
|
||||||
|
attributes,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(ParseError::User { error: HtmlParseError::TagMismatch {
|
||||||
|
open: name.into(),
|
||||||
|
close: closing.into(),
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Element = {
|
||||||
|
SingleTag,
|
||||||
|
ParentTag,
|
||||||
|
};
|
||||||
|
|
||||||
|
TextNode = Literal;
|
||||||
|
|
||||||
|
CodeBlock: Group = BraceGroupToken => match <> {
|
||||||
|
Token::Group(_, group) => group,
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
Node: Node = {
|
||||||
|
Element => Node::Element(<>),
|
||||||
|
TextNode => Node::Text(<>),
|
||||||
|
CodeBlock => Node::Block(<>),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub NodeWithType: (Node, Option<Vec<Token>>) = {
|
||||||
|
Node => (<>, None),
|
||||||
|
<Node> ":" <TypeSpec> => {
|
||||||
|
let (node, spec) = (<>);
|
||||||
|
(node, Some(spec))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// The declare macro
|
||||||
|
|
||||||
|
TypePath: Vec<Token> = {
|
||||||
|
IdentToken => vec![<>],
|
||||||
|
TypePath ":" ":" IdentToken => {
|
||||||
|
let (mut path, c1, c2, last) = (<>);
|
||||||
|
path.push(c1);
|
||||||
|
path.push(c2);
|
||||||
|
path.push(last);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Reference: Vec<Token> = "&" ("'" IdentToken)? => {
|
||||||
|
let (amp, lifetime) = (<>);
|
||||||
|
let mut out = vec![amp];
|
||||||
|
if let Some((tick, ident)) = lifetime {
|
||||||
|
out.push(tick);
|
||||||
|
out.push(ident);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeArgs: Vec<Token> = {
|
||||||
|
TypeSpec,
|
||||||
|
TypeArgs "," TypeSpec => {
|
||||||
|
let (mut args, comma, last) = (<>);
|
||||||
|
args.push(comma);
|
||||||
|
args.extend(last);
|
||||||
|
args
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeArgList: Vec<Token> = "<" TypeArgs ">" => {
|
||||||
|
let (left, mut args, right) = (<>);
|
||||||
|
args.insert(0, left);
|
||||||
|
args.push(right);
|
||||||
|
args
|
||||||
|
};
|
||||||
|
|
||||||
|
FnReturnType: Vec<Token> = "-" ">" TypeSpec => {
|
||||||
|
let (dash, right, spec) = (<>);
|
||||||
|
let mut out = vec![dash, right];
|
||||||
|
out.extend(spec);
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
FnArgList: Vec<Token> = ParenGroupToken FnReturnType? => {
|
||||||
|
let (args, rt) = (<>);
|
||||||
|
let mut out = vec![args];
|
||||||
|
if let Some(rt) = rt {
|
||||||
|
out.extend(rt);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeArgSpec = {
|
||||||
|
TypeArgList,
|
||||||
|
FnArgList,
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeSpec: Vec<Token> = Reference? TypePath TypeArgSpec? => {
|
||||||
|
let (reference, path, args) = (<>);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if let Some(reference) = reference {
|
||||||
|
out.extend(reference);
|
||||||
|
}
|
||||||
|
out.extend(path);
|
||||||
|
if let Some(args) = args {
|
||||||
|
out.extend(args);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeDecl: (Ident, Vec<Token>) = <HtmlIdent> ":" <TypeSpec>;
|
||||||
|
|
||||||
|
TypeDecls: Vec<(Ident, Vec<Token>)> = {
|
||||||
|
TypeDecl => vec![<>],
|
||||||
|
<decls:TypeDecls> "," <decl:TypeDecl> => {
|
||||||
|
let mut decls = decls;
|
||||||
|
decls.push(decl);
|
||||||
|
decls
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Attributes = "{" <TypeDecls> ","? "}";
|
||||||
|
|
||||||
|
TypePathList = "[" <Separated<TypePath, ",">> "]";
|
||||||
|
|
||||||
|
IdentList = "[" <Separated<Ident, ",">> "]";
|
||||||
|
|
||||||
|
Groups = "in" <TypePathList>;
|
||||||
|
|
||||||
|
Children: (Option<Vec<Token>>) = "with" <opt:TypePath?> => {
|
||||||
|
opt
|
||||||
|
};
|
||||||
|
|
||||||
|
extern {
|
||||||
|
type Location = usize;
|
||||||
|
type Error = HtmlParseError;
|
||||||
|
|
||||||
|
enum lexer::Token {
|
||||||
|
"<" => Token::Punct('<', _),
|
||||||
|
">" => Token::Punct('>', _),
|
||||||
|
"/" => Token::Punct('/', _),
|
||||||
|
"=" => Token::Punct('=', _),
|
||||||
|
"-" => Token::Punct('-', _),
|
||||||
|
":" => Token::Punct(':', _),
|
||||||
|
"." => Token::Punct('.', _),
|
||||||
|
"," => Token::Punct(',', _),
|
||||||
|
"&" => Token::Punct('&', _),
|
||||||
|
"'" => Token::Punct('\'', _),
|
||||||
|
";" => Token::Punct(';', _),
|
||||||
|
"{" => Token::GroupOpen(Delimiter::Brace, _),
|
||||||
|
"}" => Token::GroupClose(Delimiter::Brace, _),
|
||||||
|
"[" => Token::GroupOpen(Delimiter::Bracket, _),
|
||||||
|
"]" => Token::GroupClose(Delimiter::Bracket, _),
|
||||||
|
"in" => Token::Keyword(lexer::Keyword::In, _),
|
||||||
|
"with" => Token::Keyword(lexer::Keyword::With, _),
|
||||||
|
IdentToken => Token::Ident(_),
|
||||||
|
LiteralToken => Token::Literal(_),
|
||||||
|
ParenGroupToken => Token::Group(Delimiter::Parenthesis, _),
|
||||||
|
BraceGroupToken => Token::Group(Delimiter::Brace, _),
|
||||||
|
BracketGroupToken => Token::Group(Delimiter::Bracket, _),
|
||||||
|
}
|
||||||
|
}
|
||||||
20
macros/src/ident.rs
Normal file
20
macros/src/ident.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//! Utility functions, originally written by Bodil Stokke
|
||||||
|
//! over in [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub fn new_raw(string: &str, span: Span) -> Ident {
|
||||||
|
// Validate that it is an ident.
|
||||||
|
let _ = Ident::new(string, span);
|
||||||
|
|
||||||
|
let s = format!("r#{}", string);
|
||||||
|
let tts = TokenStream::from_str(&s).unwrap();
|
||||||
|
let mut ident = match tts.into_iter().next().unwrap() {
|
||||||
|
TokenTree::Ident(ident) => ident,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
ident.set_span(span);
|
||||||
|
ident
|
||||||
|
}
|
||||||
142
macros/src/lexer.rs
Normal file
142
macros/src/lexer.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
//! Implements the Lexer used for parsing RSX, originally
|
||||||
|
//! written by Bodil Stokke over in
|
||||||
|
//! [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
use crate::error::HtmlParseError;
|
||||||
|
use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
|
||||||
|
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
pub type Spanned<Tok, Loc, Error> = Result<(Loc, Tok, Loc), Error>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Token {
|
||||||
|
Ident(Ident),
|
||||||
|
Literal(Literal),
|
||||||
|
Punct(char, Punct),
|
||||||
|
Group(Delimiter, Group),
|
||||||
|
GroupOpen(Delimiter, Span),
|
||||||
|
GroupClose(Delimiter, Span),
|
||||||
|
Keyword(Keyword, Ident),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
pub fn span(&self) -> Span {
|
||||||
|
match self {
|
||||||
|
Token::Ident(ident) => ident.span(),
|
||||||
|
Token::Literal(literal) => literal.span(),
|
||||||
|
Token::Punct(_, punct) => punct.span(),
|
||||||
|
Token::Group(_, group) => group.span(),
|
||||||
|
Token::GroupOpen(_, span) => *span,
|
||||||
|
Token::GroupClose(_, span) => *span,
|
||||||
|
Token::Keyword(_, ident) => ident.span(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_ident(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Token::Ident(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Token> for TokenTree {
|
||||||
|
fn from(token: Token) -> Self {
|
||||||
|
match token {
|
||||||
|
Token::Ident(ident) => TokenTree::Ident(ident),
|
||||||
|
Token::Literal(literal) => TokenTree::Literal(literal),
|
||||||
|
Token::Punct(_, punct) => TokenTree::Punct(punct),
|
||||||
|
Token::Group(_, group) => TokenTree::Group(group),
|
||||||
|
Token::GroupOpen(_, _) => panic!("Can't convert a GroupOpen token to a TokenTree"),
|
||||||
|
Token::GroupClose(_, _) => panic!("Can't convert a GroupClose token to a TokenTree"),
|
||||||
|
Token::Keyword(_, ident) => TokenTree::Ident(ident),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Token> for TokenStream {
|
||||||
|
fn from(token: Token) -> Self {
|
||||||
|
TokenStream::from_iter(vec![TokenTree::from(token)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ident> for Token {
|
||||||
|
fn from(ident: Ident) -> Self {
|
||||||
|
Token::Ident(ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Literal> for Token {
|
||||||
|
fn from(literal: Literal) -> Self {
|
||||||
|
Token::Literal(literal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Punct> for Token {
|
||||||
|
fn from(punct: Punct) -> Self {
|
||||||
|
Token::Punct(punct.as_char(), punct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Group> for Token {
|
||||||
|
fn from(group: Group) -> Self {
|
||||||
|
Token::Group(group.delimiter(), group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Keyword {
|
||||||
|
In,
|
||||||
|
With,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_stream<I: IntoIterator<Item = Token>>(tokens: I) -> TokenStream {
|
||||||
|
let mut stream = TokenStream::new();
|
||||||
|
stream.extend(tokens.into_iter().map(TokenTree::from));
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unroll_stream(stream: TokenStream, deep: bool) -> Vec<Token> {
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
for tt in stream {
|
||||||
|
match tt {
|
||||||
|
TokenTree::Ident(ident) => vec.push(ident.into()),
|
||||||
|
TokenTree::Literal(literal) => vec.push(literal.into()),
|
||||||
|
TokenTree::Punct(punct) => vec.push(punct.into()),
|
||||||
|
TokenTree::Group(ref group) if deep && group.delimiter() != Delimiter::Parenthesis => {
|
||||||
|
vec.push(Token::GroupOpen(group.delimiter(), group.span()));
|
||||||
|
let sub = unroll_stream(group.stream(), deep);
|
||||||
|
vec.extend(sub);
|
||||||
|
vec.push(Token::GroupClose(group.delimiter(), group.span()));
|
||||||
|
}
|
||||||
|
TokenTree::Group(group) => vec.push(group.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lexer<'a> {
|
||||||
|
stream: &'a [Token],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Lexer<'a> {
|
||||||
|
pub fn new(stream: &'a [Token]) -> Self {
|
||||||
|
Lexer { stream, pos: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Lexer<'a> {
|
||||||
|
type Item = Spanned<Token, usize, HtmlParseError>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.stream.get(self.pos) {
|
||||||
|
None => None,
|
||||||
|
Some(token) => {
|
||||||
|
self.pos += 1;
|
||||||
|
Some(Ok((self.pos - 1, token.clone(), self.pos)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
macros/src/lib.rs
Normal file
79
macros/src/lib.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#![recursion_limit = "128"]
|
||||||
|
#![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))]
|
||||||
|
|
||||||
|
//! Implements macros used in Alchemy.
|
||||||
|
//!
|
||||||
|
//! - `rsx! {}`, which turns RSX tags into `RSX` node trees.
|
||||||
|
//! - `styles! {}`, which turns CSS stylesheet strings into `Vec<Styles>`.
|
||||||
|
//!
|
||||||
|
//! In general, you should prefer using these to constructing the above values manually.
|
||||||
|
//!
|
||||||
|
//! Much of the `rsx! {}` support is achieved by forking code riginally written by Bodil Stokke
|
||||||
|
//! over in [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod rsx;
|
||||||
|
mod ident;
|
||||||
|
mod lexer;
|
||||||
|
mod map;
|
||||||
|
mod parser;
|
||||||
|
mod span;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::{TokenStream as TokenStream2, Literal};
|
||||||
|
use proc_macro_hack::proc_macro_hack;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use alchemy_styles::cssparser::{Parser, ParserInput, RuleListParser};
|
||||||
|
use alchemy_styles::styles_parser::{Rule, RuleParser};
|
||||||
|
|
||||||
|
/// Implements the `rsx! {}` macro, which turns RSX tags into `RSX` node trees.
|
||||||
|
#[proc_macro_hack]
|
||||||
|
pub fn rsx(input: TokenStream) -> TokenStream {
|
||||||
|
let stream = lexer::unroll_stream(input.into(), false);
|
||||||
|
let result = rsx::expand_rsx(&stream);
|
||||||
|
TokenStream::from(match result {
|
||||||
|
Err(err) => error::parse_error(&stream, &err),
|
||||||
|
Ok((node, ty)) => match node.into_token_stream(&ty) {
|
||||||
|
Err(err) => err,
|
||||||
|
Ok(success) => success,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements the `styles! {}` macro, which turns CSS stylesheet strings into `Vec<Styles>`.
|
||||||
|
#[proc_macro_hack]
|
||||||
|
pub fn styles(input: TokenStream) -> TokenStream {
|
||||||
|
let s = input.to_string().replace(" ", "");
|
||||||
|
let mut input = ParserInput::new(&s);
|
||||||
|
let mut parser = Parser::new(&mut input);
|
||||||
|
|
||||||
|
let parsed: Vec<Rule> = RuleListParser::new_for_stylesheet(&mut parser, RuleParser {})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|rule| {
|
||||||
|
rule.ok()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut body = TokenStream2::new();
|
||||||
|
for rule in parsed {
|
||||||
|
let mut stream = TokenStream2::new();
|
||||||
|
for style in rule.styles {
|
||||||
|
stream.extend(quote!(#style,));
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = Literal::string(&rule.key);
|
||||||
|
body.extend(quote!(styles.insert(#key, vec![#stream]);))
|
||||||
|
}
|
||||||
|
|
||||||
|
quote!(alchemy::theme::StyleSheet::new({
|
||||||
|
use alchemy::theme::styles::*;
|
||||||
|
use alchemy::theme::color::Color;
|
||||||
|
let mut styles = std::collections::HashMap::new();
|
||||||
|
#body
|
||||||
|
styles
|
||||||
|
})).into()
|
||||||
|
}
|
||||||
54
macros/src/map.rs
Normal file
54
macros/src/map.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! Implements StringyMap, originally written by Bodil Stokke
|
||||||
|
//! over in [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StringyMap<K, V>(BTreeMap<String, (K, V)>);
|
||||||
|
|
||||||
|
impl<K, V> StringyMap<K, V>
|
||||||
|
where
|
||||||
|
K: ToString,
|
||||||
|
{
|
||||||
|
pub fn new() -> Self {
|
||||||
|
StringyMap(BTreeMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, k: K, v: V) -> Option<V> {
|
||||||
|
let s = k.to_string();
|
||||||
|
self.0.insert(s, (k, v)).map(|(_, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, k: &K) -> Option<V> {
|
||||||
|
let s = k.to_string();
|
||||||
|
self.0.remove(&s).map(|(_, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &(K, V)> {
|
||||||
|
self.0.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self) -> impl Iterator<Item = &K> {
|
||||||
|
self.0.values().map(|(k, _)| k)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, OK, OV> From<Vec<(OK, OV)>> for StringyMap<K, V>
|
||||||
|
where
|
||||||
|
OK: Into<K>,
|
||||||
|
OV: Into<V>,
|
||||||
|
K: ToString,
|
||||||
|
{
|
||||||
|
fn from(vec: Vec<(OK, OV)>) -> Self {
|
||||||
|
let mut out = Self::new();
|
||||||
|
for (key, value) in vec {
|
||||||
|
out.insert(key.into(), value.into());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
6
macros/src/parser.rs
Normal file
6
macros/src/parser.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
//! Implements parsing, originally written by Bodil Stokke
|
||||||
|
//! over in [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
use lalrpop_util::lalrpop_mod;
|
||||||
|
|
||||||
|
lalrpop_mod!(pub grammar);
|
||||||
244
macros/src/rsx.rs
Normal file
244
macros/src/rsx.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree};
|
||||||
|
use quote::{quote, quote_spanned};
|
||||||
|
|
||||||
|
use crate::error::ParseError;
|
||||||
|
use crate::ident;
|
||||||
|
use crate::lexer::{/*to_stream, */Lexer, Token};
|
||||||
|
use crate::map::StringyMap;
|
||||||
|
use crate::parser::grammar;
|
||||||
|
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Node {
|
||||||
|
Element(Element),
|
||||||
|
Text(Literal),
|
||||||
|
Block(Group),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn into_token_stream(self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
||||||
|
match self {
|
||||||
|
Node::Element(el) => el.into_token_stream(ty),
|
||||||
|
Node::Text(text) => {
|
||||||
|
let text = TokenTree::Literal(text);
|
||||||
|
Ok(quote!(alchemy::RSX::text(#text.to_string())))
|
||||||
|
}
|
||||||
|
Node::Block(group) => {
|
||||||
|
let span = group.span();
|
||||||
|
let error =
|
||||||
|
"you cannot use a block as a top level element or a required child element";
|
||||||
|
Err(quote_spanned! { span=>
|
||||||
|
compile_error! { #error }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_child_stream(self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
||||||
|
match self {
|
||||||
|
Node::Element(el) => {
|
||||||
|
let el = el.into_token_stream(ty)?;
|
||||||
|
Ok(quote!(
|
||||||
|
/*element.*/children.push(#el);
|
||||||
|
))
|
||||||
|
}
|
||||||
|
tx @ Node::Text(_) => {
|
||||||
|
let tx = tx.into_token_stream(ty)?;
|
||||||
|
Ok(quote!(
|
||||||
|
/*element.*/children.push(#tx);
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Node::Block(group) => {
|
||||||
|
let group: TokenTree = group.into();
|
||||||
|
Ok(quote!(
|
||||||
|
for child in #group.into_iter() {
|
||||||
|
/*element.*/children.push(child);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Element {
|
||||||
|
pub name: Ident,
|
||||||
|
pub attributes: StringyMap<Ident, TokenTree>,
|
||||||
|
pub children: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_event_handlers(
|
||||||
|
attrs: &mut StringyMap<Ident, TokenTree>,
|
||||||
|
) -> StringyMap<Ident, TokenTree> {
|
||||||
|
let mut events = StringyMap::new();
|
||||||
|
let keys: Vec<Ident> = attrs.keys().cloned().collect();
|
||||||
|
for key in keys {
|
||||||
|
let key_name = key.to_string();
|
||||||
|
let prefix = "on";
|
||||||
|
if key_name.starts_with(prefix) {
|
||||||
|
let event_name = &key_name[prefix.len()..];
|
||||||
|
let value = attrs.remove(&key).unwrap();
|
||||||
|
events.insert(ident::new_raw(event_name, key.span()), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_value(value: &TokenTree) -> TokenStream {
|
||||||
|
match value {
|
||||||
|
TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => {
|
||||||
|
let content = g.stream();
|
||||||
|
quote!( [ #content ] )
|
||||||
|
}
|
||||||
|
TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => {
|
||||||
|
let content = g.stream();
|
||||||
|
quote!( ( #content ) )
|
||||||
|
}
|
||||||
|
v => TokenStream::from_iter(vec![v.clone()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_string_literal(literal: &Literal) -> bool {
|
||||||
|
// This is the worst API
|
||||||
|
literal.to_string().starts_with('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn stringify_ident(ident: &Ident) -> String {
|
||||||
|
let s = ident.to_string();
|
||||||
|
if s.starts_with("r#") {
|
||||||
|
s[2..].to_string()
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
fn into_token_stream(mut self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
||||||
|
let name = self.name;
|
||||||
|
let name_str = name.to_string();
|
||||||
|
let typename: TokenTree = Ident::new(&name_str, name.span()).into();
|
||||||
|
|
||||||
|
let events = extract_event_handlers(&mut self.attributes);
|
||||||
|
let attrs = self.attributes.iter().map(|(key, value)| {
|
||||||
|
let name = key.to_string();
|
||||||
|
let token = TokenTree::Ident(ident::new_raw(&name, key.span()));
|
||||||
|
(name, token, value)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let mut attributes = TokenStream::new();
|
||||||
|
let mut styles = TokenStream::new();
|
||||||
|
styles.extend(quote!(alchemy::SpacedSet::new()));
|
||||||
|
|
||||||
|
for (attr_str, key, value) in attrs {
|
||||||
|
match value {
|
||||||
|
TokenTree::Literal(lit) if is_string_literal(lit) => {
|
||||||
|
let mut eprintln_msg = "ERROR: ".to_owned();
|
||||||
|
#[cfg(can_show_location_of_runtime_parse_error)]
|
||||||
|
{
|
||||||
|
let span = lit.span();
|
||||||
|
eprintln_msg += &format!(
|
||||||
|
"{}:{}:{}: ",
|
||||||
|
span.unstable()
|
||||||
|
.source_file()
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or("unknown"),
|
||||||
|
span.unstable().start().line,
|
||||||
|
span.unstable().start().column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eprintln_msg += &format!(
|
||||||
|
"<{} {}={}> failed to parse attribute value: {{}}",
|
||||||
|
name_str, attr_str, lit,
|
||||||
|
);
|
||||||
|
#[cfg(not(can_show_location_of_runtime_parse_error))]
|
||||||
|
{
|
||||||
|
eprintln_msg += "\nERROR: rebuild with nightly to print source location";
|
||||||
|
}
|
||||||
|
|
||||||
|
//body.extend(quote!(
|
||||||
|
/*element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| {
|
||||||
|
eprintln!(#eprintln_msg, err);
|
||||||
|
panic!("failed to parse string literal");
|
||||||
|
}));*/
|
||||||
|
//));
|
||||||
|
},
|
||||||
|
|
||||||
|
value => {
|
||||||
|
let key = key.to_string();
|
||||||
|
let value = process_value(value);
|
||||||
|
|
||||||
|
if key == "r#styles" {
|
||||||
|
styles = quote!(std::convert::Into::into(#value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "r#key" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.extend(quote!(
|
||||||
|
attributes.insert(#key, std::convert::Into::into(#value));
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, _value) in events.iter() {
|
||||||
|
if ty.is_none() {
|
||||||
|
let mut err = quote_spanned! { key.span() =>
|
||||||
|
compile_error! { "when using event handlers, you must declare the output type inside the rsx! macro" }
|
||||||
|
};
|
||||||
|
let hint = quote_spanned! { Span::call_site() =>
|
||||||
|
compile_error! { "for example: change rsx!(<div>...</div>) to rsx!(<div>...</div> : String)" }
|
||||||
|
};
|
||||||
|
err.extend(hint);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
//let key = TokenTree::Ident(key.clone());
|
||||||
|
//let value = process_value(value);
|
||||||
|
/*body.extend(quote!(
|
||||||
|
element.events.#key = Some(alchemy::dom::events::IntoEventHandler::into_event_handler(#value));
|
||||||
|
));*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/*let mut args = TokenStream::new();
|
||||||
|
let mut type_annotation = TokenStream::new();
|
||||||
|
if let Some(ty) = ty {
|
||||||
|
let type_var = to_stream(ty.clone());
|
||||||
|
type_annotation.extend(quote!(: #typename<#type_var>));
|
||||||
|
}*/
|
||||||
|
|
||||||
|
let mut children = TokenStream::new();
|
||||||
|
children.extend(self.children.into_iter().map(|node| {
|
||||||
|
node.into_child_stream(ty)
|
||||||
|
}).collect::<Result<Vec<TokenStream>, TokenStream>>()?);
|
||||||
|
|
||||||
|
let component_name = Literal::string(&typename.to_string());
|
||||||
|
|
||||||
|
Ok(quote!(
|
||||||
|
alchemy::RSX::node(#component_name, || Box::new(#typename::default()), alchemy::Props {
|
||||||
|
attributes: {
|
||||||
|
let mut attributes = std::collections::HashMap::new();
|
||||||
|
#attributes
|
||||||
|
attributes
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
let mut children = vec![];
|
||||||
|
#children
|
||||||
|
children
|
||||||
|
},
|
||||||
|
key: "".into(),
|
||||||
|
styles: #styles
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME report a decent error when the macro contains multiple top level elements
|
||||||
|
pub fn expand_rsx(input: &[Token]) -> Result<(Node, Option<Vec<Token>>), ParseError> {
|
||||||
|
grammar::NodeWithTypeParser::new().parse(Lexer::new(input))
|
||||||
|
}
|
||||||
13
macros/src/span.rs
Normal file
13
macros/src/span.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
//! Utility functions, originally written by Bodil Stokke
|
||||||
|
//! over in [typed-html](https://github.com/bodil/typed-html).
|
||||||
|
|
||||||
|
use proc_macro;
|
||||||
|
use proc_macro2;
|
||||||
|
|
||||||
|
pub fn from_unstable(span: proc_macro::Span) -> proc_macro2::Span {
|
||||||
|
let ident = proc_macro::Ident::new("_", span);
|
||||||
|
let tt = proc_macro::TokenTree::Ident(ident);
|
||||||
|
let tts = proc_macro::TokenStream::from(tt);
|
||||||
|
let tts2 = proc_macro2::TokenStream::from(tts);
|
||||||
|
tts2.into_iter().next().unwrap().span()
|
||||||
|
}
|
||||||
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1.34.2
|
||||||
23
styles/Cargo.toml
Normal file
23
styles/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "alchemy-styles"
|
||||||
|
description = "Style parsing and hoisting for Alchemy, the Rust cross-platform GUI framework."
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
authors = ["Ryan McGrath <ryan@rymc.io>"]
|
||||||
|
license = "MPL-2.0+"
|
||||||
|
repository = "https://github.com/ryanmcgrath/alchemy"
|
||||||
|
categories = ["gui", "rendering::engine", "multimedia"]
|
||||||
|
keywords = ["gui", "css", "styles", "layout", "ui"]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
tokenize = ["proc-macro2", "quote"]
|
||||||
|
parser = ["cssparser"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cssparser = { version = "0.25.5", optional = true }
|
||||||
|
lazy_static = "1.3"
|
||||||
|
proc-macro2 = { version = "0.4.24", optional = true }
|
||||||
|
quote = { version = "0.6.10", optional = true }
|
||||||
5
styles/README.md
Normal file
5
styles/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Alchemy-Styles
|
||||||
|
This crate implements CSS parsing and Flexbox layout. CSS parsing relies on the [CSS Parser from Servo](https://github.com/servo/rust-cssparser). Flexbox is implemented with [Stretch](https://github.com/vislyhq/stretch), albeit currently [a fork by msilgreith](https://github.com/msiglreith/stretch/tree/index), cloned into here to serve a few small changes (a change for more thread safety, and to push appearance based styles that Flexbox doesn't concern itself with). Down the road, I could see this not including Stretch inline.
|
||||||
|
|
||||||
|
## Questions, Comments?
|
||||||
|
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
|
||||||
667
styles/src/color.rs
Normal file
667
styles/src/color.rs
Normal file
|
|
@ -0,0 +1,667 @@
|
||||||
|
//! Implements `Color`. Heavily based on the `Color` module in Servo's CSS parser, but tweaked
|
||||||
|
//! for (what I believe) is a friendlier API, and to separate out the parsing into a separate
|
||||||
|
//! module.
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
use std::{fmt, f32::consts::PI};
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
use cssparser::{BasicParseError, ParseError, Parser, ToCss, Token};
|
||||||
|
|
||||||
|
/// A color with red, green, blue, and alpha components, in a byte each.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
|
pub struct Color {
|
||||||
|
/// The red component.
|
||||||
|
pub red: u8,
|
||||||
|
/// The green component.
|
||||||
|
pub green: u8,
|
||||||
|
/// The blue component.
|
||||||
|
pub blue: u8,
|
||||||
|
/// The alpha component.
|
||||||
|
pub alpha: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Color {
|
||||||
|
fn default() -> Color {
|
||||||
|
Color { red: 0, green: 0, blue: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
/// Constructs a new Color value from float components. It expects the red,
|
||||||
|
/// green, blue and alpha channels in that order, and all values will be
|
||||||
|
/// clamped to the 0.0 ... 1.0 range.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
|
||||||
|
Self::new(
|
||||||
|
clamp_unit_f32(red),
|
||||||
|
clamp_unit_f32(green),
|
||||||
|
clamp_unit_f32(blue),
|
||||||
|
clamp_unit_f32(alpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a transparent color.
|
||||||
|
#[inline]
|
||||||
|
pub fn transparent() -> Self {
|
||||||
|
Self::new(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
|
||||||
|
Color {
|
||||||
|
red: red,
|
||||||
|
green: green,
|
||||||
|
blue: blue,
|
||||||
|
alpha: alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the red channel in a floating point number form, from 0 to 1.
|
||||||
|
#[inline]
|
||||||
|
pub fn red_f32(&self) -> f32 {
|
||||||
|
self.red as f32 / 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the green channel in a floating point number form, from 0 to 1.
|
||||||
|
#[inline]
|
||||||
|
pub fn green_f32(&self) -> f32 {
|
||||||
|
self.green as f32 / 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the blue channel in a floating point number form, from 0 to 1.
|
||||||
|
#[inline]
|
||||||
|
pub fn blue_f32(&self) -> f32 {
|
||||||
|
self.blue as f32 / 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the alpha channel in a floating point number form, from 0 to 1.
|
||||||
|
#[inline]
|
||||||
|
pub fn alpha_f32(&self) -> f32 {
|
||||||
|
self.alpha as f32 / 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a <color> value, per CSS Color Module Level 3.
|
||||||
|
///
|
||||||
|
/// FIXME(#2) Deprecated CSS2 System Colors are not supported yet.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub fn parse_with<'i, 't, ComponentParser>(
|
||||||
|
component_parser: &ComponentParser,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<Color, ParseError<'i, ComponentParser::Error>>
|
||||||
|
where
|
||||||
|
ComponentParser: ColorComponentParser<'i>,
|
||||||
|
{
|
||||||
|
// FIXME: remove clone() when lifetimes are non-lexical
|
||||||
|
let location = input.current_source_location();
|
||||||
|
let token = input.next()?.clone();
|
||||||
|
match token {
|
||||||
|
Token::Hash(ref value) | Token::IDHash(ref value) => {
|
||||||
|
Color::parse_hash(value.as_bytes())
|
||||||
|
}
|
||||||
|
Token::Ident(ref value) => parse_color_keyword(&*value),
|
||||||
|
Token::Function(ref name) => {
|
||||||
|
return input.parse_nested_block(|arguments| {
|
||||||
|
parse_color_function(component_parser, &*name, arguments)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
.map_err(|()| location.new_unexpected_token_error(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a <color> value, per CSS Color Module Level 3.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Color, BasicParseError<'i>> {
|
||||||
|
let component_parser = DefaultComponentParser;
|
||||||
|
Self::parse_with(&component_parser, input).map_err(ParseError::basic)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a color hash, without the leading '#' character.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
pub fn parse_hash(value: &[u8]) -> Result<Self, ()> {
|
||||||
|
match value.len() {
|
||||||
|
8 => Ok(rgba(
|
||||||
|
from_hex(value[0])? * 16 + from_hex(value[1])?,
|
||||||
|
from_hex(value[2])? * 16 + from_hex(value[3])?,
|
||||||
|
from_hex(value[4])? * 16 + from_hex(value[5])?,
|
||||||
|
from_hex(value[6])? * 16 + from_hex(value[7])?,
|
||||||
|
)),
|
||||||
|
6 => Ok(rgb(
|
||||||
|
from_hex(value[0])? * 16 + from_hex(value[1])?,
|
||||||
|
from_hex(value[2])? * 16 + from_hex(value[3])?,
|
||||||
|
from_hex(value[4])? * 16 + from_hex(value[5])?,
|
||||||
|
)),
|
||||||
|
4 => Ok(rgba(
|
||||||
|
from_hex(value[0])? * 17,
|
||||||
|
from_hex(value[1])? * 17,
|
||||||
|
from_hex(value[2])? * 17,
|
||||||
|
from_hex(value[3])? * 17,
|
||||||
|
)),
|
||||||
|
3 => Ok(rgb(
|
||||||
|
from_hex(value[0])? * 17,
|
||||||
|
from_hex(value[1])? * 17,
|
||||||
|
from_hex(value[2])? * 17,
|
||||||
|
)),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
impl ToCss for Color {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
|
||||||
|
where
|
||||||
|
W: fmt::Write,
|
||||||
|
{
|
||||||
|
let serialize_alpha = self.alpha != 255;
|
||||||
|
|
||||||
|
dest.write_str(if serialize_alpha { "rgba(" } else { "rgb(" })?;
|
||||||
|
self.red.to_css(dest)?;
|
||||||
|
dest.write_str(", ")?;
|
||||||
|
self.green.to_css(dest)?;
|
||||||
|
dest.write_str(", ")?;
|
||||||
|
self.blue.to_css(dest)?;
|
||||||
|
if serialize_alpha {
|
||||||
|
dest.write_str(", ")?;
|
||||||
|
|
||||||
|
// Try first with two decimal places, then with three.
|
||||||
|
let mut rounded_alpha = (self.alpha_f32() * 100.).round() / 100.;
|
||||||
|
if clamp_unit_f32(rounded_alpha) != self.alpha {
|
||||||
|
rounded_alpha = (self.alpha_f32() * 1000.).round() / 1000.;
|
||||||
|
}
|
||||||
|
|
||||||
|
rounded_alpha.to_css(dest)?;
|
||||||
|
}
|
||||||
|
dest.write_char(')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either a number or a percentage.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub enum NumberOrPercentage {
|
||||||
|
/// `<number>`.
|
||||||
|
Number {
|
||||||
|
/// The numeric value parsed, as a float.
|
||||||
|
value: f32,
|
||||||
|
},
|
||||||
|
/// `<percentage>`
|
||||||
|
Percentage {
|
||||||
|
/// The value as a float, divided by 100 so that the nominal range is
|
||||||
|
/// 0.0 to 1.0.
|
||||||
|
unit_value: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
impl NumberOrPercentage {
|
||||||
|
fn unit_value(&self) -> f32 {
|
||||||
|
match *self {
|
||||||
|
NumberOrPercentage::Number { value } => value,
|
||||||
|
NumberOrPercentage::Percentage { unit_value } => unit_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either an angle or a number.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub enum AngleOrNumber {
|
||||||
|
/// `<number>`.
|
||||||
|
Number {
|
||||||
|
/// The numeric value parsed, as a float.
|
||||||
|
value: f32,
|
||||||
|
},
|
||||||
|
/// `<angle>`
|
||||||
|
Angle {
|
||||||
|
/// The value as a number of degrees.
|
||||||
|
degrees: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
impl AngleOrNumber {
|
||||||
|
fn degrees(&self) -> f32 {
|
||||||
|
match *self {
|
||||||
|
AngleOrNumber::Number { value } => value,
|
||||||
|
AngleOrNumber::Angle { degrees } => degrees,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that can be used to hook into how `cssparser` parses color
|
||||||
|
/// components, with the intention of implementing more complicated behavior.
|
||||||
|
///
|
||||||
|
/// For example, this is used by Servo to support calc() in color.
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub trait ColorComponentParser<'i> {
|
||||||
|
/// A custom error type that can be returned from the parsing functions.
|
||||||
|
type Error: 'i;
|
||||||
|
|
||||||
|
/// Parse an `<angle>` or `<number>`.
|
||||||
|
///
|
||||||
|
/// Returns the result in degrees.
|
||||||
|
fn parse_angle_or_number<'t>(
|
||||||
|
&self,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
|
||||||
|
let location = input.current_source_location();
|
||||||
|
Ok(match *input.next()? {
|
||||||
|
Token::Number { value, .. } => AngleOrNumber::Number { value },
|
||||||
|
Token::Dimension {
|
||||||
|
value: v, ref unit, ..
|
||||||
|
} => {
|
||||||
|
let degrees = match_ignore_ascii_case! { &*unit,
|
||||||
|
"deg" => v,
|
||||||
|
"grad" => v * 360. / 400.,
|
||||||
|
"rad" => v * 360. / (2. * PI),
|
||||||
|
"turn" => v * 360.,
|
||||||
|
_ => return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
AngleOrNumber::Angle { degrees }
|
||||||
|
}
|
||||||
|
ref t => return Err(location.new_unexpected_token_error(t.clone())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a `<percentage>` value.
|
||||||
|
///
|
||||||
|
/// Returns the result in a number from 0.0 to 1.0.
|
||||||
|
fn parse_percentage<'t>(
|
||||||
|
&self,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<f32, ParseError<'i, Self::Error>> {
|
||||||
|
input.expect_percentage().map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a `<number>` value.
|
||||||
|
fn parse_number<'t>(
|
||||||
|
&self,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<f32, ParseError<'i, Self::Error>> {
|
||||||
|
input.expect_number().map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a `<number>` value or a `<percentage>` value.
|
||||||
|
fn parse_number_or_percentage<'t>(
|
||||||
|
&self,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
|
||||||
|
let location = input.current_source_location();
|
||||||
|
Ok(match *input.next()? {
|
||||||
|
Token::Number { value, .. } => NumberOrPercentage::Number { value },
|
||||||
|
Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value },
|
||||||
|
ref t => return Err(location.new_unexpected_token_error(t.clone())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
struct DefaultComponentParser;
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
impl<'i> ColorComponentParser<'i> for DefaultComponentParser {
|
||||||
|
type Error = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn rgb(red: u8, green: u8, blue: u8) -> Color {
|
||||||
|
rgba(red, green, blue, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
|
||||||
|
Color::new(red, green, blue, alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the named color with the given name.
|
||||||
|
///
|
||||||
|
/// Matching is case-insensitive in the ASCII range.
|
||||||
|
/// CSS escaping (if relevant) should be resolved before calling this function.
|
||||||
|
/// (For example, the value of an `Ident` token is fine.)
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
pub fn parse_color_keyword(ident: &str) -> Result<Color, ()> {
|
||||||
|
macro_rules! rgb {
|
||||||
|
($red: expr, $green: expr, $blue: expr) => {
|
||||||
|
Color {
|
||||||
|
red: $red,
|
||||||
|
green: $green,
|
||||||
|
blue: $blue,
|
||||||
|
alpha: 255,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ascii_case_insensitive_phf_map! {
|
||||||
|
keyword -> Color = {
|
||||||
|
"black" => rgb!(0, 0, 0),
|
||||||
|
"silver" => rgb!(192, 192, 192),
|
||||||
|
"gray" => rgb!(128, 128, 128),
|
||||||
|
"white" => rgb!(255, 255, 255),
|
||||||
|
"maroon" => rgb!(128, 0, 0),
|
||||||
|
"red" => rgb!(255, 0, 0),
|
||||||
|
"purple" => rgb!(128, 0, 128),
|
||||||
|
"fuchsia" => rgb!(255, 0, 255),
|
||||||
|
"green" => rgb!(0, 128, 0),
|
||||||
|
"lime" => rgb!(0, 255, 0),
|
||||||
|
"olive" => rgb!(128, 128, 0),
|
||||||
|
"yellow" => rgb!(255, 255, 0),
|
||||||
|
"navy" => rgb!(0, 0, 128),
|
||||||
|
"blue" => rgb!(0, 0, 255),
|
||||||
|
"teal" => rgb!(0, 128, 128),
|
||||||
|
"aqua" => rgb!(0, 255, 255),
|
||||||
|
|
||||||
|
"aliceblue" => rgb!(240, 248, 255),
|
||||||
|
"antiquewhite" => rgb!(250, 235, 215),
|
||||||
|
"aquamarine" => rgb!(127, 255, 212),
|
||||||
|
"azure" => rgb!(240, 255, 255),
|
||||||
|
"beige" => rgb!(245, 245, 220),
|
||||||
|
"bisque" => rgb!(255, 228, 196),
|
||||||
|
"blanchedalmond" => rgb!(255, 235, 205),
|
||||||
|
"blueviolet" => rgb!(138, 43, 226),
|
||||||
|
"brown" => rgb!(165, 42, 42),
|
||||||
|
"burlywood" => rgb!(222, 184, 135),
|
||||||
|
"cadetblue" => rgb!(95, 158, 160),
|
||||||
|
"chartreuse" => rgb!(127, 255, 0),
|
||||||
|
"chocolate" => rgb!(210, 105, 30),
|
||||||
|
"coral" => rgb!(255, 127, 80),
|
||||||
|
"cornflowerblue" => rgb!(100, 149, 237),
|
||||||
|
"cornsilk" => rgb!(255, 248, 220),
|
||||||
|
"crimson" => rgb!(220, 20, 60),
|
||||||
|
"cyan" => rgb!(0, 255, 255),
|
||||||
|
"darkblue" => rgb!(0, 0, 139),
|
||||||
|
"darkcyan" => rgb!(0, 139, 139),
|
||||||
|
"darkgoldenrod" => rgb!(184, 134, 11),
|
||||||
|
"darkgray" => rgb!(169, 169, 169),
|
||||||
|
"darkgreen" => rgb!(0, 100, 0),
|
||||||
|
"darkgrey" => rgb!(169, 169, 169),
|
||||||
|
"darkkhaki" => rgb!(189, 183, 107),
|
||||||
|
"darkmagenta" => rgb!(139, 0, 139),
|
||||||
|
"darkolivegreen" => rgb!(85, 107, 47),
|
||||||
|
"darkorange" => rgb!(255, 140, 0),
|
||||||
|
"darkorchid" => rgb!(153, 50, 204),
|
||||||
|
"darkred" => rgb!(139, 0, 0),
|
||||||
|
"darksalmon" => rgb!(233, 150, 122),
|
||||||
|
"darkseagreen" => rgb!(143, 188, 143),
|
||||||
|
"darkslateblue" => rgb!(72, 61, 139),
|
||||||
|
"darkslategray" => rgb!(47, 79, 79),
|
||||||
|
"darkslategrey" => rgb!(47, 79, 79),
|
||||||
|
"darkturquoise" => rgb!(0, 206, 209),
|
||||||
|
"darkviolet" => rgb!(148, 0, 211),
|
||||||
|
"deeppink" => rgb!(255, 20, 147),
|
||||||
|
"deepskyblue" => rgb!(0, 191, 255),
|
||||||
|
"dimgray" => rgb!(105, 105, 105),
|
||||||
|
"dimgrey" => rgb!(105, 105, 105),
|
||||||
|
"dodgerblue" => rgb!(30, 144, 255),
|
||||||
|
"firebrick" => rgb!(178, 34, 34),
|
||||||
|
"floralwhite" => rgb!(255, 250, 240),
|
||||||
|
"forestgreen" => rgb!(34, 139, 34),
|
||||||
|
"gainsboro" => rgb!(220, 220, 220),
|
||||||
|
"ghostwhite" => rgb!(248, 248, 255),
|
||||||
|
"gold" => rgb!(255, 215, 0),
|
||||||
|
"goldenrod" => rgb!(218, 165, 32),
|
||||||
|
"greenyellow" => rgb!(173, 255, 47),
|
||||||
|
"grey" => rgb!(128, 128, 128),
|
||||||
|
"honeydew" => rgb!(240, 255, 240),
|
||||||
|
"hotpink" => rgb!(255, 105, 180),
|
||||||
|
"indianred" => rgb!(205, 92, 92),
|
||||||
|
"indigo" => rgb!(75, 0, 130),
|
||||||
|
"ivory" => rgb!(255, 255, 240),
|
||||||
|
"khaki" => rgb!(240, 230, 140),
|
||||||
|
"lavender" => rgb!(230, 230, 250),
|
||||||
|
"lavenderblush" => rgb!(255, 240, 245),
|
||||||
|
"lawngreen" => rgb!(124, 252, 0),
|
||||||
|
"lemonchiffon" => rgb!(255, 250, 205),
|
||||||
|
"lightblue" => rgb!(173, 216, 230),
|
||||||
|
"lightcoral" => rgb!(240, 128, 128),
|
||||||
|
"lightcyan" => rgb!(224, 255, 255),
|
||||||
|
"lightgoldenrodyellow" => rgb!(250, 250, 210),
|
||||||
|
"lightgray" => rgb!(211, 211, 211),
|
||||||
|
"lightgreen" => rgb!(144, 238, 144),
|
||||||
|
"lightgrey" => rgb!(211, 211, 211),
|
||||||
|
"lightpink" => rgb!(255, 182, 193),
|
||||||
|
"lightsalmon" => rgb!(255, 160, 122),
|
||||||
|
"lightseagreen" => rgb!(32, 178, 170),
|
||||||
|
"lightskyblue" => rgb!(135, 206, 250),
|
||||||
|
"lightslategray" => rgb!(119, 136, 153),
|
||||||
|
"lightslategrey" => rgb!(119, 136, 153),
|
||||||
|
"lightsteelblue" => rgb!(176, 196, 222),
|
||||||
|
"lightyellow" => rgb!(255, 255, 224),
|
||||||
|
"limegreen" => rgb!(50, 205, 50),
|
||||||
|
"linen" => rgb!(250, 240, 230),
|
||||||
|
"magenta" => rgb!(255, 0, 255),
|
||||||
|
"mediumaquamarine" => rgb!(102, 205, 170),
|
||||||
|
"mediumblue" => rgb!(0, 0, 205),
|
||||||
|
"mediumorchid" => rgb!(186, 85, 211),
|
||||||
|
"mediumpurple" => rgb!(147, 112, 219),
|
||||||
|
"mediumseagreen" => rgb!(60, 179, 113),
|
||||||
|
"mediumslateblue" => rgb!(123, 104, 238),
|
||||||
|
"mediumspringgreen" => rgb!(0, 250, 154),
|
||||||
|
"mediumturquoise" => rgb!(72, 209, 204),
|
||||||
|
"mediumvioletred" => rgb!(199, 21, 133),
|
||||||
|
"midnightblue" => rgb!(25, 25, 112),
|
||||||
|
"mintcream" => rgb!(245, 255, 250),
|
||||||
|
"mistyrose" => rgb!(255, 228, 225),
|
||||||
|
"moccasin" => rgb!(255, 228, 181),
|
||||||
|
"navajowhite" => rgb!(255, 222, 173),
|
||||||
|
"oldlace" => rgb!(253, 245, 230),
|
||||||
|
"olivedrab" => rgb!(107, 142, 35),
|
||||||
|
"orange" => rgb!(255, 165, 0),
|
||||||
|
"orangered" => rgb!(255, 69, 0),
|
||||||
|
"orchid" => rgb!(218, 112, 214),
|
||||||
|
"palegoldenrod" => rgb!(238, 232, 170),
|
||||||
|
"palegreen" => rgb!(152, 251, 152),
|
||||||
|
"paleturquoise" => rgb!(175, 238, 238),
|
||||||
|
"palevioletred" => rgb!(219, 112, 147),
|
||||||
|
"papayawhip" => rgb!(255, 239, 213),
|
||||||
|
"peachpuff" => rgb!(255, 218, 185),
|
||||||
|
"peru" => rgb!(205, 133, 63),
|
||||||
|
"pink" => rgb!(255, 192, 203),
|
||||||
|
"plum" => rgb!(221, 160, 221),
|
||||||
|
"powderblue" => rgb!(176, 224, 230),
|
||||||
|
"rebeccapurple" => rgb!(102, 51, 153),
|
||||||
|
"rosybrown" => rgb!(188, 143, 143),
|
||||||
|
"royalblue" => rgb!(65, 105, 225),
|
||||||
|
"saddlebrown" => rgb!(139, 69, 19),
|
||||||
|
"salmon" => rgb!(250, 128, 114),
|
||||||
|
"sandybrown" => rgb!(244, 164, 96),
|
||||||
|
"seagreen" => rgb!(46, 139, 87),
|
||||||
|
"seashell" => rgb!(255, 245, 238),
|
||||||
|
"sienna" => rgb!(160, 82, 45),
|
||||||
|
"skyblue" => rgb!(135, 206, 235),
|
||||||
|
"slateblue" => rgb!(106, 90, 205),
|
||||||
|
"slategray" => rgb!(112, 128, 144),
|
||||||
|
"slategrey" => rgb!(112, 128, 144),
|
||||||
|
"snow" => rgb!(255, 250, 250),
|
||||||
|
"springgreen" => rgb!(0, 255, 127),
|
||||||
|
"steelblue" => rgb!(70, 130, 180),
|
||||||
|
"tan" => rgb!(210, 180, 140),
|
||||||
|
"thistle" => rgb!(216, 191, 216),
|
||||||
|
"tomato" => rgb!(255, 99, 71),
|
||||||
|
"turquoise" => rgb!(64, 224, 208),
|
||||||
|
"violet" => rgb!(238, 130, 238),
|
||||||
|
"wheat" => rgb!(245, 222, 179),
|
||||||
|
"whitesmoke" => rgb!(245, 245, 245),
|
||||||
|
"yellowgreen" => rgb!(154, 205, 50),
|
||||||
|
|
||||||
|
"transparent" => Color { red: 0, green: 0, blue: 0, alpha: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyword(ident).cloned().ok_or(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn from_hex(c: u8) -> Result<u8, ()> {
|
||||||
|
match c {
|
||||||
|
b'0'...b'9' => Ok(c - b'0'),
|
||||||
|
b'a'...b'f' => Ok(c - b'a' + 10),
|
||||||
|
b'A'...b'F' => Ok(c - b'A' + 10),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_unit_f32(val: f32) -> u8 {
|
||||||
|
// Whilst scaling by 256 and flooring would provide
|
||||||
|
// an equal distribution of integers to percentage inputs,
|
||||||
|
// this is not what Gecko does so we instead multiply by 255
|
||||||
|
// and round (adding 0.5 and flooring is equivalent to rounding)
|
||||||
|
//
|
||||||
|
// Chrome does something similar for the alpha value, but not
|
||||||
|
// the rgb values.
|
||||||
|
//
|
||||||
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1340484
|
||||||
|
//
|
||||||
|
// Clamping to 256 and rounding after would let 1.0 map to 256, and
|
||||||
|
// `256.0_f32 as u8` is undefined behavior:
|
||||||
|
//
|
||||||
|
// https://github.com/rust-lang/rust/issues/10184
|
||||||
|
clamp_floor_256_f32(val * 255.)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_floor_256_f32(val: f32) -> u8 {
|
||||||
|
val.round().max(0.).min(255.) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn parse_color_function<'i, 't, ComponentParser>(
|
||||||
|
component_parser: &ComponentParser,
|
||||||
|
name: &str,
|
||||||
|
arguments: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<Color, ParseError<'i, ComponentParser::Error>>
|
||||||
|
where
|
||||||
|
ComponentParser: ColorComponentParser<'i>,
|
||||||
|
{
|
||||||
|
let (red, green, blue, uses_commas) = match_ignore_ascii_case! { name,
|
||||||
|
"rgb" | "rgba" => parse_rgb_components_rgb(component_parser, arguments)?,
|
||||||
|
"hsl" | "hsla" => parse_rgb_components_hsl(component_parser, arguments)?,
|
||||||
|
_ => return Err(arguments.new_unexpected_token_error(Token::Ident(name.to_owned().into()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let alpha = if !arguments.is_exhausted() {
|
||||||
|
if uses_commas {
|
||||||
|
arguments.expect_comma()?;
|
||||||
|
} else {
|
||||||
|
arguments.expect_delim('/')?;
|
||||||
|
};
|
||||||
|
clamp_unit_f32(
|
||||||
|
component_parser
|
||||||
|
.parse_number_or_percentage(arguments)?
|
||||||
|
.unit_value(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
255
|
||||||
|
};
|
||||||
|
|
||||||
|
arguments.expect_exhausted()?;
|
||||||
|
Ok(rgba(red, green, blue, alpha))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn parse_rgb_components_rgb<'i, 't, ComponentParser>(
|
||||||
|
component_parser: &ComponentParser,
|
||||||
|
arguments: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<(u8, u8, u8, bool), ParseError<'i, ComponentParser::Error>>
|
||||||
|
where
|
||||||
|
ComponentParser: ColorComponentParser<'i>,
|
||||||
|
{
|
||||||
|
// Either integers or percentages, but all the same type.
|
||||||
|
// https://drafts.csswg.org/css-color/#rgb-functions
|
||||||
|
let (red, is_number) = match component_parser.parse_number_or_percentage(arguments)? {
|
||||||
|
NumberOrPercentage::Number { value } => (clamp_floor_256_f32(value), true),
|
||||||
|
NumberOrPercentage::Percentage { unit_value } => (clamp_unit_f32(unit_value), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let uses_commas = arguments.try_parse(|i| i.expect_comma()).is_ok();
|
||||||
|
|
||||||
|
let green;
|
||||||
|
let blue;
|
||||||
|
if is_number {
|
||||||
|
green = clamp_floor_256_f32(component_parser.parse_number(arguments)?);
|
||||||
|
if uses_commas {
|
||||||
|
arguments.expect_comma()?;
|
||||||
|
}
|
||||||
|
blue = clamp_floor_256_f32(component_parser.parse_number(arguments)?);
|
||||||
|
} else {
|
||||||
|
green = clamp_unit_f32(component_parser.parse_percentage(arguments)?);
|
||||||
|
if uses_commas {
|
||||||
|
arguments.expect_comma()?;
|
||||||
|
}
|
||||||
|
blue = clamp_unit_f32(component_parser.parse_percentage(arguments)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((red, green, blue, uses_commas))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[inline]
|
||||||
|
fn parse_rgb_components_hsl<'i, 't, ComponentParser>(
|
||||||
|
component_parser: &ComponentParser,
|
||||||
|
arguments: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<(u8, u8, u8, bool), ParseError<'i, ComponentParser::Error>>
|
||||||
|
where
|
||||||
|
ComponentParser: ColorComponentParser<'i>,
|
||||||
|
{
|
||||||
|
// Hue given as an angle
|
||||||
|
// https://drafts.csswg.org/css-values/#angles
|
||||||
|
let hue_degrees = component_parser.parse_angle_or_number(arguments)?.degrees();
|
||||||
|
|
||||||
|
// Subtract an integer before rounding, to avoid some rounding errors:
|
||||||
|
let hue_normalized_degrees = hue_degrees - 360. * (hue_degrees / 360.).floor();
|
||||||
|
let hue = hue_normalized_degrees / 360.;
|
||||||
|
|
||||||
|
// Saturation and lightness are clamped to 0% ... 100%
|
||||||
|
// https://drafts.csswg.org/css-color/#the-hsl-notation
|
||||||
|
let uses_commas = arguments.try_parse(|i| i.expect_comma()).is_ok();
|
||||||
|
|
||||||
|
let saturation = component_parser.parse_percentage(arguments)?;
|
||||||
|
let saturation = saturation.max(0.).min(1.);
|
||||||
|
|
||||||
|
if uses_commas {
|
||||||
|
arguments.expect_comma()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lightness = component_parser.parse_percentage(arguments)?;
|
||||||
|
let lightness = lightness.max(0.).min(1.);
|
||||||
|
|
||||||
|
// https://drafts.csswg.org/css-color/#hsl-color
|
||||||
|
// except with h pre-multiplied by 3, to avoid some rounding errors.
|
||||||
|
fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 {
|
||||||
|
if h3 < 0. {
|
||||||
|
h3 += 3.
|
||||||
|
}
|
||||||
|
if h3 > 3. {
|
||||||
|
h3 -= 3.
|
||||||
|
}
|
||||||
|
|
||||||
|
if h3 * 2. < 1. {
|
||||||
|
m1 + (m2 - m1) * h3 * 2.
|
||||||
|
} else if h3 * 2. < 3. {
|
||||||
|
m2
|
||||||
|
} else if h3 < 2. {
|
||||||
|
m1 + (m2 - m1) * (2. - h3) * 2.
|
||||||
|
} else {
|
||||||
|
m1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let m2 = if lightness <= 0.5 {
|
||||||
|
lightness * (saturation + 1.)
|
||||||
|
} else {
|
||||||
|
lightness + saturation - lightness * saturation
|
||||||
|
};
|
||||||
|
let m1 = lightness * 2. - m2;
|
||||||
|
let hue_times_3 = hue * 3.;
|
||||||
|
let red = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3 + 1.));
|
||||||
|
let green = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3));
|
||||||
|
let blue = clamp_unit_f32(hue_to_rgb(m1, m2, hue_times_3 - 1.));
|
||||||
|
return Ok((red, green, blue, uses_commas));
|
||||||
|
}
|
||||||
15
styles/src/lib.rs
Normal file
15
styles/src/lib.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
//! This crate hoists various styles and layout parameters for implementing
|
||||||
|
//! Flexbox in Alchemy. For all intents and purposes, you can essentially consider
|
||||||
|
//! this to be the root crate for Alchemy, as just about everything ends up using it.
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
#[macro_use] pub extern crate cssparser;
|
||||||
|
|
||||||
|
mod stretch;
|
||||||
|
pub use stretch::{geometry, node, number, result, Stretch, Error};
|
||||||
|
|
||||||
|
pub mod color;
|
||||||
|
pub mod styles;
|
||||||
|
|
||||||
|
#[cfg(feature="parser")]
|
||||||
|
pub mod styles_parser;
|
||||||
1363
styles/src/stretch/algo.rs
Normal file
1363
styles/src/stretch/algo.rs
Normal file
File diff suppressed because it is too large
Load diff
136
styles/src/stretch/geometry.rs
Normal file
136
styles/src/stretch/geometry.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
use core::ops::Add;
|
||||||
|
|
||||||
|
use crate::number::Number;
|
||||||
|
use crate::styles as style;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Rect<T> {
|
||||||
|
pub start: T,
|
||||||
|
pub end: T,
|
||||||
|
pub top: T,
|
||||||
|
pub bottom: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Rect<T> {
|
||||||
|
pub(crate) fn map<R, F>(self, f: F) -> Rect<R>
|
||||||
|
where
|
||||||
|
F: Fn(T) -> R,
|
||||||
|
{
|
||||||
|
Rect { start: f(self.start), end: f(self.end), top: f(self.top), bottom: f(self.bottom) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Rect<T>
|
||||||
|
where
|
||||||
|
T: Add<Output = T> + Copy + Clone,
|
||||||
|
{
|
||||||
|
pub(crate) fn horizontal(&self) -> T {
|
||||||
|
self.start + self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn vertical(&self) -> T {
|
||||||
|
self.top + self.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn main(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.start + self.end,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.top + self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cross(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.top + self.bottom,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.start + self.end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Rect<T>
|
||||||
|
where
|
||||||
|
T: Copy + Clone,
|
||||||
|
{
|
||||||
|
pub(crate) fn main_start(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.start,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn main_end(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.end,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cross_start(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.top,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cross_end(&self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.bottom,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Size<T> {
|
||||||
|
pub width: T,
|
||||||
|
pub height: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size<()> {
|
||||||
|
pub fn undefined() -> Size<Number> {
|
||||||
|
Size { width: Number::Undefined, height: Number::Undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Size<T> {
|
||||||
|
pub(crate) fn map<R, F>(self, f: F) -> Size<R>
|
||||||
|
where
|
||||||
|
F: Fn(T) -> R,
|
||||||
|
{
|
||||||
|
Size { width: f(self.width), height: f(self.height) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_main(&mut self, direction: style::FlexDirection, value: T) {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.width = value,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.height = value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_cross(&mut self, direction: style::FlexDirection, value: T) {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.height = value,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.width = value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn main(self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.width,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cross(self, direction: style::FlexDirection) -> T {
|
||||||
|
match direction {
|
||||||
|
style::FlexDirection::Row | style::FlexDirection::RowReverse => self.height,
|
||||||
|
style::FlexDirection::Column | style::FlexDirection::ColumnReverse => self.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Point<T> {
|
||||||
|
pub x: T,
|
||||||
|
pub y: T,
|
||||||
|
}
|
||||||
36
styles/src/stretch/id.rs
Normal file
36
styles/src/stretch/id.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
//! Identifier for a Node
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) struct Id {
|
||||||
|
id: u32,
|
||||||
|
generation: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Allocator {
|
||||||
|
new_id: u32,
|
||||||
|
free_ids: Vec<Id>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Allocator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Allocator { new_id: 0, free_ids: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate(&mut self) -> Id {
|
||||||
|
// TODO: better balancing
|
||||||
|
match self.free_ids.pop() {
|
||||||
|
Some(id) => Id { id: id.id, generation: id.generation + 1 },
|
||||||
|
None => {
|
||||||
|
let id = self.new_id;
|
||||||
|
self.new_id += 1;
|
||||||
|
Id { id, generation: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free(&mut self, ids: &[Id]) {
|
||||||
|
self.free_ids.extend(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
styles/src/stretch/mod.rs
Normal file
35
styles/src/stretch/mod.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
pub mod geometry;
|
||||||
|
pub mod node;
|
||||||
|
pub mod number;
|
||||||
|
pub mod result;
|
||||||
|
|
||||||
|
mod algo;
|
||||||
|
mod id;
|
||||||
|
|
||||||
|
pub use crate::node::Stretch;
|
||||||
|
|
||||||
|
use core::any::Any;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
InvalidNode(node::Node),
|
||||||
|
Measure(Box<Any>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Error::InvalidNode(ref node) => write!(f, "Invalid node {:?}", node),
|
||||||
|
Error::Measure(_) => write!(f, "Error during measurement"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
match *self {
|
||||||
|
Error::InvalidNode(_) => "The node is not part of the stretch instance",
|
||||||
|
Error::Measure(_) => "Error occurred inside a measurement function",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
239
styles/src/stretch/node.rs
Normal file
239
styles/src/stretch/node.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
use core::any::Any;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::Drop;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use crate::geometry::Size;
|
||||||
|
use crate::stretch::id;
|
||||||
|
use crate::number::Number;
|
||||||
|
use crate::result::{Cache, Layout};
|
||||||
|
use crate::styles::*;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
type MeasureFunc = Box<Fn(Size<Number>) -> Result<Size<f32>, Box<Any>> + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// Global stretch instance id allocator.
|
||||||
|
static ref INSTANCE_ALLOCATOR: Mutex<id::Allocator> = Mutex::new(id::Allocator::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Node {
|
||||||
|
instance: id::Id,
|
||||||
|
local: id::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Storage<T>(HashMap<Node, T>);
|
||||||
|
|
||||||
|
impl<T> Storage<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Storage(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, node: Node) -> Result<&T, Error> {
|
||||||
|
match self.0.get(&node) {
|
||||||
|
Some(v) => Ok(v),
|
||||||
|
None => Err(Error::InvalidNode(node)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, node: Node) -> Result<&mut T, Error> {
|
||||||
|
match self.0.get_mut(&node) {
|
||||||
|
Some(v) => Ok(v),
|
||||||
|
None => Err(Error::InvalidNode(node)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, node: Node, value: T) -> Option<T> {
|
||||||
|
self.0.insert(node, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::ops::Index<&Node> for Storage<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn index(&self, idx: &Node) -> &T {
|
||||||
|
&(self.0)[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Stretch {
|
||||||
|
id: id::Id,
|
||||||
|
nodes: id::Allocator,
|
||||||
|
pub(crate) style: Storage<Style>,
|
||||||
|
pub(crate) parents: Storage<Vec<Node>>,
|
||||||
|
pub(crate) children: Storage<Vec<Node>>,
|
||||||
|
pub(crate) measure: Storage<Option<MeasureFunc>>,
|
||||||
|
pub(crate) layout: Storage<Layout>,
|
||||||
|
pub(crate) layout_cache: Storage<Option<Cache>>,
|
||||||
|
pub(crate) is_dirty: Storage<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stretch {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Stretch {
|
||||||
|
id: INSTANCE_ALLOCATOR.lock().unwrap().allocate(),
|
||||||
|
nodes: id::Allocator::new(),
|
||||||
|
style: Storage::new(),
|
||||||
|
parents: Storage::new(),
|
||||||
|
children: Storage::new(),
|
||||||
|
measure: Storage::new(),
|
||||||
|
layout: Storage::new(),
|
||||||
|
layout_cache: Storage::new(),
|
||||||
|
is_dirty: Storage::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_node(&mut self) -> Node {
|
||||||
|
let local = self.nodes.allocate();
|
||||||
|
Node { instance: self.id, local }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_leaf(&mut self, style: Style, measure: MeasureFunc) -> Node {
|
||||||
|
let node = self.allocate_node();
|
||||||
|
|
||||||
|
self.style.insert(node, style);
|
||||||
|
self.parents.insert(node, Vec::with_capacity(1));
|
||||||
|
self.children.insert(node, Vec::with_capacity(0));
|
||||||
|
self.measure.insert(node, Some(measure));
|
||||||
|
self.layout.insert(node, Layout::new());
|
||||||
|
self.layout_cache.insert(node, None);
|
||||||
|
self.is_dirty.insert(node, true);
|
||||||
|
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_node(&mut self, style: Style, children: Vec<Node>) -> Result<Node, Error> {
|
||||||
|
let node = self.allocate_node();
|
||||||
|
|
||||||
|
for child in &children {
|
||||||
|
self.parents.get_mut(*child)?.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.style.insert(node, style);
|
||||||
|
self.parents.insert(node, Vec::with_capacity(1));
|
||||||
|
self.children.insert(node, children);
|
||||||
|
self.measure.insert(node, None);
|
||||||
|
self.layout.insert(node, Layout::new());
|
||||||
|
self.layout_cache.insert(node, None);
|
||||||
|
self.is_dirty.insert(node, true);
|
||||||
|
|
||||||
|
Ok(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_measure(&mut self, node: Node, measure: Option<MeasureFunc>) -> Result<(), Error> {
|
||||||
|
*self.measure.get_mut(node)? = measure;
|
||||||
|
self.mark_dirty(node)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_child(&mut self, node: Node, child: Node) -> Result<(), Error> {
|
||||||
|
self.parents.get_mut(child)?.push(node);
|
||||||
|
self.children.get_mut(node)?.push(child);
|
||||||
|
self.mark_dirty(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_children(&mut self, node: Node, children: Vec<Node>) -> Result<(), Error> {
|
||||||
|
// Remove node as parent from all its current children.
|
||||||
|
for child in self.children.get(node)? {
|
||||||
|
self.parents.get_mut(*child)?.retain(|p| *p != node);
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.children.get_mut(node)? = Vec::with_capacity(children.len());
|
||||||
|
|
||||||
|
// Build up relation node <-> child
|
||||||
|
for child in children {
|
||||||
|
self.parents.get_mut(child)?.push(node);
|
||||||
|
self.children.get_mut(node)?.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mark_dirty(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_child(&mut self, node: Node, child: Node) -> Result<Node, Error> {
|
||||||
|
match self.children(node)?.iter().position(|n| *n == child) {
|
||||||
|
Some(index) => self.remove_child_at_index(node, index),
|
||||||
|
None => Err(Error::InvalidNode(child)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_child_at_index(&mut self, node: Node, index: usize) -> Result<Node, Error> {
|
||||||
|
let child = self.children.get_mut(node)?.remove(index);
|
||||||
|
self.parents.get_mut(child)?.retain(|p| *p != node);
|
||||||
|
|
||||||
|
self.mark_dirty(node)?;
|
||||||
|
|
||||||
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_child_at_index(&mut self, node: Node, index: usize, child: Node) -> Result<Node, Error> {
|
||||||
|
self.parents.get_mut(child)?.push(node);
|
||||||
|
let old_child = std::mem::replace(&mut self.children.get_mut(node)?[index], child);
|
||||||
|
self.parents.get_mut(old_child)?.retain(|p| *p != node);
|
||||||
|
|
||||||
|
self.mark_dirty(node)?;
|
||||||
|
|
||||||
|
Ok(old_child)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self, node: Node) -> Result<Vec<Node>, Error> {
|
||||||
|
self.children.get(node).map(Clone::clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn child_count(&self, node: Node) -> Result<usize, Error> {
|
||||||
|
self.children.get(node).map(Vec::len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_style(&mut self, node: Node, style: Style) -> Result<(), Error> {
|
||||||
|
*self.style.get_mut(node)? = style;
|
||||||
|
self.mark_dirty(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(&self, node: Node) -> Result<&Style, Error> {
|
||||||
|
self.style.get(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout(&self, node: Node) -> Result<&Layout, Error> {
|
||||||
|
self.layout.get(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_dirty(&mut self, node: Node) -> Result<(), Error> {
|
||||||
|
fn mark_dirty_impl(
|
||||||
|
node: Node,
|
||||||
|
layout_cache: &mut Storage<Option<Cache>>,
|
||||||
|
is_dirty: &mut Storage<bool>,
|
||||||
|
parents: &Storage<Vec<Node>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
*layout_cache.get_mut(node)? = None;
|
||||||
|
*is_dirty.get_mut(node)? = true;
|
||||||
|
|
||||||
|
for parent in parents.get(node)? {
|
||||||
|
mark_dirty_impl(*parent, layout_cache, is_dirty, parents)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_dirty_impl(node, &mut self.layout_cache, &mut self.is_dirty, &self.parents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dirty(&self, node: Node) -> Result<bool, Error> {
|
||||||
|
self.is_dirty.get(node).map(|v| *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_layout(&mut self, node: Node, size: Size<Number>) -> Result<(), Error> {
|
||||||
|
match self.layout.get(node) {
|
||||||
|
Ok(_) => self.compute(node, size).map_err(|err| Error::Measure(err)),
|
||||||
|
_ => Err(Error::InvalidNode(node)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Stretch {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
INSTANCE_ALLOCATOR.lock().unwrap().free(&[self.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
220
styles/src/stretch/number.rs
Normal file
220
styles/src/stretch/number.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
use core::ops;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Number {
|
||||||
|
Defined(f32),
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToNumber {
|
||||||
|
fn to_number(self) -> Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OrElse<T> {
|
||||||
|
fn or_else(self, other: T) -> T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Number {
|
||||||
|
fn default() -> Number {
|
||||||
|
Number::Undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrElse<f32> for Number {
|
||||||
|
fn or_else(self, other: f32) -> f32 {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => val,
|
||||||
|
Number::Undefined => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrElse<Number> for Number {
|
||||||
|
fn or_else(self, other: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(_) => self,
|
||||||
|
Number::Undefined => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Number {
|
||||||
|
pub fn is_defined(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Number::Defined(_) => true,
|
||||||
|
Number::Undefined => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_undefined(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Number::Defined(_) => false,
|
||||||
|
Number::Undefined => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MinMax<In, Out> {
|
||||||
|
fn maybe_min(self, rhs: In) -> Out;
|
||||||
|
fn maybe_max(self, rhs: In) -> Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinMax<Number, Number> for Number {
|
||||||
|
fn maybe_min(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val.min(other)),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_max(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val.max(other)),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinMax<f32, Number> for Number {
|
||||||
|
fn maybe_min(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val.min(rhs)),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_max(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val.max(rhs)),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinMax<Number, f32> for f32 {
|
||||||
|
fn maybe_min(self, rhs: Number) -> f32 {
|
||||||
|
match rhs {
|
||||||
|
Number::Defined(val) => self.min(val),
|
||||||
|
Number::Undefined => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_max(self, rhs: Number) -> f32 {
|
||||||
|
match rhs {
|
||||||
|
Number::Defined(val) => self.max(val),
|
||||||
|
Number::Undefined => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToNumber for f32 {
|
||||||
|
fn to_number(self) -> Number {
|
||||||
|
Number::Defined(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<f32> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn add(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val + rhs),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<Number> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn add(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val + other),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Sub<f32> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn sub(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val - rhs),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Sub<Number> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val - other),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Mul<f32> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn mul(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val * rhs),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Mul<Number> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn mul(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val * other),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Div<f32> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn div(self, rhs: f32) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => Number::Defined(val / rhs),
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Div<Number> for Number {
|
||||||
|
type Output = Number;
|
||||||
|
|
||||||
|
fn div(self, rhs: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Number::Defined(val) => match rhs {
|
||||||
|
Number::Defined(other) => Number::Defined(val / other),
|
||||||
|
Number::Undefined => self,
|
||||||
|
},
|
||||||
|
Number::Undefined => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
styles/src/stretch/result.rs
Normal file
25
styles/src/stretch/result.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::stretch::algo::ComputeResult;
|
||||||
|
use crate::geometry::{Point, Size};
|
||||||
|
use crate::number::Number;
|
||||||
|
|
||||||
|
#[derive(Copy, Debug, Clone)]
|
||||||
|
pub struct Layout {
|
||||||
|
pub(crate) order: u32,
|
||||||
|
pub size: Size<f32>,
|
||||||
|
pub location: Point<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Layout { order: 0, size: Size { width: 0.0, height: 0.0 }, location: Point { x: 0.0, y: 0.0 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct Cache {
|
||||||
|
pub(crate) node_size: Size<Number>,
|
||||||
|
pub(crate) parent_size: Size<Number>,
|
||||||
|
pub(crate) perform_layout: bool,
|
||||||
|
|
||||||
|
pub(crate) result: ComputeResult,
|
||||||
|
}
|
||||||
737
styles/src/styles.rs
Normal file
737
styles/src/styles.rs
Normal file
|
|
@ -0,0 +1,737 @@
|
||||||
|
/// Implements the various `Style` types used for computing Flexbox layouts,
|
||||||
|
/// along with appearance-based styles (`Color`s, etc).
|
||||||
|
|
||||||
|
#[cfg(feature="tokenize")]
|
||||||
|
use proc_macro2::{TokenStream, Ident, Span};
|
||||||
|
|
||||||
|
#[cfg(feature="tokenize")]
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
|
||||||
|
pub use crate::geometry::{Rect, Size};
|
||||||
|
pub use crate::number::Number;
|
||||||
|
pub use crate::color::Color;
|
||||||
|
pub use crate::stretch::result::Layout;
|
||||||
|
|
||||||
|
/// Describes how items should be aligned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum AlignItems {
|
||||||
|
FlexStart,
|
||||||
|
FlexEnd,
|
||||||
|
Center,
|
||||||
|
Baseline,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AlignItems {
|
||||||
|
fn default() -> AlignItems {
|
||||||
|
AlignItems::Stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how this item should be aligned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum AlignSelf {
|
||||||
|
Auto,
|
||||||
|
FlexStart,
|
||||||
|
FlexEnd,
|
||||||
|
Center,
|
||||||
|
Baseline,
|
||||||
|
Stretch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AlignSelf {
|
||||||
|
fn default() -> AlignSelf {
|
||||||
|
AlignSelf::Auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how content should be aligned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum AlignContent {
|
||||||
|
FlexStart,
|
||||||
|
FlexEnd,
|
||||||
|
Center,
|
||||||
|
Stretch,
|
||||||
|
SpaceBetween,
|
||||||
|
SpaceAround,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AlignContent {
|
||||||
|
fn default() -> AlignContent {
|
||||||
|
AlignContent::Stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how things should flow - particularly important for start/end positions.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Direction {
|
||||||
|
Inherit,
|
||||||
|
LTR,
|
||||||
|
RTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Direction {
|
||||||
|
fn default() -> Direction {
|
||||||
|
Direction::Inherit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes whether an item is visible or not.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Display {
|
||||||
|
Flex,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Display {
|
||||||
|
fn default() -> Display {
|
||||||
|
Display::Flex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how items should be aligned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FlexDirection {
|
||||||
|
Row,
|
||||||
|
Column,
|
||||||
|
RowReverse,
|
||||||
|
ColumnReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlexDirection {
|
||||||
|
fn default() -> FlexDirection {
|
||||||
|
FlexDirection::Row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlexDirection {
|
||||||
|
/// Checks if this is a row.
|
||||||
|
pub(crate) fn is_row(self) -> bool {
|
||||||
|
self == FlexDirection::Row || self == FlexDirection::RowReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this is a column.
|
||||||
|
pub(crate) fn is_column(self) -> bool {
|
||||||
|
self == FlexDirection::Column || self == FlexDirection::ColumnReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this is a reversed direction.
|
||||||
|
pub(crate) fn is_reverse(self) -> bool {
|
||||||
|
self == FlexDirection::RowReverse || self == FlexDirection::ColumnReverse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how content should be justified.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum JustifyContent {
|
||||||
|
FlexStart,
|
||||||
|
FlexEnd,
|
||||||
|
Center,
|
||||||
|
SpaceBetween,
|
||||||
|
SpaceAround,
|
||||||
|
SpaceEvenly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for JustifyContent {
|
||||||
|
fn default() -> JustifyContent {
|
||||||
|
JustifyContent::FlexStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how content should overflow.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Overflow {
|
||||||
|
Visible,
|
||||||
|
Hidden,
|
||||||
|
Scroll,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Overflow {
|
||||||
|
fn default() -> Overflow {
|
||||||
|
Overflow::Visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how content should be positioned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum PositionType {
|
||||||
|
Relative,
|
||||||
|
Absolute,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PositionType {
|
||||||
|
fn default() -> PositionType {
|
||||||
|
PositionType::Relative
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how content should wrap.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FlexWrap {
|
||||||
|
NoWrap,
|
||||||
|
Wrap,
|
||||||
|
WrapReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlexWrap {
|
||||||
|
fn default() -> FlexWrap {
|
||||||
|
FlexWrap::NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a Dimension; automatic, undefined, or a value.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Dimension {
|
||||||
|
Undefined,
|
||||||
|
Auto,
|
||||||
|
Points(f32),
|
||||||
|
Percent(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Dimension {
|
||||||
|
fn default() -> Dimension {
|
||||||
|
Dimension::Undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dimension {
|
||||||
|
/// Internal method for Stretch.
|
||||||
|
pub(crate) fn resolve(self, parent_width: Number) -> Number {
|
||||||
|
match self {
|
||||||
|
Dimension::Points(points) => Number::Defined(points),
|
||||||
|
Dimension::Percent(percent) => parent_width * percent,
|
||||||
|
_ => Number::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this Dimension is defined by a value or not.
|
||||||
|
pub(crate) fn is_defined(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Dimension::Points(_) => true,
|
||||||
|
Dimension::Percent(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Rect<Dimension> {
|
||||||
|
fn default() -> Rect<Dimension> {
|
||||||
|
Rect { start: Default::default(), end: Default::default(), top: Default::default(), bottom: Default::default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Size<Dimension> {
|
||||||
|
fn default() -> Size<Dimension> {
|
||||||
|
Size { width: Dimension::Auto, height: Dimension::Auto }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the backface-visibility for a view. This may be removed in a later release.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum BackfaceVisibility {
|
||||||
|
Visible,
|
||||||
|
Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BackfaceVisibility {
|
||||||
|
fn default() -> BackfaceVisibility {
|
||||||
|
BackfaceVisibility::Visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a font style.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FontStyle {
|
||||||
|
Normal,
|
||||||
|
Italic,
|
||||||
|
Oblique
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FontStyle {
|
||||||
|
fn default() -> FontStyle {
|
||||||
|
FontStyle::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a font weight.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FontWeight {
|
||||||
|
Normal,
|
||||||
|
Bold
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FontWeight {
|
||||||
|
fn default() -> FontWeight {
|
||||||
|
FontWeight::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how text should be aligned.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum TextAlignment {
|
||||||
|
Auto,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
Justify
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextAlignment {
|
||||||
|
fn default() -> TextAlignment {
|
||||||
|
TextAlignment::Auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a border style.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum BorderStyle {
|
||||||
|
None, // The CSS value is None, but it's a reserved term in Rust ;P
|
||||||
|
Hidden,
|
||||||
|
Solid
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BorderStyle {
|
||||||
|
fn default() -> BorderStyle {
|
||||||
|
BorderStyle::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how a Font Family
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FontFamily {
|
||||||
|
SansSerif // @TODO This is tricky because of &str/String/Copy. Revisit later.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FontFamily {
|
||||||
|
fn default() -> Self {
|
||||||
|
FontFamily::SansSerif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Style` is passed into the Stretch Flexbox rendering system to produce a computed
|
||||||
|
/// `Layout`. This is also passed to native nodes, to transform into per-platform style
|
||||||
|
/// commands.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct Style {
|
||||||
|
pub display: Display,
|
||||||
|
pub position_type: PositionType,
|
||||||
|
pub direction: Direction,
|
||||||
|
pub flex_direction: FlexDirection,
|
||||||
|
pub flex_wrap: FlexWrap,
|
||||||
|
pub overflow: Overflow,
|
||||||
|
pub align_items: AlignItems,
|
||||||
|
pub align_self: AlignSelf,
|
||||||
|
pub align_content: AlignContent,
|
||||||
|
pub justify_content: JustifyContent,
|
||||||
|
pub position: Rect<Dimension>,
|
||||||
|
pub margin: Rect<Dimension>,
|
||||||
|
pub padding: Rect<Dimension>,
|
||||||
|
pub border: Rect<Dimension>,
|
||||||
|
pub flex_grow: f32,
|
||||||
|
pub flex_shrink: f32,
|
||||||
|
pub flex_basis: Dimension,
|
||||||
|
pub size: Size<Dimension>,
|
||||||
|
pub min_size: Size<Dimension>,
|
||||||
|
pub max_size: Size<Dimension>,
|
||||||
|
pub aspect_ratio: Number,
|
||||||
|
|
||||||
|
// Appearance-based styles
|
||||||
|
pub background_color: Color
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Style {
|
||||||
|
Style {
|
||||||
|
display: Default::default(),
|
||||||
|
position_type: Default::default(),
|
||||||
|
direction: Default::default(),
|
||||||
|
flex_direction: Default::default(),
|
||||||
|
flex_wrap: Default::default(),
|
||||||
|
overflow: Default::default(),
|
||||||
|
align_items: Default::default(),
|
||||||
|
align_self: Default::default(),
|
||||||
|
align_content: Default::default(),
|
||||||
|
justify_content: Default::default(),
|
||||||
|
position: Default::default(),
|
||||||
|
margin: Default::default(),
|
||||||
|
padding: Default::default(),
|
||||||
|
border: Default::default(),
|
||||||
|
flex_grow: 0.0,
|
||||||
|
flex_shrink: 1.0,
|
||||||
|
flex_basis: Dimension::Auto,
|
||||||
|
size: Default::default(),
|
||||||
|
min_size: Default::default(),
|
||||||
|
max_size: Default::default(),
|
||||||
|
aspect_ratio: Default::default(),
|
||||||
|
background_color: Color::transparent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Determines the minimum main size, given flex direction.
|
||||||
|
pub(crate) fn min_main_size(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.min_size.width,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.min_size.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the maximum main size, given flex direction.
|
||||||
|
pub(crate) fn max_main_size(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.max_size.width,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.max_size.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the main margin start, given flex direction.
|
||||||
|
pub(crate) fn main_margin_start(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.margin.start,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.margin.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the main margin end, given flex direction.
|
||||||
|
pub(crate) fn main_margin_end(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.margin.end,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.margin.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the cross size, given flex direction.
|
||||||
|
pub(crate) fn cross_size(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.size.height,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.size.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the minimum cross size, given flex direction.
|
||||||
|
pub(crate) fn min_cross_size(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.min_size.height,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.min_size.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the maximum cross size, given flex direction.
|
||||||
|
pub(crate) fn max_cross_size(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.max_size.height,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.max_size.width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the cross margin start, given flex direction.
|
||||||
|
pub(crate) fn cross_margin_start(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.margin.top,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.margin.start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the cross margin end, given flex direction.
|
||||||
|
pub(crate) fn cross_margin_end(&self, direction: FlexDirection) -> Dimension {
|
||||||
|
match direction {
|
||||||
|
FlexDirection::Row | FlexDirection::RowReverse => self.margin.bottom,
|
||||||
|
FlexDirection::Column | FlexDirection::ColumnReverse => self.margin.end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the inherited align_self style, given a parent `&Style`.
|
||||||
|
pub(crate) fn align_self(&self, parent: &Style) -> AlignSelf {
|
||||||
|
if self.align_self == AlignSelf::Auto {
|
||||||
|
match parent.align_items {
|
||||||
|
AlignItems::FlexStart => AlignSelf::FlexStart,
|
||||||
|
AlignItems::FlexEnd => AlignSelf::FlexEnd,
|
||||||
|
AlignItems::Center => AlignSelf::Center,
|
||||||
|
AlignItems::Baseline => AlignSelf::Baseline,
|
||||||
|
AlignItems::Stretch => AlignSelf::Stretch,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.align_self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// These exist purely for use in the parser code.
|
||||||
|
///
|
||||||
|
/// A `Style` is what's used for a node; `Styles` are what's parsed and stored.
|
||||||
|
/// At render-time, the rendering engine takes n styles and reduces them down into 1 `Style`
|
||||||
|
/// that's applied to the node in question.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Styles {
|
||||||
|
AlignContent(AlignContent),
|
||||||
|
AlignItems(AlignItems),
|
||||||
|
AlignSelf(AlignSelf),
|
||||||
|
AspectRatio(Number),
|
||||||
|
BackfaceVisibility(BackfaceVisibility),
|
||||||
|
BackgroundColor(Color),
|
||||||
|
|
||||||
|
BorderColor(Color),
|
||||||
|
BorderEndColor(Color),
|
||||||
|
BorderBottomColor(Color),
|
||||||
|
BorderLeftColor(Color),
|
||||||
|
BorderRightColor(Color),
|
||||||
|
BorderTopColor(Color),
|
||||||
|
BorderStartColor(Color),
|
||||||
|
|
||||||
|
BorderStyle(BorderStyle),
|
||||||
|
BorderEndStyle(BorderStyle),
|
||||||
|
BorderBottomStyle(BorderStyle),
|
||||||
|
BorderLeftStyle(BorderStyle),
|
||||||
|
BorderRightStyle(BorderStyle),
|
||||||
|
BorderTopStyle(BorderStyle),
|
||||||
|
BorderStartStyle(BorderStyle),
|
||||||
|
|
||||||
|
BorderWidth(f32),
|
||||||
|
BorderEndWidth(f32),
|
||||||
|
BorderBottomWidth(f32),
|
||||||
|
BorderLeftWidth(f32),
|
||||||
|
BorderRightWidth(f32),
|
||||||
|
BorderTopWidth(f32),
|
||||||
|
BorderStartWidth(f32),
|
||||||
|
|
||||||
|
BorderRadius(f32),
|
||||||
|
BorderBottomEndRadius(f32),
|
||||||
|
BorderBottomLeftRadius(f32),
|
||||||
|
BorderBottomRightRadius(f32),
|
||||||
|
BorderBottomStartRadius(f32),
|
||||||
|
BorderTopLeftRadius(f32),
|
||||||
|
BorderTopRightRadius(f32),
|
||||||
|
BorderTopEndRadius(f32),
|
||||||
|
BorderTopStartRadius(f32),
|
||||||
|
|
||||||
|
Bottom(f32),
|
||||||
|
Direction(Direction),
|
||||||
|
Display(Display),
|
||||||
|
End(f32),
|
||||||
|
FlexBasis(f32),
|
||||||
|
FlexDirection(FlexDirection),
|
||||||
|
FlexGrow(f32),
|
||||||
|
FlexShrink(f32),
|
||||||
|
FlexWrap(FlexWrap),
|
||||||
|
FontFamily(FontFamily),
|
||||||
|
FontLineHeight(f32),
|
||||||
|
FontSize(f32),
|
||||||
|
FontStyle(FontStyle),
|
||||||
|
FontWeight(FontWeight),
|
||||||
|
Height(f32),
|
||||||
|
JustifyContent(JustifyContent),
|
||||||
|
Left(f32),
|
||||||
|
MarginBottom(f32),
|
||||||
|
MarginEnd(f32),
|
||||||
|
MarginLeft(f32),
|
||||||
|
MarginRight(f32),
|
||||||
|
MarginStart(f32),
|
||||||
|
MarginTop(f32),
|
||||||
|
MaxHeight(f32),
|
||||||
|
MaxWidth(f32),
|
||||||
|
MinHeight(f32),
|
||||||
|
MinWidth(f32),
|
||||||
|
Opacity(f32),
|
||||||
|
Overflow(Overflow),
|
||||||
|
PaddingBottom(f32),
|
||||||
|
PaddingEnd(f32),
|
||||||
|
PaddingLeft(f32),
|
||||||
|
PaddingRight(f32),
|
||||||
|
PaddingStart(f32),
|
||||||
|
PaddingTop(f32),
|
||||||
|
PositionType(PositionType),
|
||||||
|
Right(f32),
|
||||||
|
Start(f32),
|
||||||
|
TextAlignment(TextAlignment),
|
||||||
|
TextColor(Color),
|
||||||
|
TextDecorationColor(Color),
|
||||||
|
TextShadowColor(Color),
|
||||||
|
TintColor(Color),
|
||||||
|
Top(f32),
|
||||||
|
Width(f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A method for tokenizing a `Color` for a given attribute (e.g, `BackgroundColor`).
|
||||||
|
#[cfg(feature="tokenize")]
|
||||||
|
fn color_tokens(tokens: &mut TokenStream, color: &Color, style: &str) {
|
||||||
|
let red = color.red;
|
||||||
|
let green = color.green;
|
||||||
|
let blue = color.blue;
|
||||||
|
let alpha = color.alpha;
|
||||||
|
let s = Ident::new(style, Span::call_site());
|
||||||
|
|
||||||
|
tokens.extend(quote!(Styles::#s(Color {
|
||||||
|
red: #red,
|
||||||
|
green: #green,
|
||||||
|
blue: #blue,
|
||||||
|
alpha: #alpha
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts `Styles` into tokenized `Styles` representations, for use in the `styles! {}` macro.
|
||||||
|
#[cfg(feature="tokenize")]
|
||||||
|
impl ToTokens for Styles {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) { match self {
|
||||||
|
Styles::AlignContent(align_content) => { match align_content {
|
||||||
|
AlignContent::FlexStart => tokens.extend(quote!(Styles::AlignContent(AlignContent::FlexStart))),
|
||||||
|
AlignContent::FlexEnd => tokens.extend(quote!(Styles::AlignContent(AlignContent::FlexEnd))),
|
||||||
|
AlignContent::Center => tokens.extend(quote!(Styles::AlignContent(AlignContent::Center))),
|
||||||
|
AlignContent::Stretch => tokens.extend(quote!(Styles::AlignContent(AlignContent::Stretch))),
|
||||||
|
AlignContent::SpaceAround => tokens.extend(quote!(Styles::AlignContent(AlignContent::SpaceAround))),
|
||||||
|
AlignContent::SpaceBetween => tokens.extend(quote!(Styles::AlignContent(AlignContent::SpaceBetween)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::AlignItems(align_items) => { match align_items {
|
||||||
|
AlignItems::FlexStart => tokens.extend(quote!(Styles::AlignItems(AlignItems::FlexStart))),
|
||||||
|
AlignItems::FlexEnd => tokens.extend(quote!(Styles::AlignItems(AlignItems::FlexEnd))),
|
||||||
|
AlignItems::Center => tokens.extend(quote!(Styles::AlignItems(AlignItems::Center))),
|
||||||
|
AlignItems::Baseline => tokens.extend(quote!(Styles::AlignItems(AlignItems::Baseline))),
|
||||||
|
AlignItems::Stretch => tokens.extend(quote!(Styles::AlignItems(AlignItems::Stretch)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::AlignSelf(align_self) => { match align_self {
|
||||||
|
AlignSelf::Auto => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::Auto))),
|
||||||
|
AlignSelf::FlexStart => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::FlexStart))),
|
||||||
|
AlignSelf::FlexEnd => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::FlexEnd))),
|
||||||
|
AlignSelf::Center => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::Center))),
|
||||||
|
AlignSelf::Baseline => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::Baseline))),
|
||||||
|
AlignSelf::Stretch => tokens.extend(quote!(Styles::AlignSelf(AlignSelf::Stretch)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::AspectRatio(_) => {},
|
||||||
|
|
||||||
|
Styles::BackfaceVisibility(visibility) => { match visibility {
|
||||||
|
BackfaceVisibility::Visible => tokens.extend(quote!(Styles::BackfaceVisibility(BackfaceVisibility::Visible))),
|
||||||
|
BackfaceVisibility::Hidden => tokens.extend(quote!(Styles::BackfaceVisibility(BackfaceVisibility::Hidden)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::BackgroundColor(color) => color_tokens(tokens, color, "BackgroundColor"),
|
||||||
|
Styles::BorderColor(color) => color_tokens(tokens, color, "BorderColor"),
|
||||||
|
Styles::BorderEndColor(color) => color_tokens(tokens, color, "BorderEndColor"),
|
||||||
|
Styles::BorderBottomColor(color) => color_tokens(tokens, color, "BorderBottomColor"),
|
||||||
|
Styles::BorderLeftColor(color) => color_tokens(tokens, color, "BorderLeftColor"),
|
||||||
|
Styles::BorderRightColor(color) => color_tokens(tokens, color, "BorderRightColor"),
|
||||||
|
Styles::BorderTopColor(color) => color_tokens(tokens, color, "BorderTopColor"),
|
||||||
|
Styles::BorderStartColor(color) => color_tokens(tokens, color, "BorderStartColor"),
|
||||||
|
Styles::BorderStyle(_) => {},
|
||||||
|
Styles::BorderEndStyle(_) => {},
|
||||||
|
Styles::BorderBottomStyle(_) => {},
|
||||||
|
Styles::BorderLeftStyle(_) => {},
|
||||||
|
Styles::BorderRightStyle(_) => {},
|
||||||
|
Styles::BorderTopStyle(_) => {},
|
||||||
|
Styles::BorderStartStyle(_) => {},
|
||||||
|
Styles::BorderWidth(border_width) => tokens.extend(quote!(Styles::BorderWidth(#border_width))),
|
||||||
|
Styles::BorderEndWidth(border_end_width) => tokens.extend(quote!(Styles::BorderEndWidth(#border_end_width))),
|
||||||
|
Styles::BorderBottomWidth(border_bottom_width) => tokens.extend(quote!(Styles::BorderBottomWidth(#border_bottom_width))),
|
||||||
|
Styles::BorderLeftWidth(border_left_width) => tokens.extend(quote!(Styles::BorderLeftWidth(#border_left_width))),
|
||||||
|
Styles::BorderRightWidth(border_right_width) => tokens.extend(quote!(Styles::BorderRightWidth(#border_right_width))),
|
||||||
|
Styles::BorderTopWidth(border_top_width) => tokens.extend(quote!(Styles::BorderTopWidth(#border_top_width))),
|
||||||
|
Styles::BorderStartWidth(border_start_width) => tokens.extend(quote!(Styles::BorderStartWidth(#border_start_width))),
|
||||||
|
Styles::BorderRadius(border_radius) => tokens.extend(quote!(Styles::BorderRadius(#border_radius))),
|
||||||
|
Styles::BorderBottomEndRadius(border_bottom_end_radius) => tokens.extend(quote!(Styles::BorderBottomEndRadius(#border_bottom_end_radius))),
|
||||||
|
Styles::BorderBottomLeftRadius(border_bottom_left_radius) => tokens.extend(quote!(Styles::BorderBottomLeftRadius(#border_bottom_left_radius))),
|
||||||
|
Styles::BorderBottomRightRadius(border_bottom_right_radius) => tokens.extend(quote!(Styles::BorderBottomRightRadius(#border_bottom_right_radius))),
|
||||||
|
Styles::BorderBottomStartRadius(border_bottom_start_radius) => tokens.extend(quote!(Styles::BorderBottomStartRadius(#border_bottom_start_radius))),
|
||||||
|
Styles::BorderTopLeftRadius(border_top_left_radius) => tokens.extend(quote!(Styles::BorderTopLeftRadius(#border_top_left_radius))),
|
||||||
|
Styles::BorderTopRightRadius(border_top_right_radius) => tokens.extend(quote!(Styles::BorderTopRightRadius(#border_top_right_radius))),
|
||||||
|
Styles::BorderTopEndRadius(border_top_end_radius) => tokens.extend(quote!(Styles::BorderTopEndRadius(#border_top_end_radius))),
|
||||||
|
Styles::BorderTopStartRadius(border_top_start_radius) => tokens.extend(quote!(Styles::BorderTopStartRadius(#border_top_start_radius))),
|
||||||
|
Styles::Bottom(bottom) => tokens.extend(quote!(Styles::Bottom(#bottom))),
|
||||||
|
|
||||||
|
Styles::Direction(direction) => { match direction {
|
||||||
|
Direction::Inherit => tokens.extend(quote!(Styles::Direction(Direction::Inherit))),
|
||||||
|
Direction::LTR => tokens.extend(quote!(Styles::Direction(Direction::LTR))),
|
||||||
|
Direction::RTL => tokens.extend(quote!(Styles::Direction(Direction::RTL)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::Display(display) => { match display {
|
||||||
|
Display::Flex => tokens.extend(quote!(Styles::Display(Display::Flex))),
|
||||||
|
Display::None => tokens.extend(quote!(Styles::Display(Display::None)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::End(end) => tokens.extend(quote!(Styles::End(#end))),
|
||||||
|
Styles::FlexBasis(flex_basis) => tokens.extend(quote!(Styles::FlexBasis(#flex_basis))),
|
||||||
|
|
||||||
|
Styles::FlexDirection(direction) => { match direction {
|
||||||
|
FlexDirection::Row => tokens.extend(quote!(Styles::FlexDirection(FlexDirection::Row))),
|
||||||
|
FlexDirection::Column => tokens.extend(quote!(Styles::FlexDirection(FlexDirection::Column))),
|
||||||
|
FlexDirection::RowReverse => tokens.extend(quote!(Styles::FlexDirection(FlexDirection::RowReverse))),
|
||||||
|
FlexDirection::ColumnReverse => tokens.extend(quote!(Styles::FlexDirection(FlexDirection::ColumnReverse)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::FlexGrow(flex_grow) => tokens.extend(quote!(Styles::FlexGrow(#flex_grow))),
|
||||||
|
Styles::FlexShrink(flex_shrink) => tokens.extend(quote!(Styles::FlexShrink(#flex_shrink))),
|
||||||
|
|
||||||
|
Styles::FlexWrap(wrap) => { match wrap {
|
||||||
|
FlexWrap::NoWrap => tokens.extend(quote!(Styles::FlexWrap(FlexWrap::NoWrap))),
|
||||||
|
FlexWrap::Wrap => tokens.extend(quote!(Styles::FlexWrap(FlexWrap::Wrap))),
|
||||||
|
FlexWrap::WrapReverse => tokens.extend(quote!(Styles::FlexWrap(FlexWrap::WrapReverse)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::FontFamily(_family) => {},
|
||||||
|
Styles::FontLineHeight(line_height) => tokens.extend(quote!(Styles::LineHeight(#line_height))),
|
||||||
|
Styles::FontSize(font_size) => tokens.extend(quote!(Styles::FontSize(#font_size))),
|
||||||
|
Styles::FontStyle(_style) => {},
|
||||||
|
Styles::FontWeight(_weight) => {},
|
||||||
|
Styles::Height(height) => tokens.extend(quote!(Styles::Height(#height))),
|
||||||
|
|
||||||
|
Styles::JustifyContent(justify) => { match justify {
|
||||||
|
JustifyContent::FlexStart => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::FlexStart))),
|
||||||
|
JustifyContent::FlexEnd => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::FlexEnd))),
|
||||||
|
JustifyContent::Center => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::Center))),
|
||||||
|
JustifyContent::SpaceBetween => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::SpaceBetween))),
|
||||||
|
JustifyContent::SpaceAround => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::SpaceAround))),
|
||||||
|
JustifyContent::SpaceEvenly => tokens.extend(quote!(Styles::JustifyContent(JustifyContent::SpaceEvenly)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::Left(left) => tokens.extend(quote!(Styles::Left(#left))),
|
||||||
|
Styles::MarginBottom(margin_bottom) => tokens.extend(quote!(Styles::MarginBottom(#margin_bottom))),
|
||||||
|
Styles::MarginEnd(margin_end) => tokens.extend(quote!(Styles::MarginEnd(#margin_end))),
|
||||||
|
Styles::MarginLeft(margin_left) => tokens.extend(quote!(Styles::MarginLeft(#margin_left))),
|
||||||
|
Styles::MarginRight(margin_right) => tokens.extend(quote!(Styles::MarginRight(#margin_right))),
|
||||||
|
Styles::MarginStart(margin_start) => tokens.extend(quote!(Styles::MarginStart(#margin_start))),
|
||||||
|
Styles::MarginTop(top) => tokens.extend(quote!(Styles::Top(#top))),
|
||||||
|
Styles::MaxHeight(max_height) => tokens.extend(quote!(Styles::MaxHeight(#max_height))),
|
||||||
|
Styles::MaxWidth(max_width) => tokens.extend(quote!(Styles::MaxWidth(#max_width))),
|
||||||
|
Styles::MinHeight(min_height) => tokens.extend(quote!(Styles::MinHeight(#min_height))),
|
||||||
|
Styles::MinWidth(min_width) => tokens.extend(quote!(Styles::MinWidth(#min_width))),
|
||||||
|
Styles::Opacity(opacity) => tokens.extend(quote!(Styles::Opacity(#opacity))),
|
||||||
|
|
||||||
|
Styles::Overflow(overflow) => { match overflow {
|
||||||
|
Overflow::Visible => tokens.extend(quote!(Styles::Overflow(Overflow::Visible))),
|
||||||
|
Overflow::Hidden => tokens.extend(quote!(Styles::Overflow(Overflow::Hidden))),
|
||||||
|
Overflow::Scroll => tokens.extend(quote!(Styles::Overflow(Overflow::Scroll)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::PaddingBottom(padding_bottom) => tokens.extend(quote!(Styles::PaddingBottom(#padding_bottom))),
|
||||||
|
Styles::PaddingEnd(padding_end) => tokens.extend(quote!(Styles::PaddingEnd(#padding_end))),
|
||||||
|
Styles::PaddingLeft(padding_left) => tokens.extend(quote!(Styles::PaddingLeft(#padding_left))),
|
||||||
|
Styles::PaddingRight(padding_right) => tokens.extend(quote!(Styles::PaddingRight(#padding_right))),
|
||||||
|
Styles::PaddingStart(padding_start) => tokens.extend(quote!(Styles::PaddingStart(#padding_start))),
|
||||||
|
Styles::PaddingTop(padding_top) => tokens.extend(quote!(Styles::PaddingTop(#padding_top))),
|
||||||
|
|
||||||
|
Styles::PositionType(position_type) => { match position_type {
|
||||||
|
PositionType::Relative => tokens.extend(quote!(Styles::PositionType(PositionType::Relative))),
|
||||||
|
PositionType::Absolute => tokens.extend(quote!(Styles::PositionType(PositionType::Absolute)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::Right(right) => tokens.extend(quote!(Styles::Right(#right))),
|
||||||
|
Styles::Start(start) => tokens.extend(quote!(Styles::Start(#start))),
|
||||||
|
|
||||||
|
Styles::TextAlignment(alignment) => { match alignment {
|
||||||
|
TextAlignment::Auto => tokens.extend(quote!(Styles::TextAlignment(TextAlignment::Auto))),
|
||||||
|
TextAlignment::Left => tokens.extend(quote!(Styles::TextAlignment(TextAlignment::Left))),
|
||||||
|
TextAlignment::Right => tokens.extend(quote!(Styles::TextAlignment(TextAlignment::Right))),
|
||||||
|
TextAlignment::Center => tokens.extend(quote!(Styles::TextAlignment(TextAlignment::Center))),
|
||||||
|
TextAlignment::Justify => tokens.extend(quote!(Styles::TextAlignment(TextAlignment::Justify)))
|
||||||
|
}},
|
||||||
|
|
||||||
|
Styles::TextColor(color) => color_tokens(tokens, color, "TextColor"),
|
||||||
|
Styles::TextDecorationColor(color) => color_tokens(tokens, color, "TextDecorationColor"),
|
||||||
|
Styles::TextShadowColor(color) => color_tokens(tokens, color, "TextShadowColor"),
|
||||||
|
Styles::TintColor(color) => color_tokens(tokens, color, "TintColor"),
|
||||||
|
Styles::Top(top) => tokens.extend(quote!(Styles::Top(#top))),
|
||||||
|
Styles::Width(width) => tokens.extend(quote!(Styles::Width(#width)))
|
||||||
|
}}
|
||||||
|
}
|
||||||
304
styles/src/styles_parser.rs
Normal file
304
styles/src/styles_parser.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
//! CSS parsing logic. Mostly relies from the rust-cssparser crate,
|
||||||
|
//! slightly modified to fit the `Styles` structure we want internally.
|
||||||
|
|
||||||
|
use cssparser::{
|
||||||
|
AtRuleParser, BasicParseError, CowRcStr,
|
||||||
|
DeclarationListParser, DeclarationParser,
|
||||||
|
Parser, ParseError, QualifiedRuleParser,
|
||||||
|
SourceLocation, Token
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::styles::*;
|
||||||
|
|
||||||
|
/// Represents a style rule, a `key: [values...];` pair.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Rule {
|
||||||
|
pub key: String,
|
||||||
|
pub styles: Vec<Styles>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parser itself.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RuleParser;
|
||||||
|
|
||||||
|
/// Some type information for our parser.
|
||||||
|
impl<'i> AtRuleParser<'i> for RuleParser {
|
||||||
|
type PreludeBlock = ();
|
||||||
|
type PreludeNoBlock = ();
|
||||||
|
type AtRule = Rule;
|
||||||
|
type Error = BasicParseError<'i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The actual work our parser does. Walks style rules and attempts to
|
||||||
|
/// extract the key/value pairings from a given stylesheet string.
|
||||||
|
impl<'i> QualifiedRuleParser<'i> for RuleParser {
|
||||||
|
type Prelude = String;
|
||||||
|
type QualifiedRule = Rule;
|
||||||
|
type Error = BasicParseError<'i>;
|
||||||
|
|
||||||
|
/// Parses out the selector.
|
||||||
|
fn parse_prelude<'t>(
|
||||||
|
&mut self,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
|
||||||
|
let location = input.current_source_location();
|
||||||
|
|
||||||
|
let selector = match input.next()? {
|
||||||
|
Token::Ident(ref element_name) => element_name.to_string(),
|
||||||
|
t => { return Err(location.new_unexpected_token_error(t.clone())); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there's a next, someone is writing their code assuming cascading. Let's... warn them.
|
||||||
|
/*match input.next()? {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {}
|
||||||
|
};*/
|
||||||
|
|
||||||
|
Ok(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the block (`{...}`) into a Rule struct.
|
||||||
|
fn parse_block<'t>(
|
||||||
|
&mut self,
|
||||||
|
key: Self::Prelude,
|
||||||
|
_location: SourceLocation,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
|
||||||
|
let styles = DeclarationListParser::new(input, StyleParser {}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Rule {
|
||||||
|
key: key,
|
||||||
|
styles: styles.into_iter().filter_map(|decl| {
|
||||||
|
if !decl.is_ok() {
|
||||||
|
eprintln!("{:?}", decl);
|
||||||
|
}
|
||||||
|
|
||||||
|
decl.ok()
|
||||||
|
}).collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains logic for matching CSS attributes to their `Styles` counterpart.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StyleParser;
|
||||||
|
|
||||||
|
/// Types, etc.
|
||||||
|
impl<'i> AtRuleParser<'i> for StyleParser {
|
||||||
|
type PreludeBlock = ();
|
||||||
|
type PreludeNoBlock = ();
|
||||||
|
type AtRule = Styles;
|
||||||
|
type Error = BasicParseError<'i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A utility method for dereferencing a value, to make some code later on a bit more clean.
|
||||||
|
fn ident<'a>(token: &'a Token) -> &'a str {
|
||||||
|
match token {
|
||||||
|
Token::Ident(ref value) => &*value,
|
||||||
|
_ => ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'i> DeclarationParser<'i> for StyleParser {
|
||||||
|
type Declaration = Styles;
|
||||||
|
type Error = BasicParseError<'i>;
|
||||||
|
|
||||||
|
/// Parses a value (e.g, `background-color: #307ace;`) into a `Styles` value.
|
||||||
|
fn parse_value<'t>(
|
||||||
|
&mut self,
|
||||||
|
name: CowRcStr<'i>,
|
||||||
|
input: &mut Parser<'i, 't>,
|
||||||
|
) -> Result<Self::Declaration, ParseError<'i, Self::Error>> {
|
||||||
|
let style = match &*name {
|
||||||
|
"align-content" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"flex-start" => Styles::AlignContent(AlignContent::FlexStart),
|
||||||
|
"flex-end" => Styles::AlignContent(AlignContent::FlexEnd),
|
||||||
|
"center" => Styles::AlignContent(AlignContent::Center),
|
||||||
|
"stretch" => Styles::AlignContent(AlignContent::Stretch),
|
||||||
|
"space-between" => Styles::AlignContent(AlignContent::SpaceBetween),
|
||||||
|
"space-around" => Styles::AlignContent(AlignContent::SpaceAround),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"align-items" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"flex-start" => Styles::AlignItems(AlignItems::FlexStart),
|
||||||
|
"flex-end" => Styles::AlignItems(AlignItems::FlexEnd),
|
||||||
|
"center" => Styles::AlignItems(AlignItems::Center),
|
||||||
|
"baseline" => Styles::AlignItems(AlignItems::Baseline),
|
||||||
|
"stretch" => Styles::AlignItems(AlignItems::Stretch),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"align_self" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"auto" => Styles::AlignSelf(AlignSelf::Auto),
|
||||||
|
"flex-start" => Styles::AlignSelf(AlignSelf::FlexStart),
|
||||||
|
"flex-end" => Styles::AlignSelf(AlignSelf::FlexEnd),
|
||||||
|
"center" => Styles::AlignSelf(AlignSelf::Center),
|
||||||
|
"baseline" => Styles::AlignSelf(AlignSelf::Baseline),
|
||||||
|
"stretch" => Styles::AlignSelf(AlignSelf::Stretch),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
// @TODO: Aspect Ratio... could be string, no? Should this be handled better?
|
||||||
|
"aspect-ratio" => Styles::AspectRatio(Number::Defined(parse_floaty_mcfloatface_value(input)?)),
|
||||||
|
|
||||||
|
"backface-visibility" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"visible" => Styles::BackfaceVisibility(BackfaceVisibility::Visible),
|
||||||
|
"hidden" => Styles::BackfaceVisibility(BackfaceVisibility::Hidden),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"background-color" => Styles::BackgroundColor(Color::parse(input)?),
|
||||||
|
|
||||||
|
// Border values~
|
||||||
|
"border-color" => Styles::BorderColor(Color::parse(input)?),
|
||||||
|
"border-top-color" => Styles::BorderTopColor(Color::parse(input)?),
|
||||||
|
"border-bottom-color" => Styles::BorderBottomColor(Color::parse(input)?),
|
||||||
|
"border-left-color" => Styles::BorderLeftColor(Color::parse(input)?),
|
||||||
|
"border-right-color" => Styles::BorderRightColor(Color::parse(input)?),
|
||||||
|
|
||||||
|
"bottom" => Styles::Bottom(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"color" => Styles::TextColor(Color::parse(input)?),
|
||||||
|
|
||||||
|
"direction" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"inherit" => Styles::Direction(Direction::Inherit),
|
||||||
|
"ltr" => Styles::Direction(Direction::LTR),
|
||||||
|
"rtl" => Styles::Direction(Direction::RTL),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"display" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"flex" => Styles::Display(Display::Flex),
|
||||||
|
"none" => Styles::Display(Display::None),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"end" => Styles::End(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"flex-basis" => Styles::FlexBasis(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"flex-direction" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"row" => Styles::FlexDirection(FlexDirection::Row),
|
||||||
|
"row-reverse" => Styles::FlexDirection(FlexDirection::RowReverse),
|
||||||
|
"column" => Styles::FlexDirection(FlexDirection::Column),
|
||||||
|
"column-reverse" => Styles::FlexDirection(FlexDirection::ColumnReverse),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"flex-grow" => Styles::FlexGrow(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"flex-shrink" => Styles::FlexShrink(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"flex-wrap" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"no-wrap" => Styles::FlexWrap(FlexWrap::NoWrap),
|
||||||
|
"wrap" => Styles::FlexWrap(FlexWrap::Wrap),
|
||||||
|
"wrap-reverse" => Styles::FlexWrap(FlexWrap::WrapReverse),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
//FontFamily(FontFamily),
|
||||||
|
"font-size" => Styles::FontSize(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"font-style" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"normal" => Styles::FontStyle(FontStyle::Normal),
|
||||||
|
"italic" => Styles::FontStyle(FontStyle::Italic),
|
||||||
|
"oblique" => Styles::FontStyle(FontStyle::Oblique),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"font-weight" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"normal" => Styles::FontWeight(FontWeight::Normal),
|
||||||
|
"bold" => Styles::FontWeight(FontWeight::Bold),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"height" => Styles::Height(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"justify-content" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"flex-start" => Styles::JustifyContent(JustifyContent::FlexStart),
|
||||||
|
"flex-end" => Styles::JustifyContent(JustifyContent::FlexEnd),
|
||||||
|
"center" => Styles::JustifyContent(JustifyContent::Center),
|
||||||
|
"space-between" => Styles::JustifyContent(JustifyContent::SpaceBetween),
|
||||||
|
"space-around" => Styles::JustifyContent(JustifyContent::SpaceAround),
|
||||||
|
"space-evenly" => Styles::JustifyContent(JustifyContent::SpaceEvenly),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"left" => Styles::Left(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"line-height" => Styles::FontLineHeight(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"margin-bottom" => Styles::MarginBottom(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"margin-end" => Styles::MarginEnd(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"margin-left" => Styles::MarginLeft(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"margin-right" => Styles::MarginRight(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"margin-start" => Styles::MarginStart(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"margin-top" => Styles::MarginTop(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"max-height" => Styles::MaxHeight(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"max-width" => Styles::MaxWidth(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"min-height" => Styles::MinHeight(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"min-width" => Styles::MinWidth(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"opacity" => Styles::Opacity(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"overflow" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"visible" => Styles::Overflow(Overflow::Visible),
|
||||||
|
"hidden" => Styles::Overflow(Overflow::Hidden),
|
||||||
|
"scroll" => Styles::Overflow(Overflow::Scroll),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"padding-bottom" => Styles::PaddingBottom(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"padding-end" => Styles::PaddingEnd(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"padding-left" => Styles::PaddingLeft(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"padding-right" => Styles::PaddingRight(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"padding-start" => Styles::PaddingStart(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"padding-top" => Styles::PaddingTop(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"position" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"absolute" => Styles::PositionType(PositionType::Absolute),
|
||||||
|
"relative" => Styles::PositionType(PositionType::Relative),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"right" => Styles::Right(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"start" => Styles::Start(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
"text-align" => { let s = input.current_source_location(); let t = input.next()?; match ident(&t) {
|
||||||
|
"auto" => Styles::TextAlignment(TextAlignment::Auto),
|
||||||
|
"left" => Styles::TextAlignment(TextAlignment::Left),
|
||||||
|
"right" => Styles::TextAlignment(TextAlignment::Right),
|
||||||
|
"center" => Styles::TextAlignment(TextAlignment::Center),
|
||||||
|
"justify" => Styles::TextAlignment(TextAlignment::Justify),
|
||||||
|
_ => { return Err(s.new_unexpected_token_error(t.clone())); }
|
||||||
|
}},
|
||||||
|
|
||||||
|
"text-decoration-color" => Styles::TextDecorationColor(Color::parse(input)?),
|
||||||
|
"text-shadow-color" => Styles::TextShadowColor(Color::parse(input)?),
|
||||||
|
"tint-color" => Styles::TintColor(Color::parse(input)?),
|
||||||
|
|
||||||
|
"top" => Styles::Top(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
"width" => Styles::Width(parse_floaty_mcfloatface_value(input)?),
|
||||||
|
|
||||||
|
t => {
|
||||||
|
let location = input.current_source_location();
|
||||||
|
return Err(location.new_unexpected_token_error(Token::Ident(t.to_string().into())));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A utility method for handling some float values.
|
||||||
|
/// Mostly used to reduce code verbosity in the massive switch table for `Styles` parsing.
|
||||||
|
fn parse_floaty_mcfloatface_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<f32, BasicParseError<'i>> {
|
||||||
|
let location = input.current_source_location();
|
||||||
|
let token = input.next()?;
|
||||||
|
|
||||||
|
match token {
|
||||||
|
Token::Number { value, .. } => Ok(*value),
|
||||||
|
_ => Err(location.new_basic_unexpected_token_error(token.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in a new issue