Merge branch 'master' into intigrate-collect-assets
This commit is contained in:
commit
66e2c02bf7
167 files changed
+3725
-856
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace.
|
||||||
|
[env]
|
||||||
|
DIOXUS_FRONT_ISSUER_URL = ""
|
||||||
|
DIOXUS_FRONT_CLIENT_ID = ""
|
||||||
|
DIOXUS_FRONT_URL = ""
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.3
|
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
branch: gh-pages # The branch the action should deploy to.
|
branch: gh-pages # The branch the action should deploy to.
|
||||||
folder: docs/nightly # The folder the action should deploy.
|
folder: docs/nightly # The folder the action should deploy.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: Deploy 🚀
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.3
|
uses: JamesIves/github-pages-deploy-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
branch: gh-pages # The branch the action should deploy to.
|
branch: gh-pages # The branch the action should deploy to.
|
||||||
folder: docs/nightly # The folder the action should deploy.
|
folder: docs/nightly # The folder the action should deploy.
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ilammy/setup-nasm@v1
|
- uses: ilammy/setup-nasm@v1
|
||||||
|
|
||||||
- name: install stable
|
- name: install stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
@@ -146,6 +145,11 @@ jobs:
|
|||||||
workspaces: core -> ../target
|
workspaces: core -> ../target
|
||||||
save-if: ${{ matrix.features.key == 'all' }}
|
save-if: ${{ matrix.features.key == 'all' }}
|
||||||
|
|
||||||
|
- name: Install rustfmt
|
||||||
|
run: rustup component add rustfmt
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
|
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ jobs:
|
|||||||
|
|
||||||
# working-directory: tokio
|
# working-directory: tokio
|
||||||
env:
|
env:
|
||||||
# todo: disable memory leaks ignore
|
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
|
||||||
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
|
|
||||||
PROPTEST_CASES: 10
|
PROPTEST_CASES: 10
|
||||||
|
|
||||||
# Cache the global cargo directory, but NOT the local `target` directory which
|
# Cache the global cargo directory, but NOT the local `target` directory which
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
# Do our best to cache the toolchain and node install steps
|
# Do our best to cache the toolchain and node install steps
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ilammy/setup-nasm@v1
|
- uses: ilammy/setup-nasm@v1
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
|
|||||||
-171
@@ -1,171 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
### Commit Statistics
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
- 1 commit contributed to the release over the course of 7 calendar days.
|
|
||||||
- 0 commits where understood as [conventional](https://www.conventionalcommits.org).
|
|
||||||
- 0 issues like '(#ID)' where seen in commit messages
|
|
||||||
|
|
||||||
### Commit Details
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
<details><summary>view details</summary>
|
|
||||||
|
|
||||||
* **Uncategorized**
|
|
||||||
- Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## v0.1.7 (2022-01-08)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- <csr-id-bd341f5571580cdf5e495379b49ca988fd9211c3/> tests
|
|
||||||
|
|
||||||
### Commit Statistics
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
- 1 commit contributed to the release over the course of 2 calendar days.
|
|
||||||
- 1 commit where understood as [conventional](https://www.conventionalcommits.org).
|
|
||||||
- 0 issues like '(#ID)' where seen in commit messages
|
|
||||||
|
|
||||||
### Commit Details
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
<details><summary>view details</summary>
|
|
||||||
|
|
||||||
* **Uncategorized**
|
|
||||||
- tests ([`bd341f5`](https://github.comgit//DioxusLabs/dioxus/commit/bd341f5571580cdf5e495379b49ca988fd9211c3))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## v0.1.1 (2021-12-15)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
|
|
||||||
- <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
|
|
||||||
- <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
|
|
||||||
- <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
|
|
||||||
- <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
|
|
||||||
- <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
|
|
||||||
- <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
|
|
||||||
- <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
|
|
||||||
- <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
|
|
||||||
- <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
|
|
||||||
- <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
|
|
||||||
- <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
|
|
||||||
- <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
|
|
||||||
|
|
||||||
## v0.1.0 (2021-12-15)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
|
|
||||||
- <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
|
|
||||||
- <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
|
|
||||||
- <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
|
|
||||||
- <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
|
|
||||||
- <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
|
|
||||||
- <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
|
|
||||||
- <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
|
|
||||||
- <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
|
|
||||||
- <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
|
|
||||||
- <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
|
|
||||||
- <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
|
|
||||||
- <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
|
|
||||||
|
|
||||||
## v0.0.1 (2022-01-03)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
|
|
||||||
- <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
|
|
||||||
- <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
|
|
||||||
- <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
|
|
||||||
- <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
|
|
||||||
- <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
|
|
||||||
- <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
|
|
||||||
- <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
|
|
||||||
- <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
|
|
||||||
- <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
|
|
||||||
- <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
|
|
||||||
- <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
|
|
||||||
- <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
|
|
||||||
|
|
||||||
### Commit Statistics
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
- 40 commits contributed to the release over the course of 193 calendar days.
|
|
||||||
- 38 commits where understood as [conventional](https://www.conventionalcommits.org).
|
|
||||||
- 0 issues like '(#ID)' where seen in commit messages
|
|
||||||
|
|
||||||
### Commit Details
|
|
||||||
|
|
||||||
<csr-read-only-do-not-edit/>
|
|
||||||
|
|
||||||
<details><summary>view details</summary>
|
|
||||||
|
|
||||||
* **Uncategorized**
|
|
||||||
- remove runner on hook and then update docs ([`d156045`](https://github.comgit//DioxusLabs/dioxus/commit/d1560450bac55f9566e00e00ea405bd1c70b57e5))
|
|
||||||
- polish some more things ([`1496102`](https://github.comgit//DioxusLabs/dioxus/commit/14961023f927b3a8bde83cfc7883aa8bfcca9e85))
|
|
||||||
- upgrade hooks ([`b3ac2ee`](https://github.comgit//DioxusLabs/dioxus/commit/b3ac2ee3f76549cd1c7b6f9eee7e3382b07d873c))
|
|
||||||
- docs ([`8814977`](https://github.comgit//DioxusLabs/dioxus/commit/8814977eeebe06748a3b9677a8070e42a037ebd7))
|
|
||||||
- prepare to change our fragment pattern. Add some more docs ([`2c3a046`](https://github.comgit//DioxusLabs/dioxus/commit/2c3a0464264fa11e8100df025d863931f9606cdb))
|
|
||||||
- it compiles once more ([`8acdd2e`](https://github.comgit//DioxusLabs/dioxus/commit/8acdd2ea830b995b608d8bac2ef527db8d40e662))
|
|
||||||
- some docs and suspense ([`93d4b8c`](https://github.comgit//DioxusLabs/dioxus/commit/93d4b8ca7c1b133e5dba2a8dc9a310dbe1357001))
|
|
||||||
- docs and router ([`a5f05d7`](https://github.comgit//DioxusLabs/dioxus/commit/a5f05d73acc0e47b05cff64a373482519414bc7c))
|
|
||||||
- Merge branch 'master' into jk/remove_node_safety ([`db00047`](https://github.comgit//DioxusLabs/dioxus/commit/db0004758c77331cc3b93ea8cf227c060028e12e))
|
|
||||||
- improve docs ([`7800744`](https://github.comgit//DioxusLabs/dioxus/commit/78007445f944f259170307d840e0f16242b7b4b6))
|
|
||||||
- Various typos/grammar/rewording ([`5747e00`](https://github.comgit//DioxusLabs/dioxus/commit/5747e00b27b1b69c4f9c2820e7e78030feaff71e))
|
|
||||||
- bubbling in progress ([`a21020e`](https://github.comgit//DioxusLabs/dioxus/commit/a21020ea575e467ba0d608737269fe1b0792dba7))
|
|
||||||
- update local examples and docs to support new syntaxes ([`4de16c4`](https://github.comgit//DioxusLabs/dioxus/commit/4de16c4779648e591b3869b5df31271ae603c812))
|
|
||||||
- massage lifetimes ([`9726a06`](https://github.comgit//DioxusLabs/dioxus/commit/9726a065b0d4fb1ede5b53a2ddd58c855e51539f))
|
|
||||||
- major cleanups to scheduler ([`2933e4b`](https://github.comgit//DioxusLabs/dioxus/commit/2933e4bc11b3074c2bde8d76ec55364fca841988))
|
|
||||||
- threadsafe ([`82953f2`](https://github.comgit//DioxusLabs/dioxus/commit/82953f2ac37913f83a822333acd0c47e20777d31))
|
|
||||||
- move macro crate out of core ([`7bdad1e`](https://github.comgit//DioxusLabs/dioxus/commit/7bdad1e2e6f67e74c9f67dde2150140cf8a090e8))
|
|
||||||
- amazingly awesome error handling ([`4a72b31`](https://github.comgit//DioxusLabs/dioxus/commit/4a72b3140bd244da602deada1eeecded65ff5848))
|
|
||||||
- some ideas ([`05c909f`](https://github.comgit//DioxusLabs/dioxus/commit/05c909f320765aec1bf4c1c55ca59ffd5525a2c7))
|
|
||||||
- big updates to the reference ([`583fdfa`](https://github.comgit//DioxusLabs/dioxus/commit/583fdfa5618e11d660985b97e570d4503be2ff49))
|
|
||||||
- docs, html! macro, more ([`caf772c`](https://github.comgit//DioxusLabs/dioxus/commit/caf772cf249d2f56c8d0b0fa2737ad48e32c6e82))
|
|
||||||
- cleanup workspace ([`8f0bb5d`](https://github.comgit//DioxusLabs/dioxus/commit/8f0bb5dc5bfa3e775af567c4b569622cdd932af1))
|
|
||||||
- svgs working in webview ([`3bedcb9`](https://github.comgit//DioxusLabs/dioxus/commit/3bedcb93cacec5bdf134adc38ff02eadbf96c1c6))
|
|
||||||
- mvoe away from compound context ([`a2c7d17`](https://github.comgit//DioxusLabs/dioxus/commit/a2c7d17b0595769f60bc1c2bbf7cbe32cec37486))
|
|
||||||
- more suspended nodes! ([`de9f61b`](https://github.comgit//DioxusLabs/dioxus/commit/de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2))
|
|
||||||
- add aria ([`4091846`](https://github.comgit//DioxusLabs/dioxus/commit/4091846934b4b3b2bc03d3ca8aaf7712aebd4e36))
|
|
||||||
- more examples ([`56e7eb8`](https://github.comgit//DioxusLabs/dioxus/commit/56e7eb83a97ebd6d5bcd23464cfb9d718e5ac26d))
|
|
||||||
- more refactor for async ([`975fa56`](https://github.comgit//DioxusLabs/dioxus/commit/975fa566f9809f8fa2bb0bdb07fbfc7f855dcaeb))
|
|
||||||
- enable arbitrary body in rsx! macro ([`7aec40d`](https://github.comgit//DioxusLabs/dioxus/commit/7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2))
|
|
||||||
- move CLI into its own "studio" app ([`fd79335`](https://github.comgit//DioxusLabs/dioxus/commit/fd7933561fe81922e4d5d77f6ac3b6f19efb5a90))
|
|
||||||
- move some examples around ([`98a0933`](https://github.comgit//DioxusLabs/dioxus/commit/98a09339fd3190799ea4dd316908f0a53fdf2413))
|
|
||||||
- fix issues with lifetimes ([`a38a81e`](https://github.comgit//DioxusLabs/dioxus/commit/a38a81e1290375cae685f7c49d3745e4298fab26))
|
|
||||||
- more examples ([`11f89e5`](https://github.comgit//DioxusLabs/dioxus/commit/11f89e5d338d14a7aeece0a6275c24ae65913ce7))
|
|
||||||
- lib.rs docs ([`bf21c82`](https://github.comgit//DioxusLabs/dioxus/commit/bf21c82de04e25daee60a06232b9a16b640508f2))
|
|
||||||
- rename ctx to cx ([`81382e7`](https://github.comgit//DioxusLabs/dioxus/commit/81382e7044fb3dba61d4abb1e6086b7b29143116))
|
|
||||||
- move around examples ([`70cd46d`](https://github.comgit//DioxusLabs/dioxus/commit/70cd46dbb2a689ae2d512e142b8aee9c80798430))
|
|
||||||
- start moving events to rc<event> ([`b9ff95f`](https://github.comgit//DioxusLabs/dioxus/commit/b9ff95fa12c46365fe73b64a4926a506d5da2342))
|
|
||||||
- rename recoil to atoms ([`36ea39a`](https://github.comgit//DioxusLabs/dioxus/commit/36ea39ae30aa3f1fb2d718c0fdf08850c6bfd3ac))
|
|
||||||
- more examples and docs ([`7fbaf69`](https://github.comgit//DioxusLabs/dioxus/commit/7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9))
|
|
||||||
- docs ([`f5683a2`](https://github.comgit//DioxusLabs/dioxus/commit/f5683a23464992ecace463a61414795b5a2c58c8))
|
|
||||||
</details>
|
|
||||||
|
|
||||||
+5
-4
@@ -41,6 +41,7 @@ members = [
|
|||||||
"examples/tailwind",
|
"examples/tailwind",
|
||||||
"examples/PWA-example",
|
"examples/PWA-example",
|
||||||
"examples/query_segments_demo",
|
"examples/query_segments_demo",
|
||||||
|
"examples/openid_connect_demo",
|
||||||
# Playwright tests
|
# Playwright tests
|
||||||
"playwright-tests/liveview",
|
"playwright-tests/liveview",
|
||||||
"playwright-tests/web",
|
"playwright-tests/web",
|
||||||
@@ -49,7 +50,7 @@ members = [
|
|||||||
exclude = ["examples/mobile_demo"]
|
exclude = ["examples/mobile_demo"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
|
|
||||||
# dependencies that are shared across packages
|
# dependencies that are shared across packages
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -76,7 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
|
|||||||
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
|
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
|
||||||
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
|
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
|
||||||
dioxus-signals = { path = "packages/signals" }
|
dioxus-signals = { path = "packages/signals" }
|
||||||
generational-box = { path = "packages/generational-box" }
|
generational-box = { path = "packages/generational-box", version = "0.4.3" }
|
||||||
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
|
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
|
||||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
||||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
||||||
@@ -87,7 +88,7 @@ slab = "0.4.2"
|
|||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
futures-util = { version = "0.3", default-features = false }
|
futures-util = { version = "0.3", default-features = false }
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
wasm-bindgen = "0.2.87"
|
wasm-bindgen = "0.2.88"
|
||||||
html_parser = "0.7.0"
|
html_parser = "0.7.0"
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
prettyplease = { package = "prettier-please", version = "0.2", features = [
|
prettyplease = { package = "prettier-please", version = "0.2", features = [
|
||||||
@@ -98,7 +99,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
|
|||||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||||
[package]
|
[package]
|
||||||
name = "dioxus-examples"
|
name = "dioxus-examples"
|
||||||
version = "0.0.0"
|
version = "0.4.3"
|
||||||
authors = ["Jonathan Kelley"]
|
authors = ["Jonathan Kelley"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Top level crate for the Dioxus repository"
|
description = "Top level crate for the Dioxus repository"
|
||||||
|
|||||||
+70
-4
@@ -24,12 +24,64 @@ script = [
|
|||||||
]
|
]
|
||||||
script_runner = "@duckscript"
|
script_runner = "@duckscript"
|
||||||
|
|
||||||
|
[tasks.format]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["fmt", "--all"]
|
||||||
|
|
||||||
|
[tasks.check]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["check", "--workspace", "--examples", "--tests"]
|
||||||
|
|
||||||
|
[tasks.clippy]
|
||||||
|
command = "cargo"
|
||||||
|
args = [
|
||||||
|
"clippy",
|
||||||
|
"--workspace",
|
||||||
|
"--examples",
|
||||||
|
"--tests",
|
||||||
|
"--",
|
||||||
|
"-D",
|
||||||
|
"warnings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.tidy]
|
||||||
|
category = "Formatting"
|
||||||
|
dependencies = ["format", "check", "clippy"]
|
||||||
|
description = "Format and Check workspace"
|
||||||
|
|
||||||
|
[tasks.install-miri]
|
||||||
|
toolchain = "nightly"
|
||||||
|
install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
|
||||||
|
private = true
|
||||||
|
|
||||||
|
[tasks.miri-native]
|
||||||
|
command = "cargo"
|
||||||
|
toolchain = "nightly"
|
||||||
|
dependencies = ["install-miri"]
|
||||||
|
args = [
|
||||||
|
"miri",
|
||||||
|
"test",
|
||||||
|
"--package",
|
||||||
|
"dioxus-native-core",
|
||||||
|
"--test",
|
||||||
|
"miri_native",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.miri-stress]
|
||||||
|
command = "cargo"
|
||||||
|
toolchain = "nightly"
|
||||||
|
dependencies = ["install-miri"]
|
||||||
|
args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
|
||||||
|
|
||||||
|
[tasks.miri]
|
||||||
|
dependencies = ["miri-native", "miri-stress"]
|
||||||
|
|
||||||
[tasks.tests]
|
[tasks.tests]
|
||||||
category = "Testing"
|
category = "Testing"
|
||||||
dependencies = ["tests-setup"]
|
dependencies = ["tests-setup"]
|
||||||
description = "Run all tests"
|
description = "Run all tests"
|
||||||
env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
|
env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
|
||||||
run_task = {name = ["test-flow", "test-with-browser"], fork = true}
|
run_task = { name = ["test-flow", "test-with-browser"], fork = true }
|
||||||
|
|
||||||
[tasks.build]
|
[tasks.build]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
@@ -42,10 +94,24 @@ private = true
|
|||||||
[tasks.test]
|
[tasks.test]
|
||||||
dependencies = ["build"]
|
dependencies = ["build"]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"]
|
args = [
|
||||||
|
"test",
|
||||||
|
"--lib",
|
||||||
|
"--bins",
|
||||||
|
"--tests",
|
||||||
|
"--examples",
|
||||||
|
"--workspace",
|
||||||
|
"--exclude",
|
||||||
|
"dioxus-router",
|
||||||
|
"--exclude",
|
||||||
|
"dioxus-desktop",
|
||||||
|
]
|
||||||
private = true
|
private = true
|
||||||
|
|
||||||
[tasks.test-with-browser]
|
[tasks.test-with-browser]
|
||||||
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] }
|
env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
|
||||||
|
"**/packages/router",
|
||||||
|
"**/packages/desktop",
|
||||||
|
] }
|
||||||
private = true
|
private = true
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -159,6 +159,7 @@ So... Dioxus is great, but why won't it work for me?
|
|||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
|
||||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||||
- Join the discord and ask questions!
|
- Join the discord and ask questions!
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
div { id: "wrapper",
|
div { id: "wrapper",
|
||||||
div { class: "app",
|
div { class: "app",
|
||||||
div { class: "calculator",
|
div { class: "calculator",
|
||||||
|
tabindex: "0",
|
||||||
onkeydown: handle_key_down_event,
|
onkeydown: handle_key_down_event,
|
||||||
div { class: "calculator-display", val.to_string() }
|
div { class: "calculator-display", val.to_string() }
|
||||||
div { class: "calculator-keypad",
|
div { class: "calculator-keypad",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/dist
|
||||||
|
.env
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "openid_auth_demo"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
dioxus-logger = "0.4.1"
|
||||||
|
dioxus = { path = "../../packages/dioxus", version = "*" }
|
||||||
|
dioxus-router = { path = "../../packages/router", version = "*" }
|
||||||
|
dioxus-web = { path = "../../packages/web", version = "*" }
|
||||||
|
fermi = { path = "../../packages/fermi", version = "*" }
|
||||||
|
form_urlencoded = "1.2.0"
|
||||||
|
gloo-storage = "0.3.0"
|
||||||
|
log = "0.4"
|
||||||
|
openidconnect = "3.4.0"
|
||||||
|
reqwest = "0.11.20"
|
||||||
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
|
serde_json = "1.0.105"
|
||||||
|
thiserror = "1.0.48"
|
||||||
|
uuid = "1.4"
|
||||||
|
web-sys = { version = "0.3", features = ["Request", "Document"] }
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[application]
|
||||||
|
|
||||||
|
# dioxus project name
|
||||||
|
name = "OpenID Connect authentication demo"
|
||||||
|
|
||||||
|
# default platfrom
|
||||||
|
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||||
|
# value: web | desktop
|
||||||
|
default_platform = "web"
|
||||||
|
|
||||||
|
# Web `build` & `serve` dist path
|
||||||
|
out_dir = "dist"
|
||||||
|
|
||||||
|
# resource (static) file folder
|
||||||
|
asset_dir = "public"
|
||||||
|
|
||||||
|
[web.app]
|
||||||
|
|
||||||
|
# HTML title tag content
|
||||||
|
title = "OpenID Connect authentication demo"
|
||||||
|
|
||||||
|
[web.watcher]
|
||||||
|
|
||||||
|
index_on_404 = true
|
||||||
|
|
||||||
|
watch_path = ["src"]
|
||||||
|
|
||||||
|
# include `assets` in web platform
|
||||||
|
[web.resource]
|
||||||
|
|
||||||
|
# CSS style file
|
||||||
|
style = []
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[web.resource.dev]
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
# serve: [dev-server] only
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[application.plugins]
|
||||||
|
|
||||||
|
available = true
|
||||||
|
|
||||||
|
required = []
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# OpenID Connect example to show how to authenticate an user
|
||||||
|
|
||||||
|
The environment variables in `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)).
|
||||||
|
|
||||||
|
Once they are set, you can run `dx serve`
|
||||||
|
|
||||||
|
### Environment variables summary
|
||||||
|
|
||||||
|
```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url
|
||||||
|
|
||||||
|
```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id
|
||||||
|
|
||||||
|
```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
|
||||||
|
pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Discovery error: {0}")]
|
||||||
|
OpenIdConnect(
|
||||||
|
#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
|
||||||
|
),
|
||||||
|
#[error("Parsing error: {0}")]
|
||||||
|
Parse(#[from] url::ParseError),
|
||||||
|
#[error("Request token error: {0}")]
|
||||||
|
RequestToken(
|
||||||
|
#[from]
|
||||||
|
RequestTokenError<
|
||||||
|
openidconnect::reqwest::Error<reqwest::Error>,
|
||||||
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use fermi::*;
|
||||||
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
use log::LevelFilter;
|
||||||
|
pub(crate) mod constants;
|
||||||
|
pub(crate) mod errors;
|
||||||
|
pub(crate) mod model;
|
||||||
|
pub(crate) mod oidc;
|
||||||
|
pub(crate) mod props;
|
||||||
|
pub(crate) mod router;
|
||||||
|
pub(crate) mod storage;
|
||||||
|
pub(crate) mod views;
|
||||||
|
use oidc::{AuthRequestState, AuthTokenState};
|
||||||
|
use router::Route;
|
||||||
|
|
||||||
|
use dioxus_router::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||||
|
oidc::ClientState,
|
||||||
|
};
|
||||||
|
pub static FERMI_CLIENT: fermi::AtomRef<ClientState> = AtomRef(|_| ClientState::default());
|
||||||
|
|
||||||
|
// An option is required to prevent the component from being constantly refreshed
|
||||||
|
pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
|
||||||
|
pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
|
||||||
|
|
||||||
|
pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
|
||||||
|
pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
|
||||||
|
pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
|
||||||
|
|
||||||
|
fn App(cx: Scope) -> Element {
|
||||||
|
use_init_atom_root(cx);
|
||||||
|
|
||||||
|
// Retrieve the value stored in the browser's storage
|
||||||
|
let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(AuthTokenState::default());
|
||||||
|
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||||
|
if fermi_auth_token.read().is_none() {
|
||||||
|
*fermi_auth_token.write() = Some(stored_auth_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(AuthRequestState::default());
|
||||||
|
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||||
|
if fermi_auth_request.read().is_none() {
|
||||||
|
*fermi_auth_request.write() = Some(stored_auth_request);
|
||||||
|
}
|
||||||
|
render! { Router::<Route> {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
log::info!("starting app");
|
||||||
|
dioxus_web::launch(App);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod user;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use openidconnect::{
|
||||||
|
core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse},
|
||||||
|
reqwest::async_http_client,
|
||||||
|
url::Url,
|
||||||
|
AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl,
|
||||||
|
LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError,
|
||||||
|
StandardErrorResponse,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ClientState {
|
||||||
|
pub oidc_client: Option<ClientProps>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State that holds the nonce and authorization url and the nonce generated to log in an user
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct AuthRequestState {
|
||||||
|
pub auth_request: Option<AuthRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub nonce: Nonce,
|
||||||
|
pub authorize_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State the tokens returned once the user is authenticated
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||||
|
pub struct AuthTokenState {
|
||||||
|
/// Token used to identify the user
|
||||||
|
pub id_token: Option<CoreIdToken>,
|
||||||
|
/// Token used to refresh the tokens if they expire
|
||||||
|
pub refresh_token: Option<RefreshToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(
|
||||||
|
client: CoreClient,
|
||||||
|
id_token: CoreIdToken,
|
||||||
|
nonce: Nonce,
|
||||||
|
) -> Result<String, ClaimsVerificationError> {
|
||||||
|
match id_token.claims(&client.id_token_verifier(), &nonce) {
|
||||||
|
Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
|
||||||
|
Err(error) => Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorize_url(client: CoreClient) -> AuthRequest {
|
||||||
|
let (authorize_url, _csrf_state, nonce) = client
|
||||||
|
.authorize_url(
|
||||||
|
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
)
|
||||||
|
.add_scope(openidconnect::Scope::new("email".to_string()))
|
||||||
|
.add_scope(openidconnect::Scope::new("profile".to_string()))
|
||||||
|
.url();
|
||||||
|
AuthRequest {
|
||||||
|
authorize_url: authorize_url.to_string(),
|
||||||
|
nonce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
|
||||||
|
let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
|
||||||
|
Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
|
||||||
|
let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
|
||||||
|
let provider_metadata = init_provider_metadata().await?;
|
||||||
|
let client_secret = None;
|
||||||
|
let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
client_id.clone(),
|
||||||
|
CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
|
||||||
|
.set_redirect_uri(redirect_url),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
///TODO: Add pkce_pacifier
|
||||||
|
pub async fn token_response(
|
||||||
|
oidc_client: CoreClient,
|
||||||
|
code: String,
|
||||||
|
) -> Result<CoreTokenResponse, crate::errors::Error> {
|
||||||
|
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
Ok(oidc_client
|
||||||
|
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||||
|
// .set_pkce_verifier(pkce_verifier)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exchange_refresh_token(
|
||||||
|
oidc_client: CoreClient,
|
||||||
|
refresh_token: RefreshToken,
|
||||||
|
) -> Result<
|
||||||
|
CoreTokenResponse,
|
||||||
|
RequestTokenError<
|
||||||
|
openidconnect::reqwest::Error<reqwest::Error>,
|
||||||
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
oidc_client
|
||||||
|
.exchange_refresh_token(&refresh_token)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
|
||||||
|
let provider_metadata = init_provider_metadata().await?;
|
||||||
|
let end_session_url = provider_metadata
|
||||||
|
.additional_metadata()
|
||||||
|
.clone()
|
||||||
|
.end_session_endpoint
|
||||||
|
.unwrap();
|
||||||
|
let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
|
||||||
|
Ok(logout_request
|
||||||
|
.set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
|
||||||
|
.set_id_token_hint(&id_token_hint)
|
||||||
|
.http_get_url())
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use openidconnect::{core::CoreClient, ClientId};
|
||||||
|
|
||||||
|
#[derive(Props, Clone, Debug)]
|
||||||
|
pub struct ClientProps {
|
||||||
|
pub client: CoreClient,
|
||||||
|
pub client_id: ClientId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ClientProps {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.client_id == other.client_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientProps {
|
||||||
|
pub fn new(client_id: ClientId, client: CoreClient) -> Self {
|
||||||
|
ClientProps { client_id, client }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod client;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_router::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Routable, Clone)]
|
||||||
|
pub enum Route {
|
||||||
|
#[layout(AuthHeader)]
|
||||||
|
#[route("/")]
|
||||||
|
Home {},
|
||||||
|
|
||||||
|
// https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
|
||||||
|
#[route("/login?:query_string")]
|
||||||
|
Login { query_string: String },
|
||||||
|
#[end_layout]
|
||||||
|
#[route("/:..route")]
|
||||||
|
NotFound { route: Vec<String> },
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use fermi::UseAtomRef;
|
||||||
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
|
||||||
|
oidc::{AuthRequestState, AuthTokenState},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct StorageEntry<T> {
|
||||||
|
pub key: String,
|
||||||
|
pub value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PersistentWrite<T: Serialize + Clone> {
|
||||||
|
fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentWrite<AuthTokenState> for AuthTokenState {
|
||||||
|
fn persistent_set(
|
||||||
|
atom_ref: &UseAtomRef<Option<AuthTokenState>>,
|
||||||
|
entry: Option<AuthTokenState>,
|
||||||
|
) {
|
||||||
|
*atom_ref.write() = entry.clone();
|
||||||
|
LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentWrite<AuthRequestState> for AuthRequestState {
|
||||||
|
fn persistent_set(
|
||||||
|
atom_ref: &UseAtomRef<Option<AuthRequestState>>,
|
||||||
|
entry: Option<AuthRequestState>,
|
||||||
|
) {
|
||||||
|
*atom_ref.write() = entry.clone();
|
||||||
|
LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
use crate::{
|
||||||
|
oidc::{
|
||||||
|
authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
|
||||||
|
AuthRequestState, AuthTokenState, ClientState,
|
||||||
|
},
|
||||||
|
props::client::ClientProps,
|
||||||
|
router::Route,
|
||||||
|
storage::PersistentWrite,
|
||||||
|
FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||||
|
};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_router::prelude::{Link, Outlet};
|
||||||
|
use fermi::*;
|
||||||
|
use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn LogOut(cx: Scope<ClientProps>) -> Element {
|
||||||
|
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||||
|
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||||
|
let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
|
||||||
|
cx.render(match fermi_auth_token_read {
|
||||||
|
Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
|
||||||
|
Some(id_token) => match log_out_url_state.get() {
|
||||||
|
Some(log_out_url_result) => match log_out_url_result {
|
||||||
|
Some(uri) => match uri {
|
||||||
|
Ok(uri) => {
|
||||||
|
rsx! {
|
||||||
|
Link {
|
||||||
|
onclick: move |_| {
|
||||||
|
{
|
||||||
|
AuthTokenState::persistent_set(
|
||||||
|
fermi_auth_token,
|
||||||
|
Some(AuthTokenState::default()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
to: uri.to_string(),
|
||||||
|
"Log out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
rsx! {
|
||||||
|
div { format!{"Failed to load disconnection url: {:?}", error} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
rsx! { div { "Loading... Please wait" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let logout_url_task = move || {
|
||||||
|
cx.spawn({
|
||||||
|
let log_out_url_state = log_out_url_state.to_owned();
|
||||||
|
async move {
|
||||||
|
let logout_url = log_out_url(id_token).await;
|
||||||
|
let logout_url_option = Some(logout_url);
|
||||||
|
log_out_url_state.set(Some(logout_url_option));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
logout_url_task();
|
||||||
|
rsx! { div{"Loading log out url... Please wait"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
|
||||||
|
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||||
|
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||||
|
let fermi_auth_token_read = fermi_auth_token.read().clone();
|
||||||
|
cx.render(match fermi_auth_token_read {
|
||||||
|
Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
|
||||||
|
Some(refresh_token) => {
|
||||||
|
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||||
|
let fermi_auth_request = fermi_auth_request.to_owned();
|
||||||
|
let client = cx.props.client.clone();
|
||||||
|
let exchange_refresh_token_spawn = move || {
|
||||||
|
cx.spawn({
|
||||||
|
async move {
|
||||||
|
let exchange_refresh_token =
|
||||||
|
exchange_refresh_token(client, refresh_token).await;
|
||||||
|
match exchange_refresh_token {
|
||||||
|
Ok(response_token) => {
|
||||||
|
AuthTokenState::persistent_set(
|
||||||
|
&fermi_auth_token,
|
||||||
|
Some(AuthTokenState {
|
||||||
|
id_token: response_token.id_token().cloned(),
|
||||||
|
refresh_token: response_token.refresh_token().cloned(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_error) => {
|
||||||
|
AuthTokenState::persistent_set(
|
||||||
|
&fermi_auth_token,
|
||||||
|
Some(AuthTokenState::default()),
|
||||||
|
);
|
||||||
|
AuthRequestState::persistent_set(
|
||||||
|
&fermi_auth_request,
|
||||||
|
Some(AuthRequestState::default()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
exchange_refresh_token_spawn();
|
||||||
|
rsx! { div { "Refreshing session, please wait" } }
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
rsx! { div { "Id token expired and no refresh token found" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn LoadClient(cx: Scope) -> Element {
|
||||||
|
let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
|
||||||
|
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||||
|
cx.render(match init_client_future.value() {
|
||||||
|
Some(client_props) => match client_props {
|
||||||
|
Ok((client_id, client)) => {
|
||||||
|
*fermi_client.write() = ClientState {
|
||||||
|
oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
|
||||||
|
};
|
||||||
|
rsx! {
|
||||||
|
div { "Client successfully loaded" }
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
rsx! {
|
||||||
|
div { format!{"Failed to load client: {:?}", error} }
|
||||||
|
log::info!{"Failed to load client: {:?}", error},
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
div { "Loading client, please wait" }
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AuthHeader(cx: Scope) -> Element {
|
||||||
|
let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||||
|
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||||
|
let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
|
||||||
|
let client = fermi_client.read().oidc_client.clone();
|
||||||
|
let auth_request_read = fermi_auth_request.read().clone();
|
||||||
|
let auth_token_read = auth_token.read().clone();
|
||||||
|
cx.render(match (client, auth_request_read, auth_token_read) {
|
||||||
|
// We have everything we need to attempt to authenticate the user
|
||||||
|
(Some(client_props), Some(auth_request), Some(auth_token)) => {
|
||||||
|
match auth_request.auth_request {
|
||||||
|
Some(auth_request) => {
|
||||||
|
match auth_token.id_token {
|
||||||
|
Some(id_token) => {
|
||||||
|
match email(
|
||||||
|
client_props.client.clone(),
|
||||||
|
id_token.clone(),
|
||||||
|
auth_request.nonce.clone(),
|
||||||
|
) {
|
||||||
|
Ok(email) => {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
div { email }
|
||||||
|
LogOut { client_id: client_props.client_id, client: client_props.client }
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Id token failed to be decoded
|
||||||
|
Err(error) => match error {
|
||||||
|
// Id token failed to be decoded because it expired, we refresh it
|
||||||
|
openidconnect::ClaimsVerificationError::Expired(_message) => {
|
||||||
|
log::info!("Token expired");
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
RefreshToken {client_id: client_props.client_id, client: client_props.client}
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other issue with token decoding
|
||||||
|
_ => {
|
||||||
|
log::info!("Other issue with token");
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
div { error.to_string() }
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// User is not logged in
|
||||||
|
None => {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
Link { to: auth_request.authorize_url.clone(), "Log in" }
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let auth_request = authorize_url(client_props.client);
|
||||||
|
AuthRequestState::persistent_set(
|
||||||
|
fermi_auth_request,
|
||||||
|
Some(AuthRequestState {
|
||||||
|
auth_request: Some(auth_request),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rsx! { div { "Loading nonce" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Client is not initialized yet, we need it for everything
|
||||||
|
(None, _, _) => {
|
||||||
|
rsx! { LoadClient {} }
|
||||||
|
}
|
||||||
|
// We need everything loaded before doing anything
|
||||||
|
(_client, _auth_request, _auth_token) => {
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
pub fn Home(cx: Scope) -> Element {
|
||||||
|
render! { div { "Hello world" } }
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::{
|
||||||
|
oidc::{token_response, AuthRequestState, AuthTokenState},
|
||||||
|
router::Route,
|
||||||
|
storage::PersistentWrite,
|
||||||
|
DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
|
||||||
|
};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_router::prelude::{Link, NavigationTarget};
|
||||||
|
use fermi::*;
|
||||||
|
use openidconnect::{OAuth2TokenResponse, TokenResponse};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Login(cx: Scope, query_string: String) -> Element {
|
||||||
|
let fermi_client = use_atom_ref(cx, &FERMI_CLIENT);
|
||||||
|
let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
|
||||||
|
let home_url: NavigationTarget<Route> = DIOXUS_FRONT_URL.parse().unwrap();
|
||||||
|
let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
|
||||||
|
let client = fermi_client.read().oidc_client.clone();
|
||||||
|
let auth_token_read = fermi_auth_token.read().clone();
|
||||||
|
cx.render(match (client, auth_token_read) {
|
||||||
|
(Some(client_props), Some(auth_token_read)) => {
|
||||||
|
match (auth_token_read.id_token, auth_token_read.refresh_token) {
|
||||||
|
(Some(_id_token), Some(_refresh_token)) => {
|
||||||
|
rsx! {
|
||||||
|
div { "Sign in successful" }
|
||||||
|
Link { to: home_url, "Go back home" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
|
||||||
|
(None, Some(_)) | (Some(_), None) => {
|
||||||
|
rsx! {
|
||||||
|
div { "Error while attempting to log in" }
|
||||||
|
Link {
|
||||||
|
to: home_url,
|
||||||
|
onclick: move |_| {
|
||||||
|
AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
|
||||||
|
AuthRequestState::persistent_set(
|
||||||
|
fermi_auth_request,
|
||||||
|
Some(AuthRequestState::default()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"Go back home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
|
||||||
|
let code_pair = query_pairs.find(|(key, _value)| key == "code");
|
||||||
|
match code_pair {
|
||||||
|
Some((_key, code)) => {
|
||||||
|
let auth_code = code.to_string();
|
||||||
|
let token_response_spawn = move ||{
|
||||||
|
cx.spawn({
|
||||||
|
let fermi_auth_token = fermi_auth_token.to_owned();
|
||||||
|
async move {
|
||||||
|
let token_response_result = token_response(client_props.client, auth_code).await;
|
||||||
|
match token_response_result{
|
||||||
|
Ok(token_response) => {
|
||||||
|
let id_token = token_response.id_token().unwrap();
|
||||||
|
AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
|
||||||
|
id_token: Some(id_token.clone()),
|
||||||
|
refresh_token: token_response.refresh_token().cloned()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!{"{error}"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
token_response_spawn();
|
||||||
|
rsx!{ div {} }
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
rsx! { div { "No code provided" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(_, _) => {
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub(crate) mod header;
|
||||||
|
pub(crate) mod home;
|
||||||
|
pub(crate) mod login;
|
||||||
|
pub(crate) mod not_found;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
|
||||||
|
let routes = route.join("");
|
||||||
|
render! {rsx! {div{routes}}}
|
||||||
|
}
|
||||||
@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
|
|||||||
a: "asd".to_string(),
|
a: "asd".to_string(),
|
||||||
c: "asd".to_string(),
|
c: "asd".to_string(),
|
||||||
d: Some("asd".to_string()),
|
d: Some("asd".to_string()),
|
||||||
|
e: Some("asd".to_string()),
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
a: "asd".to_string(),
|
||||||
|
b: "asd".to_string(),
|
||||||
|
c: "asd".to_string(),
|
||||||
|
d: Some("asd".to_string()),
|
||||||
e: "asd".to_string(),
|
e: "asd".to_string(),
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
a: "asd".to_string(),
|
||||||
|
c: "asd".to_string(),
|
||||||
|
d: Some("asd".to_string()),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
name = "query_segments_demo"
|
name = "query_segments_demo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -6,11 +6,17 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
let running = dioxus_signals::use_signal(cx, || true);
|
||||||
let mut count = dioxus_signals::use_signal(cx, || 0);
|
let mut count = dioxus_signals::use_signal(cx, || 0);
|
||||||
|
let saved_values = dioxus_signals::use_signal(cx, || vec![0.to_string()]);
|
||||||
|
|
||||||
|
// Signals can be used in async functions without an explicit clone since they're 'static and Copy
|
||||||
|
// Signals are backed by a runtime that is designed to deeply integrate with Dioxus apps
|
||||||
use_future!(cx, || async move {
|
use_future!(cx, || async move {
|
||||||
loop {
|
loop {
|
||||||
count += 1;
|
if running.value() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(400)).await;
|
tokio::time::sleep(Duration::from_millis(400)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -19,9 +25,25 @@ fn app(cx: Scope) -> Element {
|
|||||||
h1 { "High-Five counter: {count}" }
|
h1 { "High-Five counter: {count}" }
|
||||||
button { onclick: move |_| count += 1, "Up high!" }
|
button { onclick: move |_| count += 1, "Up high!" }
|
||||||
button { onclick: move |_| count -= 1, "Down low!" }
|
button { onclick: move |_| count -= 1, "Down low!" }
|
||||||
|
button { onclick: move |_| running.toggle(), "Toggle counter" }
|
||||||
|
button { onclick: move |_| saved_values.push(count.value().to_string()), "Save this value" }
|
||||||
|
button { onclick: move |_| saved_values.write().clear(), "Clear saved values" }
|
||||||
|
|
||||||
|
// We can do boolean operations on the current signal value
|
||||||
if count.value() > 5 {
|
if count.value() > 5 {
|
||||||
rsx!{ h2 { "High five!" } }
|
rsx!{ h2 { "High five!" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We can cleanly map signals with iterators
|
||||||
|
for value in saved_values.read().iter() {
|
||||||
|
h3 { "Saved value: {value}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can also use the signal value as a slice
|
||||||
|
if let [ref first, .., ref last] = saved_values.read().as_slice() {
|
||||||
|
rsx! { li { "First and last: {first}, {last}" } }
|
||||||
|
} else {
|
||||||
|
rsx! { "No saved values" }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+114
-68
@@ -24,8 +24,6 @@ pub struct TodoItem {
|
|||||||
pub fn app(cx: Scope<()>) -> Element {
|
pub fn app(cx: Scope<()>) -> Element {
|
||||||
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
|
let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
|
||||||
let filter = use_state(cx, || FilterState::All);
|
let filter = use_state(cx, || FilterState::All);
|
||||||
let draft = use_state(cx, || "".to_string());
|
|
||||||
let todo_id = use_state(cx, || 0);
|
|
||||||
|
|
||||||
// Filter the todos based on the filter state
|
// Filter the todos based on the filter state
|
||||||
let mut filtered_todos = todos
|
let mut filtered_todos = todos
|
||||||
@@ -47,42 +45,11 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
|
|
||||||
let show_clear_completed = todos.values().any(|todo| todo.checked);
|
let show_clear_completed = todos.values().any(|todo| todo.checked);
|
||||||
|
|
||||||
let selected = |state| {
|
|
||||||
if *filter == state {
|
|
||||||
"selected"
|
|
||||||
} else {
|
|
||||||
"false"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
section { class: "todoapp",
|
section { class: "todoapp",
|
||||||
style { include_str!("./assets/todomvc.css") }
|
style { include_str!("./assets/todomvc.css") }
|
||||||
header { class: "header",
|
TodoHeader {
|
||||||
h1 {"todos"}
|
todos: todos,
|
||||||
input {
|
|
||||||
class: "new-todo",
|
|
||||||
placeholder: "What needs to be done?",
|
|
||||||
value: "{draft}",
|
|
||||||
autofocus: "true",
|
|
||||||
oninput: move |evt| {
|
|
||||||
draft.set(evt.value.clone());
|
|
||||||
},
|
|
||||||
onkeydown: move |evt| {
|
|
||||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
|
||||||
todos.make_mut().insert(
|
|
||||||
**todo_id,
|
|
||||||
TodoItem {
|
|
||||||
id: **todo_id,
|
|
||||||
checked: false,
|
|
||||||
contents: draft.to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
*todo_id.make_mut() += 1;
|
|
||||||
draft.set("".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
class: "main",
|
class: "main",
|
||||||
@@ -111,44 +78,56 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
(!todos.is_empty()).then(|| rsx!(
|
(!todos.is_empty()).then(|| rsx!(
|
||||||
footer { class: "footer",
|
ListFooter {
|
||||||
span { class: "todo-count",
|
active_todo_count: active_todo_count,
|
||||||
strong {"{active_todo_count} "}
|
active_todo_text: active_todo_text,
|
||||||
span {"{active_todo_text} left"}
|
show_clear_completed: show_clear_completed,
|
||||||
}
|
todos: todos,
|
||||||
ul { class: "filters",
|
filter: filter,
|
||||||
for (state, state_text, url) in [
|
|
||||||
(FilterState::All, "All", "#/"),
|
|
||||||
(FilterState::Active, "Active", "#/active"),
|
|
||||||
(FilterState::Completed, "Completed", "#/completed"),
|
|
||||||
] {
|
|
||||||
li {
|
|
||||||
a {
|
|
||||||
href: url,
|
|
||||||
class: selected(state),
|
|
||||||
onclick: move |_| filter.set(state),
|
|
||||||
prevent_default: "onclick",
|
|
||||||
state_text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
show_clear_completed.then(|| rsx!(
|
|
||||||
button {
|
|
||||||
class: "clear-completed",
|
|
||||||
onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked),
|
|
||||||
"Clear completed"
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
footer { class: "info",
|
PageFooter {}
|
||||||
p { "Double-click to edit a todo" }
|
})
|
||||||
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
}
|
||||||
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct TodoHeaderProps<'a> {
|
||||||
|
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
||||||
|
let draft = use_state(cx, || "".to_string());
|
||||||
|
let todo_id = use_state(cx, || 0);
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
header { class: "header",
|
||||||
|
h1 {"todos"}
|
||||||
|
input {
|
||||||
|
class: "new-todo",
|
||||||
|
placeholder: "What needs to be done?",
|
||||||
|
value: "{draft}",
|
||||||
|
autofocus: "true",
|
||||||
|
oninput: move |evt| {
|
||||||
|
draft.set(evt.value.clone());
|
||||||
|
},
|
||||||
|
onkeydown: move |evt| {
|
||||||
|
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||||
|
cx.props.todos.make_mut().insert(
|
||||||
|
**todo_id,
|
||||||
|
TodoItem {
|
||||||
|
id: **todo_id,
|
||||||
|
checked: false,
|
||||||
|
contents: draft.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
*todo_id.make_mut() += 1;
|
||||||
|
draft.set("".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,3 +188,70 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct ListFooterProps<'a> {
|
||||||
|
todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
|
||||||
|
active_todo_count: usize,
|
||||||
|
active_todo_text: &'a str,
|
||||||
|
show_clear_completed: bool,
|
||||||
|
filter: &'a UseState<FilterState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
|
||||||
|
let active_todo_count = cx.props.active_todo_count;
|
||||||
|
let active_todo_text = cx.props.active_todo_text;
|
||||||
|
|
||||||
|
let selected = |state| {
|
||||||
|
if *cx.props.filter == state {
|
||||||
|
"selected"
|
||||||
|
} else {
|
||||||
|
"false"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
footer { class: "footer",
|
||||||
|
span { class: "todo-count",
|
||||||
|
strong {"{active_todo_count} "}
|
||||||
|
span {"{active_todo_text} left"}
|
||||||
|
}
|
||||||
|
ul { class: "filters",
|
||||||
|
for (state, state_text, url) in [
|
||||||
|
(FilterState::All, "All", "#/"),
|
||||||
|
(FilterState::Active, "Active", "#/active"),
|
||||||
|
(FilterState::Completed, "Completed", "#/completed"),
|
||||||
|
] {
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
href: url,
|
||||||
|
class: selected(state),
|
||||||
|
onclick: move |_| cx.props.filter.set(state),
|
||||||
|
prevent_default: "onclick",
|
||||||
|
state_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cx.props.show_clear_completed {
|
||||||
|
cx.render(rsx! {
|
||||||
|
button {
|
||||||
|
class: "clear-completed",
|
||||||
|
onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked),
|
||||||
|
"Clear completed"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn PageFooter(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
footer { class: "info",
|
||||||
|
p { "Double-click to edit a todo" }
|
||||||
|
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
||||||
|
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
|
|||||||
|
|
||||||
use dioxus_rsx::IfmtInput;
|
use dioxus_rsx::IfmtInput;
|
||||||
|
|
||||||
use crate::write_ifmt;
|
use crate::{indent::IndentOptions, write_ifmt};
|
||||||
|
|
||||||
/// The output buffer that tracks indent and string
|
/// The output buffer that tracks indent and string
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
pub buf: String,
|
pub buf: String,
|
||||||
pub indent: usize,
|
pub indent_level: usize,
|
||||||
|
pub indent: IndentOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
@@ -31,16 +32,16 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab(&mut self) -> Result {
|
pub fn tab(&mut self) -> Result {
|
||||||
self.write_tabs(self.indent)
|
self.write_tabs(self.indent_level)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indented_tab(&mut self) -> Result {
|
pub fn indented_tab(&mut self) -> Result {
|
||||||
self.write_tabs(self.indent + 1)
|
self.write_tabs(self.indent_level + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
|
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
|
||||||
for _ in 0..num {
|
for _ in 0..num {
|
||||||
write!(self.buf, " ")?
|
write!(self.buf, "{}", self.indent.indent_str())?
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl Writer<'_> {
|
|||||||
|
|
||||||
// check if we have a lot of attributes
|
// check if we have a lot of attributes
|
||||||
let attr_len = self.is_short_attrs(attributes);
|
let attr_len = self.is_short_attrs(attributes);
|
||||||
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
|
||||||
let children_len = self.is_short_children(children);
|
let children_len = self.is_short_children(children);
|
||||||
let is_small_children = children_len.is_some();
|
let is_small_children = children_len.is_some();
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ impl Writer<'_> {
|
|||||||
|
|
||||||
// if we have few children and few attributes, make it a one-liner
|
// if we have few children and few attributes, make it a one-liner
|
||||||
if is_short_attr_list && is_small_children {
|
if is_short_attr_list && is_small_children {
|
||||||
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
|
||||||
opt_level = ShortOptimization::Oneliner;
|
opt_level = ShortOptimization::Oneliner;
|
||||||
} else {
|
} else {
|
||||||
opt_level = ShortOptimization::PropsOnTop;
|
opt_level = ShortOptimization::PropsOnTop;
|
||||||
@@ -185,11 +185,11 @@ impl Writer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while let Some(attr) = attr_iter.next() {
|
while let Some(attr) = attr_iter.next() {
|
||||||
self.out.indent += 1;
|
self.out.indent_level += 1;
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.write_comments(attr.attr.start())?;
|
self.write_comments(attr.attr.start())?;
|
||||||
}
|
}
|
||||||
self.out.indent -= 1;
|
self.out.indent_level -= 1;
|
||||||
|
|
||||||
if !sameline {
|
if !sameline {
|
||||||
self.out.indented_tabbed_line()?;
|
self.out.indented_tabbed_line()?;
|
||||||
@@ -398,14 +398,14 @@ impl Writer<'_> {
|
|||||||
for idx in start.line..end.line {
|
for idx in start.line..end.line {
|
||||||
let line = &self.src[idx];
|
let line = &self.src[idx];
|
||||||
if line.trim().starts_with("//") {
|
if line.trim().starts_with("//") {
|
||||||
for _ in 0..self.out.indent + 1 {
|
for _ in 0..self.out.indent_level + 1 {
|
||||||
write!(self.out, " ")?
|
write!(self.out, " ")?
|
||||||
}
|
}
|
||||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _ in 0..self.out.indent {
|
for _ in 0..self.out.indent_level {
|
||||||
write!(self.out, " ")?
|
write!(self.out, " ")?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl Writer<'_> {
|
|||||||
let first_line = &self.src[start.line - 1];
|
let first_line = &self.src[start.line - 1];
|
||||||
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
|
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
|
||||||
|
|
||||||
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
|
let prev_block_indent_level = self.out.indent.count_indents(first_line);
|
||||||
|
|
||||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||||
writeln!(self.out)?;
|
writeln!(self.out)?;
|
||||||
@@ -43,9 +43,9 @@ impl Writer<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// trim the leading whitespace
|
// trim the leading whitespace
|
||||||
let previous_indent = crate::leading_whitespaces(line) / 4;
|
let previous_indent = self.out.indent.count_indents(line);
|
||||||
let offset = previous_indent.saturating_sub(prev_block_indent_level);
|
let offset = previous_indent.saturating_sub(prev_block_indent_level);
|
||||||
let required_indent = self.out.indent + offset;
|
let required_indent = self.out.indent_level + offset;
|
||||||
self.out.write_tabs(required_indent)?;
|
self.out.write_tabs(required_indent)?;
|
||||||
|
|
||||||
let line = line.trim_start();
|
let line = line.trim_start();
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum IndentType {
|
||||||
|
Spaces,
|
||||||
|
Tabs,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IndentOptions {
|
||||||
|
width: usize,
|
||||||
|
indent_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndentOptions {
|
||||||
|
pub fn new(typ: IndentType, width: usize) -> Self {
|
||||||
|
assert_ne!(width, 0, "Cannot have an indent width of 0");
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
indent_string: match typ {
|
||||||
|
IndentType::Tabs => "\t".into(),
|
||||||
|
IndentType::Spaces => " ".repeat(width),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a string containing one indent worth of whitespace
|
||||||
|
pub fn indent_str(&self) -> &str {
|
||||||
|
&self.indent_string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the line length in characters, counting tabs as the indent width.
|
||||||
|
pub fn line_length(&self, line: &str) -> usize {
|
||||||
|
line.chars()
|
||||||
|
.map(|ch| if ch == '\t' { self.width } else { 1 })
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimates how many times the line has been indented.
|
||||||
|
pub fn count_indents(&self, mut line: &str) -> usize {
|
||||||
|
let mut indent = 0;
|
||||||
|
while !line.is_empty() {
|
||||||
|
// Try to count tabs
|
||||||
|
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
|
||||||
|
if num_tabs > 0 {
|
||||||
|
indent += num_tabs;
|
||||||
|
line = &line[num_tabs..];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to count spaces
|
||||||
|
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
|
||||||
|
if num_spaces >= self.width {
|
||||||
|
// Intentionally floor here to take only the amount of space that matches an indent
|
||||||
|
let num_space_indents = num_spaces / self.width;
|
||||||
|
indent += num_space_indents;
|
||||||
|
line = &line[num_space_indents * self.width..];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line starts with either non-indent characters or an unevent amount of spaces,
|
||||||
|
// so no more indent remains.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
indent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IndentOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(IndentType::Spaces, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn count_indents() {
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-15
@@ -16,8 +16,11 @@ mod collect_macros;
|
|||||||
mod component;
|
mod component;
|
||||||
mod element;
|
mod element;
|
||||||
mod expr;
|
mod expr;
|
||||||
|
mod indent;
|
||||||
mod writer;
|
mod writer;
|
||||||
|
|
||||||
|
pub use indent::{IndentOptions, IndentType};
|
||||||
|
|
||||||
/// A modification to the original file to be applied by an IDE
|
/// A modification to the original file to be applied by an IDE
|
||||||
///
|
///
|
||||||
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
|
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
|
||||||
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
|
|||||||
/// back to the file precisely.
|
/// back to the file precisely.
|
||||||
///
|
///
|
||||||
/// Nested blocks of RSX will be handled automatically
|
/// Nested blocks of RSX will be handled automatically
|
||||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||||
let mut formatted_blocks = Vec::new();
|
let mut formatted_blocks = Vec::new();
|
||||||
|
|
||||||
let parsed = syn::parse_file(contents).unwrap();
|
let parsed = syn::parse_file(contents).unwrap();
|
||||||
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut writer = Writer::new(contents);
|
let mut writer = Writer::new(contents);
|
||||||
|
writer.out.indent = indent;
|
||||||
|
|
||||||
// Don't parse nested macros
|
// Don't parse nested macros
|
||||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||||
@@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||||||
|
|
||||||
let rsx_start = macro_path.span().start();
|
let rsx_start = macro_path.span().start();
|
||||||
|
|
||||||
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
|
writer.out.indent_level = writer
|
||||||
|
.out
|
||||||
|
.indent
|
||||||
|
.count_indents(writer.src[rsx_start.line - 1]);
|
||||||
|
|
||||||
write_body(&mut writer, &body);
|
write_body(&mut writer, &body);
|
||||||
|
|
||||||
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
|||||||
buf.consume()
|
buf.consume()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
|
||||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||||
|
|
||||||
let mut buf = Writer::new(block);
|
let mut buf = Writer::new(block);
|
||||||
|
|
||||||
buf.out.indent = indent_level;
|
buf.out.indent = indent;
|
||||||
|
buf.out.indent_level = indent_level;
|
||||||
|
|
||||||
write_body(&mut buf, &body);
|
write_body(&mut buf, &body);
|
||||||
|
|
||||||
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
|
|||||||
let display = DisplayIfmt(input);
|
let display = DisplayIfmt(input);
|
||||||
write!(writable, "{}", display)
|
write!(writable, "{}", display)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leading_whitespaces(input: &str) -> usize {
|
|
||||||
input
|
|
||||||
.chars()
|
|
||||||
.map_while(|c| match c {
|
|
||||||
' ' => Some(1),
|
|
||||||
'\t' => Some(4),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
|
|||||||
|
|
||||||
// Push out the indent level and write each component, line by line
|
// Push out the indent level and write each component, line by line
|
||||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
||||||
self.out.indent += 1;
|
self.out.indent_level += 1;
|
||||||
|
|
||||||
self.write_body_no_indent(children)?;
|
self.write_body_no_indent(children)?;
|
||||||
|
|
||||||
self.out.indent -= 1;
|
self.out.indent_level -= 1;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ macro_rules! twoway {
|
|||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||||
let formatted = dioxus_autofmt::fmt_file(src);
|
let formatted = dioxus_autofmt::fmt_file(src, Default::default());
|
||||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||||
// normalize line endings
|
// normalize line endings
|
||||||
let out = out.replace("\r", "");
|
let out = out.replace("\r", "");
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||||
|
|
||||||
macro_rules! twoway {
|
macro_rules! twoway {
|
||||||
($val:literal => $name:ident) => {
|
($val:literal => $name:ident ($indent:expr)) => {
|
||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
|
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
|
||||||
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
||||||
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
|
||||||
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
||||||
|
|
||||||
// normalize line endings
|
// normalize line endings
|
||||||
@@ -16,8 +18,11 @@ macro_rules! twoway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
twoway!("comments" => comments);
|
twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||||
|
twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||||
|
|
||||||
twoway!("multi" => multi);
|
twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||||
|
twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||||
|
|
||||||
twoway!("multiexpr" => multiexpr);
|
twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||||
|
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,7 @@
|
|||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
// Comments
|
||||||
|
class: "asdasd",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
// Comments
|
||||||
|
class: "asdasd", "hello world" }
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,3 @@
|
|||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! { div { "hello world" } })
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {"hello world" }
|
||||||
|
})
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,8 @@
|
|||||||
|
fn ItWroks() {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
|
||||||
|
left,
|
||||||
|
right
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fn ItWroks() {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dioxus-cli"
|
name = "dioxus-cli"
|
||||||
version = "0.4.1"
|
version = "0.4.3"
|
||||||
authors = ["Jonathan Kelley"]
|
authors = ["Jonathan Kelley"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ It handles building, bundling, development and publishing to simplify developmen
|
|||||||
### Install the stable version (recommended)
|
### Install the stable version (recommended)
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install dioxus-cli --locked
|
cargo install dioxus-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install the latest development build through git
|
### Install the latest development build through git
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
|
|||||||
|
|
||||||
index_on_404 = true
|
index_on_404 = true
|
||||||
|
|
||||||
watch_path = ["src"]
|
watch_path = ["src", "examples"]
|
||||||
|
|
||||||
# include `assets` in web platform
|
# include `assets` in web platform
|
||||||
[web.resource]
|
[web.resource]
|
||||||
|
|||||||
@@ -49,14 +49,25 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
|
|||||||
|
|
||||||
// [1] Build the .wasm module
|
// [1] Build the .wasm module
|
||||||
log::info!("🚅 Running build command...");
|
log::info!("🚅 Running build command...");
|
||||||
|
|
||||||
|
let wasm_check_command = std::process::Command::new("rustup")
|
||||||
|
.args(["show"])
|
||||||
|
.output()?;
|
||||||
|
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
|
||||||
|
if !wasm_check_output.contains("wasm32-unknown-unknown") {
|
||||||
|
log::info!("wasm32-unknown-unknown target not detected, installing..");
|
||||||
|
let _ = std::process::Command::new("rustup")
|
||||||
|
.args(["target", "add", "wasm32-unknown-unknown"])
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
|
||||||
let cmd = subprocess::Exec::cmd("cargo");
|
let cmd = subprocess::Exec::cmd("cargo");
|
||||||
let cmd = cmd
|
let cmd = cmd
|
||||||
.cwd(crate_dir)
|
.cwd(crate_dir)
|
||||||
.arg("build")
|
.arg("build")
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg("wasm32-unknown-unknown")
|
.arg("wasm32-unknown-unknown")
|
||||||
.arg("--message-format=json")
|
.arg("--message-format=json");
|
||||||
.arg("--quiet");
|
|
||||||
|
|
||||||
let cmd = if config.release {
|
let cmd = if config.release {
|
||||||
cmd.arg("--release")
|
cmd.arg("--release")
|
||||||
@@ -66,7 +77,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
|
|||||||
let cmd = if config.verbose {
|
let cmd = if config.verbose {
|
||||||
cmd.arg("--verbose")
|
cmd.arg("--verbose")
|
||||||
} else {
|
} else {
|
||||||
cmd
|
cmd.arg("--quiet")
|
||||||
};
|
};
|
||||||
|
|
||||||
let cmd = if config.custom_profile.is_some() {
|
let cmd = if config.custom_profile.is_some() {
|
||||||
@@ -263,6 +274,7 @@ pub fn build_desktop(
|
|||||||
let mut cmd = subprocess::Exec::cmd("cargo")
|
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||||
.cwd(&config.crate_dir)
|
.cwd(&config.crate_dir)
|
||||||
.arg("build")
|
.arg("build")
|
||||||
|
.arg("--quiet")
|
||||||
.arg("--message-format=json");
|
.arg("--message-format=json");
|
||||||
|
|
||||||
if config.release {
|
if config.release {
|
||||||
@@ -494,7 +506,7 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.contains_key("tailwindcss")
|
.contains_key("tailwindcss")
|
||||||
{
|
{
|
||||||
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
|
style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
|
||||||
}
|
}
|
||||||
if !skip_assets {
|
if !skip_assets {
|
||||||
let manifest = config.asset_manifest();
|
let manifest = config.asset_manifest();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||||
use futures::{stream::FuturesUnordered, StreamExt};
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
use std::{fs, path::Path, process::exit};
|
use std::{fs, path::Path, process::exit};
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ impl Autoformat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(raw) = self.raw {
|
if let Some(raw) = self.raw {
|
||||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
|
let indent = indentation_for(".")?;
|
||||||
|
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
|
||||||
println!("{}", inner);
|
println!("{}", inner);
|
||||||
} else {
|
} else {
|
||||||
// exit process with error
|
// exit process with error
|
||||||
@@ -46,17 +48,21 @@ impl Autoformat {
|
|||||||
|
|
||||||
// Format single file
|
// Format single file
|
||||||
if let Some(file) = self.file {
|
if let Some(file) = self.file {
|
||||||
let file_content = if file == "-" {
|
let file_content;
|
||||||
|
let indent;
|
||||||
|
if file == "-" {
|
||||||
|
indent = indentation_for(".")?;
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
std::io::stdin().read_to_string(&mut contents)?;
|
std::io::stdin().read_to_string(&mut contents)?;
|
||||||
Ok(contents)
|
file_content = Ok(contents);
|
||||||
} else {
|
} else {
|
||||||
fs::read_to_string(&file)
|
indent = indentation_for(".")?;
|
||||||
|
file_content = fs::read_to_string(&file);
|
||||||
};
|
};
|
||||||
|
|
||||||
match file_content {
|
match file_content {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
let edits = dioxus_autofmt::fmt_file(&s);
|
let edits = dioxus_autofmt::fmt_file(&s, indent);
|
||||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||||
if file == "-" {
|
if file == "-" {
|
||||||
print!("{}", out);
|
print!("{}", out);
|
||||||
@@ -93,6 +99,12 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||||||
let mut files_to_format = vec![];
|
let mut files_to_format = vec![];
|
||||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||||
|
|
||||||
|
if files_to_format.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let indent = indentation_for(&files_to_format[0])?;
|
||||||
|
|
||||||
let counts = files_to_format
|
let counts = files_to_format
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|file| {
|
.filter(|file| {
|
||||||
@@ -104,10 +116,11 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||||||
})
|
})
|
||||||
.map(|path| async {
|
.map(|path| async {
|
||||||
let _path = path.clone();
|
let _path = path.clone();
|
||||||
|
let _indent = indent.clone();
|
||||||
let res = tokio::spawn(async move {
|
let res = tokio::spawn(async move {
|
||||||
let contents = tokio::fs::read_to_string(&path).await?;
|
let contents = tokio::fs::read_to_string(&path).await?;
|
||||||
|
|
||||||
let edits = dioxus_autofmt::fmt_file(&contents);
|
let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone());
|
||||||
let len = edits.len();
|
let len = edits.len();
|
||||||
|
|
||||||
if !edits.is_empty() {
|
if !edits.is_empty() {
|
||||||
@@ -151,6 +164,49 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
|
||||||
|
let out = std::process::Command::new("cargo")
|
||||||
|
.args(["fmt", "--", "--print-config", "current"])
|
||||||
|
.arg(file_or_dir.as_ref())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::inherit())
|
||||||
|
.output()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return Err(Error::CargoError("cargo fmt failed".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = String::from_utf8_lossy(&out.stdout);
|
||||||
|
|
||||||
|
let hard_tabs = config
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.starts_with("hard_tabs "))
|
||||||
|
.and_then(|line| line.split_once('='))
|
||||||
|
.map(|(_, value)| value.trim() == "true")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
|
||||||
|
})?;
|
||||||
|
let tab_spaces = config
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.starts_with("tab_spaces "))
|
||||||
|
.and_then(|line| line.split_once('='))
|
||||||
|
.map(|(_, value)| value.trim().parse::<usize>())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
|
||||||
|
})?
|
||||||
|
.map_err(|_| {
|
||||||
|
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(IndentOptions::new(
|
||||||
|
if hard_tabs {
|
||||||
|
IndentType::Tabs
|
||||||
|
} else {
|
||||||
|
IndentType::Spaces
|
||||||
|
},
|
||||||
|
tab_spaces,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||||
let Ok(folder) = folder.read_dir() else {
|
let Ok(folder) = folder.read_dir() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ impl Build {
|
|||||||
.platform
|
.platform
|
||||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
// #[cfg(feature = "plugin")]
|
||||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||||
|
|
||||||
match platform {
|
match platform {
|
||||||
Platform::Web => {
|
Platform::Web => {
|
||||||
@@ -98,8 +98,8 @@ impl Build {
|
|||||||
)?;
|
)?;
|
||||||
file.write_all(temp.as_bytes())?;
|
file.write_all(temp.as_bytes())?;
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
// #[cfg(feature = "plugin")]
|
||||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl Default for DioxusConfig {
|
|||||||
},
|
},
|
||||||
proxy: Some(vec![]),
|
proxy: Some(vec![]),
|
||||||
watcher: WebWatcherConfig {
|
watcher: WebWatcherConfig {
|
||||||
watch_path: Some(vec![PathBuf::from("src")]),
|
watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
|
||||||
reload_html: Some(false),
|
reload_html: Some(false),
|
||||||
index_on_404: Some(true),
|
index_on_404: Some(true),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub enum Error {
|
|||||||
#[error("Cargo Error: {0}")]
|
#[error("Cargo Error: {0}")]
|
||||||
CargoError(String),
|
CargoError(String),
|
||||||
|
|
||||||
|
#[error("Couldn't retrieve cargo metadata")]
|
||||||
|
CargoMetadata(#[source] cargo_metadata::Error),
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
CustomError(String),
|
CustomError(String),
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,19 @@ pub fn set_up_logging() {
|
|||||||
message = message,
|
message = message,
|
||||||
));
|
));
|
||||||
})
|
})
|
||||||
.level(log::LevelFilter::Info)
|
.level(match std::env::var("DIOXUS_LOG") {
|
||||||
|
Ok(level) => match level.to_lowercase().as_str() {
|
||||||
|
"error" => log::LevelFilter::Error,
|
||||||
|
"warn" => log::LevelFilter::Warn,
|
||||||
|
"info" => log::LevelFilter::Info,
|
||||||
|
"debug" => log::LevelFilter::Debug,
|
||||||
|
"trace" => log::LevelFilter::Trace,
|
||||||
|
_ => {
|
||||||
|
panic!("Invalid log level: {}", level)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => log::LevelFilter::Info,
|
||||||
|
})
|
||||||
.chain(std::io::stdout())
|
.chain(std::io::stdout())
|
||||||
.apply()
|
.apply()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
+28
-39
@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
|
|||||||
|
|
||||||
use Commands::*;
|
use Commands::*;
|
||||||
|
|
||||||
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
|
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||||
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
|
let metadata = cargo_metadata::MetadataCommand::new()
|
||||||
|
.exec()
|
||||||
|
.map_err(Error::CargoMetadata)?;
|
||||||
|
let package = if let Some(bin) = bin {
|
||||||
|
metadata
|
||||||
|
.workspace_packages()
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.name == bin)
|
||||||
|
.ok_or(format!("no such package: {}", bin))
|
||||||
|
.map_err(Error::CargoError)?
|
||||||
|
} else {
|
||||||
|
metadata
|
||||||
|
.root_package()
|
||||||
|
.ok_or("no root package?".into())
|
||||||
|
.map_err(Error::CargoError)?
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(ref bin) = bin {
|
let crate_dir = package
|
||||||
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
|
.manifest_path
|
||||||
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
|
.parent()
|
||||||
|
.ok_or("couldn't take parent dir".into())
|
||||||
|
.map_err(Error::CargoError)?;
|
||||||
|
|
||||||
if let Some(workspace) = manifest.workspace {
|
Ok(crate_dir.into())
|
||||||
for item in workspace.members.iter() {
|
|
||||||
let path = PathBuf::from(item);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.ends_with(bin.clone()) {
|
|
||||||
return Ok(Some(path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the bin exists but we couldn't find it
|
|
||||||
if bin.is_some() {
|
|
||||||
return Err(Error::CargoError(
|
|
||||||
"The specified bin does not exist.".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -55,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let bin = get_bin(args.bin)?;
|
let bin = get_bin(args.bin)?;
|
||||||
|
|
||||||
let _dioxus_config = DioxusConfig::load(bin.clone())
|
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||||
@@ -72,15 +61,15 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||||
|
|
||||||
Build(opts) => opts
|
Build(opts) => opts
|
||||||
.build(bin.clone())
|
.build(Some(bin.clone()))
|
||||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||||
|
|
||||||
Clean(opts) => opts
|
Clean(opts) => opts
|
||||||
.clean(bin.clone())
|
.clean(Some(bin.clone()))
|
||||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||||
|
|
||||||
Serve(opts) => opts
|
Serve(opts) => opts
|
||||||
.serve(bin.clone())
|
.serve(Some(bin.clone()))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||||
|
|
||||||
@@ -93,7 +82,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||||
|
|
||||||
Bundle(opts) => opts
|
Bundle(opts) => opts
|
||||||
.bundle(bin.clone())
|
.bundle(Some(bin.clone()))
|
||||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
|
|||||||
|
|
||||||
let hot_reload_tx = broadcast::channel(100).0;
|
let hot_reload_tx = broadcast::channel(100).0;
|
||||||
|
|
||||||
clear_paths();
|
|
||||||
|
|
||||||
Some(HotReloadState {
|
Some(HotReloadState {
|
||||||
messages: hot_reload_tx.clone(),
|
messages: hot_reload_tx.clone(),
|
||||||
file_map: file_map.clone(),
|
file_map: file_map.clone(),
|
||||||
@@ -103,7 +101,14 @@ async fn serve<P: Platform + Send + 'static>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
|
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
|
||||||
match LocalSocketListener::bind("@dioxusin") {
|
let metadata = cargo_metadata::MetadataCommand::new()
|
||||||
|
.no_deps()
|
||||||
|
.exec()
|
||||||
|
.unwrap();
|
||||||
|
let target_dir = metadata.target_directory.as_std_path();
|
||||||
|
let path = target_dir.join("dioxusin");
|
||||||
|
clear_paths(&path);
|
||||||
|
match LocalSocketListener::bind(path) {
|
||||||
Ok(local_socket_stream) => {
|
Ok(local_socket_stream) => {
|
||||||
let aborted = Arc::new(Mutex::new(false));
|
let aborted = Arc::new(Mutex::new(false));
|
||||||
// States
|
// States
|
||||||
@@ -115,9 +120,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||||||
let file_map = hot_reload_state.file_map.clone();
|
let file_map = hot_reload_state.file_map.clone();
|
||||||
let channels = channels.clone();
|
let channels = channels.clone();
|
||||||
let aborted = aborted.clone();
|
let aborted = aborted.clone();
|
||||||
let _ = local_socket_stream.set_nonblocking(true);
|
|
||||||
move || {
|
move || {
|
||||||
loop {
|
loop {
|
||||||
|
//accept() will block the thread when local_socket_stream is in blocking mode (default)
|
||||||
match local_socket_stream.accept() {
|
match local_socket_stream.accept() {
|
||||||
Ok(mut connection) => {
|
Ok(mut connection) => {
|
||||||
// send any templates than have changed before the socket connected
|
// send any templates than have changed before the socket connected
|
||||||
@@ -175,17 +180,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_paths() {
|
fn clear_paths(file_socket_path: &std::path::Path) {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
// On unix, if you force quit the application, it can leave the file socket open
|
// On unix, if you force quit the application, it can leave the file socket open
|
||||||
// This will cause the local socket listener to fail to open
|
// This will cause the local socket listener to fail to open
|
||||||
// We check if the file socket is already open from an old session and then delete it
|
// We check if the file socket is already open from an old session and then delete it
|
||||||
let paths = ["./dioxusin", "./@dioxusin"];
|
|
||||||
for path in paths {
|
if file_socket_path.exists() {
|
||||||
let path = std::path::PathBuf::from(path);
|
let _ = std::fs::remove_file(file_socket_path);
|
||||||
if path.exists() {
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
|||||||
.watcher
|
.watcher
|
||||||
.watch_path
|
.watch_path
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
.unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
|
||||||
|
|
||||||
let watcher_config = config.clone();
|
let watcher_config = config.clone();
|
||||||
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
||||||
@@ -56,6 +56,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround for notify and vscode-like editor:
|
||||||
|
// when edit & save a file in vscode, there will be two notifications,
|
||||||
|
// the first one is a file with empty content.
|
||||||
|
// filter the empty file notification to avoid false rebuild during hot-reload
|
||||||
|
if let Ok(metadata) = fs::metadata(path) {
|
||||||
|
if metadata.len() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
||||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||||
messages.extend(msgs);
|
messages.extend(msgs);
|
||||||
@@ -122,12 +132,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for sub_path in allow_watch_path {
|
for sub_path in allow_watch_path {
|
||||||
watcher
|
if let Err(err) = watcher.watch(
|
||||||
.watch(
|
&config.crate_dir.join(sub_path),
|
||||||
&config.crate_dir.join(sub_path),
|
notify::RecursiveMode::Recursive,
|
||||||
notify::RecursiveMode::Recursive,
|
) {
|
||||||
)
|
log::error!("Failed to watch path: {}", err);
|
||||||
.unwrap();
|
}
|
||||||
}
|
}
|
||||||
Ok(watcher)
|
Ok(watcher)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ pub fn print_console_info(
|
|||||||
options: PrettierOptions,
|
options: PrettierOptions,
|
||||||
web_info: Option<WebServerInfo>,
|
web_info: Option<WebServerInfo>,
|
||||||
) {
|
) {
|
||||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
// Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
|
||||||
"cls"
|
if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
|
||||||
} else {
|
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||||
"clear"
|
"cls"
|
||||||
})
|
} else {
|
||||||
.output()
|
"clear"
|
||||||
{
|
})
|
||||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
.output()
|
||||||
} else {
|
{
|
||||||
// Try ANSI-Escape characters
|
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||||
print!("\x1b[2J\x1b[H");
|
} else {
|
||||||
|
// Try ANSI-Escape characters
|
||||||
|
print!("\x1b[2J\x1b[H");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
|
|||||||
dioxus-rsx = { workspace = true }
|
dioxus-rsx = { workspace = true }
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
constcat = "0.3.0"
|
constcat = "0.3.0"
|
||||||
|
prettyplease = "0.2.15"
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
|
|||||||
block: parse_quote! {
|
block: parse_quote! {
|
||||||
{
|
{
|
||||||
#[warn(non_snake_case)]
|
#[warn(non_snake_case)]
|
||||||
|
#[allow(clippy::inline_always)]
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
#inner_comp_fn
|
#inner_comp_fn
|
||||||
#inner_comp_ident (#cx_pat)
|
#inner_comp_ident (#cx_pat)
|
||||||
|
|||||||
@@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
|
|||||||
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
||||||
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
||||||
Ok(InlinePropsDeserializerOutput {
|
Ok(InlinePropsDeserializerOutput {
|
||||||
comp_fn: Self::get_function(component_body),
|
comp_fn: get_function(component_body),
|
||||||
props_struct: Self::get_props_struct(component_body),
|
props_struct: get_props_struct(component_body),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InlinePropsDeserializerArgs {
|
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
||||||
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
let ComponentBody { item_fn, .. } = component_body;
|
||||||
let ComponentBody { item_fn, .. } = component_body;
|
let ItemFn { vis, sig, .. } = item_fn;
|
||||||
let ItemFn { vis, sig, .. } = item_fn;
|
let Signature {
|
||||||
let Signature {
|
inputs,
|
||||||
inputs,
|
ident: fn_ident,
|
||||||
ident: fn_ident,
|
generics,
|
||||||
generics,
|
..
|
||||||
..
|
} = sig;
|
||||||
} = sig;
|
|
||||||
|
|
||||||
// Skip first arg since that's the context
|
// Skip first arg since that's the context
|
||||||
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
||||||
match f {
|
match f {
|
||||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||||
FnArg::Typed(pt) => {
|
FnArg::Typed(pt) => {
|
||||||
let arg_pat = &pt.pat; // Pattern (identifier)
|
let arg_pat = &pt.pat; // Pattern (identifier)
|
||||||
let arg_colon = &pt.colon_token;
|
let arg_colon = &pt.colon_token;
|
||||||
let arg_ty = &pt.ty; // Type
|
let arg_ty = &pt.ty; // Type
|
||||||
let arg_attrs = &pt.attrs; // Attributes
|
let arg_attrs = &pt.attrs; // Attributes
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#(#arg_attrs)
|
#(#arg_attrs)
|
||||||
*
|
*
|
||||||
#vis #arg_pat #arg_colon #arg_ty
|
#vis #arg_pat #arg_colon #arg_ty
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
|
||||||
|
|
||||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
|
||||||
Some(lt)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let struct_attrs = if first_lifetime.is_some() {
|
|
||||||
quote! { #[derive(Props)] }
|
|
||||||
} else {
|
|
||||||
quote! { #[derive(Props, PartialEq)] }
|
|
||||||
};
|
|
||||||
|
|
||||||
let struct_generics = if first_lifetime.is_some() {
|
|
||||||
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
|
||||||
.item_fn
|
|
||||||
.sig
|
|
||||||
.generics
|
|
||||||
.params
|
|
||||||
.iter()
|
|
||||||
.map(|it| match it {
|
|
||||||
GenericParam::Type(tp) => {
|
|
||||||
let mut tp = tp.clone();
|
|
||||||
tp.bounds.push(parse_quote!( 'a ));
|
|
||||||
|
|
||||||
GenericParam::Type(tp)
|
|
||||||
}
|
|
||||||
_ => it.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
quote! { <#struct_generics> }
|
|
||||||
} else {
|
|
||||||
quote! { #generics }
|
|
||||||
};
|
|
||||||
|
|
||||||
parse_quote! {
|
|
||||||
#struct_attrs
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#vis struct #struct_ident #struct_generics
|
|
||||||
{
|
|
||||||
#(#struct_fields),*
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||||
let ComponentBody {
|
|
||||||
item_fn,
|
|
||||||
cx_pat_type,
|
|
||||||
..
|
|
||||||
} = component_body;
|
|
||||||
let ItemFn {
|
|
||||||
attrs: fn_attrs,
|
|
||||||
vis,
|
|
||||||
sig,
|
|
||||||
block: fn_block,
|
|
||||||
} = item_fn;
|
|
||||||
let Signature {
|
|
||||||
inputs,
|
|
||||||
ident: fn_ident,
|
|
||||||
generics,
|
|
||||||
output: fn_output,
|
|
||||||
asyncness,
|
|
||||||
..
|
|
||||||
} = sig;
|
|
||||||
let Generics { where_clause, .. } = generics;
|
|
||||||
|
|
||||||
let cx_pat = &cx_pat_type.pat;
|
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
Some(lt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Skip first arg since that's the context
|
let struct_attrs = if first_lifetime.is_some() {
|
||||||
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
quote! { #[derive(Props)] }
|
||||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
} else {
|
||||||
FnArg::Typed(t) => Some(&t.pat),
|
quote! { #[derive(Props, PartialEq)] }
|
||||||
});
|
};
|
||||||
|
|
||||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
let struct_generics = if first_lifetime.is_some() {
|
||||||
Some(lt)
|
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||||
} else {
|
.item_fn
|
||||||
None
|
.sig
|
||||||
};
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|it| match it {
|
||||||
|
GenericParam::Type(tp) => {
|
||||||
|
let mut tp = tp.clone();
|
||||||
|
tp.bounds.push(parse_quote!( 'a ));
|
||||||
|
|
||||||
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
GenericParam::Type(tp)
|
||||||
(quote! { #lt, }, generics.clone())
|
}
|
||||||
} else {
|
_ => it.clone(),
|
||||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut fn_generics = generics.clone();
|
quote! { <#struct_generics> }
|
||||||
fn_generics
|
} else {
|
||||||
.params
|
quote! { #generics }
|
||||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
};
|
||||||
|
|
||||||
(quote! { #lifetime, }, fn_generics)
|
parse_quote! {
|
||||||
};
|
#struct_attrs
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
let generics_no_bounds = {
|
#vis struct #struct_ident #struct_generics
|
||||||
let mut generics = generics.clone();
|
{
|
||||||
generics.params = generics
|
#(#struct_fields),*
|
||||||
.params
|
|
||||||
.iter()
|
|
||||||
.map(|it| match it {
|
|
||||||
GenericParam::Type(tp) => {
|
|
||||||
let mut tp = tp.clone();
|
|
||||||
tp.bounds.clear();
|
|
||||||
|
|
||||||
GenericParam::Type(tp)
|
|
||||||
}
|
|
||||||
_ => it.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
generics
|
|
||||||
};
|
|
||||||
|
|
||||||
parse_quote! {
|
|
||||||
#(#fn_attrs)*
|
|
||||||
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
|
|
||||||
#where_clause
|
|
||||||
{
|
|
||||||
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
|
|
||||||
#fn_block
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||||
|
if inputs.len() <= 1 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let arg_docs = inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| match f {
|
||||||
|
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||||
|
FnArg::Typed(pt) => {
|
||||||
|
let arg_doc = pt
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|attr| {
|
||||||
|
// TODO: Error reporting
|
||||||
|
// Check if the path of the attribute is "doc"
|
||||||
|
if !is_attr_doc(attr) {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Meta::NameValue(meta_name_value) = &attr.meta else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Expr::Lit(doc_lit) = &meta_name_value.value else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(doc_lit_str.value())
|
||||||
|
})
|
||||||
|
.fold(String::new(), |mut doc, next_doc_line| {
|
||||||
|
doc.push('\n');
|
||||||
|
doc.push_str(&next_doc_line);
|
||||||
|
doc
|
||||||
|
});
|
||||||
|
|
||||||
|
Some((
|
||||||
|
&pt.pat,
|
||||||
|
&pt.ty,
|
||||||
|
pt.attrs.iter().find_map(|attr| {
|
||||||
|
if attr.path() != &parse_quote!(deprecated) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Err(e) => panic!("{}", e.to_string()),
|
||||||
|
Ok(v) => Some(v),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
arg_doc,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut props_docs = Vec::with_capacity(5);
|
||||||
|
let props_def_link = fn_ident.to_string() + "Props";
|
||||||
|
let header =
|
||||||
|
format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
|
||||||
|
|
||||||
|
props_docs.push(parse_quote! {
|
||||||
|
#[doc = #header]
|
||||||
|
});
|
||||||
|
|
||||||
|
for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
|
||||||
|
let arg_name = arg_name.into_token_stream().to_string();
|
||||||
|
let arg_type = crate::utils::format_type_string(arg_type);
|
||||||
|
|
||||||
|
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||||
|
.replace("\n\n", "</p><p>");
|
||||||
|
let prop_def_link = format!("{props_def_link}::{arg_name}");
|
||||||
|
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
|
||||||
|
|
||||||
|
if let Some(deprecation) = deprecation {
|
||||||
|
arg_doc.push_str("<p>👎 Deprecated");
|
||||||
|
|
||||||
|
if let Some(since) = deprecation.since {
|
||||||
|
arg_doc.push_str(&format!(" since {since}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(note) = deprecation.note {
|
||||||
|
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " ");
|
||||||
|
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " ");
|
||||||
|
|
||||||
|
arg_doc.push_str(&format!(": {note}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
arg_doc.push_str("</p>");
|
||||||
|
|
||||||
|
if !input_arg_doc.is_empty() {
|
||||||
|
arg_doc.push_str("<hr/>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !input_arg_doc.is_empty() {
|
||||||
|
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
props_docs.push(parse_quote! {
|
||||||
|
#[doc = #arg_doc]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
props_docs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
||||||
|
let ComponentBody {
|
||||||
|
item_fn,
|
||||||
|
cx_pat_type,
|
||||||
|
..
|
||||||
|
} = component_body;
|
||||||
|
let ItemFn {
|
||||||
|
attrs: fn_attrs,
|
||||||
|
vis,
|
||||||
|
sig,
|
||||||
|
block: fn_block,
|
||||||
|
} = item_fn;
|
||||||
|
let Signature {
|
||||||
|
inputs,
|
||||||
|
ident: fn_ident,
|
||||||
|
generics,
|
||||||
|
output: fn_output,
|
||||||
|
asyncness,
|
||||||
|
..
|
||||||
|
} = sig;
|
||||||
|
let Generics { where_clause, .. } = generics;
|
||||||
|
|
||||||
|
let cx_pat = &cx_pat_type.pat;
|
||||||
|
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||||
|
|
||||||
|
// Skip first arg since that's the context
|
||||||
|
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
||||||
|
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||||
|
FnArg::Typed(pt) => Some(&pt.pat),
|
||||||
|
});
|
||||||
|
|
||||||
|
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||||
|
Some(lt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
||||||
|
(quote! { #lt, }, generics.clone())
|
||||||
|
} else {
|
||||||
|
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||||
|
|
||||||
|
let mut fn_generics = generics.clone();
|
||||||
|
fn_generics
|
||||||
|
.params
|
||||||
|
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||||
|
|
||||||
|
(quote! { #lifetime, }, fn_generics)
|
||||||
|
};
|
||||||
|
|
||||||
|
let generics_no_bounds = {
|
||||||
|
let mut generics = generics.clone();
|
||||||
|
generics.params = generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|it| match it {
|
||||||
|
GenericParam::Type(tp) => {
|
||||||
|
let mut tp = tp.clone();
|
||||||
|
tp.bounds.clear();
|
||||||
|
|
||||||
|
GenericParam::Type(tp)
|
||||||
|
}
|
||||||
|
_ => it.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
generics
|
||||||
|
};
|
||||||
|
|
||||||
|
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
|
||||||
|
|
||||||
|
parse_quote! {
|
||||||
|
#(#fn_attrs)*
|
||||||
|
#(#props_docs)*
|
||||||
|
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
|
||||||
|
#where_clause
|
||||||
|
{
|
||||||
|
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
|
||||||
|
#fn_block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the attribute is a `#[doc]` attribute.
|
||||||
|
fn is_attr_doc(attr: &Attribute) -> bool {
|
||||||
|
attr.path() == &parse_quote!(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keep_up_to_n_consecutive_chars(
|
||||||
|
input: &str,
|
||||||
|
n_of_consecutive_chars_allowed: usize,
|
||||||
|
target_char: char,
|
||||||
|
) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut prev_char: Option<char> = None;
|
||||||
|
let mut consecutive_count = 0;
|
||||||
|
|
||||||
|
for c in input.chars() {
|
||||||
|
match prev_char {
|
||||||
|
Some(prev) if c == target_char && prev == target_char => {
|
||||||
|
if consecutive_count < n_of_consecutive_chars_allowed {
|
||||||
|
output.push(c);
|
||||||
|
consecutive_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
output.push(c);
|
||||||
|
prev_char = Some(c);
|
||||||
|
consecutive_count = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
|
|||||||
mod component_body;
|
mod component_body;
|
||||||
mod component_body_deserializers;
|
mod component_body_deserializers;
|
||||||
mod props;
|
mod props;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
// mod rsx;
|
// mod rsx;
|
||||||
use crate::component_body::ComponentBody;
|
use crate::component_body::ComponentBody;
|
||||||
|
|||||||
@@ -243,10 +243,6 @@ mod field_info {
|
|||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
|
||||||
type_from_inside_option(self.ty, check_option_name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@@ -551,18 +547,16 @@ mod struct_info {
|
|||||||
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
||||||
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
||||||
});
|
});
|
||||||
let phantom_generics = self.generics.params.iter().map(|param| match param {
|
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
|
||||||
syn::GenericParam::Lifetime(lifetime) => {
|
syn::GenericParam::Lifetime(lifetime) => {
|
||||||
let lifetime = &lifetime.lifetime;
|
let lifetime = &lifetime.lifetime;
|
||||||
quote!(::core::marker::PhantomData<&#lifetime ()>)
|
Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
|
||||||
}
|
}
|
||||||
syn::GenericParam::Type(ty) => {
|
syn::GenericParam::Type(ty) => {
|
||||||
let ty = &ty.ident;
|
let ty = &ty.ident;
|
||||||
quote!(::core::marker::PhantomData<#ty>)
|
Some(quote!(::core::marker::PhantomData<#ty>))
|
||||||
}
|
|
||||||
syn::GenericParam::Const(_cnst) => {
|
|
||||||
quote!()
|
|
||||||
}
|
}
|
||||||
|
syn::GenericParam::Const(_cnst) => None,
|
||||||
});
|
});
|
||||||
let builder_method_doc = match self.builder_attr.builder_method_doc {
|
let builder_method_doc = match self.builder_attr.builder_method_doc {
|
||||||
Some(ref doc) => quote!(#doc),
|
Some(ref doc) => quote!(#doc),
|
||||||
@@ -633,7 +627,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
#[doc = #builder_method_doc]
|
#[doc = #builder_method_doc]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code, clippy::type_complexity)]
|
||||||
#vis fn builder() -> #builder_name #generics_with_empty {
|
#vis fn builder() -> #builder_name #generics_with_empty {
|
||||||
#builder_name {
|
#builder_name {
|
||||||
fields: #empties_tuple,
|
fields: #empties_tuple,
|
||||||
@@ -701,6 +695,14 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||||
|
let FieldInfo {
|
||||||
|
name: field_name,
|
||||||
|
ty: field_type,
|
||||||
|
..
|
||||||
|
} = field;
|
||||||
|
if *field_name == "key" {
|
||||||
|
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
|
||||||
|
}
|
||||||
let StructInfo {
|
let StructInfo {
|
||||||
ref builder_name, ..
|
ref builder_name, ..
|
||||||
} = *self;
|
} = *self;
|
||||||
@@ -715,11 +717,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
});
|
});
|
||||||
let reconstructing = self.included_fields().map(|f| f.name);
|
let reconstructing = self.included_fields().map(|f| f.name);
|
||||||
|
|
||||||
let FieldInfo {
|
|
||||||
name: field_name,
|
|
||||||
ty: field_type,
|
|
||||||
..
|
|
||||||
} = field;
|
|
||||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||||
.generics
|
.generics
|
||||||
.params
|
.params
|
||||||
@@ -782,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
None => quote!(),
|
None => quote!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
let arg_type = field_type;
|
||||||
// nesting is different so we have to do this little dance.
|
let (arg_type, arg_expr) =
|
||||||
let arg_type = if field.builder_attr.strip_option {
|
if field.builder_attr.auto_into || field.builder_attr.strip_option {
|
||||||
field.type_from_inside_option(false).ok_or_else(|| {
|
(
|
||||||
Error::new_spanned(
|
quote!(impl ::core::convert::Into<#arg_type>),
|
||||||
field_type,
|
quote!(#field_name.into()),
|
||||||
"can't `strip_option` - field is not `Option<...>`",
|
|
||||||
)
|
)
|
||||||
})?
|
} else {
|
||||||
} else {
|
(quote!(#arg_type), quote!(#field_name))
|
||||||
field_type
|
};
|
||||||
};
|
|
||||||
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
|
|
||||||
(
|
|
||||||
quote!(impl ::core::convert::Into<#arg_type>),
|
|
||||||
quote!(#field_name.into()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(quote!(#arg_type), quote!(#field_name))
|
|
||||||
};
|
|
||||||
let arg_expr = if field.builder_attr.strip_option {
|
|
||||||
quote!(Some(#arg_expr))
|
|
||||||
} else {
|
|
||||||
arg_expr
|
|
||||||
};
|
|
||||||
|
|
||||||
let repeated_fields_error_type_name = syn::Ident::new(
|
let repeated_fields_error_type_name = syn::Ident::new(
|
||||||
&format!(
|
&format!(
|
||||||
@@ -822,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
||||||
#doc
|
#doc
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
|
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
|
||||||
let #field_name = (#arg_expr,);
|
let #field_name = (#arg_expr,);
|
||||||
let ( #(#descructuring,)* ) = self.fields;
|
let ( #(#descructuring,)* ) = self.fields;
|
||||||
@@ -840,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
#[deprecated(
|
#[deprecated(
|
||||||
note = #repeated_fields_error_message
|
note = #repeated_fields_error_message
|
||||||
)]
|
)]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
|
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use quote::ToTokens;
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
|
||||||
|
|
||||||
|
const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
|
||||||
|
const FORMATTED_TYPE_END: &str = "= todo!();";
|
||||||
|
|
||||||
|
/// Attempts to convert the given literal to a string.
|
||||||
|
/// Converts ints and floats to their base 10 counterparts.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
|
||||||
|
/// and the byte string could not be converted to UTF-8.
|
||||||
|
pub fn lit_to_string(lit: Lit) -> Option<String> {
|
||||||
|
match lit {
|
||||||
|
Lit::Str(l) => Some(l.value()),
|
||||||
|
Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
|
||||||
|
Lit::Byte(l) => Some(String::from(l.value() as char)),
|
||||||
|
Lit::Char(l) => Some(l.value().to_string()),
|
||||||
|
Lit::Int(l) => Some(l.base10_digits().to_string()),
|
||||||
|
Lit::Float(l) => Some(l.base10_digits().to_string()),
|
||||||
|
Lit::Bool(l) => Some(l.value().to_string()),
|
||||||
|
Lit::Verbatim(_) => None,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_type_string(ty: &Type) -> String {
|
||||||
|
let ty_unformatted = ty.into_token_stream().to_string();
|
||||||
|
let ty_unformatted = ty_unformatted.trim();
|
||||||
|
|
||||||
|
// This should always be valid syntax.
|
||||||
|
// Not Rust code, but syntax, which is the only thing that `syn` cares about.
|
||||||
|
let Ok(file_unformatted) = syn::parse_file(&format!(
|
||||||
|
"{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
|
||||||
|
)) else {
|
||||||
|
return ty_unformatted.to_string();
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_formatted = prettyplease::unparse(&file_unformatted);
|
||||||
|
|
||||||
|
let file_trimmed = file_formatted.trim();
|
||||||
|
let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
|
||||||
|
let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
|
||||||
|
let ty_formatted = end_removed.trim();
|
||||||
|
|
||||||
|
ty_formatted.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the `#[deprecated]` attribute.
|
||||||
|
///
|
||||||
|
/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DeprecatedAttribute {
|
||||||
|
pub since: Option<String>,
|
||||||
|
pub note: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeprecatedAttribute {
|
||||||
|
/// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
|
||||||
|
pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
|
||||||
|
if meta.path() != &parse_quote!(deprecated) {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
meta.span(),
|
||||||
|
"attribute path is not `deprecated`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match &meta {
|
||||||
|
Meta::Path(_) => Ok(Self::default()),
|
||||||
|
Meta::NameValue(name_value) => {
|
||||||
|
let Expr::Lit(expr_lit) = &name_value.value else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
name_value.span(),
|
||||||
|
"literal in `deprecated` value must be a string",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
since: None,
|
||||||
|
note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Meta::List(list) => {
|
||||||
|
let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
since: parsed.since.map(|s| s.trim().to_string()),
|
||||||
|
note: parsed.note.map(|s| s.trim().to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod kw {
|
||||||
|
use syn::custom_keyword;
|
||||||
|
custom_keyword!(since);
|
||||||
|
custom_keyword!(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeprecatedAttributeArgsParser {
|
||||||
|
since: Option<String>,
|
||||||
|
note: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for DeprecatedAttributeArgsParser {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let mut since: Option<String> = None;
|
||||||
|
let mut note: Option<String> = None;
|
||||||
|
|
||||||
|
if input.peek(kw::since) {
|
||||||
|
input.parse::<kw::since>()?;
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
|
||||||
|
since = lit_to_string(input.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.peek(Token![,]) && input.peek2(kw::note) {
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
input.parse::<kw::note>()?;
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
|
||||||
|
note = lit_to_string(input.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { since, note })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,17 +164,11 @@ impl VirtualDom {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Now that all the references are gone, we can safely drop our own references in our listeners.
|
// Now that all the references are gone, we can safely drop our own references in our listeners.
|
||||||
let mut listeners = scope.attributes_to_drop.borrow_mut();
|
let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
|
||||||
listeners.drain(..).for_each(|listener| {
|
listeners.drain(..).for_each(|listener| {
|
||||||
let listener = unsafe { &*listener };
|
let listener = unsafe { &*listener };
|
||||||
match &listener.value {
|
if let AttributeValue::Listener(l) = &listener.value {
|
||||||
AttributeValue::Listener(l) => {
|
_ = l.take();
|
||||||
_ = l.take();
|
|
||||||
}
|
|
||||||
AttributeValue::Any(a) => {
|
|
||||||
_ = a.take();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
use crate::nodes::RenderReturn;
|
use crate::nodes::RenderReturn;
|
||||||
|
use crate::{Attribute, AttributeValue, VComponent};
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::cell::{Cell, UnsafeCell};
|
use std::cell::{Cell, UnsafeCell};
|
||||||
|
|
||||||
pub(crate) struct BumpFrame {
|
pub(crate) struct BumpFrame {
|
||||||
pub bump: UnsafeCell<Bump>,
|
pub bump: UnsafeCell<Bump>,
|
||||||
pub node: Cell<*const RenderReturn<'static>>,
|
pub node: Cell<*const RenderReturn<'static>>,
|
||||||
|
|
||||||
|
// The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
|
||||||
|
pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
|
||||||
|
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BumpFrame {
|
impl BumpFrame {
|
||||||
@@ -13,6 +19,8 @@ impl BumpFrame {
|
|||||||
Self {
|
Self {
|
||||||
bump: UnsafeCell::new(bump),
|
bump: UnsafeCell::new(bump),
|
||||||
node: Cell::new(std::ptr::null()),
|
node: Cell::new(std::ptr::null()),
|
||||||
|
attributes_to_drop_before_reset: Default::default(),
|
||||||
|
props_to_drop_before_reset: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,8 +39,38 @@ impl BumpFrame {
|
|||||||
unsafe { &*self.bump.get() }
|
unsafe { &*self.bump.get() }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::mut_from_ref)]
|
pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
|
||||||
pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
|
self.attributes_to_drop_before_reset
|
||||||
unsafe { &mut *self.bump.get() }
|
.borrow_mut()
|
||||||
|
.push(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the bump allocator and drop all the attributes and props that were allocated in it.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
|
||||||
|
pub(crate) unsafe fn reset(&self) {
|
||||||
|
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
|
||||||
|
attributes.drain(..).for_each(|attribute| {
|
||||||
|
let attribute = unsafe { &*attribute };
|
||||||
|
if let AttributeValue::Any(l) = &attribute.value {
|
||||||
|
_ = l.take();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut props = self.props_to_drop_before_reset.borrow_mut();
|
||||||
|
props.drain(..).for_each(|prop| {
|
||||||
|
let prop = unsafe { &*prop };
|
||||||
|
_ = prop.props.borrow_mut().take();
|
||||||
|
});
|
||||||
|
unsafe {
|
||||||
|
let bump = &mut *self.bump.get();
|
||||||
|
bump.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BumpFrame {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { self.reset() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +560,7 @@ impl<'b> VirtualDom {
|
|||||||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||||
// create the new children afresh.
|
// create the new children afresh.
|
||||||
if shared_keys.is_empty() {
|
if shared_keys.is_empty() {
|
||||||
if old.get(0).is_some() {
|
if old.first().is_some() {
|
||||||
self.remove_nodes(&old[1..]);
|
self.remove_nodes(&old[1..]);
|
||||||
self.replace(&old[0], new);
|
self.replace(&old[0], new);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -107,8 +107,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
|
|
||||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||||
///
|
///
|
||||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||||
|
|||||||
@@ -23,8 +23,37 @@ use crate::{innerlude::VNode, ScopeState};
|
|||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
/// LazyNodes::new(|f| {
|
||||||
|
/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
|
||||||
|
/// name: "main.rs:5:5:20", // Source location of the template for hot reloading
|
||||||
|
/// roots: &[
|
||||||
|
/// dioxus::core::TemplateNode::Element {
|
||||||
|
/// tag: dioxus_elements::div::TAG_NAME,
|
||||||
|
/// namespace: dioxus_elements::div::NAME_SPACE,
|
||||||
|
/// attrs: &[],
|
||||||
|
/// children: &[],
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// node_paths: &[],
|
||||||
|
/// attr_paths: &[],
|
||||||
|
/// };
|
||||||
|
/// dioxus::core::VNode {
|
||||||
|
/// parent: None,
|
||||||
|
/// key: None,
|
||||||
|
/// template: std::cell::Cell::new(TEMPLATE),
|
||||||
|
/// root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
|
||||||
|
/// 1usize,
|
||||||
|
/// f.bump(),
|
||||||
|
/// )
|
||||||
|
/// .into(),
|
||||||
|
/// dynamic_nodes: f.bump().alloc([]),
|
||||||
|
/// dynamic_attrs: f.bump().alloc([]),
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
|
||||||
|
|
||||||
pub struct LazyNodes<'a, 'b> {
|
pub struct LazyNodes<'a, 'b> {
|
||||||
#[cfg(not(miri))]
|
#[cfg(not(miri))]
|
||||||
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||||
@@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// Call the closure with the given factory to produce real [`VNode`].
|
/// Call the closure with the given factory to produce real [`VNode`].
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, ignore
|
||||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
/// let f = LazyNodes::new(/* Closure for creating VNodes */);
|
||||||
///
|
///
|
||||||
/// let node = f.call(cac);
|
/// let node = f.call(cac);
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -91,9 +91,9 @@ pub mod prelude {
|
|||||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||||
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
||||||
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
||||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
|
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
|
||||||
Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||||
TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ pub enum Mutation<'a> {
|
|||||||
id: ElementId,
|
id: ElementId,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create an placeholder int he DOM that we will use later.
|
/// Create a placeholder in the DOM that we will use later.
|
||||||
///
|
///
|
||||||
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
||||||
CreatePlaceholder {
|
CreatePlaceholder {
|
||||||
|
|||||||
@@ -707,7 +707,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
|||||||
impl IntoDynNode<'_> for String {
|
impl IntoDynNode<'_> for String {
|
||||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||||
DynamicNode::Text(VText {
|
DynamicNode::Text(VText {
|
||||||
value: cx.bump().alloc(self),
|
value: cx.bump().alloc_str(&self),
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -791,6 +791,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoAttributeValue<'a> for String {
|
||||||
|
fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
|
||||||
|
AttributeValue::Text(cx.alloc_str(&self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> IntoAttributeValue<'a> for f64 {
|
impl<'a> IntoAttributeValue<'a> for f64 {
|
||||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||||
AttributeValue::Float(self)
|
AttributeValue::Float(self)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl VirtualDom {
|
|||||||
hook_idx: Default::default(),
|
hook_idx: Default::default(),
|
||||||
|
|
||||||
borrowed_props: Default::default(),
|
borrowed_props: Default::default(),
|
||||||
attributes_to_drop: Default::default(),
|
attributes_to_drop_before_render: Default::default(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let context =
|
let context =
|
||||||
@@ -54,7 +54,7 @@ impl VirtualDom {
|
|||||||
|
|
||||||
let new_nodes = unsafe {
|
let new_nodes = unsafe {
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id.0];
|
||||||
scope.previous_frame().bump_mut().reset();
|
scope.previous_frame().reset();
|
||||||
|
|
||||||
scope.context().suspended.set(false);
|
scope.context().suspended.set(false);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ pub struct ScopeState {
|
|||||||
pub(crate) hook_idx: Cell<usize>,
|
pub(crate) hook_idx: Cell<usize>,
|
||||||
|
|
||||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
|
||||||
|
|
||||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||||
}
|
}
|
||||||
@@ -348,25 +348,36 @@ impl<'src> ScopeState {
|
|||||||
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
||||||
let element = rsx.call(self);
|
let element = rsx.call(self);
|
||||||
|
|
||||||
let mut listeners = self.attributes_to_drop.borrow_mut();
|
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
|
||||||
for attr in element.dynamic_attrs {
|
for attr in element.dynamic_attrs {
|
||||||
match attr.value {
|
match attr.value {
|
||||||
AttributeValue::Any(_) | AttributeValue::Listener(_) => {
|
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
|
||||||
|
AttributeValue::Listener(_) => {
|
||||||
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||||
listeners.push(unbounded);
|
listeners.push(unbounded);
|
||||||
}
|
}
|
||||||
|
// We need to drop any values manually to make sure that their drop implementation is called before the next render
|
||||||
|
AttributeValue::Any(_) => {
|
||||||
|
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||||
|
self.previous_frame().add_attribute_to_drop(unbounded);
|
||||||
|
}
|
||||||
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut props = self.borrowed_props.borrow_mut();
|
let mut props = self.borrowed_props.borrow_mut();
|
||||||
|
let mut drop_props = self
|
||||||
|
.previous_frame()
|
||||||
|
.props_to_drop_before_reset
|
||||||
|
.borrow_mut();
|
||||||
for node in element.dynamic_nodes {
|
for node in element.dynamic_nodes {
|
||||||
if let DynamicNode::Component(comp) = node {
|
if let DynamicNode::Component(comp) = node {
|
||||||
|
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||||
if !comp.static_props {
|
if !comp.static_props {
|
||||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
|
||||||
props.push(unbounded);
|
props.push(unbounded);
|
||||||
}
|
}
|
||||||
|
drop_props.push(unbounded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ devtools = ["wry/devtools"]
|
|||||||
tray = ["wry/tray"]
|
tray = ["wry/tray"]
|
||||||
dox = ["wry/dox"]
|
dox = ["wry/dox"]
|
||||||
hot-reload = ["dioxus-hot-reload"]
|
hot-reload = ["dioxus-hot-reload"]
|
||||||
|
gnu = []
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
fn main() {
|
||||||
|
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
|
||||||
|
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
|
||||||
|
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
|
||||||
|
&& !cfg!(feature = "gnu")
|
||||||
|
{
|
||||||
|
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ use wry::{application::window::WindowId, webview::WebContext};
|
|||||||
///
|
///
|
||||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
@@ -78,11 +78,12 @@ pub fn launch(root: Component) {
|
|||||||
///
|
///
|
||||||
/// You can configure the WebView window with a configuration closure
|
/// You can configure the WebView window with a configuration closure
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_desktop::*;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
|
/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn app(cx: Scope) -> Element {
|
/// fn app(cx: Scope) -> Element {
|
||||||
@@ -101,8 +102,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
|||||||
///
|
///
|
||||||
/// You can configure the WebView window with a configuration closure
|
/// You can configure the WebView window with a configuration closure
|
||||||
///
|
///
|
||||||
/// ```rust, ignore
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_desktop::Config;
|
||||||
///
|
///
|
||||||
/// fn main() {
|
/// fn main() {
|
||||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
||||||
@@ -165,6 +167,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||||||
// iOS panics if we create a window before the event loop is started
|
// iOS panics if we create a window before the event loop is started
|
||||||
let props = Rc::new(Cell::new(Some(props)));
|
let props = Rc::new(Cell::new(Some(props)));
|
||||||
let cfg = Rc::new(Cell::new(Some(cfg)));
|
let cfg = Rc::new(Cell::new(Some(cfg)));
|
||||||
|
let mut is_visible_before_start = true;
|
||||||
|
|
||||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||||
*control_flow = ControlFlow::Wait;
|
*control_flow = ControlFlow::Wait;
|
||||||
@@ -214,6 +217,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||||||
// Create a dom
|
// Create a dom
|
||||||
let dom = VirtualDom::new_with_props(root, props);
|
let dom = VirtualDom::new_with_props(root, props);
|
||||||
|
|
||||||
|
is_visible_before_start = cfg.window.window.visible;
|
||||||
|
|
||||||
let handler = create_new_window(
|
let handler = create_new_window(
|
||||||
cfg,
|
cfg,
|
||||||
event_loop,
|
event_loop,
|
||||||
@@ -327,6 +332,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||||
let view = webviews.get_mut(&event.1).unwrap();
|
let view = webviews.get_mut(&event.1).unwrap();
|
||||||
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
||||||
|
view.desktop_context
|
||||||
|
.webview
|
||||||
|
.window()
|
||||||
|
.set_visible(is_visible_before_start);
|
||||||
}
|
}
|
||||||
|
|
||||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub fn build(
|
|||||||
proxy: EventLoopProxy<UserWindowEvent>,
|
proxy: EventLoopProxy<UserWindowEvent>,
|
||||||
) -> (WebView, WebContext) {
|
) -> (WebView, WebContext) {
|
||||||
let builder = cfg.window.clone();
|
let builder = cfg.window.clone();
|
||||||
let window = builder.build(event_loop).unwrap();
|
let window = builder.with_visible(false).build(event_loop).unwrap();
|
||||||
let file_handler = cfg.file_drop_handler.take();
|
let file_handler = cfg.file_drop_handler.take();
|
||||||
let custom_head = cfg.custom_head.clone();
|
let custom_head = cfg.custom_head.clone();
|
||||||
let index_file = cfg.custom_index.clone();
|
let index_file = cfg.custom_index.clone();
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element {
|
|||||||
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
|
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
|
||||||
// disable templates so that every node has an id and can be queried
|
// disable templates so that every node has an id and can be queried
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div{
|
div {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
background_color: "hsl({hue}, 70%, {brightness}%)",
|
background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||||
onmousemove: move |evt| {
|
onmousemove: move |evt| {
|
||||||
if let RenderReturn::Ready(node) = cx.root_node() {
|
if let RenderReturn::Ready(node) = cx.root_node() {
|
||||||
if let Some(id) = node.root_ids.borrow().get(0).cloned() {
|
if let Some(id) = node.root_ids.borrow().first().cloned() {
|
||||||
let node = tui_query.get(mapping.get_node_id(id).unwrap());
|
let node = tui_query.get(mapping.get_node_id(id).unwrap());
|
||||||
let Size{width, height} = node.size().unwrap();
|
let Size { width, height } = node.size().unwrap();
|
||||||
let pos = evt.inner().element_coordinates();
|
let pos = evt.inner().element_coordinates();
|
||||||
hue.set((pos.x as f32/width as f32)*255.0);
|
hue.set((pos.x as f32 / width as f32) * 255.0);
|
||||||
brightness.set((pos.y as f32/height as f32)*100.0);
|
brightness.set((pos.y as f32 / height as f32) * 100.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hsl({hue}, 70%, {brightness}%)",
|
"hsl({hue}, 70%, {brightness}%)"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,39 @@
|
|||||||
//! This file exports functions into the vscode extension
|
//! This file exports functions into the vscode extension
|
||||||
|
|
||||||
use dioxus_autofmt::FormattedBlock;
|
use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn format_rsx(raw: String) -> String {
|
pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
let block = dioxus_autofmt::fmt_block(
|
||||||
|
&raw,
|
||||||
|
0,
|
||||||
|
IndentOptions::new(
|
||||||
|
if use_tabs {
|
||||||
|
IndentType::Tabs
|
||||||
|
} else {
|
||||||
|
IndentType::Spaces
|
||||||
|
},
|
||||||
|
indent_size,
|
||||||
|
),
|
||||||
|
);
|
||||||
block.unwrap()
|
block.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn format_selection(raw: String) -> String {
|
pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
let block = dioxus_autofmt::fmt_block(
|
||||||
|
&raw,
|
||||||
|
0,
|
||||||
|
IndentOptions::new(
|
||||||
|
if use_tabs {
|
||||||
|
IndentType::Tabs
|
||||||
|
} else {
|
||||||
|
IndentType::Spaces
|
||||||
|
},
|
||||||
|
indent_size,
|
||||||
|
),
|
||||||
|
);
|
||||||
block.unwrap()
|
block.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +57,18 @@ impl FormatBlockInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn format_file(contents: String) -> FormatBlockInstance {
|
pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
|
||||||
let _edits = dioxus_autofmt::fmt_file(&contents);
|
let _edits = dioxus_autofmt::fmt_file(
|
||||||
|
&contents,
|
||||||
|
IndentOptions::new(
|
||||||
|
if use_tabs {
|
||||||
|
IndentType::Tabs
|
||||||
|
} else {
|
||||||
|
IndentType::Spaces
|
||||||
|
},
|
||||||
|
indent_size,
|
||||||
|
),
|
||||||
|
);
|
||||||
let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
|
let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
|
||||||
FormatBlockInstance { new: out, _edits }
|
FormatBlockInstance { new: out, _edits }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) {
|
|||||||
if (!editor) return; // Need an editor to apply text edits.
|
if (!editor) return; // Need an editor to apply text edits.
|
||||||
|
|
||||||
const contents = editor.document.getText();
|
const contents = editor.document.getText();
|
||||||
const formatted = dioxus.format_file(contents);
|
let tabSize: number;
|
||||||
|
if (typeof editor.options.tabSize === 'number') {
|
||||||
|
tabSize = editor.options.tabSize;
|
||||||
|
} else {
|
||||||
|
tabSize = 4;
|
||||||
|
}
|
||||||
|
const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, tabSize);
|
||||||
|
|
||||||
// Replace the entire text document
|
// Replace the entire text document
|
||||||
// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using
|
// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ impl CallbackApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
|
pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use std::{
|
|||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
#[must_use]
|
||||||
pub fn use_atom_ref<'a, T: 'static>(
|
pub fn use_atom_ref<'a, T: 'static>(
|
||||||
cx: &'a ScopeState,
|
cx: &'a ScopeState,
|
||||||
atom: &'static AtomRef<T>,
|
atom: &'static AtomRef<T>,
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ use dioxus_core::ScopeState;
|
|||||||
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
|
||||||
cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
|
cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
|
||||||
Some(root) => root,
|
Some(root) => root,
|
||||||
None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"),
|
None => panic!("No atom root found in context. Did you forget to call use_init_atom_root at the top of your app?"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ use crate::{use_atom_root, AtomId, AtomRoot, Readable};
|
|||||||
use dioxus_core::{ScopeId, ScopeState};
|
use dioxus_core::{ScopeId, ScopeState};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn use_read<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &V {
|
pub fn use_read<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &V {
|
||||||
use_read_rc(cx, f).as_ref()
|
use_read_rc(cx, f).as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn use_read_rc<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &Rc<V> {
|
pub fn use_read_rc<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &Rc<V> {
|
||||||
let root = use_atom_root(cx);
|
let root = use_atom_root(cx);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::{use_atom_root, Writable};
|
|||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn use_set<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &Rc<dyn Fn(T)> {
|
pub fn use_set<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &Rc<dyn Fn(T)> {
|
||||||
let root = use_atom_root(cx);
|
let root = use_atom_root(cx);
|
||||||
cx.use_hook(|| {
|
cx.use_hook(|| {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use std::{
|
|||||||
/// ))
|
/// ))
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn use_atom_state<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &AtomState<T> {
|
pub fn use_atom_state<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &AtomState<T> {
|
||||||
let root = crate::use_atom_root(cx);
|
let root = crate::use_atom_root(cx);
|
||||||
|
|
||||||
@@ -85,7 +86,9 @@ impl<T: 'static> AtomState<T> {
|
|||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn current(&self) -> Rc<T> {
|
pub fn current(&self) -> Rc<T> {
|
||||||
self.value.as_ref().unwrap().clone()
|
let atoms = self.root.atoms.borrow();
|
||||||
|
let slot = atoms.get(&self.id).unwrap();
|
||||||
|
slot.value.clone().downcast().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the `setter` function directly without the `AtomState` wrapper.
|
/// Get the `setter` function directly without the `AtomState` wrapper.
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ mod atoms {
|
|||||||
pub use atom::*;
|
pub use atom::*;
|
||||||
pub use atomfamily::*;
|
pub use atomfamily::*;
|
||||||
pub use atomref::*;
|
pub use atomref::*;
|
||||||
pub use selector::*;
|
|
||||||
pub use selectorfamily::*;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod hooks {
|
pub mod hooks {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ keywords = ["ui", "gui", "react", "ssr", "fullstack"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# server functions
|
# server functions
|
||||||
server_fn = { version = "0.4.6", default-features = false }
|
server_fn = { version = "0.5.2", default-features = false }
|
||||||
dioxus_server_macro = { workspace = true }
|
dioxus_server_macro = { workspace = true }
|
||||||
|
|
||||||
# warp
|
# warp
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ fn app(cx: Scope<AppProps>) -> Element {
|
|||||||
|
|
||||||
let mut count = use_state(cx, || 0);
|
let mut count = use_state(cx, || 0);
|
||||||
let text = use_state(cx, || "...".to_string());
|
let text = use_state(cx, || "...".to_string());
|
||||||
|
let eval = use_eval(cx);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
|
|||||||
@@ -372,15 +372,65 @@ fn apply_request_parts_to_response<B>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSR renderer handler for Axum
|
/// SSR renderer handler for Axum with added context injection.
|
||||||
pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
///
|
||||||
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
|
/// # Example
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// #![allow(non_snake_case)]
|
||||||
|
/// use std::sync::{Arc, Mutex};
|
||||||
|
///
|
||||||
|
/// use axum::routing::get;
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
|
||||||
|
///
|
||||||
|
/// fn app(cx: Scope) -> Element {
|
||||||
|
/// render! {
|
||||||
|
/// "hello!"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() {
|
||||||
|
/// let cfg = ServeConfigBuilder::new(app, ())
|
||||||
|
/// .assets_path("dist")
|
||||||
|
/// .build();
|
||||||
|
/// let ssr_state = SSRState::new(&cfg);
|
||||||
|
///
|
||||||
|
/// // This could be any state you want to be accessible from your server
|
||||||
|
/// // functions using `[DioxusServerContext::get]`.
|
||||||
|
/// let state = Arc::new(Mutex::new("state".to_string()));
|
||||||
|
///
|
||||||
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
|
/// axum::Server::bind(&addr)
|
||||||
|
/// .serve(
|
||||||
|
/// axum::Router::new()
|
||||||
|
/// // Register server functions, etc.
|
||||||
|
/// // Note you probably want to use `register_server_fns_with_handler`
|
||||||
|
/// // to inject the context into server functions running outside
|
||||||
|
/// // of an SSR render context.
|
||||||
|
/// .fallback(get(render_handler_with_context).with_state((
|
||||||
|
/// move |ctx| ctx.insert(state.clone()).unwrap(),
|
||||||
|
/// cfg,
|
||||||
|
/// ssr_state,
|
||||||
|
/// )))
|
||||||
|
/// .into_make_service(),
|
||||||
|
/// )
|
||||||
|
/// .await
|
||||||
|
/// .unwrap();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub async fn render_handler_with_context<
|
||||||
|
P: Clone + serde::Serialize + Send + Sync + 'static,
|
||||||
|
F: FnMut(&mut DioxusServerContext),
|
||||||
|
>(
|
||||||
|
State((mut inject_context, cfg, ssr_state)): State<(F, ServeConfig<P>, SSRState)>,
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let (parts, _) = request.into_parts();
|
let (parts, _) = request.into_parts();
|
||||||
let url = parts.uri.path_and_query().unwrap().to_string();
|
let url = parts.uri.path_and_query().unwrap().to_string();
|
||||||
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
|
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
|
||||||
let server_context = DioxusServerContext::new(parts.clone());
|
let mut server_context = DioxusServerContext::new(parts.clone());
|
||||||
|
inject_context(&mut server_context);
|
||||||
|
|
||||||
match ssr_state.render(url, &cfg, &server_context).await {
|
match ssr_state.render(url, &cfg, &server_context).await {
|
||||||
Ok(rendered) => {
|
Ok(rendered) => {
|
||||||
@@ -398,6 +448,14 @@ pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SSR renderer handler for Axum
|
||||||
|
pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
|
||||||
|
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
|
||||||
|
request: Request<Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
render_handler_with_context(State((|_: &mut _| (), cfg, ssr_state)), request).await
|
||||||
|
}
|
||||||
|
|
||||||
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
|
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
|||||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user