diff --git a/.docker/Dockerfile_base_test_image b/.docker/Dockerfile_base_test_image new file mode 100644 index 000000000..a29eac927 --- /dev/null +++ b/.docker/Dockerfile_base_test_image @@ -0,0 +1,14 @@ +FROM rust:1.58-buster + +RUN apt update +RUN apt install -y \ + libglib2.0-dev \ + libgtk-3-dev \ + libsoup2.4-dev \ + libappindicator3-dev \ + libwebkit2gtk-4.0-dev \ + firefox-esr \ + # for Tarpaulin code coverage + liblzma-dev binutils-dev libcurl4-openssl-dev libdw-dev libelf-dev + +CMD ["exit"] diff --git a/.docker/Dockerfile_code_coverage b/.docker/Dockerfile_code_coverage new file mode 100644 index 000000000..abcccb56d --- /dev/null +++ b/.docker/Dockerfile_code_coverage @@ -0,0 +1,7 @@ +FROM dioxus-test-image + +WORKDIR /run_test +RUN cargo install cargo-tarpaulin +RUN cargo cache -a + +ENTRYPOINT [ "bash" ] diff --git a/.docker/Dockerfile_pre_test b/.docker/Dockerfile_pre_test new file mode 100644 index 000000000..cc6f8810e --- /dev/null +++ b/.docker/Dockerfile_pre_test @@ -0,0 +1,8 @@ +FROM dioxus-base-test-image + +RUN cargo install cargo-binstall +RUN cargo install cargo-make +RUN cargo install wasm-pack +RUN cargo install cargo-cache && cargo cache -a + +CMD ["exit"] diff --git a/.docker/Dockerfile_test b/.docker/Dockerfile_test new file mode 100644 index 000000000..4cd644051 --- /dev/null +++ b/.docker/Dockerfile_test @@ -0,0 +1,9 @@ +FROM dioxus-pre-test + +RUN mkdir run_test +COPY tmp /run_test +WORKDIR /run_test +RUN cargo make tests +RUN cargo cache -a + +CMD ["exit"] diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 000000000..57578f405 --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,27 @@ +# Why this? + +This part is used to test whole package before pushing it + +# How to use it? + +Just run in the folder: +`bash run_local_tests.sh`. If nothing fails, then you can push your code to the repo. +or run: +`bash run_local_tests.sh --with-full-docker-cleanup` +for cleaning up images as well + +# How is it composed of? + + 1. `Dockerfile_pre_test` will build the base image for the tests to be run into + 2. `Dockerfile_test` will run the actual tests based on 1. + 3. `run_local_tests.sh` to wrap this up + +# Warning + +The task requires some amount of CPU work and disk space (5GB per tests). Some clean up is included in the script. + +# Requirements + + * [docker](https://docs.docker.com/engine/install/) + * bash + * rsync \ No newline at end of file diff --git a/.docker/run_local_tests.sh b/.docker/run_local_tests.sh new file mode 100644 index 000000000..b6da68541 --- /dev/null +++ b/.docker/run_local_tests.sh @@ -0,0 +1,44 @@ +set -eux + +echo "Test script started" + +function run_script { + if [[ -d tmp ]] + then + rm -rf tmp + fi + mkdir tmp + # copy files first + rsync -a --progress ../ tmp --exclude target --exclude docker + + # build base image + docker build -f Dockerfile_base_test_image -t dioxus-base-test-image . + docker build -f Dockerfile_pre_test -t dioxus-pre-test . + # run test + docker build -f Dockerfile_test -t dioxus-test-image . + # code coverage + docker build -f Dockerfile_code_coverage -t dioxus-code-coverage . + + # exec test coverage + cd .. && \ + echo "rustup default nightly && cargo +nightly tarpaulin --verbose --all-features --tests --workspace --exclude core-macro --timeout 120 --out Html" | docker run -i --rm --security-opt seccomp=unconfined -v "/home/elios/project/prs/dioxus/:/run_test" dioxus-code-coverage + + # clean up + rm -rf tmp + if [ $# -ge 1 ] + then + echo "Got some parameter" + if [ $1 = "--with-full-docker-cleanup" ] + then + docker image rm dioxus-base-test-image + docker image rm dioxus-test-image + fi + fi +} + +run_script || echo "Error occured.. cleaning a bit." && \ + docker system prune -f; + +docker system prune -f + +echo "Script finished to execute" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..332e03eb2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: jkelleyrtp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +open_collective: dioxus-labs # Replace with a single Open Collective username diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e31219bb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve Dioxus +--- + +**Problem** + + + +**Steps To Reproduce** + +Steps to reproduce the behavior: + +- +- +- + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Environment:** + - Dioxus version: [e.g. v0.17, `master`] + - Rust version: [e.g. 1.43.0, `nightly`] + - OS info: [e.g. MacOS] + - App platform: [e.g. `web`, `desktop`] + +**Questionnaire** + +- [ ] I'm interested in fixing this myself but don't know where to start +- [ ] I would like to fix and I have a solution +- [ ] I don't have time to fix this right now, but maybe later \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_requst.md b/.github/ISSUE_TEMPLATE/feature_requst.md new file mode 100644 index 000000000..f8735b9a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_requst.md @@ -0,0 +1,16 @@ +--- +name: Feature Request +about: If you have any interesting advice, you can tell us. +--- + +## Specific Demand + + + +## Implement Suggestion + + \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..0e19ecaec --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: github pages + +on: + push: + paths: + - docs/** + - .github/workflows/docs.yml + branches: + - master + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: docs + steps: + - uses: actions/checkout@v2 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: "0.4.10" + + - name: Build + run: cd docs && + cd guide && mdbook build -d ../nightly/guide && cd .. && + cd reference && mdbook build -d ../nightly/reference && cd .. && + cd router && mdbook build -d ../nightly/router && cd .. + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4.2.3 + with: + branch: gh-pages # The branch the action should deploy to. + folder: docs/nightly # The folder the action should deploy. + target-folder: docs/nightly + repository-name: dioxuslabs/docsite + clean: false + token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 57b408a23..718922a57 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,9 +1,21 @@ -on: [push, pull_request] - name: macOS tests +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + jobs: test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: macos-latest steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b941960b7..49ed00901 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,21 @@ -on: [push, pull_request] - name: Rust CI +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml + jobs: check: + if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest steps: @@ -21,6 +33,7 @@ jobs: command: check test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest steps: @@ -33,12 +46,16 @@ jobs: - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev + - uses: davidB/rust-cargo-make@v1 + - uses: browser-actions/setup-firefox@latest + - uses: jetli/wasm-pack-action@v0.3.0 - uses: actions-rs/cargo@v1 with: - command: test - args: --features "desktop, ssr, router" + command: make + args: tests fmt: + if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest steps: @@ -56,6 +73,7 @@ jobs: args: --all -- --check clippy: + if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest steps: @@ -73,3 +91,24 @@ jobs: with: command: clippy args: -- -D warnings + + # Coverage is disabled until we can fix it + # coverage: + # name: Coverage + # runs-on: ubuntu-latest + # container: + # image: xd009642/tarpaulin:develop-nightly + # options: --security-opt seccomp=unconfined + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + # - name: Generate code coverage + # run: | + # apt-get update &&\ + # apt-get install build-essential &&\ + # apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + # cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + # - name: Upload to codecov.io + # uses: codecov/codecov-action@v2 + # with: + # fail_ci_if_error: false diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 183fb282d..75953807a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,12 +1,21 @@ name: windows + on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master - pull_request: + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml jobs: test: + if: github.event.pull_request.draft == false runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) env: @@ -19,13 +28,7 @@ jobs: max-parallel: 2 fail-fast: false matrix: - target: - [ - i686-pc-windows-gnu, - i686-pc-windows-msvc, - x86_64-pc-windows-gnu, - x86_64-pc-windows-msvc, - ] + target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc] cfg_release_channel: [stable] steps: @@ -47,12 +50,6 @@ jobs: rustup target add ${{ matrix.target }} shell: powershell - - name: Add mingw32 to path for i686-gnu - run: | - echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH - if: matrix.target == 'i686-pc-windows-gnu' && matrix.channel == 'nightly' - shell: bash - - name: Add mingw64 to path for x86_64-gnu run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' @@ -62,7 +59,7 @@ jobs: run: | rustc -Vv cargo -V - set RUST_BACKTRACE=1 + set RUST_BACKTRACE=1 cargo build --features "desktop, ssr, router" cargo test --features "desktop, ssr, router" shell: cmd diff --git a/.gitignore b/.gitignore index 9534dc207..854f5cb26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/dist Cargo.lock .DS_Store @@ -7,3 +8,4 @@ Cargo.lock !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +tarpaulin-report.html \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index ac2195ccd..44bcb08e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,8 @@ "desktop", "router" ], + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + } } diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index bdaf7abb8..06845ebf9 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -69,3 +69,13 @@ Jank noderef reborrow VirtualDoms +bootstrapper +WebkitGtk +laymans +iter +cloneable +fudamental +clonable +oninput +Webview +idanarye diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b624794..b056a9590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ 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 + + + + - 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 + + + +
view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.1.7 (2022-01-08) ### Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..d4e41af61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,37 @@ [package] name = "dioxus" -version = "0.1.7" +version = "0.1.8" authors = ["Jonathan Kelley"] -edition = "2018" +edition = "2021" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] +rust-version = "1.56.0" [dependencies] -dioxus-core = { path = "./packages/core", version = "^0.1.7" } -dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } -dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } -dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } +dioxus-core = { path = "./packages/core", version = "^0.1.9" } +dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } +dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } +dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } +dioxus-rsx = { path = "./packages/rsx", optional = true } +fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } -dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } -dioxus-desktop = { path = "./packages/desktop", version = "^0.1.5", optional = true } -dioxus-ssr = { path = "./packages/ssr", version = "^0.1.2", optional = true } +dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } +dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } +dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true } -dioxus-router = { path = "./packages/router", version = "^0.1.0", optional = true } +dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true } dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true } +dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", optional = true } # dioxus-liveview = { path = "./packages/liveview", optional = true } [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] @@ -35,35 +39,31 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] -# "dioxus-router/web" -# "dioxus-router/desktop" -# desktop = ["dioxus-desktop", "dioxus-router/desktop"] -# mobile = ["dioxus-mobile"] -# liveview = ["dioxus-liveview"] - - [workspace] members = [ "packages/core", "packages/core-macro", + "packages/rsx", "packages/html", "packages/hooks", "packages/web", "packages/ssr", "packages/desktop", "packages/mobile", + "packages/interpreter", + "packages/fermi", ] [dev-dependencies] -futures-util = "0.3.17" +futures-util = "0.3.21" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" -serde = { version = "1.0.131", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } im-rc = "15.0.0" -anyhow = "1.0.51" -serde_json = "1.0.73" +anyhow = "1.0.53" +serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } -tokio = { version = "1.14.0", features = ["full"] } -reqwest = { version = "0.11.8", features = ["json"] } -dioxus = { path = ".", features = ["desktop", "ssr", "router"] } +tokio = { version = "1.16.1", features = ["full"] } +reqwest = { version = "0.11.9", features = ["json"] } +dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] } diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dbc080b8..000000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -MIT/Apache-2 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 000000000..1b5ec8b78 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..31aa79387 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 000000000..17c32fd15 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,51 @@ +[config] +default_to_workspace = false +min_version = "0.32.4" + +[env] +CARGO_MAKE_CLIPPY_ARGS = "-- --deny=warnings" +CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true + +[config.modify_core_tasks] +namespace = "core" +private = true + +[tasks.tests-setup] +private = true +script = [ + """ + test_flags = array --headless --firefox + dioxus_test_features = set wasm_test + dioxus_test_flags = array_join ${test_flags} " " + echo "running tests with flags: ${dioxus_test_flags} and features: ${dioxus_test_features}" + set_env DIOXUS_TEST_FLAGS ${dioxus_test_flags} + set_env DIOXUS_TEST_FEATURES ${dioxus_test_features} + """, +] +script_runner = "@duckscript" + +[tasks.tests] +category = "Testing" +dependencies = ["tests-setup"] +description = "Run all tests" +env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]} +run_task = {name = ["test-flow", "test-with-browser"], fork = true} + +[tasks.build] +command = "cargo" +args = ["build"] + +[tasks.test-flow] +dependencies = ["test"] +private = true + +[tasks.test] +dependencies = ["build"] +command = "cargo" +args = ["test", "--all-targets", "--workspace", "--exclude", "dioxus-router"] +private = true + +[tasks.test-with-browser] +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] } +private = true +workspace = true diff --git a/README.md b/README.md index 658a6e48d..23cd9c516 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
-

🌗🚀 Dioxus

+

Dioxus

Frontend that scales.

@@ -26,9 +26,7 @@ CI status -
-
Awesome Page @@ -45,9 +43,11 @@

Website | - Guide - | Examples + | + Guide (0.1.8) + | + Guide (Master)

@@ -65,12 +65,12 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-pla ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` @@ -101,10 +101,10 @@ cargo run --example EXAMPLE - - - - + + + +
TutorialWebDesktopSSRMobileWebDesktopSSRMobile State
diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..325c05ec8 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +comment: false +fail_ci_if_error: false diff --git a/docs/fermi/.gitignore b/docs/fermi/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/fermi/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/fermi/book.toml b/docs/fermi/book.toml new file mode 100644 index 000000000..dd6d2eacb --- /dev/null +++ b/docs/fermi/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Fermi Guide" diff --git a/docs/fermi/src/SUMMARY.md b/docs/fermi/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/fermi/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/fermi/src/chapter_1.md b/docs/fermi/src/chapter_1.md new file mode 100644 index 000000000..b743fda35 --- /dev/null +++ b/docs/fermi/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c00dcd274..da6c0156f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -5,24 +5,24 @@ **Dioxus** is a framework and ecosystem for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get started with Dioxus running on the Web, Desktop, Mobile, and more. ```rust -fn App(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); +fn app(cx: Scope) -> Element { + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) }; ``` In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you! -> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead. +> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead. ## Multiplatform -Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. +Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to WebSys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. Right now, we have several 1st-party renderers: - WebSys (for WASM) @@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. -[Jump to the getting started guide for the web.]() +[Jump to the getting started guide for the web.](/reference/platforms/web) Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan let contents = dioxus::ssr::render_vdom(&dom); ``` -[Jump to the getting started guide for SSR.]() +[Jump to the getting started guide for SSR.](/reference/platforms/ssr) Examples: - [Example DocSite](https://github.com/dioxusLabs/docsite) @@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -[Jump to the getting started guide for Desktop.]() +[Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: - [File explorer](https://github.com/dioxusLabs/file-explorer/) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) -[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/) +[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ### Mobile Support --- @@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -[Jump to the getting started guide for Mobile.]() +[Jump to the getting started guide for Mobile.](/reference/platforms/mobile) Examples: - [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) diff --git a/docs/guide/src/ROADMAP.md b/docs/guide/src/ROADMAP.md new file mode 100644 index 000000000..4f78e6043 --- /dev/null +++ b/docs/guide/src/ROADMAP.md @@ -0,0 +1,114 @@ +# Roadmap & Feature-set + +Before we dive into Dioxus, feel free to take a look at our feature set and roadmap to see if what Dioxus can do today works for you. + +If a feature that you need doesn't exist or you want to contribute to projects on the roadmap, feel free to get involved by [joining the discord](https://discord.gg/XgGxMSkvUM). + +Generally, here's the status of each platform: + +- **Web**: Dioxus is a great choice for pure web-apps - especially for CRUD/complex apps. However, it does lack the ecosystem of React, so you might be missing a component library or some useful hook. + +- **SSR**: Dioxus is a great choice for pre-rendering, hydration, and rendering HTML on a web endpoint. Be warned - the VirtualDom is not (currently) `Send + Sync`. + +- **Desktop**: You can build very competent single-window desktop apps right now. However, multi-window apps require support from Dioxus core and are not ready. + +- **Mobile**: Mobile support is very young. You'll be figuring things out as you go and there are not many support crates for peripherals. + +- **LiveView**: LiveView support is very young. You'll be figuring things out as you go. Thankfully, none of it is too hard and any work can be upstreamed into Dioxus. + +## Features +--- + +| Feature | Status | Description | +| ------------------------- | ------ | -------------------------------------------------------------------- | +| Conditional Rendering | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | map/filter/reduce to produce rsx! | +| Keyed Components | ✅ | advanced diffing with keys | +| Web | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | renderer for desktop | +| Shared State (Context) | ✅ | share state through the tree | +| Hooks | ✅ | memory cells in components | +| SSR | ✅ | render directly to string | +| Component Children | ✅ | cx.children() as a list of nodes | +| Headless components | ✅ | components that don't return real elements | +| Fragments | ✅ | multiple elements without a real root | +| Manual Props | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | stateful wrappers around inputs | +| CSS/Inline Styles | ✅ | syntax for inline styles/attribute groups | +| Custom elements | ✅ | Define new element primitives | +| Suspense | ✅ | schedule future render from future/promise | +| Integrated error handling | ✅ | Gracefully handle errors with ? syntax | +| NodeRef | ✅ | gain direct access to nodes | +| Re-hydration | ✅ | Pre-render to HTML to speed up first contentful paint | +| Jank-Free Rendering | ✅ | Large diffs are segmented across frames for silky-smooth transitions | +| Effects | ✅ | Run effects after a component has been committed to render | +| Portals | 🛠 | Render nodes outside of the traditional tree structure | +| Cooperative Scheduling | 🛠 | Prioritize important events over non-important events | +| Server Components | 🛠 | Hybrid components for SPA and Server | +| Bundle Splitting | 👀 | Efficiently and asynchronously load the app | +| Lazy Components | 👀 | Dynamically load the new components as the page is loaded | +| 1st class global state | ✅ | redux/recoil/mobx on top of context | +| Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) | +| Subtree Memoization | ✅ | skip diffing static element subtrees | +| High-efficiency templates | 🛠 | rsx! calls are translated to templates on the DOM's side | +| Compile-time correct | ✅ | Throw errors on invalid template layouts | +| Heuristic Engine | ✅ | track component memory usage to minimize future allocations | +| Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates | + +- ✅ = implemented and working +- 🛠 = actively being worked on +- 👀 = not yet implemented or being worked on +- ❓ = not sure if will or can implement + + +## Roadmap +--- + + +Core: +- [x] Release of Dioxus Core +- [x] Upgrade documentation to include more theory and be more comprehensive +- [ ] Support for HTML-side templates for lightning-fast dom manipulation +- [ ] Support for multiple renderers for same virtualdom (subtrees) +- [ ] Support for ThreadSafe (Send + Sync) +- [ ] Support for Portals + +SSR +- [x] SSR Support + Hydration +- [ ] Integrated suspense support for SSR + +Desktop +- [ ] Declarative window management +- [ ] Templates for building/bundling +- [ ] Fully native renderer +- [ ] Access to Canvas/WebGL context natively + +Mobile +- [ ] Mobile standard library + - [ ] GPS + - [ ] Camera + - [ ] filesystem + - [ ] Biometrics + - [ ] WiFi + - [ ] Bluetooth + - [ ] Notifications + - [ ] Clipboard + - [ ] + +Bundling (CLI) +- [x] translation from HTML into RSX +- [ ] dev server +- [ ] live reload +- [ ] translation from JSX into RSX +- [ ] hot module replacement +- [ ] code splitting +- [ ] asset macros +- [ ] css pipeline +- [ ] image pipeline + +Essential hooks +- [ ] Router +- [ ] Global state management +- [ ] Resize observer + + diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 2fa29f39e..fff21fd02 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,19 +1,23 @@ # Summary - [Introduction](README.md) +- [Roadmap](ROADMAP.md) - [Getting Set Up](setup.md) - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) - [Intro to Elements](elements/vnodes.md) - - [Intro to Components](elements/components.md) - - [The Props Macro](elements/propsmacro.md) - - [Reusing, Importing, and Exporting Components](elements/exporting_components.md) - - [Passing children and attributes](elements/component_children.md) - [Conditional Rendering](elements/conditional_rendering.md) - [Lists](elements/lists.md) + - [Special Attributes](elements/special_attributes.md) +- [Components](components/index.md) + - [Properties](components/propsmacro.md) + - [Reusing, Importing, and Exporting](components/exporting_components.md) + - [Children and Attributes](components/component_children.md) + - [How Data Flows](components/composing.md) - [Adding Interactivity](interactivity/index.md) - [Hooks and Internal State](interactivity/hooks.md) - - [Event handlers](interactivity/event_handlers.md) + - [UseState and UseRef](interactivity/importanthooks.md) + - [Event Listeners](interactivity/event_handlers.md) - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) @@ -41,3 +45,4 @@ + diff --git a/docs/guide/src/async/fetching.md b/docs/guide/src/async/fetching.md index 99e726c7a..b31c3d03b 100644 --- a/docs/guide/src/async/fetching.md +++ b/docs/guide/src/async/fetching.md @@ -1 +1,4 @@ # Fetching + + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/async/index.md b/docs/guide/src/async/index.md index 47b84e81a..0ec02e57d 100644 --- a/docs/guide/src/async/index.md +++ b/docs/guide/src/async/index.md @@ -17,3 +17,6 @@ Writing apps that deal with Send/Sync can be frustrating at times. Under the hoo All async code in your app is polled on a `LocalSet`, so any async code we w + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/components/component_children.md similarity index 92% rename from docs/guide/src/elements/component_children.md rename to docs/guide/src/components/component_children.md index d99273394..1259422af 100644 --- a/docs/guide/src/elements/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -64,7 +64,7 @@ struct ClickableProps<'a> { body: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format: ```rust -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -137,7 +137,7 @@ fn clickable(cx: Scope) -> Element { } ``` -## Passing attributes + ## Passing handlers @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) @@ -215,5 +215,3 @@ In this chapter, we learned: - How to convert `listeners` into `EventHandlers` for components - How to extend any node with custom attributes and children -Next chapter, we'll talk about conditionally rendering parts of your user interface. - diff --git a/docs/guide/src/components/component_example_title.png b/docs/guide/src/components/component_example_title.png new file mode 100644 index 000000000..60a5a329e Binary files /dev/null and b/docs/guide/src/components/component_example_title.png differ diff --git a/docs/guide/src/components/component_example_votes.png b/docs/guide/src/components/component_example_votes.png new file mode 100644 index 000000000..b46739b24 Binary files /dev/null and b/docs/guide/src/components/component_example_votes.png differ diff --git a/docs/guide/src/components/composing.md b/docs/guide/src/components/composing.md new file mode 100644 index 000000000..785387e1b --- /dev/null +++ b/docs/guide/src/components/composing.md @@ -0,0 +1,243 @@ +# Thinking in React + +We've finally reached the point in our tutorial where we can talk about the theory of Reactive interfaces. We've talked about defining a declarative view, but not about the aspects that make our code *reactive*. + +Understanding the theory of reactive programming is essential to making sense of Dioxus and writing effective, performant UIs. + +In this section, we'll talk about: + +- One-way data flow +- Modifying data +- Forcing renders +- How renders propagate + +This section is a bit long, but worth the read. We recommend coffee, tea, and/or snacks. + +## Reactive Programming + +Dioxus is one the very few Rust libraries that provide a "Reactive Programming Model". The term "Reactive programming" is a classification of programming paradigm - much like functional or imperative programming. This is a very important distinction since it affects how we *think* about our code. + +Reactive programming is a programming model concerned with deriving computations from asynchronous data flow. Most reactive programs are comprised of datasources, intermediate computations, and a final result. + +We consider the rendered GUI to be the final result of our Dioxus apps. The datasources for our apps include local and global state. + +For example, the model presented in the figure below is comprised of two data sources: time and a constant. These values are passed through our computation graph to achieve a final result: `g`. + +![Reactive Model](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Reactive_programming_glitches.svg/440px-Reactive_programming_glitches.svg.png) + +Whenever our `seconds` variable changes, we will then reevaluate the computation for `t`. Because `g` relies on `t`, we will also reevaluate its computation too. Notice that we would've reevaluated the computation for `g` even if `t` didn't change because `seconds` is used to calculate `g`. + +However, if we somehow changed our constant from `1` to `2`, then we need to reevaluate `t`. If, for whatever reason, this change did not affect the result of `t`, then we wouldn't try to reevaluate `g`. + +In Reactive Programming, we don't think about whether or not we should reevaluate `t` or `g`; instead, we simply provide functions of computation and let the framework figure out the rest for us. + +In Rust, our reactive app would look something like: + +```rust +fn compute_g(t: i32, seconds: i32) -> bool { + t > seconds +} + +fn compute_t(constant: i32, seconds: i32) -> i32 { + constant + seconds +} + +fn compute_graph(constant: i32, seconds: i32) -> bool { + let t = compute_t(constant, seconds); + let g = compute_g(t, seconds); + g +} +``` + +## How is Dioxus Reactive? + +The Dioxus VirtualDom provides us a framework for reactive programming. When we build apps with dioxus, we need to provide our own datasources. This can be either initial props or some values fetched from the network. We then pass this data through our app into components through properties. + +If we represented the reactive graph presented above in Dioxus, it would look very similar: + +```rust +// Declare a component that holds our datasources and calculates `g` +fn RenderGraph(cx: Scope) -> Element { + let seconds = use_datasource(SECONDS); + let constant = use_state(&cx, || 1); + + cx.render(rsx!( + RenderG { seconds: seconds } + RenderT { seconds: seconds, constant: constant } + )) +} + +// "calculate" g by rendering `t` and `seconds` +#[inline_props] +fn RenderG(cx: Scope, seconds: i32) -> Element { + cx.render(rsx!{ "There are {seconds} seconds remaining..." }) +} + +// calculate and render `t` in its own component +#[inline_props] +fn RenderT(cx: Scope, seconds: i32, constant: i32) -> Element { + let res = seconds + constant; + cx.render(rsx!{ "{res}" }) +} +``` + +With this app, we've defined three components. Our top-level component provides our datasources (the hooks), computation nodes (child components), and a final value (what's "rendered"). + +Now, whenever the `constant` changes, our `RenderT` component will be re-rendered. However, if `seconds` doesn't change, then we don't need to re-render `RenderG` because the input is the same. If `seconds` *does* change, then both RenderG and RenderT will be reevaluated. + +Dioxus is "Reactive" because it provides this framework for us. All we need to do is write our own tiny units of computation and Dioxus figures out which components need to be reevaluated automatically. + +These extra checks and algorithms add some overhead, which is why you see projects like [Sycamore](http://sycamore-rs.netlify.app) and [SolidJS](http://solidjs.com) eliminating them altogether. Dioxus is *really* fast, so we're willing to exchange the added overhead for improved developer experience. + +## How do we update values in our dataflow graph? + +Dioxus will automatically figure out how to regenerate parts of our app when datasources change. But how exactly can we update our data sources? + +In Dioxus there are two datasources: + +1. Local state in `use_hook` and all other hooks +2. Global state through `provide_context`. + +Technically, the root props of the VirtualDom are a third datasource, but since we cannot modify them, they are not worth talking about. + +### Local State + +For local state in hooks, Dioxus gives us the `use_hook` method which returns an `&mut T` without any requirements. This means raw hook values are not tracked by Dioxus. In fact, we could write a component that modifies a hook value directly: + +```rust +fn app(cx: Scope) -> Element { + let mut count = cx.use_hook(|_| 0); + cx.render(rsx!{ + button { + onclick: move |_| *count += 1, + "Count: {count}" + } + }) +} +``` + +However, when this value is written to, the component does not know to be reevaluated. We must explicitly tell Dioxus that this component is "dirty" and needs to be re-rendered. This is done through the `cx.needs_update` method: + +```rust +button { + onclick: move |_| { + *count += 1; + cx.needs_update(); + }, + "Count: {count}" +} +``` + +Now, whenever we click the button, the value will change and the component will be re-rendered. + +> Re-rendering is when Dioxus calls your function component *again*. Component functions will be called over and over throughout their lifetime, so they should be mostly side-effect free. + +### Understand this! + +Your component functions will be called ("rendered" in our lingo) for as long as the component is present in the tree. + +A single component will be called multiple times, modifying its own internal state or rendering new nodes with new values from its properties. + +### App-Global State + +With the `provide_context` and `consume_context` methods on `Scope`, we can share values to descendants without having to pass values through component props. This has the side-effect of making our datasources less obvious from a high-level perspective, but it makes our components more modular within the same codebase. + +To make app-global state easier to reason about, Dioxus makes all values provided through `provide_context` immutable. This means any library built on top of `provide_context` needs to use interior mutability to modify shared global state. + +In these cases, App-Global state needs to manually track which components need to be re-generated. + +To regenerate *any* component in your app, you can get a handle to the Dioxus' internal scheduler through `schedule_update_any`: + +```rust +let force_render = cx.schedule_update_any(); + +// force a render of the root component +force_render(ScopeId(0)); +``` + +## What does it mean for a component to "re-render"? + +In our guides, we frequently use the phrase "re-render" to describe updates to our app. You'll often hear this paired with "preventing unnecessary re-renders." But what exactly does this mean? + +When we call `dioxus::desktop::launch`, Dioxus will create a new `Scope` object and call the component we gave it. Our `rsx!` calls will create new nodes which we return back to the VirtualDom. Dioxus will then look through these nodes for child components, call their functions, and so on until every component has been "rendered." We consider these nodes "rendered" because they were created because of our explicit actions. + +The tree of UI that dioxus creates will roughly look like the tree of components presented earlier: + +![Tree of UI](../images/component_tree.png) + +But what happens when we call `needs_update` after modifying some important state? Well, if Dioxus called our component's function again, then we would produce new, different nodes. In fact, this is exactly what Dioxus does! + +At this point, we have some old nodes and some new nodes. Again, we call this "rendering" because Dioxus had to create new nodes because of our explicit actions. Any time new nodes get created, our VirtualDom is being "rendered." + +These nodes are stored in an extremely efficient memory allocator called a "bump arena." For example, a div with a handler and attribute would be stored in memory in two locations: the "old" tree and the "new" tree. + +![Bump Arenas](../images/oldnew.png) + +From here, Dioxus computes the difference between these trees and updates the Real DOM to make it look like the new version of what we've declared. + +![Diffing](../images/diffing.png) + +## Suppressing Renders + +So, we know how to make Dioxus render, but how do we *stop* it? What if we *know* that our state didn't change and we shouldn't render and diff new nodes because they'll be exactly the same as the last time? + +In these cases, you want to reach for *memoization*. In Dioxus, memoization involves preventing a component from rendering again if its props didn't change since the last time it attempted to render. + +Visually, you can tell that a component will only re-render if the new value is sufficiently different than the old one. + +| props.val | re-render | +| --------- | --------- | +| 10 | true | +| 20 | true | +| 20 | false | +| 20 | false | +| 10 | true | +| 30 | false | + +This is why when you `derive(Props)`, you must also implement the `PartialEq` trait. To override the memoization strategy for a component, you can simply implement your own PartialEq. + +```rust +struct CustomProps { + val: i32, +} + +impl PartialEq for CustomProps { + fn partial_eq(&self, other: &Self) -> bool { + // we don't render components that have a val less than 5 + if other.val > 5 && self.val > 5{ + self.val == other.val + } + } +} +``` + +However, for components that borrow data, it doesn't make sense to implement PartialEq since the actual references in memory might be different. + +You can technically override this behavior by implementing the `Props` trait manually, though it's unsafe and easy to mess up: + +```rust +impl Properties for CustomProps { + fn memoize(&self, other &Self) -> bool { + self != other + } +} +``` + +TLDR: +- Dioxus checks if props changed between renders +- If props changed according to PartialEq, Dioxus re-renders the component +- Props that have a lifetime (ie `<'a>`) will always be re-rendered + +## Wrapping Up + +Wow, that was a lot of material! + +Let's see if we can recap what was presented: + +- Reactive programming calculates a final value from datasources and computation +- Dioxus is "reactive" since it figures out which computations to check +- `schedule_update` must be called to mark a component as dirty +- dirty components will be re-rendered (called multiple times) to produce a new UI +- Renders can be suppressed with memoization + +This theory is crucial to understand how to compose components and how to control renders in your app. diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/components/exporting_components.md similarity index 96% rename from docs/guide/src/elements/exporting_components.md rename to docs/guide/src/components/exporting_components.md index 117249db8..727e5b6b6 100644 --- a/docs/guide/src/elements/exporting_components.md +++ b/docs/guide/src/components/exporting_components.md @@ -1,4 +1,3 @@ - # Reusing, Importing, and Exporting Components As your application grows in size, you'll want to start breaking your UI into components and, eventually, different files. This is a great idea to encapsulate functionality of your UI and scale your team. @@ -66,6 +65,16 @@ fn ActionCard(Scope) -> Element {} We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. +```rust +// src/post/mod.rs + +use dioxus::prelude::*; + +#[derive(PartialEq, Props)] +struct PostProps {} +fn Post(Scope) -> Element {} +``` + ```shell ├── Cargo.toml └── src @@ -78,8 +87,6 @@ We should also create a `mod.rs` file in the `post` folder so we can use it from └── mod.rs ``` - - In our `main.rs`, we'll want to declare the `post` module so we can access our `Post` component. ```rust @@ -164,7 +171,6 @@ pub fn Post(Scope) -> Element { } ``` - Ultimately, including and exporting components is governed by Rust's module system. [The Rust book is a great resource to learn about these concepts in greater detail.](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html) ## Final structure: @@ -284,14 +290,3 @@ use dioxus::prelude::*; pub struct ActionCardProps {} pub fn ActionCard(Scope) -> Element {} ``` - -## Moving forward - -Next chapter, we'll start to add use code to hide and show Elements with conditional rendering. - -For more reading on components: - -- [Components in depth]() -- [Lifecycles]() -- [The Context object]() -- [Optional Prop fields]() diff --git a/docs/guide/src/components/index.md b/docs/guide/src/components/index.md new file mode 100644 index 000000000..ff4811a33 --- /dev/null +++ b/docs/guide/src/components/index.md @@ -0,0 +1,42 @@ +# Introduction to Components + +In the previous chapter, we learned about Elements and how they can be composed to create a basic user interface. Now, we'll learn how to group Elements together to form Components. We'll cover: + +- What makes a Component +- How to model a component and its properties in Dioxus +- How to "think declaratively" + +## What is a component? + +In short, a component is a special function that takes input properties and outputs an Element. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task – typically, rendering an isolated part of the user interface. + +### Real-world example + +Let's use a Reddit post as an example: + +![Reddit Post](../images/reddit_post.png) + +If we look at the layout of the component, we notice quite a few buttons and pieces of functionality: + +- Upvote/Downvote +- View comments +- Share +- Save +- Hide +- Give award +- Report +- Crosspost +- Filter by site +- View article +- Visit user + +If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into Components: + +![Post as Component](../images/reddit_post_components.png) + +- **VoteButton**: Upvote/Downvote +- **TitleCard**: Title, Filter-By-Url +- **MetaCard**: Original Poster, Time Submitted +- **ActionCard**: View comments, Share, Save, Hide, Give award, Report, Crosspost + +In this chapter, we'll learn how to define these components. diff --git a/docs/guide/src/components/propsmacro.md b/docs/guide/src/components/propsmacro.md new file mode 100644 index 000000000..83b01a3ea --- /dev/null +++ b/docs/guide/src/components/propsmacro.md @@ -0,0 +1,200 @@ +# Component Properties +Dioxus components are functions that accept Props as input and output an Element. In fact, the `App` function you saw in the previous chapter was a component with no Props! Most components, however, will need to take some Props to render something useful – so, in this section, we'll learn about props: + +- Deriving the Props trait +- Memoization through PartialEq +- Optional fields on props +- The inline_props macro + +## Props +The input of your Component must be passed in a single struct, which must implement the `Props` trait. We can derive this trait automatically with `#[derive(Props)]`. + +> Dioxus `Props` is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters. + +There are 2 flavors of Props: owned and borrowed. + +- All Owned Props must implement `PartialEq` +- Borrowed props [borrow](https://doc.rust-lang.org/beta/rust-by-example/scope/borrow.html) values from the parent Component + +### Owned Props + +Owned Props are very simple – they don't borrow anything. Example: +```rust +// Remember: owned props must implement PartialEq! +#[derive(PartialEq, Props)] +struct VoteButtonProps { + score: i32 +} + +fn VoteButton(cx: Scope) -> Element { + cx.render(rsx!{ + div { + div { "+" } + div { "{cx.props.score}"} + div { "-" } + } + }) +} +``` + +Now, we can use the VoteButton Component like we would use a regular HTML element: + +```rust +fn main() { + dioxus::desktop::launch(App); +} + +fn App(cx: Scope) -> Element { + cx.render(rsx! ( + VoteButton { score: 42 } + )) +} +``` + +And we can see that the Component indeed gets rendered: + +![Screenshot of running app. Text: "+ \ 42 \ -"](component_example_votes.png) + +> The simplest Owned Props you can have is `()` - or no value at all. This is what the `App` Component takes as props. `Scope` accepts a generic for the Props which defaults to `()`. +> +> ```rust +>// this scope +>Scope<()> +> +>// is the same as this scope +>Scope +>``` + +### Borrowed Props + +Owning props works well if your props are easy to copy around - like a single number. But what if we need to pass a larger data type, like a String from an `App` Component to a `TitleCard` subcomponent? A naive solution might be to [`.clone()`](https://doc.rust-lang.org/std/clone/trait.Clone.html) the String, creating a copy of it for the subcomponent – but this would be inefficient, especially for larger Strings. + +Rust allows for something more efficient – borrowing the String as a `&str`. Instead of creating a copy, this will give us a reference to the original String – this is what Borrowed Props are for! + +However, if we create a reference a String, Rust will require us to show that the String will not go away while we're using the reference. Otherwise, if we referenced something that doesn't exist, Bad Things could happen. To prevent this, Rust asks us to define a lifetime for the reference: + +```rust +#[derive(Props)] +struct TitleCardProps<'a> { + title: &'a str, +} + +fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { + cx.render(rsx!{ + h1 { "{cx.props.title}" } + }) +} +``` + +This lifetime `'a` tells the compiler that as long as `title` exists, the String it was created from must also exist. Dioxus will happily accept such a component – we can now render it alongside our VoteButton! + +```rust +fn App(cx: Scope) -> Element { + // For the sake of an example, we create the &str here. + // But you might as well borrow it from an owned String type. + let hello = "Hello Dioxus!"; + + cx.render(rsx! ( + VoteButton { score: 42 }, + TitleCard { title: hello } + )) +} +``` +![New screenshot of running app, now including a "Hello Dioxus!" heading.](component_example_title.png) + +## Memoization + +Dioxus uses Memoization for a more efficient user interface. Memoization is the process in which we check if a component actually needs to be re-rendered when its props change. If a component's properties change but they wouldn't affect the output, then we don't need to re-render the component, saving time! + +For example, let's say we have a component that has two children: + +```rust +fn Demo(cx: Scope) -> Element { + // don't worry about these 2, we'll cover them later + let name = use_state(&cx, || String::from("bob")); + let age = use_state(&cx, || 21); + + cx.render(rsx!{ + Name { name: name } + Age { age: age } + }) +} +``` + +If `name` changes but `age` does not, then there is no reason to re-render our `Age` component since the contents of its props did not meaningfully change. + +Dioxus memoizes owned components. It uses `PartialEq` to determine if a component needs rerendering, or if it has stayed the same. This is why you must derive PartialEq! + +> This means you can always rely on props with `PartialEq` or no props at all to act as barriers in your app. This can be extremely useful when building larger apps where properties frequently change. By moving our state into a global state management solution, we can achieve precise, surgical re-renders, improving the performance of our app. + +Borrowed Props cannot be safely memoized. However, this is not a problem – Dioxus relies on the memoization of their parents to determine if they need to be rerendered. + +## Optional Props + +You can easily create optional fields by attaching the `optional` modifier to a field: + +```rust +#[derive(Props, PartialEq)] +struct MyProps { + name: String, + + #[props(optional)] + description: Option +} + +fn Demo(cx: MyProps) -> Element { + todo!() +} +``` + +Then, we can completely omit the description field when calling the component: + +```rust +rsx!{ + Demo { + name: "Thing".to_string(), + // description is omitted + } +} +``` + +The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes: + +- `default` - automatically add the field using its `Default` implementation +- `strip_option` - automatically wrap values at the call site in `Some` +- `optional` - alias for `default` and `strip_option` +- `into` - automatically call `into` on the value at the callsite + +For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. + +## The `inline_props` macro + +So far, every Component function we've seen had a corresponding ComponentProps struct to pass in props. This was quite verbose... Wouldn't it be nice to have props as simple function arguments? Then we wouldn't need to define a Props struct, and instead of typing `cx.props.whatever`, we could just use `whatever` directly! + +`inline_props` allows you to do just that. Instead of typing the "full" version: + +```rust +#[derive(Props, PartialEq)] +struct TitleCardProps { + title: String, +} + +fn TitleCard(cx: Scope) -> Element { + cx.render(rsx!{ + h1 { "{cx.props.title}" } + }) +} +``` + +...you can define a function that accepts props as arguments. Then, just annotate it with `#[inline_props]`, and the macro will turn it into a regular Component for you: + +```rust +#[inline_props] +fn TitleCard(cx: Scope, title: String) -> Element { + cx.render(rsx!{ + h1 { "{title}" } + }) +} +``` + +> While the new Component is shorter and easier to read, this macro should not be used by library authors since you have less control over Prop documentation. diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md deleted file mode 100644 index ea43034d9..000000000 --- a/docs/guide/src/elements/components.md +++ /dev/null @@ -1,227 +0,0 @@ -# Introduction to Components - -In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components. - -In this chapter, we'll learn: -- What makes a Component -- How to model a component and its properties in Dioxus -- How to "think declaratively" - -## What is a component? - -In short, a component is a special function that takes input properties and outputs an Element. Typically, Components serve a single purpose: group functionality of a User Interface. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task. - -### Learning through prior art - -Let's take a look at a post on r/rust and see if we can sketch out a component representation. - -![Reddit Post](../images/reddit_post.png) - -This component has a bunch of important information: - -- The score -- The number of comments -- How long ago it was posted -- The url short address -- The title -- The username of the original poster - -If we wanted to sketch out these requirements in Rust, we would start with a struct: - -```rust -struct PostData { - score: i32, - comment_count: u32, - post_time: Instant, - url: String, - title: String, - original_poster_name: String -} -``` - -If we look at the layout of the component, we notice quite a few buttons and pieces of functionality: - -- Upvote/Downvote -- View comments -- Share -- Save -- Hide -- Give award -- Report -- Crosspost -- Filter by site -- View article -- Visit user - -If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into some core pieces: - -![Post as Component](../images/reddit_post_components.png) - -- **VoteButton**: Upvote/Downvote -- **TitleCard**: Title, Filter-By-Url -- **MetaCard**: Original Poster, Time Submitted -- **ActionCard**: View comments, Share, Save, Hide, Give award, Report, Crosspost - -### Modeling with Dioxus - -We can start by sketching out the Element hierarchy using Dioxus. In general, our "Post" component will be comprised of the four sub-components listed above. First, let's define our `Post` component. - -Unlike normal functions, Dioxus components must explicitly define a single struct to contain all the inputs. These are commonly called "Properties" (props). Our component will be a combination of these properties and a function to render them. - -Our props must implement the `Props` trait and - if the component does not borrow any data - `PartialEq`. Both of these can be done automatically through derive macros: -```rust -#[derive(Props, PartialEq)] -struct PostProps { - id: Uuid, - score: i32, - comment_count: u32, - post_time: Instant, - url: String, - title: String, - original_poster: String -} -``` - -And our render function: -```rust -fn Post(cx: Scope) -> Element { - cx.render(rsx!{ - div { class: "post-container" - VoteButton { - score: cx.props.score, - } - TitleCard { - title: cx.props.title, - url: cx.props.url, - } - MetaCard { - original_poster: cx.props.original_poster, - post_time: cx.props.post_time, - } - ActionCard { - post_id: cx.props.id - } - } - }) -} -``` - -When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Dioxus will automatically call "into" on the property fields, cloning when necessary. Our `Post` component is simply a collection of smaller components wrapped together in a single container. - -Let's take a look at the `VoteButton` component. For now, we won't include any interactivity - just the rendering the score and buttons to the screen. - -Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`. - -As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. - -```rust -#[derive(PartialEq, Props)] -struct VoteButtonProps { - score: i32 -} - -fn VoteButton(cx: Scope) -> Element { - cx.render(rsx!{ - div { class: "votebutton" - div { class: "arrow up" } - div { class: "score", "{cx.props.score}"} - div { class: "arrow down" } - } - }) -} -``` - -## Borrowing - -You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. - -Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. - -To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props. - -```rust -#[derive(Props)] -struct TitleCardProps<'a> { - title: &'a str, -} - -fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { - cx.render(rsx!{ - h1 { "{cx.props.title}" } - }) -} -``` - -For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow. - -This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles. - -## The inline_props macro - -Yes - *another* macro! However, this one is entirely optional. - -For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component. - -Our title card above would be transformed from: - -```rust -#[derive(Props, PartialEq)] -struct TitleCardProps { - title: String, -} - -fn TitleCard(cx: Scope) -> Element { - cx.render(rsx!{ - h1 { "{cx.props.title}" } - }) -} -``` - -to: - -```rust -#[inline_props] -fn TitleCard(cx: Scope, title: String) -> Element { - cx.render(rsx!{ - h1 { "{title}" } - }) -} -``` - -Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. - -However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate. - -## The `Scope` object - -Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. - -Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? - -React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs. - -```javascript -function Component(props) { - let [state, set_state] = useState(10); -} -``` - - -Because Dioxus needs to work with the rules of Rust it uses the `Scope` object to maintain some internal bookkeeping. That's what the `Scope` object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Scope` object to build robust and performant extensions for Dioxus. - -```rust -fn Post(cx: Scope) -> Element { - cx.render(rsx!("hello")) -} -``` -## Moving forward - -Next chapter, we'll talk about composing Elements and Components across files to build a larger Dioxus App. - -For more references on components, make sure to check out: - -- [Components in depth]() -- [Lifecycles]() -- [The Scope object]() -- [Optional Prop fields]() - diff --git a/docs/guide/src/elements/conditional_rendering.md b/docs/guide/src/elements/conditional_rendering.md index c13263cf6..5c19de237 100644 --- a/docs/guide/src/elements/conditional_rendering.md +++ b/docs/guide/src/elements/conditional_rendering.md @@ -67,7 +67,7 @@ fn App(cx: Scope)-> Element { } ``` -Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a unique type. To turn our `rsx!` into Elements, we need to call `cx.render`. +Do note: the `rsx!` macro does not return an Element, but rather a wrapper struct for a `Closure` (an anonymous function). To turn our `rsx!` into an Element, we need to call `cx.render`. To make patterns like these less verbose, the `rsx!` macro accepts an optional first argument on which it will call `render`. Our previous component can be shortened with this alternative syntax: @@ -136,34 +136,6 @@ cx.render(rsx!{ }) ``` - -## Boolean Mapping - -In the spirit of highly-functional apps, we suggest using the "boolean mapping" pattern when trying to conditionally hide/show an Element. - -By default, Rust lets you convert any `boolean` into any other type by calling `and_then()`. We can exploit this functionality in components by mapping to some Element. - -```rust -let show_title = true; -rsx!( - div { - show_title.and_then(|| rsx!{ - "This is the title" - }) - } -) -``` - -We can use this pattern for many things, including options: -```rust -let user_name = Some("bob"); -rsx!( - div { - user_name.map(|name| rsx!("Hello {name}")) - } -) -``` - ## Rendering Nothing Sometimes, you don't want your component to return anything at all. Under the hood, the `Element` type is just an alias for `Option`, so you can simply return `None`. @@ -176,11 +148,37 @@ fn demo(cx: Scope) -> Element { } ``` +## Boolean Mapping + +In the spirit of highly-functional apps, we suggest using the "boolean mapping" pattern when trying to conditionally hide/show an Element. + +By default, Rust lets you convert any `boolean` into an Option of any other type with [`then()`](https://doc.rust-lang.org/std/primitive.bool.html#method.then). We can use this in Components by mapping to some Element. + +```rust +let show_title = true; +rsx!( + div { + // Renders nothing by returning None when show_title is false + show_title.then(|| rsx!{ + "This is the title" + }) + } +) +``` + +We can use this pattern for many things, including options: +```rust +let user_name = Some("bob"); +rsx!( + div { + // Renders nothing if user_name is None + user_name.map(|name| rsx!("Hello {name}")) + } +) +``` + ## Moving Forward: -In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex User Interfaces! +In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex user interfaces! In the next chapter, we'll cover how to renderer lists inside your `rsx!`. - -Related Reading: -- [RSX in Depth]() diff --git a/docs/guide/src/elements/index.md b/docs/guide/src/elements/index.md index aedc39e34..c814945d1 100644 --- a/docs/guide/src/elements/index.md +++ b/docs/guide/src/elements/index.md @@ -68,6 +68,6 @@ Remember: this concept is not new! Many frameworks are declarative - with React Here's some reading about declaring UI in React: -- [https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js) +- [Difference between declarative and imperative in React.js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js), a StackOverflow thread -- [https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44) +- [Declarative vs Imperative](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44), a blog post by Myung Kim diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 37e87b7da..8cd58c841 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -10,14 +10,24 @@ In this chapter, you will learn: ## Rendering data from lists -Thinking back to our analysis of the `r/reddit` page, we notice a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app like: +If we wanted to build the Reddit app, then we need to implement a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app directly, like so: ```rust +// we shouldn't ship our app with posts that don't update! rsx!( div { - Post {/* some properties */} - Post {/* some properties */} - Post {/* some properties */} + Post { + title: "Post A", + votes: 120, + } + Post { + title: "Post B", + votes: 14, + } + Post { + title: "Post C", + votes: 999, + } } ) ``` @@ -32,7 +42,7 @@ As a simple example, let's render a list of names. First, start with our input d let names = ["jim", "bob", "jane", "doe"]; ``` -Then, we create a new iterator by calling `iter` and then `map`. In our `map` function, we'll place render our template. +Then, we create a new iterator by calling `iter` and then [`map`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map). In our `map` function, we'll render our template. ```rust let name_list = names.iter().map(|name| rsx!( @@ -40,16 +50,28 @@ let name_list = names.iter().map(|name| rsx!( )); ``` -Finally, we can include this list in the final structure: +We can include this list in the final Element: ```rust rsx!( ul { - {name_list} + name_list } ) ``` -The HTML-rendered version of this list would follow what you would expect: + +Rather than storing `name_list` in a temporary variable, we could also include the iterator inline: +```rust +rsx!( + ul { + names.iter().map(|name| rsx!( + li { "{name}" } + )) + } +) +``` + +The rendered HTML list is what you would expect: ```html
  • jim
  • @@ -59,54 +81,13 @@ The HTML-rendered version of this list would follow what you would expect:
``` -### Rendering our posts with a PostList component - -Let's start by modeling this problem with a component and some properties. - -For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. - -```rust -#[derive(Props, PartialEq)] -struct PostListProps<'a> { - posts: &'a [PostData] -} -``` -Next, we're going to define our component: - -```rust -fn App(cx: Scope) -> Element { - // First, we create a new iterator by mapping the post array - let posts = cx.props.posts.iter().map(|post| rsx!{ - Post { - title: post.title, - age: post.age, - original_poster: post.original_poster - } - }); - - // Finally, we render the post list inside of a container - cx.render(rsx!{ - ul { class: "post-list" - {posts} - } - }) -} -``` - - ## Filtering Iterators Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. -As a very simple example, let's set up a filter where we only list names that begin with the letter "J". +As a very simple example, let's set up a filter where we only list names that begin with the letter "j". -Let's make our list of names: - -```rust -let names = ["jim", "bob", "jane", "doe"]; -``` - -Then, we create a new iterator by calling `iter`, then `filter`, then `map`. In our `filter` function, we'll only allow "j" names, and in our `map` function, we'll render our template. +Using the list from above, let's create a new iterator. Before we render the list with `map` as in the previous example, we'll [`filter`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter) the names to only allow those that start with "j". ```rust let name_list = names @@ -115,53 +96,43 @@ let name_list = names .map(|name| rsx!( li { "{name}" })); ``` -Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce. +Rust's Iterators are very versatile – check out [their documentation](https://doc.rust-lang.org/std/iter/trait.Iterator.html) for more things you can do with them! -For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. +For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collect`ed our filtered list into new Vec, we would need to make an allocation to store these new elements, which slows down rendering. Instead, we create an entirely new _lazy_ iterator which Dioxus will consume in the `render` call. The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. -The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. +## Keeping list items in order with `key` -## Keeping list items in order with key +The examples above demonstrate the power of iterators in `rsx!` but all share the same issue: if your array items move (e.g. due to sorting), get inserted, or get deleted, Dioxus has no way of knowing what happened. This can cause Elements to be unnecessarily removed, changed and rebuilt when all that was needed was to change their position – this is inneficient. - -The examples above demonstrate the power of iterators in `rsx!` but all share the same issue: a lack of "keys". Whenever you render a list of elements, each item in the list must be **uniquely identifiable**. To make each item unique, you need to give it a "key". - -In Dioxus, keys are strings that uniquely identifies it among other items in that array: +To solve this problem, each item in the list must be **uniquely identifiable**. You can achieve this by giving it a unique, fixed "key". In Dioxus, a key is a string that identifies an item among others in the list. ```rust rsx!( li { key: "a" } ) ``` -Keys tell Dioxus which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps Dioxus infer what exactly has happened, and make the correct updates to the screen - +Now, if an item has already been rendered once, Dioxus can use the key to match it up later to make the correct updates – and avoid unnecessary work. NB: the language from this section is strongly borrowed from [React's guide on keys](https://reactjs.org/docs/lists-and-keys.html). + ### Where to get your key Different sources of data provide different sources of keys: - _Data from a database_: If your data is coming from a database, you can use the database keys/IDs, which are unique by nature. -- _Locally generated data_: If your data is generated and persisted locally (e.g. notes in a note-taking app), use an incrementing counter or a package like `uuid` when creating items. +- _Locally generated data_: If your data is generated and persisted locally (e.g. notes in a note-taking app), keep track of keys along with your data. You can use an incrementing counter or a package like `uuid` to generate keys for new items – but make sure they stay the same for the item's lifetime. + +Remember: keys let Dioxus uniquely identify an item among its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime. ### Rules of keys - Keys must be unique among siblings. However, it’s okay to use the same keys for Elements in different arrays. -- Keys must not change or that defeats their purpose! Don’t generate them while rendering. +- An item's key must not change – **don’t generate them on the fly** while rendering. Otherwise, Dioxus will be unable to keep track of which item is which, and we're back to square one. -### Why does Dioxus need keys? +You might be tempted to use an item's index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions. In all other cases, do not use the index for the key – it will lead to the performance problems described above. -Imagine that files on your desktop didn’t have names. Instead, you’d refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on. - -File names in a folder and Element keys in an array serve a similar purpose. They let us uniquely identify an item between its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime. - -### Gotcha -You might be tempted to use an item’s index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs. - -Similarly, do not generate keys on the fly, `gen_random`. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data. - -Note that your components won’t receive key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: +Note that if you pass the key to a [custom component](../components/index.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop: ```rust -Post { key: "{key}", id: "{id}" } +Post { key: "{key}", id: "{key}" } ``` ## Moving on @@ -171,4 +142,4 @@ In this section, we learned: - How to use iterator tools to filter and transform data - How to use keys to render lists efficiently -Moving forward, we'll finally cover user input and interactivity. +Moving forward, we'll learn more about attributes. diff --git a/docs/guide/src/elements/propsmacro.md b/docs/guide/src/elements/propsmacro.md deleted file mode 100644 index 7426a73bf..000000000 --- a/docs/guide/src/elements/propsmacro.md +++ /dev/null @@ -1 +0,0 @@ -# The Props Macro diff --git a/docs/guide/src/elements/special_attributes.md b/docs/guide/src/elements/special_attributes.md new file mode 100644 index 000000000..a09ce0bf7 --- /dev/null +++ b/docs/guide/src/elements/special_attributes.md @@ -0,0 +1,192 @@ +# Special Attributes + +Dioxus tries its hardest to stay close to React, but there are some divergences and "special behavior" that you should review before moving on. + +In this section, we'll cover special attributes built into Dioxus: + +- `dangerous_inner_html` +- Boolean attributes +- `prevent_default` + +- event handlers as string attributes +- `value`, `checked`, and `selected` + +## The HTML escape hatch: `dangerous_inner_html` + +One thing you might've missed from React is the ability to render raw HTML directly to the DOM. If you're working with pre-rendered assets, output from templates, or output from a JS library, then you might want to pass HTML directly instead of going through Dioxus. In these instances, reach for `dangerous_inner_html`. + +For example, shipping a markdown-to-Dioxus converter might significantly bloat your final application size. Instead, you'll want to pre-render your markdown to HTML and then include the HTML directly in your output. We use this approach for the [Dioxus homepage](https://dioxuslabs.com): + + +```rust +fn BlogPost(cx: Scope) -> Element { + let contents = include_str!("../post.html"); + cx.render(rsx!{ + div { + class: "markdown", + dangerous_inner_html: "{contents}", + } + }) +} +``` + +> Note! This attribute is called "dangerous_inner_html" because it is **dangerous** to pass it data you don't trust. If you're not careful, you can easily expose cross-site-scripting (XSS) attacks to your users. +> +> If you're handling untrusted input, make sure to sanitize your HTML before passing it into `dangerous_inner_html` – or just pass it to a Text Element to escape any HTML tags. + + +## Boolean Attributes + +Most attributes, when rendered, will be rendered exactly as the input you provided. However, some attributes are considered "boolean" attributes and just their presence determines whether or not they affect the output. For these attributes, a provided value of `"false"` will cause them to be removed from the target element. + +So this RSX: + +```rust +rsx!{ + div { + hidden: "false", + "hello" + } +} +``` +wouldn't actually render the `hidden` attribute: +```html +
hello
+``` + +Not all attributes work like this however. *Only the following attributes* have this behavior: + +- `allowfullscreen` +- `allowpaymentrequest` +- `async` +- `autofocus` +- `autoplay` +- `checked` +- `controls` +- `default` +- `defer` +- `disabled` +- `formnovalidate` +- `hidden` +- `ismap` +- `itemscope` +- `loop` +- `multiple` +- `muted` +- `nomodule` +- `novalidate` +- `open` +- `playsinline` +- `readonly` +- `required` +- `reversed` +- `selected` +- `truespeed` + +For any other attributes, a value of `"false"` will be sent directly to the DOM. + +## Stopping form input and navigation with `prevent_default` + +Currently, calling `prevent_default` on events in EventHandlers is not possible from Desktop/Mobile. Until this is supported, it's possible to prevent default using the `prevent_default` attribute. + +> Note: you cannot conditionally prevent default with this approach. This is a limitation until synchronous event handling is available across the Webview boundary + +To use `prevent_default`, simply attach the `prevent_default` attribute to a given element and set it to the name of the event handler you want to prevent default on. We can attach this attribute multiple times for multiple attributes. + +```rust +rsx!{ + input { + oninput: move |_| {}, + prevent_default: "oninput", + + onclick: move |_| {}, + prevent_default: "onclick", + } +} +``` + + +## Controlled inputs and `value`, `checked`, and `selected` + +In Dioxus, there is a distinction between controlled and uncontrolled inputs. Most inputs you'll use are controlled, meaning we both drive the `value` of the input and react to the `oninput`. + +Controlled components: +```rust +let value = use_state(&cx, || String::from("hello world")); + +rsx! { + input { + oninput: move |evt| value.set(evt.value.clone()), + value: "{value}", + } +} +``` + +With uncontrolled inputs, we won't actually drive the value from the component. This has its advantages when we don't want to re-render the component when the user inputs a value. We could either select the element directly - something Dioxus doesn't support across platforms - or we could handle `oninput` and modify a value without causing an update: + +```rust +let value = use_ref(&cx, || String::from("hello world")); + +rsx! { + input { + oninput: move |evt| *value.write_silent() = evt.value.clone(), + // no "value" is driven here – the input keeps track of its own value, and you can't change it + } +} +``` + +## Strings for handlers like `onclick` + +For element fields that take a handler like `onclick` or `oninput`, Dioxus will let you attach a closure. Alternatively, you can also pass a string using normal attribute syntax and assign this attribute on the DOM. + +This lets you use JavaScript (only if your renderer can execute JavaScript). + +```rust +rsx!{ + div { + // handle oninput with rust + oninput: move |_| {}, + + // or handle oninput with javascript + oninput: "alert('hello world')", + } +} + +``` + +## Wrapping up + +In this chapter, we learned: +- How to declare elements +- How to conditionally render parts of your UI +- How to render lists +- Which attributes are "special" + + diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index d441693d7..4adbf962d 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -56,7 +56,7 @@ With the default configuration, any Element defined within the `dioxus-html` cra ## Text Elements -Dioxus also supports a special type of Element: Text. Text Elements do not accept children, but rather just string literals denoted with double quotes. +Dioxus also supports a special type of Element: Text. Text Elements do not accept children, just a string literal denoted by double quotes. ```rust rsx! ( @@ -74,20 +74,20 @@ rsx! ( ) ``` -Text can also be formatted with any value that implements `Display`. We use [f-string formatting](https://docs.rs/fstrings/0.2.3/fstrings/) - a "coming soon" feature for stable Rust that is familiar for Python and JavaScript users: +Text can also be formatted with any value that implements `Display`. We use the same syntax as Rust [format strings](https://www.rustnote.com/blog/format_strings.html) – which will already be familiar for Python and JavaScript users: ```rust let name = "Bob"; rsx! ( "hello {name}" ) ``` -Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of how the `rsx!` macro (we'll cover later), our call to `format_args` must be contained within curly braces *and* square braces. +Unfortunately, you cannot drop in arbitrary expressions directly into the string literal. In the cases where we need to compute a complex value, we'll want to use `format_args!` directly. Due to specifics of the `rsx!` macro (which we'll cover later), our call to `format_args` must be contained within square braces. ```rust -rsx!( {[format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )]} ) +rsx!( [format_args!("Hello {}", if enabled { "Jack" } else { "Bob" } )] ) ``` -Alternatively, `&str` can be included directly, though it must be inside of an array: +Alternatively, `&str` can be included directly, though it must also be inside square braces: ```rust rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) @@ -104,7 +104,7 @@ rsx! ( "hello {name}" ) ## Attributes -Every Element in your User Interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID. +Every Element in your user interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID. To do this, we use the familiar struct-style syntax that Rust provides: @@ -123,7 +123,7 @@ Each field is defined as a method on the element in the `dioxus-html` crate. Thi 1) file an issue if the attribute _should_ be enabled 2) add a custom attribute on-the-fly -To use custom attributes, simply put the attribute name in quotes followed by a colon: +To use custom attributes, simply put the attribute name in quotes: ```rust rsx!( @@ -170,4 +170,4 @@ We learned: - Elements can either be a named container or text - Some Elements have properties that the renderer can use to draw the UI to the screen -Next, we'll compose Elements together to form components. +Next, we'll compose Elements together using Rust-based logic. diff --git a/docs/guide/src/images/oldnew.png b/docs/guide/src/images/oldnew.png new file mode 100644 index 000000000..5aca37e40 Binary files /dev/null and b/docs/guide/src/images/oldnew.png differ diff --git a/docs/guide/src/interactivity/event_handlers.md b/docs/guide/src/interactivity/event_handlers.md index ee4560b1e..8fcfe6edb 100644 --- a/docs/guide/src/interactivity/event_handlers.md +++ b/docs/guide/src/interactivity/event_handlers.md @@ -1 +1,3 @@ # Event handlers + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index adf43c6ef..a84a0b129 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -76,6 +76,78 @@ This is why hooks called out of order will fail - if we try to downcast a `Hook< This pattern might seem strange at first, but it can be a significant upgrade over structs as blobs of state, which tend to be difficult to use in [Rust given the ownership system](https://rust-lang.github.io/rfcs/2229-capture-disjoint-fields.html). + +## Rules of hooks + +Hooks are sensitive to how they are used. To use hooks, you must abide by the +"rules of hooks" ([borrowed from react](https://reactjs.org/docs/hooks-rules.html)): + +- Functions with "use_" should not be called in callbacks +- Functions with "use_" should not be called out of order +- Functions with "use_" should not be called in loops or conditionals + +Examples of "no-nos" include: + +### ❌ Nested uses + +```rust +// ❌ don't call use_hook or any `use_` function *inside* use_hook! +cx.use_hook(|_| { + let name = cx.use_hook(|_| "ads"); +}) + +// ✅ instead, move the first hook above +let name = cx.use_hook(|_| "ads"); +cx.use_hook(|_| { + // do something with name here +}) +``` + +### ❌ Uses in conditionals +```rust +// ❌ don't call use_ in conditionals! +if do_thing { + let name = use_state(&cx, || 0); +} + +// ✅ instead, *always* call use_state but leave your logic +let name = use_state(&cx, || 0); +if do_thing { + // do thing with name here +} +``` + +### ❌ Uses in loops + + +```rust +// ❌ Do not use hooks in loops! +let mut nodes = vec![]; + +for name in names { + let age = use_state(&cx, |_| 0); + nodes.push(cx.render(rsx!{ + div { "{age}" } + })) +} + +// ✅ Instead, consider refactoring your usecase into components +#[inline_props] +fn Child(cx: Scope, name: String) -> Element { + let age = use_state(&cx, |_| 0); + cx.render(rsx!{ div { "{age}" } }) +} + +// ✅ Or, use a hashmap with use_ref +```rust +let ages = use_ref(&cx, || HashMap::new()); + +names.iter().map(|name| { + let age = ages.get(name).unwrap(); + cx.render(rsx!{ div { "{age}" } }) +}) +``` + ## Building new Hooks However, most hooks you'll interact with *don't* return an `&mut T` since this is not very useful in a real-world situation. @@ -168,23 +240,15 @@ fn example(cx: Scope) -> Element { ``` + ## Hooks provided by the `Dioxus-Hooks` package By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free to click on each hook to view its definition and associated documentation. -- [use_state](https://docs.rs/dioxus_hooks/use_state) - store state with ergonomic updates -- [use_ref](https://docs.rs/dioxus_hooks/use_ref) - store non-clone state with a refcell -- [use_future](https://docs.rs/dioxus_hooks/use_future) - store a future to be polled after initialization -- [use_coroutine](https://docs.rs/dioxus_hooks/use_coroutine) - store a future that can be stopped/started/communicated with -- [use_noderef](https://docs.rs/dioxus_hooks/use_noderef) - store a handle to the native element -- [use_callback](https://docs.rs/dioxus_hooks/use_callback) - store a callback that implements PartialEq for memoization -- [use_provide_context](https://docs.rs/dioxus_hooks/use_provide_context) - expose state to descendent components -- [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context` - -For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference. - -## Wrapping up - -In this chapter, we learned about the mechanics and intricacies of storing state inside a component. - -In the next chapter, we'll cover event listeners in similar depth, and how to combine the two to build interactive components. +- [use_state](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_state.html) - store state with ergonomic updates +- [use_ref](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_ref.html) - store non-clone state with a refcell +- [use_future](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_future.html) - store a future to be polled after initialization +- [use_coroutine](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_coroutine.html) - store a future that can be stopped/started/communicated with +- [use_context_provider](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context_provider.html) - expose state to descendent components +- [use_context](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context.html) - consume state provided by `use_provide_context` +- [use_suspense](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_suspense.html) diff --git a/docs/guide/src/interactivity/importanthooks.md b/docs/guide/src/interactivity/importanthooks.md new file mode 100644 index 000000000..161c5becf --- /dev/null +++ b/docs/guide/src/interactivity/importanthooks.md @@ -0,0 +1,127 @@ +# `use_state` and `use_ref` + +Most components you will write in Dioxus will need to store state somehow. For local state, we provide two very convenient hooks: + +- [use_state](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_state.html) +- [use_ref](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_ref.html) + +Both of these hooks are extremely powerful and flexible, so we've dedicated this section to understanding them properly. + +> These two hooks are not the only way to store state. You can always build your own hooks! + +## `use_state` + +The `use_state` hook is very similar to its React counterpart. When we use it, we get two values: + +- The value itself as an `&T` +- A handle to set the value `&UseState` + +```rust +let (count, set_count) = use_state(&cx, || 0); + +// then to set the count +set_count(count + 1) +``` + +However, the `set_count` object is more powerful than it looks. You can use it as a closure, but you can also call methods on it to do more powerful operations. + +For instance, we can get a handle to the current value at any time: + +```rust +let current: Rc = set_count.current(); +``` + +Or, we can get the inner handle to set the value + +```rust +let setter: Rc = set_count.setter(); +``` + +Or, we can set a new value using the current value as a reference: + +```rust +set_count.modify(|i| i + 1); +``` + +If the value is cheaply cloneable, then we can call `with_mut` to get a mutable reference to the value: + +```rust +set_count.with_mut(|i| *i += 1); +``` + +Alternatively, we can call `make_mut` which gives us a `RefMut` to the underlying value: + +```rust +*set_count.make_mut() += 1; +``` + +Plus, the `set_count` handle is cloneable into async contexts, so we can use it in futures. + +```rust +// count up infinitely +cx.spawn({ + to_owned![set_count]; + async move { + loop { + wait_ms(100).await; + set_count.modify(|i| i + 1); + } + } +}) +``` + +## `use_ref` + +You might've noticed a fundamental limitation to `use_state`: to modify the value in-place, it must be cheaply cloneable. But what if your type is not cheap to clone? + +In these cases, you should reach for `use_ref` which is essentially just a glorified `Rc>` (Rust [smart pointers](https://doc.rust-lang.org/book/ch15-04-rc.html)). + +This provides us some runtime locks around our data, trading reliability for performance. For most cases though, you will find it hard to make `use_ref` crash. + +> Note: this is *not* exactly like its React counterpart since calling `write` will cause a re-render. For more React parity, use the `write_silent` method. + +To use the hook: + +```rust +let names = use_ref(&cx, || vec!["susie", "calvin"]); +``` + +To read the hook values, use the `read()` method: + +```rust +cx.render(rsx!{ + ul { + names.read().iter().map(|name| { + rsx!{ "{name}" } + }) + } +}) +``` + +And then to write into the hook value, use the `write` method: + +```rust +names.write().push("Tiger"); +``` + +If you don't want to re-render the component when names is updated, then we can use the `write_silent` method: + +```rust +names.write_silent().push("Transmogrifier"); +``` + +Again, like `UseState`, the `UseRef` handle is clonable into async contexts: + + +```rust +// infinitely push calvin into the list +cx.spawn({ + to_owned![names]; + async move { + loop { + wait_ms(100).await; + names.write().push("Calvin"); + } + } +}) +``` diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 0e1a61252..6075a4a5a 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -1,6 +1,6 @@ # Adding Interactivity -So far, we've learned how to describe the structure and properties of our user interfaces. Unfortunately, they're static and quite a bit uninteresting. In this chapter, we're going to learn how to add interactivity through events, state, and tasks. +So far, we've learned how to describe the structure and properties of our user interfaces. Unfortunately, they're static and quite uninteresting. In this chapter, we're going to learn how to add interactivity through events, state, and tasks. ## Primer on interactivity @@ -44,9 +44,9 @@ fn App(cx: Scope) -> Element { } ``` -State in Dioxus follows a pattern called "one-way-data-flow." As your components create new components as their children, your app's structure will eventually grow into a tree where state gets passed down from the root component into "leaves" of the tree. +State in Dioxus follows a pattern called "one-way data-flow." As your components create new components as their children, your app's structure will eventually grow into a tree where state gets passed down from the root component into "leaves" of the tree. -You've probably seen the tree of UI components represented using an directed-acyclic-graph: +You've probably seen the tree of UI components represented using a directed acyclic graph: ![image](../images/component_tree.png) @@ -64,7 +64,7 @@ The most common hook you'll use for storing state is `use_state`. `use_state` pr ```rust fn App(cx: Scope)-> Element { - let post = use_state(&cx, || { + let (post, set_post) = use_state(&cx, || { PostData { id: Uuid::new_v4(), score: 10, @@ -84,10 +84,10 @@ fn App(cx: Scope)-> Element { } ``` -Whenever we have a new post that we want to render, we can call `set` on `post` and provide a new value: +Whenever we have a new post that we want to render, we can call `set_post` and provide a new value: ```rust -post.set(PostData { +set_post(PostData { id: Uuid::new_v4(), score: 20, comment_count: 0, @@ -112,11 +112,11 @@ For example, let's say we provide a button to generate a new post. Whenever the ```rust fn App(cx: Scope)-> Element { - let post = use_state(&cx, || PostData::new()); + let (post, set_post) = use_state(&cx, || PostData::new()); cx.render(rsx!{ button { - on_click: move |_| post.set(PostData::random()) + on_click: move |_| set_post(PostData::random()) "Generate a random post" } Post { props: &post } @@ -128,7 +128,12 @@ We'll dive much deeper into event listeners later. ### Updating state asynchronously -We can also update our state outside of event listeners with `coroutines`. `Coroutines` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component. Since coroutines stick around between renders, the data in them must be valid for the `'static` lifetime. We must explicitly declare which values our task will rely on to avoid the `stale props` problem common in React. +We can also update our state outside of event listeners with `futures` and `coroutines`. + +- `Futures` are Rust's version of promises that can execute asynchronous work by an efficient polling system. We can submit new futures to Dioxus either through `push_future` which returns a `TaskId` or with `spawn`. +- `Coroutines` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component. + +Since coroutines and Futures stick around between renders, the data in them must be valid for the `'static` lifetime. We must explicitly declare which values our task will rely on to avoid the `stale props` problem common in React. We can use tasks in our components to build a tiny stopwatch that ticks every second. @@ -136,14 +141,14 @@ We can use tasks in our components to build a tiny stopwatch that ticks every se ```rust fn App(cx: Scope)-> Element { - let mut sec_elapsed = use_state(&cx, || 0); + let (elapsed, set_elapsed) = use_state(&cx, || 0); use_future(&cx, || { - let mut sec_elapsed = sec_elapsed.for_async(); + to_owned![set_elapsed]; // explicitly capture this hook for use in async async move { loop { TimeoutFuture::from_ms(1000).await; - sec_elapsed += 1; + set_elapsed.modify(|i| i + 1) } } }); @@ -152,12 +157,10 @@ fn App(cx: Scope)-> Element { } ``` -Using asynchronous code can be difficult! This is just scratching the surface of what's possible. We have an entire chapter on using async properly in your Dioxus Apps. +Using asynchronous code can be difficult! This is just scratching the surface of what's possible. We have an entire chapter on using async properly in your Dioxus Apps. We have an entire section dedicated to using `async` properly later in this book. ### How do I tell Dioxus that my state changed? -So far, we've only updated our state with `.set`. However, you might've noticed that we used `AddAssign` to increment the `sec_elapsed` value in our stopwatch example *without* calling set. This is because the `AddAssign` trait is implemented for `UseState` (the wrapper around our value returned from `use_state`). Under the hood, whenever you try to mutate our value through `UseState`, you're actually calling `.set` which informs Dioxus that _this_ component needs to be updated on the screen. - Whenever you inform Dioxus that the component needs to be updated, it will "render" your component again, storing the previous and current Elements in memory. Dioxus will automatically figure out the differences between the old and the new and generate a list of edits that the renderer needs to apply to change what's on the screen. This process is called "diffing": ![Diffing](../images/diffing.png) @@ -170,11 +173,37 @@ With these building blocks, we can craft new hooks similar to `use_state` that l In general, Dioxus should be plenty fast for most use cases. However, there are some rules you should consider following to ensure your apps are quick. -- 1) **Don't call set—state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates. +- 1) **Don't call set_state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates or enter an infinite loop. - 2) **Break your state apart into smaller sections.** Hooks are explicitly designed to "unshackle" your state from the typical model-view-controller paradigm, making it easy to reuse useful bits of code with a single function. - 3) **Move local state down**. Dioxus will need to re-check child components of your app if the root component is constantly being updated. You'll get best results if rapidly-changing state does not cause major re-renders. + + +## The `Scope` object + +Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. + +Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? + +```javascript +// in React: +function Component(props) { + // This state persists between component renders, but where does it live? + let [state, set_state] = useState(10); +} +``` + +React uses global variables to store this information. However, global mutable variables must be carefully managed and are broadly discouraged in Rust programs. Because Dioxus needs to work with the rules of Rust it uses the `Scope` rather than a global state object to maintain some internal bookkeeping. + +That's what the `Scope` object is: a place for the Component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Scope` object to build robust and performant extensions for Dioxus. + +```rust +fn Post(cx: Scope) -> Element { + cx.render(rsx!("hello")) +} +``` ## Moving On diff --git a/docs/guide/src/interactivity/lifecycles.md b/docs/guide/src/interactivity/lifecycles.md index 07a02f4ea..fdaa5b5e3 100644 --- a/docs/guide/src/interactivity/lifecycles.md +++ b/docs/guide/src/interactivity/lifecycles.md @@ -1 +1,3 @@ # Lifecycle, updates, and effects + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/interactivity/user_input.md b/docs/guide/src/interactivity/user_input.md index 2424e7f14..1e7ebbcbd 100644 --- a/docs/guide/src/interactivity/user_input.md +++ b/docs/guide/src/interactivity/user_input.md @@ -1 +1,6 @@ # User Input and Controlled Components + +Handling user input is one of the most common things your app will do, but it can be tricky + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index de634dc0a..75cfc6606 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -8,7 +8,7 @@ We'll learn about: - Suggested cargo extensions -For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md). +For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md). # Setting up Dioxus @@ -19,7 +19,7 @@ Dioxus requires a few main things to get up and running: Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more. -### Installing Rust +## Installing Rust Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. @@ -29,24 +29,39 @@ Once installed, make sure to install wasm32-unknown-unknown as a target if you'r rustup target add wasm32-unknown-unknown ``` -### Platform-Specific Dependencies +## Platform-Specific Dependencies If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of Linux with no default web rendering engine, you might need to install some additional dependencies. -For windows users: download the [bootstrapper for Webview2 from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) -For linux users, we need the development libraries for libgtk. +### Windows + +Windows Desktop apps depend on WebView2 - a library which should be installed in all modern Windows distributions. If you have Edge installed, then Dioxus will work fine. If you *don't* have Webview2, [then you can install it through Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). MS provides 3 options: + +1. A tiny "evergreen" *bootstrapper* which will fetch an installer from Microsoft's CDN +2. A tiny *installer* which will fetch Webview2 from Microsoft's CDN +3. A statically linked version of Webview2 in your final binary for offline users + +For development purposes, use Option 1. + +### Linux + +Webview Linux apps require WebkitGtk. When distributing, this can be part of your dependency tree in your `.rpm` or `.deb`. However, it's very likely that your users will already have WebkitGtk. ``` sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev ``` -When distributing onto older Windows platforms or less-mainstream + +If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux). +### macOS + +Currently - everything for macOS is built right in! However, you might run into an issue if you're using nightly Rust due to some permissions issues in our Tao dependency (which have been resolved but not published). -### Dioxus-CLI for dev server, bundling, etc. +## Dioxus-CLI for dev server, bundling, etc. We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (which should be automatically installed with Rust): @@ -62,7 +77,7 @@ $ cargo install --force dioxus-cli We provide this 1st-party tool to save you from having to run potentially untrusted code every time you add a crate to your project - as is standard in the NPM ecosystem. -### Suggested extensions +## Suggested extensions If you want to keep your traditional `npm install XXX` workflow for adding packages, you might want to install `cargo-edit` and a few other fun `cargo` extensions: diff --git a/docs/guide/src/state/errorhandling.md b/docs/guide/src/state/errorhandling.md index 40436521f..767a314b0 100644 --- a/docs/guide/src/state/errorhandling.md +++ b/docs/guide/src/state/errorhandling.md @@ -9,3 +9,4 @@ fn App((cx, props): Component) -> Element { } ``` +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 1874dd73e..07584124c 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -4,5 +4,25 @@ Every app you'll build with Dioxus will have some sort of state that needs to be In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview +## Terminology + + +> This section is currently under construction! 🏗 + + + + +## Important hook: `use_state` + + + + + +## Important hook: `use_ref` + + + + +## `provide_context` and `consume_context` ## Terminology diff --git a/docs/guide/src/state/liftingstate.md b/docs/guide/src/state/liftingstate.md index 3e906a0ca..fec62d2c7 100644 --- a/docs/guide/src/state/liftingstate.md +++ b/docs/guide/src/state/liftingstate.md @@ -1 +1,4 @@ # Lifting State + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/localstate.md b/docs/guide/src/state/localstate.md index 29085ac45..e54c1e45f 100644 --- a/docs/guide/src/state/localstate.md +++ b/docs/guide/src/state/localstate.md @@ -1 +1,4 @@ # Local State + + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/state/sharedstate.md b/docs/guide/src/state/sharedstate.md index ec1ede1d1..148b6eb66 100644 --- a/docs/guide/src/state/sharedstate.md +++ b/docs/guide/src/state/sharedstate.md @@ -1 +1,7 @@ # Global State + + +cx.provide_context() +cx.consume_context() + +> This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/components.md b/docs/guide/src/tutorial/components.md index bfbb760b3..82bae6cd7 100644 --- a/docs/guide/src/tutorial/components.md +++ b/docs/guide/src/tutorial/components.md @@ -1 +1,3 @@ # Defining Components + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/state.md b/docs/guide/src/tutorial/state.md index 35df690d6..6026c7546 100644 --- a/docs/guide/src/tutorial/state.md +++ b/docs/guide/src/tutorial/state.md @@ -1 +1,3 @@ # Defining State + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/structure.md b/docs/guide/src/tutorial/structure.md index 1cbb47579..eaf8afe43 100644 --- a/docs/guide/src/tutorial/structure.md +++ b/docs/guide/src/tutorial/structure.md @@ -1 +1,4 @@ # Structuring our app + + +This section is currently under construction! 🏗 diff --git a/docs/guide/src/tutorial/styling.md b/docs/guide/src/tutorial/styling.md index 3e90761b5..b25db14d2 100644 --- a/docs/guide/src/tutorial/styling.md +++ b/docs/guide/src/tutorial/styling.md @@ -1 +1,4 @@ # Styling + + +This section is currently under construction! 🏗 diff --git a/docs/nightly/readme.md b/docs/nightly/readme.md new file mode 100644 index 000000000..99f706e81 --- /dev/null +++ b/docs/nightly/readme.md @@ -0,0 +1 @@ +this directory is for deploying into diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 3feb9fe36..18f351bfa 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -2,11 +2,12 @@ - [Introduction](README.md) -- [Web](web/index.md) -- [Desktop](desktop/index.md) -- [Mobile](mobile/index.md) -- [SSR](ssr/index.md) -- [TUI](tui/index.md) +- [Platforms](platforms/index.md) + - [Web](platforms/web.md) + - [Server Side Rendering](platforms/ssr.md) + - [Desktop](platforms/desktop.md) + - [Mobile](platforms/mobile.md) + - [TUI](platforms/tui.md) - [Advanced Guides](guide/index.md) - [RSX in Depth](guide/rsx_in_depth.md) diff --git a/docs/reference/src/desktop/index.md b/docs/reference/src/platforms/desktop.md similarity index 98% rename from docs/reference/src/desktop/index.md rename to docs/reference/src/platforms/desktop.md index 8011f1eed..18393f132 100644 --- a/docs/reference/src/desktop/index.md +++ b/docs/reference/src/platforms/desktop.md @@ -1,4 +1,4 @@ -# Desktop +# Getting Started: Desktop One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. diff --git a/docs/reference/src/platforms/index.md b/docs/reference/src/platforms/index.md new file mode 100644 index 000000000..fbbabd35d --- /dev/null +++ b/docs/reference/src/platforms/index.md @@ -0,0 +1,10 @@ +# Platforms + +Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform. + +### Setup Guides +- [Web](web.md) +- [Server Side Rendering](ssr.md) +- [Desktop](desktop.md) +- [Mobile](mobile.md) +- [TUI](tui.md) \ No newline at end of file diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/platforms/mobile.md similarity index 99% rename from docs/reference/src/mobile/index.md rename to docs/reference/src/platforms/mobile.md index 2e8344ed1..5a41ffe60 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/platforms/mobile.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/docs/reference/src/ssr/index.md b/docs/reference/src/platforms/ssr.md similarity index 88% rename from docs/reference/src/ssr/index.md rename to docs/reference/src/platforms/ssr.md index aef223dfd..09e32ba43 100644 --- a/docs/reference/src/ssr/index.md +++ b/docs/reference/src/platforms/ssr.md @@ -2,11 +2,8 @@ The Dioxus VirtualDom can be rendered to a string by traversing the Element Tree. This is implemented in the `ssr` crate where your Dioxus app can be directly rendered to HTML to be served by a web server. - - ## Setup - If you just want to render `rsx!` or a VirtualDom to HTML, check out the API docs. It's pretty simple: ```rust @@ -19,8 +16,7 @@ println!("{}", dioxus::ssr::render_vdom(&vdom)); println!( "{}", dioxus::ssr::render_lazy(rsx! { h1 { "Hello, world!" } } ); ``` - -However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. +However, for this guide, we're going to show how to use Dioxus SSR with `Axum`. Make sure you have Rust and Cargo installed, and then create a new project: @@ -29,15 +25,16 @@ $ cargo new --bin demo $ cd app ``` -Add Dioxus with the `desktop` feature: +Add Dioxus with the `ssr` feature: ```shell $ cargo add dioxus --features ssr ``` Next, add all the Axum dependencies. This will be different if you're using a different Web Framework + ``` -$ cargo add dioxus tokio --features full +$ cargo add tokio --features full $ cargo add axum ``` @@ -45,12 +42,11 @@ Your dependencies should look roughly like this: ```toml [dependencies] -axum = "0.4.3" +axum = "0.4.5" dioxus = { version = "*", features = ["ssr"] } tokio = { version = "1.15.0", features = ["full"] } ``` - Now, setup your Axum app to respond on an endpoint. ```rust @@ -63,7 +59,11 @@ async fn main() { println!("listening on http://{}", addr); axum::Server::bind(&addr) - .serve(Router::new().route("/", get(app_endpoint))) + .serve( + Router::new() + .route("/", get(app_endpoint)) + .into_make_service(), + ) .await .unwrap(); } @@ -88,14 +88,14 @@ async fn app_endpoint() -> Html { } let mut app = VirtualDom::new(app); let _ = app.rebuild(); - + Html(dioxus::ssr::render_vdom(&app)) } ``` And that's it! -> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it *must* remain on the thread it started. We are working on loosening this requirement. +> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement. ## Future Steps diff --git a/docs/reference/src/tui/index.md b/docs/reference/src/platforms/tui.md similarity index 98% rename from docs/reference/src/tui/index.md rename to docs/reference/src/platforms/tui.md index 3947883d4..e646bd4ba 100644 --- a/docs/reference/src/tui/index.md +++ b/docs/reference/src/platforms/tui.md @@ -1,4 +1,4 @@ -# TUI +# Getting Started: TUI TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/platforms/web.md similarity index 98% rename from docs/reference/src/web/index.md rename to docs/reference/src/platforms/web.md index 062cba177..2ff5f621e 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/platforms/web.md @@ -1,4 +1,4 @@ -# Getting Started: Dioxus for Web +# Getting Started: Web [*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev) diff --git a/docs/router/.gitignore b/docs/router/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/router/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/router/book.toml b/docs/router/book.toml new file mode 100644 index 000000000..216bc555e --- /dev/null +++ b/docs/router/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Dioxus Router" diff --git a/docs/router/src/SUMMARY.md b/docs/router/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/router/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/router/src/chapter_1.md b/docs/router/src/chapter_1.md new file mode 100644 index 000000000..d4a45381b --- /dev/null +++ b/docs/router/src/chapter_1.md @@ -0,0 +1,3 @@ +# Chapter 1 + +The Dioxus Router is very important!! diff --git a/examples/assets/logo.png b/examples/assets/logo.png new file mode 100644 index 000000000..7d08f2af8 Binary files /dev/null and b/examples/assets/logo.png differ diff --git a/examples/calculator.rs b/examples/calculator.rs index 8597a8897..5e23a3846 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,17 +18,19 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value: UseState = use_state(&cx, || String::from("0")); + let (display_value, set_display_value) = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { - if display_value.get() == "0" { - display_value.set(String::new()); + if display_value == "0" { + set_display_value(String::new()); } - display_value.modify().push_str(num.to_string().as_str()); + set_display_value + .make_mut() + .push_str(num.to_string().as_str()); }; let input_operator = move |key: &str| { - display_value.modify().push_str(key); + set_display_value.make_mut().push_str(key); }; cx.render(rsx!( @@ -53,7 +55,7 @@ fn app(cx: Scope) -> Element { KeyCode::Num9 => input_digit(9), KeyCode::Backspace => { if !display_value.len() != 0 { - display_value.modify().pop(); + set_display_value.make_mut().pop(); } } _ => {} @@ -65,21 +67,21 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-clear", onclick: move |_| { - display_value.set(String::new()); - if display_value != "" { - display_value.set("0".into()); + set_display_value(String::new()); + if !display_value.is_empty(){ + set_display_value("0".into()); } }, - [if display_value == "" { "C" } else { "AC" }] + [if display_value.is_empty() { "C" } else { "AC" }] } button { class: "calculator-key key-sign", onclick: move |_| { - let temp = calc_val(display_value.get().clone()); + let temp = calc_val(display_value.clone()); if temp > 0.0 { - display_value.set(format!("-{}", temp)); + set_display_value(format!("-{}", temp)); } else { - display_value.set(format!("{}", temp.abs())); + set_display_value(format!("{}", temp.abs())); } }, "±" @@ -87,8 +89,8 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-percent", onclick: move |_| { - display_value.set( - format!("{}", calc_val(display_value.get().clone()) / 100.0) + set_display_value( + format!("{}", calc_val(display_value.clone()) / 100.0) ); }, "%" @@ -98,7 +100,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push('.'), + button { class: "calculator-key key-dot", onclick: move |_| set_display_value.make_mut().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -130,7 +132,7 @@ fn app(cx: Scope) -> Element { } button { class: "calculator-key key-equals", onclick: move |_| { - display_value.set(format!("{}", calc_val(display_value.get().clone()))); + set_display_value(format!("{}", calc_val(display_value.clone()))); }, "=" } diff --git a/examples/crm.rs b/examples/crm.rs index 504459fc7..f6e7e05af 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -20,11 +20,11 @@ pub struct Client { } fn app(cx: Scope) -> Element { - let scene = use_state(&cx, || Scene::ClientsList); let clients = use_ref(&cx, || vec![] as Vec); - let firstname = use_state(&cx, String::new); - let lastname = use_state(&cx, String::new); - let description = use_state(&cx, String::new); + let (scene, set_scene) = use_state(&cx, || Scene::ClientsList); + let (firstname, set_firstname) = use_state(&cx, String::new); + let (lastname, set_lastname) = use_state(&cx, String::new); + let (description, set_description) = use_state(&cx, String::new); cx.render(rsx!( body { @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match *scene { + match scene { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } @@ -51,8 +51,8 @@ fn app(cx: Scope) -> Element { }) ) } - button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" } + button { class: "pure-button pure-button-primary", onclick: move |_| set_scene(Scene::NewClientForm), "Add New" } + button { class: "pure-button", onclick: move |_| set_scene(Scene::Settings), "Settings" } } ), Scene::NewClientForm => rsx!( @@ -63,19 +63,19 @@ fn app(cx: Scope) -> Element { class: "new-client firstname", placeholder: "First name", value: "{firstname}", - oninput: move |e| firstname.set(e.value.clone()) + oninput: move |e| set_firstname(e.value.clone()) } input { class: "new-client lastname", placeholder: "Last name", value: "{lastname}", - oninput: move |e| lastname.set(e.value.clone()) + oninput: move |e| set_lastname(e.value.clone()) } textarea { class: "new-client description", placeholder: "Description", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| set_description(e.value.clone()) } } button { @@ -86,13 +86,13 @@ fn app(cx: Scope) -> Element { first_name: (*firstname).clone(), last_name: (*lastname).clone(), }); - description.set(String::new()); - firstname.set(String::new()); - lastname.set(String::new()); + set_description(String::new()); + set_firstname(String::new()); + set_lastname(String::new()); }, "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), + button { class: "pure-button", onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } @@ -108,7 +108,7 @@ fn app(cx: Scope) -> Element { } button { class: "pure-button pure-button-primary", - onclick: move |_| scene.set(Scene::ClientsList), + onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs new file mode 100644 index 000000000..e623d5b50 --- /dev/null +++ b/examples/custom_assets.rs @@ -0,0 +1,14 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + "This should show an image:" + img { src: "examples/assets/logo.png", } + } + }) +} diff --git a/examples/disabled.rs b/examples/disabled.rs index 390a5550f..949c25369 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -5,12 +5,12 @@ fn main() { } fn app(cx: Scope) -> Element { - let disabled = use_state(&cx, || false); + let (disabled, set_disabled) = use_state(&cx, || false); cx.render(rsx! { div { button { - onclick: move |_| disabled.set(!disabled.get()), + onclick: move |_| set_disabled(!disabled), "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" } @@ -18,11 +18,6 @@ fn app(cx: Scope) -> Element { disabled: "{disabled}", "lower button" } - - input { - value: "false", - } - } }) } diff --git a/examples/dog_app.rs b/examples/dog_app.rs index f2c51d6fb..a30f3e5e8 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -16,7 +16,7 @@ struct ListBreeds { } fn app(cx: Scope) -> Element { - let fut = use_future(&cx, || async move { + let breeds = use_future(&cx, || async move { reqwest::get("https://dog.ceo/api/breeds/list/all") .await .unwrap() @@ -24,9 +24,9 @@ fn app(cx: Scope) -> Element { .await }); - let selected_breed = use_state(&cx, || None); + let (breed, set_breed) = use_state(&cx, || None); - match fut.value() { + match breeds.value() { Some(Ok(breeds)) => cx.render(rsx! { div { h1 {"Select a dog breed!"} @@ -36,14 +36,14 @@ fn app(cx: Scope) -> Element { breeds.message.keys().map(|breed| rsx!( li { button { - onclick: move |_| selected_breed.set(Some(breed.clone())), + onclick: move |_| set_breed(Some(breed.clone())), "{breed}" } } )) } div { flex: "50%", - match &*selected_breed { + match breed { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } @@ -73,9 +73,9 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); - let breed_name = use_state(&cx, || breed.clone()); - if breed_name.get() != breed { - breed_name.set(breed.clone()); + let (name, set_name) = use_state(&cx, || breed.clone()); + if name != breed { + set_name(breed.clone()); fut.restart(); } diff --git a/examples/fermi.rs b/examples/fermi.rs new file mode 100644 index 000000000..2be01d019 --- /dev/null +++ b/examples/fermi.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::prelude::*; + +fn main() { + dioxus::desktop::launch(app) +} + +static NAME: Atom = |_| "world".to_string(); + +fn app(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + + cx.render(rsx! { + div { "hello {name}!" } + Child {} + }) +} + +fn Child(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + + cx.render(rsx! { + button { + onclick: move |_| set_name("dioxus".to_string()), + "reset name" + } + }) +} diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index fe5ab23bb..728812027 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -56,7 +56,6 @@ fn app(cx: Scope) -> Element { ) }) } - }) } diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index 523405b39..e5644e2a0 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -4,7 +4,7 @@ fn main() { dioxus::desktop::launch_with_props(app, (), |c| { c.with_file_drop_handler(|_w, e| { println!("{:?}", e); - false + true }) }); } diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 000000000..5f0c992f5 --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,26 @@ +//! Forms +//! +//! Dioxus forms deviate slightly from html, automatically returning all named inputs +//! in the "values" field + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Form" } + form { + onsubmit: move |ev| println!("Submitted {:?}", ev.values), + oninput: move |ev| println!("Input {:?}", ev.values), + input { r#type: "text", name: "username" } + input { r#type: "text", name: "full-name" } + input { r#type: "password", name: "password" } + button { "Submit the form" } + } + } + }) +} diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 69e3b8faa..8dba1cb91 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -17,9 +17,9 @@ impl Label { fn new_list(num: usize) -> Vec { let mut rng = SmallRng::from_entropy(); let mut labels = Vec::with_capacity(num); - for _ in 0..num { + for x in 0..num { labels.push(Label { - key: 0, + key: x, labels: [ ADJECTIVES.choose(&mut rng).unwrap(), COLOURS.choose(&mut rng).unwrap(), @@ -33,7 +33,7 @@ impl Label { fn app(cx: Scope) -> Element { let items = use_ref(&cx, Vec::new); - let selected = use_state(&cx, || None); + let (selected, set_selected) = use_state(&cx, || None); cx.render(rsx! { div { class: "container", @@ -71,7 +71,7 @@ fn app(cx: Scope) -> Element { rsx!(tr { class: "{is_in_danger}", td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } - td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), + td { class:"col-md-1", onclick: move |_| set_selected(Some(id)), a { class: "lbl", item.labels } } td { class: "col-md-1", diff --git a/examples/heavy_compute.rs b/examples/heavy_compute.rs new file mode 100644 index 000000000..6e10a2f1f --- /dev/null +++ b/examples/heavy_compute.rs @@ -0,0 +1,28 @@ +//! This example shows that you can place heavy work on the main thread, and then +//! +//! You *should* be using `tokio::spawn_blocking` instead. +//! +//! Your app runs in an async runtime (Tokio), so you should avoid blocking +//! the rendering of the VirtualDom. +//! +//! + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + // This is discouraged + std::thread::sleep(std::time::Duration::from_millis(2_000)); + + // This is suggested + tokio::task::spawn_blocking(move || { + std::thread::sleep(std::time::Duration::from_millis(2_000)); + }); + + cx.render(rsx! { + div { "Hello, world!" } + }) +} diff --git a/examples/hydration.rs b/examples/hydration.rs index 6ec777352..83b7633df 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -20,13 +20,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let (val, set_val) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "hello world. Count: {val}" } button { - onclick: move |_| *val.modify() += 1, + onclick: move |_| set_val(val + 1), "click to increment" } } diff --git a/examples/login_form.rs b/examples/login_form.rs new file mode 100644 index 000000000..7fd91262d --- /dev/null +++ b/examples/login_form.rs @@ -0,0 +1,48 @@ +//! This example demonstrates the following: +//! Futures in a callback, Router, and Forms + +use dioxus::events::*; +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let onsubmit = move |evt: FormEvent| { + cx.spawn(async move { + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) + .send() + .await; + + match resp { + // Parse data from here, such as storing a response token + Ok(_data) => println!("Login successful"), + + //Handle any errors from the fetch here + Err(_err) => println!("Login failed"), + } + }); + }; + + cx.render(rsx! { + h1 { "Login" } + form { + onsubmit: onsubmit, + prevent_default: "onsubmit", // Prevent the default behavior of
to post + + input { "type": "text" } + label { "Username" } + br {} + input { "type": "password" } + label { "Password" } + br {} + button { "Login" } + } + }) +} diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 82634d771..04c1811a1 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -15,16 +15,16 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, PlayerState::new); + let (state, set_state) = use_state(&cx, PlayerState::new); cx.render(rsx!( div { h1 {"Select an option"} h3 { "The radio is... " [state.is_playing()] "!" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Pause), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Pause), "Pause" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Play), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Play), "Play" } } diff --git a/examples/readme.rs b/examples/readme.rs index a9ea60822..b37a8e07a 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } } }) } diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 20760ef5b..8f490bf26 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -12,7 +12,7 @@ fn main() { } fn example(cx: Scope) -> Element { - let items = use_state(&cx, || { + let (items, _set_items) = use_state(&cx, || { vec![Thing { a: "asd".to_string(), b: 10, diff --git a/examples/svg.rs b/examples/svg.rs new file mode 100644 index 000000000..6e5676fe3 --- /dev/null +++ b/examples/svg.rs @@ -0,0 +1,101 @@ +// Thanks to @japsu and their project https://github.com/japsu/jatsi for the example! + +use dioxus::{events::MouseEvent, prelude::*}; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 5); + + cx.render(rsx! { + div { + user_select: "none", + webkit_user_select: "none", + margin_left: "10%", + margin_right: "10%", + h1 { "Click die to generate a new value" } + div { + cursor: "pointer", + height: "80%", + width: "80%", + Die { + value: *val, + keep: true, + onclick: move |_| { + use rand::Rng; + let mut rng = rand::thread_rng(); + set_val(rng.gen_range(1..6)); + } + } + } + } + }) +} + +#[derive(Props)] +pub struct DieProps<'a> { + pub value: u64, + pub keep: bool, + pub onclick: EventHandler<'a, MouseEvent>, +} + +const DOTS: [(i64, i64); 7] = [(-1, -1), (-1, -0), (-1, 1), (1, -1), (1, 0), (1, 1), (0, 0)]; +const DOTS_FOR_VALUE: [[bool; 7]; 6] = [ + [false, false, false, false, false, false, true], + [false, false, true, true, false, false, false], + [false, false, true, true, false, false, true], + [true, false, true, true, false, true, false], + [true, false, true, true, false, true, true], + [true, true, true, true, true, true, false], +]; + +const OFFSET: i64 = 600; +const DOT_RADIUS: &str = "200"; +const HELD_COLOR: &str = "#aaa"; +const UNHELD_COLOR: &str = "#ddd"; + +// A six-sided die (D6) with dots. +#[allow(non_snake_case)] +pub fn Die<'a>(cx: Scope<'a, DieProps<'a>>) -> Element { + let &DieProps { value, keep, .. } = cx.props; + + let active_dots = &DOTS_FOR_VALUE[(value - 1) as usize]; + let fill = if keep { HELD_COLOR } else { UNHELD_COLOR }; + let dots = DOTS + .iter() + .zip(active_dots.iter()) + .filter(|(_, &active)| active) + .map(|((x, y), _)| { + let dcx = x * OFFSET; + let dcy = y * OFFSET; + rsx!(circle { + cx: "{dcx}", + cy: "{dcy}", + r: "{DOT_RADIUS}", + fill: "#333" + }) + }); + + rsx!(cx, + svg { + onclick: move |e| cx.props.onclick.call(e), + prevent_default: "onclick", + "dioxus-prevent-default": "onclick", + class: "die", + view_box: "-1000 -1000 2000 2000", + + rect { + x: "-1000", + y: "-1000", + width: "2000", + height: "2000", + rx: "{DOT_RADIUS}", + fill: "{fill}", + } + + dots + } + ) +} diff --git a/examples/tasks.rs b/examples/tasks.rs index f71abe38d..3c4d4cba3 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -10,14 +10,14 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, move || { - let mut count = count.for_async(); + let set_count = set_count.to_owned(); async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; - count += 1; + set_count.modify(|f| f + 1); } } }); @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Current count: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Reset the count" } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 6daddbf20..937d6992c 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, im_rc::HashMap::::default); - let filter = use_state(&cx, || FilterState::All); - let draft = use_state(&cx, || "".to_string()); - let mut todo_id = use_state(&cx, || 0); + let (todos, set_todos) = use_state(&cx, im_rc::HashMap::::default); + let (filter, set_filter) = use_state(&cx, || FilterState::All); + let (draft, set_draft) = use_state(&cx, || "".to_string()); + let (todo_id, set_todo_id) = use_state(&cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match *filter { + .filter(|(_, item)| match filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -54,25 +54,25 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| set_draft(evt.value.clone()), onkeydown: move |evt| { if evt.key == "Enter" && !draft.is_empty() { - todos.modify().insert( + set_todos.make_mut().insert( *todo_id, TodoItem { id: *todo_id, checked: false, - contents: draft.get().clone(), + contents: draft.clone(), }, ); - todo_id += 1; - draft.set("".to_string()); + set_todo_id(todo_id + 1); + set_draft("".to_string()); } } } } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, todos: todos ))) + filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, set_todos: set_todos ))) } (!todos.is_empty()).then(|| rsx!( footer { class: "footer", @@ -81,14 +81,14 @@ pub fn app(cx: Scope<()>) -> Element { span {"{item_text} left"} } ul { class: "filters", - li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} + li { class: "All", a { onclick: move |_| set_filter(FilterState::All), "All" }} + li { class: "Active", a { onclick: move |_| set_filter(FilterState::Active), "Active" }} + li { class: "Completed", a { onclick: move |_| set_filter(FilterState::Completed), "Completed" }} } (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), + onclick: move |_| set_todos.make_mut().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -106,37 +106,42 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: UseState<'a, im_rc::HashMap>, + set_todos: &'a UseState>, id: u32, } pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { - let todo = &cx.props.todos[&cx.props.id]; - let is_editing = use_state(&cx, || false); + let (is_editing, set_is_editing) = use_state(&cx, || false); + + let todos = cx.props.set_todos.get(); + let todo = &todos[&cx.props.id]; let completed = if todo.checked { "completed" } else { "" }; - let editing = if *is_editing.get() { "editing" } else { "" }; + let editing = if *is_editing { "editing" } else { "" }; rsx!(cx, li { class: "{completed} {editing}", - onclick: move |_| is_editing.set(true), - onfocusout: move |_| is_editing.set(false), div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", onchange: move |evt| { - cx.props.todos.modify()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } - label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } + label { + r#for: "cbg-{todo.id}", + onclick: move |_| set_is_editing(true), + onfocusout: move |_| set_is_editing(false), + "{todo.contents}" + } } is_editing.then(|| rsx!{ input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(), autofocus: "true", onkeydown: move |evt| { match evt.key.as_str() { - "Enter" | "Escape" | "Tab" => is_editing.set(false), + "Enter" | "Escape" | "Tab" => set_is_editing(false), _ => {} } }, diff --git a/examples/window_event.rs b/examples/window_event.rs new file mode 100644 index 000000000..cb578829f --- /dev/null +++ b/examples/window_event.rs @@ -0,0 +1,97 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch_cfg(app, |cfg| { + cfg.with_window(|w| w.with_title("BorderLess Demo").with_decorations(false)) + }); +} + +fn app(cx: Scope) -> Element { + let window = dioxus::desktop::use_window(&cx); + + // if you want to make window fullscreen, you need close the resizable. + // window.set_fullscreen(true); + // window.set_resizable(false); + + let (fullscreen, set_fullscreen) = use_state(&cx, || false); + let (always_on_top, set_always_on_top) = use_state(&cx, || false); + let (decorations, set_decorations) = use_state(&cx, || false); + + cx.render(rsx!( + link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } + header { + class: "text-gray-400 bg-gray-900 body-font", + onmousedown: move |_| window.drag(), + div { + class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", + a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0", + span { class: "ml-3 text-xl", "Dioxus"} + } + nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center" } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.set_minimized(true), + "Minimize" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + + window.set_fullscreen(!fullscreen); + window.set_resizable(*fullscreen); + + set_fullscreen(!fullscreen); + }, + "Fullscreen" + } + button { + class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-none hover:bg-gray-700 rounded text-base mt-4 md:mt-0", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| window.close(), + "Close" + } + } + } + br {} + div { + class: "container mx-auto", + div { + class: "grid grid-cols-5", + div { + button { + class: "inline-flex items-center text-white bg-green-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_always_on_top(!always_on_top); + set_always_on_top(!always_on_top); + }, + "Always On Top" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_decorations(!decorations); + set_decorations(!decorations); + }, + "Set Decorations" + } + } + div { + button { + class: "inline-flex items-center text-white bg-blue-500 border-0 py-1 px-3 hover:bg-green-700 rounded", + onmousedown: |evt| evt.cancel_bubble(), + onclick: move |_| { + window.set_title("Dioxus Application"); + }, + "Change Title" + } + } + } + } + )) +} diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 36fc81a95..2790a8a89 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let contents = use_state(&cx, || { + let (contents, set_contents) = use_state(&cx, || { String::from("") }); @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { input { value: "{contents}", r#type: "text", - oninput: move |e| contents.set(e.value.clone()), + oninput: move |e| set_contents(e.value.clone()), } } }) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 28bd07ed0..79f2b51aa 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -44,7 +44,7 @@

官网 | - 手册 + 手册 | 示例

@@ -65,12 +65,12 @@ Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平 ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` @@ -160,6 +160,15 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 - 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 +### 项目生态 + +想要加入我们一起为 Dioxus 生态努力吗?有很多项目都能在您的帮助下获得改变: + +- [TUI 渲染器](https://github.com/dioxusLabs/rink) +- [CLI 开发工具](https://github.com/dioxusLabs/cli) +- [官网及文档](https://github.com/dioxusLabs/docsite) +- 动态网站 及 Web 服务器 +- 资源系统 ## 协议 diff --git a/packages/core-macro/CHANGELOG.md b/packages/core-macro/CHANGELOG.md index 79cbe766f..cf1143e1e 100644 --- a/packages/core-macro/CHANGELOG.md +++ b/packages/core-macro/CHANGELOG.md @@ -5,6 +5,43 @@ 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 + +### New Features + + - flatten props attrs + - add "optional" flag for props + +### Bug Fixes + + - detection of f-string formatting in components + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 12 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - detection of f-string formatting in components ([`90abd9c`](https://github.comgit//DioxusLabs/dioxus/commit/90abd9c9a08c165384fae9c1f7c3fd098d512c48)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #127 from DioxusLabs/jk/handler-tweak ([`5bce294`](https://github.comgit//DioxusLabs/dioxus/commit/5bce294a86941090660b1cfd87809a2b0b76d2ce)) + - add "optional" flag for props ([`47bc4e4`](https://github.comgit//DioxusLabs/dioxus/commit/47bc4e4a44a7d08d4f42102d13f0766d9d6bf358)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -64,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 138 commits contributed to the release over the course of 352 calendar days. + - 139 commits contributed to the release over the course of 352 calendar days. - 123 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -75,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-core-macro v0.1.6 ([`db0a5bd`](https://github.comgit//DioxusLabs/dioxus/commit/db0a5bd6ec93803cddb3c6fda4172257b9c301c6)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..ed5d34e53 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2021" description = "Core macro for Dioxus Virtual DOM" @@ -15,7 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -once_cell = "1.8" +dioxus-rsx = { path = "../rsx" } proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/core-macro/examples/html_test.rs b/packages/core-macro/examples/html_test.rs deleted file mode 100644 index c8e33ef8b..000000000 --- a/packages/core-macro/examples/html_test.rs +++ /dev/null @@ -1,8 +0,0 @@ -fn main() { - // use dioxus_core_macro::*; - // let r = html! { - //
- // "hello world" - //
- // }; -} diff --git a/packages/core-macro/examples/inline_props.rs b/packages/core-macro/examples/inline_props.rs deleted file mode 100644 index 28602ac30..000000000 --- a/packages/core-macro/examples/inline_props.rs +++ /dev/null @@ -1,22 +0,0 @@ -use dioxus_core_macro::{inline_props, Props}; - -fn main() {} - -type Element<'a> = (); - -pub struct Scope<'a, T = ()> { - props: &'a T, -} - -// #[inline_props] -pub fn component<'a>( - cx: Scope<'a>, - chkk: String, - chkk2: String, - r: u32, - cat: &'a str, - drd: String, - e: String, -) -> Element<'a> { - let r = chkk.len(); -} diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs deleted file mode 100644 index 525b3c8b0..000000000 --- a/packages/core-macro/examples/prop_test.rs +++ /dev/null @@ -1,24 +0,0 @@ -fn main() {} - -pub mod dioxus { - pub mod prelude { - pub trait Properties { - type Builder; - const IS_STATIC: bool; - fn builder() -> Self::Builder; - unsafe fn memoize(&self, other: &Self) -> bool; - } - } -} - -/// This implementation should require a "PartialEq" because it memoizes (no external references) -#[derive(PartialEq, dioxus_core_macro::Props)] -struct SomeProps { - a: String, -} - -/// This implementation does not require a "PartialEq" because it does not memoize -#[derive(dioxus_core_macro::Props)] -struct SomePropsTwo<'a> { - _a: &'a str, -} diff --git a/packages/core-macro/src/htm.rs b/packages/core-macro/src/htm.rs deleted file mode 100644 index ca53096f6..000000000 --- a/packages/core-macro/src/htm.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! -//! TODO: -//! - [ ] Support for VComponents -//! - [ ] Support for inline format in text -//! - [ ] Support for expressions in attribute positions -//! - [ ] Support for iterators -//! - [ ] support for inline html! -//! -//! -//! -//! -//! -//! -//! - -use { - proc_macro2::TokenStream as TokenStream2, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitStr, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct HtmlRender { - kind: NodeOrList, -} - -impl Parse for HtmlRender { - fn parse(input: ParseStream) -> Result { - if input.peek(LitStr) { - return input.parse::()?.parse::(); - } - - // let __cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - let kind = if input.peek(token::Bracket) { - let nodes_toks; - syn::bracketed!(nodes_toks in input); - let mut nodes: Vec> = vec![nodes_toks.parse()?]; - while nodes_toks.peek(Token![,]) { - nodes_toks.parse::()?; - nodes.push(nodes_toks.parse()?); - } - NodeOrList::List(NodeList(nodes)) - } else { - NodeOrList::Node(input.parse()?) - }; - Ok(HtmlRender { kind }) - } -} - -impl ToTokens for HtmlRender { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // create a lazy tree that accepts a bump allocator - let final_tokens = quote! { - dioxus::prelude::LazyNodes::new(move |__cx| { - let bump = __cx.bump(); - - #new_toks - }) - }; - - final_tokens.to_tokens(out_tokens); - } -} - -/// ============================================= -/// Parse any child as a node or list of nodes -/// ============================================= -/// - [ ] Allow iterators -/// -/// -enum NodeOrList { - Node(Node), - List(NodeList), -} - -impl ToTokens for ToToksCtx<&NodeOrList> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self.inner { - NodeOrList::Node(node) => self.recurse(node).to_tokens(tokens), - NodeOrList::List(list) => self.recurse(list).to_tokens(tokens), - } - } -} - -struct NodeList(Vec>); - -impl ToTokens for ToToksCtx<&NodeList> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let nodes = self.inner.0.iter().map(|node| self.recurse(node)); - tokens.append_all(quote! { - dioxus::bumpalo::vec![in bump; - #(#nodes),* - ] - }); - } -} - -enum Node { - Element(Element), - Text(TextNode), -} - -impl ToTokens for ToToksCtx<&Node> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self.inner { - Node::Element(el) => self.recurse(el).to_tokens(tokens), - Node::Text(txt) => self.recurse(txt).to_tokens(tokens), - } - } -} - -impl Node { - fn _peek(s: ParseStream) -> bool { - (s.peek(Token![<]) && !s.peek2(Token![/])) || s.peek(token::Brace) || s.peek(LitStr) - } -} - -impl Parse for Node { - fn parse(s: ParseStream) -> Result { - Ok(if s.peek(Token![<]) { - Node::Element(s.parse()?) - } else { - Node::Text(s.parse()?) - }) - } -} - -/// ======================================= -/// Parse the VNode::Element type -/// ======================================= -/// - [ ] Allow VComponent -/// -/// -struct Element { - name: Ident, - attrs: Vec, - children: MaybeExpr>, -} - -impl ToTokens for ToToksCtx<&Element> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - // let __cx = self.__cx; - let name = &self.inner.name; - // let name = &self.inner.name.to_string(); - tokens.append_all(quote! { - __cx.element(dioxus_elements::#name) - // dioxus::builder::ElementBuilder::new( #name) - }); - for attr in self.inner.attrs.iter() { - self.recurse(attr).to_tokens(tokens); - } - - // if is_valid_svg_tag(&name.to_string()) { - // tokens.append_all(quote! { - // .namespace(Some("http://www.w3.org/2000/svg")) - // }); - // } - - match &self.inner.children { - MaybeExpr::Expr(expr) => tokens.append_all(quote! { - .children(#expr) - }), - MaybeExpr::Literal(nodes) => { - let mut children = nodes.iter(); - if let Some(child) = children.next() { - let mut inner_toks = TokenStream2::new(); - self.recurse(child).to_tokens(&mut inner_toks); - for child in children { - quote!(,).to_tokens(&mut inner_toks); - self.recurse(child).to_tokens(&mut inner_toks); - } - tokens.append_all(quote! { - .children([#inner_toks]) - }); - } - } - } - tokens.append_all(quote! { - .finish() - }); - } -} - -impl Parse for Element { - fn parse(s: ParseStream) -> Result { - s.parse::()?; - let name = Ident::parse_any(s)?; - let mut attrs = vec![]; - let _children: Vec = vec![]; - - // keep looking for attributes - while !s.peek(Token![>]) { - // self-closing - if s.peek(Token![/]) { - s.parse::()?; - s.parse::]>()?; - return Ok(Self { - name, - attrs, - children: MaybeExpr::Literal(vec![]), - }); - } - attrs.push(s.parse()?); - } - s.parse::]>()?; - - // Contents of an element can either be a brace (in which case we just copy verbatim), or a - // sequence of nodes. - let children = if s.peek(token::Brace) { - // expr - let content; - syn::braced!(content in s); - MaybeExpr::Expr(content.parse()?) - } else { - // nodes - let mut children = vec![]; - while !(s.peek(Token![<]) && s.peek2(Token![/])) { - children.push(s.parse()?); - } - MaybeExpr::Literal(children) - }; - - // closing element - s.parse::()?; - s.parse::()?; - let close = Ident::parse_any(s)?; - if close != name { - return Err(Error::new_spanned( - close, - "closing element does not match opening", - )); - } - s.parse::]>()?; - - Ok(Self { - name, - attrs, - children, - }) - } -} - -/// ======================================= -/// Parse a VElement's Attributes -/// ======================================= -/// - [ ] Allow expressions as attribute -/// -/// -struct Attr { - name: Ident, - ty: AttrType, -} - -impl Parse for Attr { - fn parse(s: ParseStream) -> Result { - let mut name = Ident::parse_any(s)?; - let name_str = name.to_string(); - s.parse::()?; - - // Check if this is an event handler - // If so, parse into literal tokens - let ty = if name_str.starts_with("on") { - // remove the "on" bit - name = Ident::new(name_str.trim_start_matches("on"), name.span()); - let content; - syn::braced!(content in s); - // AttrType::Value(content.parse()?) - AttrType::Event(content.parse()?) - // AttrType::Event(content.parse()?) - } else { - let lit_str = if name_str == "style" && s.peek(token::Brace) { - // special-case to deal with literal styles. - let outer; - syn::braced!(outer in s); - // double brace for inline style. - // todo!("Style support not ready yet"); - - // if outer.peek(token::Brace) { - // let inner; - // syn::braced!(inner in outer); - // let styles: Styles = inner.parse()?; - // MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site())) - // } else { - // just parse as an expression - MaybeExpr::Expr(outer.parse()?) - // } - } else { - s.parse()? - }; - AttrType::Value(lit_str) - }; - Ok(Attr { name, ty }) - } -} - -impl ToTokens for ToToksCtx<&Attr> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = self.inner.name.to_string(); - let _attr_stream = TokenStream2::new(); - match &self.inner.ty { - AttrType::Value(value) => { - let value = self.recurse(value); - if name == "xmlns" { - tokens.append_all(quote! { - .namespace(Some(#value)) - }); - } else { - tokens.append_all(quote! { - .attr(#name, format_args_f!(#value)) - }); - } - } - AttrType::Event(event) => { - tokens.append_all(quote! { - .on(#name, #event) - }); - } - } - } -} - -enum AttrType { - Value(MaybeExpr), - Event(ExprClosure), - // todo Bool(MaybeExpr) -} - -/// ======================================= -/// Parse just plain text -/// ======================================= -/// - [ ] Perform formatting automatically -/// -/// -struct TextNode(MaybeExpr); - -impl Parse for TextNode { - fn parse(s: ParseStream) -> Result { - Ok(Self(s.parse()?)) - } -} - -impl ToTokens for ToToksCtx<&TextNode> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let mut token_stream = TokenStream2::new(); - self.recurse(&self.inner.0).to_tokens(&mut token_stream); - tokens.append_all(quote! { - __cx.text(format_args_f!(#token_stream)) - }); - } -} - -#[allow(clippy::large_enum_variant)] -enum MaybeExpr { - Literal(T), - Expr(Expr), -} - -impl Parse for MaybeExpr { - fn parse(s: ParseStream) -> Result { - if s.peek(token::Brace) { - let content; - syn::braced!(content in s); - Ok(MaybeExpr::Expr(content.parse()?)) - } else { - Ok(MaybeExpr::Literal(s.parse()?)) - } - } -} - -impl<'a, T> ToTokens for ToToksCtx<&'a MaybeExpr> -where - T: 'a, - ToToksCtx<&'a T>: ToTokens, -{ - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self.inner { - MaybeExpr::Literal(v) => self.recurse(v).to_tokens(tokens), - MaybeExpr::Expr(expr) => expr.to_tokens(tokens), - } - } -} - -/// ToTokens context -struct ToToksCtx { - inner: T, -} - -impl<'a, T> ToToksCtx { - fn new(inner: T) -> Self { - ToToksCtx { inner } - } - - fn recurse(&self, inner: U) -> ToToksCtx { - ToToksCtx { inner } - } -} - -impl ToTokens for ToToksCtx<&LitStr> { - fn to_tokens(&self, tokens: &mut TokenStream2) { - self.inner.to_tokens(tokens) - } -} - -#[cfg(test)] -mod test { - fn parse(input: &str) -> super::Result { - syn::parse_str(input) - } - - #[test] - fn div() { - parse("bump,
").unwrap(); - } - - #[test] - fn nested() { - parse("bump,
\"text\"
").unwrap(); - } - - #[test] - fn complex() { - parse( - "bump, -
{contact_details}
- ", - ) - .unwrap(); - } -} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 7099311ce..96ea0ce87 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -2,11 +2,9 @@ use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -pub(crate) mod ifmt; -pub(crate) mod inlineprops; -pub(crate) mod props; -pub(crate) mod router; -pub(crate) mod rsx; +mod ifmt; +mod inlineprops; +mod props; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { @@ -180,42 +178,12 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token #[proc_macro_error::proc_macro_error] #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { - match syn::parse::(s) { + match syn::parse::(s) { Err(err) => err.to_compile_error().into(), Ok(stream) => stream.to_token_stream().into(), } } -/// Derive macro used to mark an enum as Routable. -/// -/// This macro can only be used on enums. Every varient of the macro needs to be marked -/// with the `at` attribute to specify the URL of the route. It generates an implementation of -/// `yew_router::Routable` trait and `const`s for the routes passed which are used with `Route` -/// component. -/// -/// # Example -/// -/// ``` -/// # use yew_router::Routable; -/// #[derive(Debug, Clone, Copy, PartialEq, Routable)] -/// enum Routes { -/// #[at("/")] -/// Home, -/// #[at("/secure")] -/// Secure, -/// #[at("/profile/{id}")] -/// Profile(u32), -/// #[at("/404")] -/// NotFound, -/// } -/// ``` -#[proc_macro_derive(Routable, attributes(at, not_found))] -pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - use router::{routable_derive_impl, Routable}; - let input = parse_macro_input!(input as Routable); - routable_derive_impl(input).into() -} - /// Derive props for a component within the component definition. /// /// This macro provides a simple transformation from `Scope<{}>` to `Scope

`, diff --git a/packages/core-macro/src/router.rs b/packages/core-macro/src/router.rs deleted file mode 100644 index 3edad75e3..000000000 --- a/packages/core-macro/src/router.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![allow(dead_code)] - -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant}; - -const AT_ATTR_IDENT: &str = "at"; -const NOT_FOUND_ATTR_IDENT: &str = "not_found"; - -pub struct Routable { - ident: Ident, - ats: Vec, - variants: Punctuated, - not_found_route: Option, -} - -impl Parse for Routable { - fn parse(input: ParseStream) -> syn::Result { - let DeriveInput { ident, data, .. } = input.parse()?; - - let data = match data { - Data::Enum(data) => data, - Data::Struct(s) => { - return Err(syn::Error::new( - s.struct_token.span(), - "expected enum, found struct", - )) - } - Data::Union(u) => { - return Err(syn::Error::new( - u.union_token.span(), - "expected enum, found union", - )) - } - }; - - let (not_found_route, ats) = parse_variants_attributes(&data.variants)?; - - Ok(Self { - ident, - variants: data.variants, - ats, - not_found_route, - }) - } -} - -fn parse_variants_attributes( - variants: &Punctuated, -) -> syn::Result<(Option, Vec)> { - let mut not_founds = vec![]; - let mut ats: Vec = vec![]; - - let mut not_found_attrs = vec![]; - - for variant in variants.iter() { - if let Fields::Unnamed(ref field) = variant.fields { - return Err(syn::Error::new( - field.span(), - "only named fields are supported", - )); - } - - let attrs = &variant.attrs; - let at_attrs = attrs - .iter() - .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) - .collect::>(); - - let attr = match at_attrs.len() { - 1 => *at_attrs.first().unwrap(), - 0 => { - return Err(syn::Error::new( - variant.span(), - format!( - "{} attribute must be present on every variant", - AT_ATTR_IDENT - ), - )) - } - _ => { - return Err(syn::Error::new_spanned( - quote! { #(#at_attrs)* }, - format!("only one {} attribute must be present", AT_ATTR_IDENT), - )) - } - }; - - let lit = attr.parse_args::()?; - ats.push(lit); - - for attr in attrs.iter() { - if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { - not_found_attrs.push(attr); - not_founds.push(variant.ident.clone()) - } - } - } - - if not_founds.len() > 1 { - return Err(syn::Error::new_spanned( - quote! { #(#not_found_attrs)* }, - format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), - )); - } - - Ok((not_founds.into_iter().next(), ats)) -} - -impl Routable { - // fn build_from_path(&self) -> TokenStream { - // let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let right = match &variant.fields { - // Fields::Unit => quote! { Self::#ident }, - // Fields::Named(field) => { - // let fields = field.named.iter().map(|it| { - // //named fields have idents - // it.ident.as_ref().unwrap() - // }); - // quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // }; - - // let left = self.ats.get(i).unwrap(); - // quote! { - // #left => ::std::option::Option::Some(#right) - // } - // }); - - // quote! { - // fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option { - // match path { - // #(#from_path_matches),*, - // _ => ::std::option::Option::None, - // } - // } - // } - // } - - // fn build_to_path(&self) -> TokenStream { - // let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let mut right = self.ats.get(i).unwrap().value(); - - // match &variant.fields { - // Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) }, - // Fields::Named(field) => { - // let fields = field - // .named - // .iter() - // .map(|it| it.ident.as_ref().unwrap()) - // .collect::>(); - - // for field in fields.iter() { - // // :param -> {param} - // // so we can pass it to `format!("...", param)` - // right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)) - // } - - // quote! { - // Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) - // } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // } - // }); - - // quote! { - // fn to_path(&self) -> ::std::string::String { - // match self { - // #(#to_path_matches),*, - // } - // } - // } - // } -} - -pub fn routable_derive_impl(input: Routable) -> TokenStream { - let Routable { - // ats, - // not_found_route, - // ident, - .. - } = &input; - - // let from_path = input.build_from_path(); - // let to_path = input.build_to_path(); - - quote! { - // #[automatically_derived] - // impl ::dioxus::router::Routable for #ident { - - // fn recognize(pathname: &str) -> ::std::option::Option { - // todo!() - // // ::std::thread_local! { - // // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); - // // } - // // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); - // // { - // // let route = ::std::clone::Clone::clone(&route); - // // #cache_thread_local_ident.with(move |val| { - // // *val.borrow_mut() = route; - // // }); - // // } - // // route - // } - // } - } -} diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/core-macro/src/rsxtemplate.rs deleted file mode 100644 index 9222d7117..000000000 --- a/packages/core-macro/src/rsxtemplate.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{rsx::RsxBody, util::is_valid_svg_tag}; - -use { - proc_macro::TokenStream, - proc_macro2::{Span, TokenStream as TokenStream2}, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct RsxTemplate { - inner: RsxBody, -} - -impl Parse for RsxTemplate { - fn parse(s: ParseStream) -> Result { - if s.peek(LitStr) { - use std::str::FromStr; - - let lit = s.parse::()?; - let g = lit.span(); - let mut value = lit.value(); - if value.ends_with('\n') { - value.pop(); - if value.ends_with('\r') { - value.pop(); - } - } - let lit = LitStr::new(&value, lit.span()); - - // panic!("{:#?}", lit); - match lit.parse::() { - Ok(r) => Ok(Self { inner: r }), - Err(e) => Err(e), - } - } else { - panic!("Not a str lit") - } - // let t = s.parse::()?; - - // let new_stream = TokenStream::from(t.to_s) - - // let cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - // let kind = if s.peek(token::Bracket) { - // let nodes_toks; - // syn::bracketed!(nodes_toks in s); - // let mut nodes: Vec> = vec![nodes_toks.parse()?]; - // while nodes_toks.peek(Token![,]) { - // nodes_toks.parse::()?; - // nodes.push(nodes_toks.parse()?); - // } - // NodeOrList::List(NodeList(nodes)) - // } else { - // NodeOrList::Node(s.parse()?) - // }; - // Ok(HtmlRender { kind }) - } -} - -impl ToTokens for RsxTemplate { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - self.inner.to_tokens(out_tokens); - // let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // // create a lazy tree that accepts a bump allocator - // let final_tokens = quote! { - // dioxus::prelude::LazyNodes::new(move |cx| { - // let bump = &cx.bump(); - - // #new_toks - // }) - // }; - - // final_tokens.to_tokens(out_tokens); - } -} diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a0aa9f28d..563106db3 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -5,6 +5,58 @@ 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 + +### Documentation + + - add comments for the Handler + +### New Features + + - add "spawn" method + - allow providing context to the root component + +### Bug Fixes + + - provide_root_context on root scopes + - proprogation of root context + - allow eventhandler to derive default + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 13 calendar days. + - 8 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +

view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge branch 'master' of github.com:DioxusLabs/dioxus ([`8899701`](https://github.comgit//DioxusLabs/dioxus/commit/88997019c5c74658bfd76344d6bb6f3565943b41)) + - add miri stress test ([`e9792e9`](https://github.comgit//DioxusLabs/dioxus/commit/e9792e9b95521fcf0b0b4e240d7cf0397cd79b8f)) + - add "spawn" method ([`bad4b77`](https://github.comgit//DioxusLabs/dioxus/commit/bad4b773b7bbab6a38d4f48d88d8c5a8787927ac)) + - drop hooks before resetting bump arena ([`2e4f765`](https://github.comgit//DioxusLabs/dioxus/commit/2e4f7659327b69b74e20af59b6d5e65c699b6adb)) + - provide_root_context on root scopes ([`d9a07dd`](https://github.comgit//DioxusLabs/dioxus/commit/d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #121 from DioxusLabs/jk/unify ([`b287a4c`](https://github.comgit//DioxusLabs/dioxus/commit/b287a4cab3eec9e6961a3f010846291f4f105747)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - proprogation of root context ([`c8d528b`](https://github.comgit//DioxusLabs/dioxus/commit/c8d528b3b18e320ca0cee1542ae5fc659c1cca81)) + - allow providing context to the root component ([`d2bd175`](https://github.comgit//DioxusLabs/dioxus/commit/d2bd1751436ef4bec554bb361ba87aea1357036a)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #107 from autarch/autarch/half-assed-router ([`8d3ac3f`](https://github.comgit//DioxusLabs/dioxus/commit/8d3ac3ff148aef9d10a393eda453a11c1e882f58)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Don't expect all components to have a scope in ScopeArena.ensure_drop_safety ([`9b282d8`](https://github.comgit//DioxusLabs/dioxus/commit/9b282d877ba0fe2463687cde7ea899b7f8510425)) + - add comments for the Handler ([`036a0ff`](https://github.comgit//DioxusLabs/dioxus/commit/036a0ff49a7dade0e04c9c07071a1ff49133ee24)) + - allow eventhandler to derive default ([`e47ead5`](https://github.comgit//DioxusLabs/dioxus/commit/e47ead5347ce778935f8f2127fcb14f520eda0ce)) +
+ ## v0.1.7 (2022-01-08) ### Documentation @@ -138,7 +190,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 437 commits contributed to the release over the course of 358 calendar days. + - 438 commits contributed to the release over the course of 358 calendar days. - 416 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -149,8 +201,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-core v0.1.7 ([`16d73b2`](https://github.comgit//DioxusLabs/dioxus/commit/16d73b240fc446c6f6996c19ccf52bbcda2eca79)) - add title to doc comment ([`d11f322`](https://github.comgit//DioxusLabs/dioxus/commit/d11f322f554e7dbf43b988c9cfda56498cc49872)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - better document the `EventHandler` type ([`be9f1a5`](https://github.comgit//DioxusLabs/dioxus/commit/be9f1a52ad2b04f101397ae34482ea7394df653b)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f54c6d5d9..4126eb47f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.1.7" +version = "0.1.9" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -32,7 +32,7 @@ smallvec = "1.6" slab = "0.4" -futures-channel = "0.3" +futures-channel = "0.3.21" # used for noderefs once_cell = "1.8" diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs deleted file mode 100644 index b7a9f513c..000000000 --- a/packages/core/examples/borrowed.rs +++ /dev/null @@ -1,33 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() {} - -fn app(cx: Scope) -> Element { - cx.render(rsx!(div { - app2( - p: "asd" - ) - })) -} - -#[derive(Props)] -struct Borrowed<'a> { - p: &'a str, -} - -fn app2<'a>(cx: Scope<'a, Borrowed<'a>>) -> Element { - let g = eat2(&cx); - rsx!(cx, "") -} - -fn eat2(s: &ScopeState) {} - -fn eat(f: &str) {} - -fn bleat() { - let blah = String::from("asd"); - eat(&blah); -} diff --git a/packages/core/examples/component_children.rs b/packages/core/examples/component_children.rs deleted file mode 100644 index ac76740b8..000000000 --- a/packages/core/examples/component_children.rs +++ /dev/null @@ -1,40 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() { - let mut dom = VirtualDom::new(parent); - let edits = dom.rebuild(); - dbg!(edits); -} - -fn parent(cx: Scope) -> Element { - let value = cx.use_hook(|_| String::new()); - - cx.render(rsx! { - div { - child( - name: value, - h1 {"hi"} - ) - } - }) -} - -#[derive(Props)] -struct ChildProps<'a> { - name: &'a str, - children: Element<'a>, -} - -fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - div { - "it's nested {cx.props.name}", - &cx.props.children - } - }) -} diff --git a/packages/core/examples/simple_hello.rs b/packages/core/examples/simple_hello.rs deleted file mode 100644 index 94a897e7c..000000000 --- a/packages/core/examples/simple_hello.rs +++ /dev/null @@ -1,9 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -// very tiny hello world -fn main() { - dioxus::VirtualDom::new(|cx| rsx!(cx, "hello world")); -} diff --git a/packages/core/examples/works.rs b/packages/core/examples/works.rs deleted file mode 100644 index 0de94b5c3..000000000 --- a/packages/core/examples/works.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_html as dioxus_elements; - -fn main() { - let _ = VirtualDom::new(parent); -} - -fn parent(cx: Scope) -> Element { - let value = cx.use_hook(|_| String::new()); - - cx.render(rsx! { - div { - child( name: value ) - } - }) -} - -#[derive(Props)] -struct ChildProps<'a> { - name: &'a str, -} - -fn child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - cx.render(rsx! { - div { - h1 { "it's nested" } - grandchild( name: cx.props.name ) - } - }) -} - -#[derive(Props)] -struct Grandchild<'a> { - name: &'a str, -} - -fn grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> { - cx.render(rsx! { - div { "Hello {cx.props.name}!" } - great_grandchild( name: cx.props.name ) - }) -} - -fn great_grandchild<'a>(cx: Scope<'a, Grandchild>) -> Element<'a> { - cx.render(rsx! { - div { - h1 { "it's nested" } - } - }) -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 499d9543e..d7189a710 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,33 +1,36 @@ -//! This module contains the stateful DiffState and all methods to diff VNodes, their properties, and their children. +#![warn(clippy::pedantic)] +#![allow(clippy::cast_possible_truncation)] + +//! This module contains the stateful [`DiffState`] and all methods to diff [`VNodes`], their properties, and their children. //! //! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set -//! of mutations for the RealDom to apply. +//! of mutations for the [`RealDom`] to apply. //! //! ## Notice: //! //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, SubTree memoization, incremental diffing, cancellation, NodeRefs, pausing, priority +//! Components, Fragments, Suspense, [`SubTree`] memoization, incremental diffing, cancellation, [`NodeRefs`], pausing, priority //! scheduling, and additional batching operations. //! //! ## Implementation Details: //! //! ### IDs for elements //! -------------------- -//! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. +//! All nodes are addressed by their IDs. The [`RealDom`] provides an imperative interface for making changes to these nodes. //! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose //! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element //! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive -//! garbage collection as the VirtualDOM replaces old nodes. +//! garbage collection as the [`VirtualDOM`] replaces old nodes. //! //! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, -//! we always make sure to copy over the ID. If we don't do this properly, the ElementId will be populated incorrectly +//! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly //! and brick the user's page. //! //! ### Fragment Support //! -------------------- //! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments //! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the NodeFactory - it is +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`NodeFactory`] - it is //! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding //! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to //! the platform. @@ -41,13 +44,13 @@ //! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once //! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before //! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. -//! Due to the frequent calls to "yield_now" we can get the pure "fetch-as-you-render" behavior of React Fiber. +//! Due to the frequent calls to [`yield_now`] we can get the pure "fetch-as-you-render" behavior of React Fiber. //! //! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to //! DOM, but as a placeholder. //! //! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes -//! and contents, without the need for the "use_suspense" hook. In the interim, this is the quickest way to get Suspense working. +//! and contents, without the need for the [`use_suspense`] hook. In the interim, this is the quickest way to get Suspense working. //! //! ## Subtree Memoization //! ----------------------- @@ -88,254 +91,78 @@ //! More info on how to improve this diffing algorithm: //! - -use crate::innerlude::*; +use crate::innerlude::{ + AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode, + VPlaceholder, VText, +}; use fxhash::{FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use DomEdit::*; -/// Our DiffState is an iterative tree differ. -/// -/// It uses techniques of a stack machine to allow pausing and restarting of the diff algorithm. This -/// was originally implemented using recursive techniques, but Rust lacks the ability to call async functions recursively, -/// meaning we could not "pause" the original diffing algorithm. -/// -/// Instead, we use a traditional stack machine approach to diff and create new nodes. The diff algorithm periodically -/// calls "yield_now" which allows the machine to pause and return control to the caller. The caller can then wait for -/// the next period of idle time, preventing our diff algorithm from blocking the main thread. -/// -/// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's -/// stack machines all the way down! pub(crate) struct DiffState<'bump> { pub(crate) scopes: &'bump ScopeArena, pub(crate) mutations: Mutations<'bump>, - pub(crate) stack: DiffStack<'bump>, pub(crate) force_diff: bool, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, } -impl<'bump> DiffState<'bump> { - pub(crate) fn new(scopes: &'bump ScopeArena) -> Self { +impl<'b> DiffState<'b> { + pub fn new(scopes: &'b ScopeArena) -> Self { Self { scopes, mutations: Mutations::new(), - stack: DiffStack::new(), force_diff: false, - } - } -} - -/// The stack instructions we use to diff and create new nodes. -#[derive(Debug)] -pub(crate) enum DiffInstruction<'a> { - Diff { - old: &'a VNode<'a>, - new: &'a VNode<'a>, - }, - - Create { - node: &'a VNode<'a>, - }, - - /// pushes the node elements onto the stack for use in mount - PrepareMove { - node: &'a VNode<'a>, - }, - - Mount { - and: MountType<'a>, - }, - - PopScope, - PopElement, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum MountType<'a> { - Absorb, - Append, - Replace { old: &'a VNode<'a> }, - InsertAfter { other_node: &'a VNode<'a> }, - InsertBefore { other_node: &'a VNode<'a> }, -} - -pub(crate) struct DiffStack<'bump> { - pub(crate) instructions: Vec>, - pub(crate) nodes_created_stack: SmallVec<[usize; 10]>, - pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, - pub(crate) element_stack: SmallVec<[ElementId; 10]>, -} - -impl<'bump> DiffStack<'bump> { - fn new() -> Self { - Self { - instructions: Vec::with_capacity(1000), - nodes_created_stack: smallvec![], - scope_stack: smallvec![], element_stack: smallvec![], + scope_stack: smallvec![], } } - fn pop(&mut self) -> Option> { - self.instructions.pop() - } + pub fn diff_scope(&mut self, scopeid: ScopeId) { + let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); + let scope = self.scopes.get_scope(scopeid).unwrap(); - fn pop_off_scope(&mut self) { + self.scope_stack.push(scopeid); + self.element_stack.push(scope.container); + { + self.diff_node(old, new); + } + self.element_stack.pop(); self.scope_stack.pop(); + + self.mutations.mark_dirty_scope(scopeid); } - pub(crate) fn push(&mut self, instruction: DiffInstruction<'bump>) { - self.instructions.push(instruction) - } + pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) { + use VNode::{Component, Element, Fragment, Placeholder, Text}; + match (old_node, new_node) { + (Text(old), Text(new)) => { + self.diff_text_nodes(old, new, old_node, new_node); + } - fn create_children(&mut self, children: &'bump [VNode<'bump>], and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); + (Placeholder(old), Placeholder(new)) => { + self.diff_placeholder_nodes(old, new, old_node, new_node); + } - for child in children.iter().rev() { - self.instructions - .push(DiffInstruction::Create { node: child }); + (Element(old), Element(new)) => { + self.diff_element_nodes(old, new, old_node, new_node); + } + + (Component(old), Component(new)) => { + self.diff_component_nodes(old_node, new_node, *old, *new); + } + + (Fragment(old), Fragment(new)) => { + self.diff_fragment_nodes(old, new); + } + + ( + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), + ) => self.replace_node(old_node, new_node), } } - // todo: subtrees - // fn push_subtree(&mut self) { - // self.nodes_created_stack.push(0); - // self.instructions.push(DiffInstruction::Mount { - // and: MountType::Append, - // }); - // } - - fn push_nodes_created(&mut self, count: usize) { - self.nodes_created_stack.push(count); - } - - pub(crate) fn create_node(&mut self, node: &'bump VNode<'bump>, and: MountType<'bump>) { - self.nodes_created_stack.push(0); - self.instructions.push(DiffInstruction::Mount { and }); - self.instructions.push(DiffInstruction::Create { node }); - } - - fn add_child_count(&mut self, count: usize) { - *self.nodes_created_stack.last_mut().unwrap() += count; - } - - fn pop_nodes_created(&mut self) -> usize { - self.nodes_created_stack.pop().unwrap() - } - - fn current_scope(&self) -> Option { - self.scope_stack.last().copied() - } - - fn create_component(&mut self, idx: ScopeId, node: &'bump VNode<'bump>) { - // Push the new scope onto the stack - self.scope_stack.push(idx); - - self.instructions.push(DiffInstruction::PopScope); - - // Run the creation algorithm with this scope on the stack - // ?? I think we treat components as fragments?? - self.instructions.push(DiffInstruction::Create { node }); - } -} - -impl<'bump> DiffState<'bump> { - /// Progress the diffing for this "fiber" - /// - /// This method implements a depth-first iterative tree traversal. - /// - /// We do depth-first to maintain high cache locality (nodes were originally generated recursively). - /// - /// Returns a `bool` indicating that the work completed properly. - pub fn work(&mut self, mut deadline_expired: impl FnMut() -> bool) -> bool { - while let Some(instruction) = self.stack.pop() { - match instruction { - DiffInstruction::Diff { old, new } => self.diff_node(old, new), - DiffInstruction::Create { node } => self.create_node(node), - DiffInstruction::Mount { and } => self.mount(and), - DiffInstruction::PrepareMove { node } => { - let num_on_stack = self.push_all_nodes(node); - self.stack.add_child_count(num_on_stack); - } - DiffInstruction::PopScope => self.stack.pop_off_scope(), - DiffInstruction::PopElement => { - self.stack.element_stack.pop(); - } - }; - - if deadline_expired() { - log::trace!("Deadline expired before we could finish!"); - return false; - } - } - - true - } - - // recursively push all the nodes of a tree onto the stack and return how many are there - fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize { - match node { - VNode::Text(_) | VNode::Placeholder(_) => { - self.mutations.push_root(node.mounted_id()); - 1 - } - - VNode::Fragment(_) | VNode::Component(_) => { - // - let mut added = 0; - for child in node.children() { - added += self.push_all_nodes(child); - } - added - } - - VNode::Element(el) => { - let mut num_on_stack = 0; - for child in el.children.iter() { - num_on_stack += self.push_all_nodes(child); - } - self.mutations.push_root(el.id.get().unwrap()); - - num_on_stack + 1 - } - } - } - - fn mount(&mut self, and: MountType<'bump>) { - let nodes_created = self.stack.pop_nodes_created(); - match and { - // add the nodes from this virtual list to the parent - // used by fragments and components - MountType::Absorb => { - self.stack.add_child_count(nodes_created); - } - - MountType::Replace { old } => { - self.replace_node(old, nodes_created); - } - - MountType::Append => { - self.mutations.edits.push(AppendChildren { - many: nodes_created as u32, - }); - } - - MountType::InsertAfter { other_node } => { - let root = self.find_last_element(other_node).unwrap(); - self.mutations.insert_after(root, nodes_created as u32); - } - - MountType::InsertBefore { other_node } => { - let root = self.find_first_element_id(other_node).unwrap(); - self.mutations.insert_before(root, nodes_created as u32); - } - } - } - - // ================================= - // Tools for creating new nodes - // ================================= - - fn create_node(&mut self, node: &'bump VNode<'bump>) { + pub fn create_node(&mut self, node: &'b VNode<'b>) -> usize { match node { VNode::Text(vtext) => self.create_text_node(vtext, node), VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node), @@ -345,24 +172,21 @@ impl<'bump> DiffState<'bump> { } } - fn create_text_node(&mut self, vtext: &'bump VText<'bump>, node: &'bump VNode<'bump>) { + fn create_text_node(&mut self, text: &'b VText<'b>, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - - self.mutations.create_text_node(vtext.text, real_id); - vtext.id.set(Some(real_id)); - self.stack.add_child_count(1); + text.id.set(Some(real_id)); + self.mutations.create_text_node(text.text, real_id); + 1 } - fn create_anchor_node(&mut self, anchor: &'bump VPlaceholder, node: &'bump VNode<'bump>) { + fn create_anchor_node(&mut self, anchor: &'b VPlaceholder, node: &'b VNode<'b>) -> usize { let real_id = self.scopes.reserve_node(node); - - self.mutations.create_placeholder(real_id); anchor.id.set(Some(real_id)); - - self.stack.add_child_count(1); + self.mutations.create_placeholder(real_id); + 1 } - fn create_element_node(&mut self, element: &'bump VElement<'bump>, node: &'bump VNode<'bump>) { + fn create_element_node(&mut self, element: &'b VElement<'b>, node: &'b VNode<'b>) -> usize { let VElement { tag: tag_name, listeners, @@ -372,56 +196,44 @@ impl<'bump> DiffState<'bump> { id: dom_id, parent: parent_id, .. - } = element; + } = &element; - // set the parent ID for event bubbling - self.stack.instructions.push(DiffInstruction::PopElement); + parent_id.set(self.element_stack.last().copied()); - let parent = self.stack.element_stack.last().unwrap(); - parent_id.set(Some(*parent)); - - // set the id of the element let real_id = self.scopes.reserve_node(node); - self.stack.element_stack.push(real_id); + dom_id.set(Some(real_id)); - self.mutations.create_element(tag_name, *namespace, real_id); + self.element_stack.push(real_id); + { + self.mutations.create_element(tag_name, *namespace, real_id); - self.stack.add_child_count(1); + let cur_scope_id = self.current_scope(); - if let Some(cur_scope_id) = self.stack.current_scope() { - for listener in *listeners { + for listener in listeners.iter() { listener.mounted_node.set(Some(real_id)); self.mutations.new_event_listener(listener, cur_scope_id); } - } else { - log::warn!("create element called with no scope on the stack - this is an error for a live dom"); - } - for attr in *attributes { - self.mutations.set_attribute(attr, real_id.as_u64()); - } + for attr in attributes.iter() { + self.mutations.set_attribute(attr, real_id.as_u64()); + } - // todo: the settext optimization - // - // if children.len() == 1 { - // if let VNode::Text(vtext) = children[0] { - // self.mutations.set_text(vtext.text, real_id.as_u64()); - // return; - // } - // } - - if !children.is_empty() { - self.stack.create_children(children, MountType::Append); + if !children.is_empty() { + self.create_and_append_children(children); + } } + self.element_stack.pop(); + + 1 } - fn create_fragment_node(&mut self, frag: &'bump VFragment<'bump>) { - self.stack.create_children(frag.children, MountType::Absorb); + fn create_fragment_node(&mut self, frag: &'b VFragment<'b>) -> usize { + self.create_children(frag.children) } - fn create_component_node(&mut self, vcomponent: &'bump VComponent<'bump>) { - let parent_idx = self.stack.current_scope().unwrap(); + fn create_component_node(&mut self, vcomponent: &'b VComponent<'b>) -> usize { + let parent_idx = self.current_scope(); // the component might already exist - if it does, we need to reuse it // this makes figure out when to drop the component more complicated @@ -430,116 +242,121 @@ impl<'bump> DiffState<'bump> { idx } else { // Insert a new scope into our component list - let props: Box = vcomponent.props.borrow_mut().take().unwrap(); + let props: Box = vcomponent.props.borrow_mut().take().unwrap(); let props: Box = unsafe { std::mem::transmute(props) }; - let new_idx = self.scopes.new_with_key( + self.scopes.new_with_key( vcomponent.user_fc, props, Some(parent_idx), - self.stack.element_stack.last().copied().unwrap(), + self.element_stack.last().copied().unwrap(), 0, - ); - - new_idx + ) }; // Actually initialize the caller's slot with the right address vcomponent.scope.set(Some(new_idx)); - match vcomponent.can_memoize { - true => { - // todo: implement promotion logic. save us from boxing props that we don't need - } - false => { - // track this component internally so we know the right drop order - } - } + log::trace!( + "created component \"{}\", id: {:?} parent {:?} orig: {:?}", + vcomponent.fn_name, + new_idx, + parent_idx, + vcomponent.originator + ); - // Run the scope for one iteration to initialize it - self.scopes.run_scope(new_idx); + // if vcomponent.can_memoize { + // // todo: implement promotion logic. save us from boxing props that we don't need + // } else { + // // track this component internally so we know the right drop order + // } - // Take the node that was just generated from running the component - let nextnode = self.scopes.fin_head(new_idx); - self.stack.create_component(new_idx, nextnode); + self.enter_scope(new_idx); - // Finally, insert this scope as a seen node. - self.mutations.dirty_scopes.insert(new_idx); + let created = { + // Run the scope for one iteration to initialize it + self.scopes.run_scope(new_idx); + self.mutations.mark_dirty_scope(new_idx); + + // Take the node that was just generated from running the component + let nextnode = self.scopes.fin_head(new_idx); + self.create_node(nextnode) + }; + + self.leave_scope(); + + created } - // ================================= - // Tools for diffing nodes - // ================================= - - pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) { - use VNode::*; - match (old_node, new_node) { - // Check the most common cases first - // these are *actual* elements, not wrappers around lists - (Text(old), Text(new)) => { - if let Some(root) = old.id.get() { - if old.text != new.text { - self.mutations.set_text(new.text, root.as_u64()); - } - self.scopes.update_node(new_node, root); - - new.id.set(Some(root)); - } - } - - (Placeholder(old), Placeholder(new)) => { - if let Some(root) = old.id.get() { - self.scopes.update_node(new_node, root); - new.id.set(Some(root)) - } - } - - (Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node), - - // These two sets are pointers to nodes but are not actually nodes themselves - (Component(old), Component(new)) => { - self.diff_component_nodes(old_node, new_node, *old, *new) - } - - (Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new), - - // The normal pathway still works, but generates slightly weird instructions - // This pathway ensures uses the ReplaceAll, not the InsertAfter and remove - (Placeholder(_), Fragment(new)) => { - self.stack - .create_children(new.children, MountType::Replace { old: old_node }); - } - - // Anything else is just a basic replace and create - ( - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_), - ) => self - .stack - .create_node(new_node, MountType::Replace { old: old_node }), + pub(crate) fn diff_text_nodes( + &mut self, + old: &'b VText<'b>, + new: &'b VText<'b>, + _old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + if std::ptr::eq(old, new) { + return; } + + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; + + if old.text != new.text { + self.mutations.set_text(new.text, root.as_u64()); + } + + self.scopes.update_node(new_node, root); + + new.id.set(Some(root)); + } + + pub(crate) fn diff_placeholder_nodes( + &mut self, + old: &'b VPlaceholder, + new: &'b VPlaceholder, + _old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + ) { + if std::ptr::eq(old, new) { + return; + } + + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; + + self.scopes.update_node(new_node, root); + new.id.set(Some(root)); } fn diff_element_nodes( &mut self, - old: &'bump VElement<'bump>, - new: &'bump VElement<'bump>, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, + old: &'b VElement<'b>, + new: &'b VElement<'b>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, ) { - let root = old.id.get().unwrap(); + if std::ptr::eq(old, new) { + return; + } + + // if the node is comming back not assigned, that means it was borrowed but removed + let root = match old.id.get() { + Some(id) => id, + None => self.scopes.reserve_node(new_node), + }; // If the element type is completely different, the element needs to be re-rendered completely // This is an optimization React makes due to how users structure their code // // This case is rather rare (typically only in non-keyed lists) if new.tag != old.tag || new.namespace != old.namespace { - // maybe make this an instruction? - // issue is that we need the "vnode" but this method only has the velement - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::Replace { old: old_node }, - }); - self.create_element_node(new, new_node); + self.replace_node(old_node, new_node); return; } @@ -570,7 +387,7 @@ impl<'bump> DiffState<'bump> { self.mutations.remove_attribute(attribute, root.as_u64()); } for attribute in new.attributes { - self.mutations.set_attribute(attribute, root.as_u64()) + self.mutations.set_attribute(attribute, root.as_u64()); } } @@ -582,181 +399,123 @@ impl<'bump> DiffState<'bump> { // We also need to make sure that all listeners are properly attached to the parent scope (fix_listener) // // TODO: take a more efficient path than this - if let Some(cur_scope_id) = self.stack.current_scope() { - if old.listeners.len() == new.listeners.len() { - for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { - if old_l.event != new_l.event { - self.mutations - .remove_event_listener(old_l.event, root.as_u64()); - self.mutations.new_event_listener(new_l, cur_scope_id); - } - new_l.mounted_node.set(old_l.mounted_node.get()); - } - } else { - for listener in old.listeners { + let cur_scope_id = self.current_scope(); + + if old.listeners.len() == new.listeners.len() { + for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { + if old_l.event != new_l.event { self.mutations - .remove_event_listener(listener.event, root.as_u64()); - } - for listener in new.listeners { - listener.mounted_node.set(Some(root)); - self.mutations.new_event_listener(listener, cur_scope_id); + .remove_event_listener(old_l.event, root.as_u64()); + self.mutations.new_event_listener(new_l, cur_scope_id); } + new_l.mounted_node.set(old_l.mounted_node.get()); + } + } else { + for listener in old.listeners { + self.mutations + .remove_event_listener(listener.event, root.as_u64()); + } + for listener in new.listeners { + listener.mounted_node.set(Some(root)); + self.mutations.new_event_listener(listener, cur_scope_id); } } - if old.children.is_empty() && !new.children.is_empty() { - self.mutations.edits.push(PushRoot { - root: root.as_u64(), - }); - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.stack.create_children(new.children, MountType::Append); - } else { - self.stack.element_stack.push(root); - self.stack.instructions.push(DiffInstruction::PopElement); - self.diff_children(old.children, new.children); - } - - // todo: this is for the "settext" optimization - // it works, but i'm not sure if it's the direction we want to take right away - // I haven't benchmarked the performance imporvemenet yet. Perhaps - // we can make it a config? - - // match (old.children.len(), new.children.len()) { - // (0, 0) => {} - // (1, 1) => { - // let old1 = &old.children[0]; - // let new1 = &new.children[0]; - - // match (old1, new1) { - // (VNode::Text(old_text), VNode::Text(new_text)) => { - // if old_text.text != new_text.text { - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // } - // (VNode::Text(_old_text), _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_node(new1, MountType::Append); - // } - // (_, VNode::Text(new_text)) => { - // self.remove_nodes([old1], false); - // self.mutations.set_text(new_text.text, root.as_u64()); - // } - // _ => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } - // } - // (0, 1) => { - // if let VNode::Text(text) = &new.children[0] { - // self.mutations.set_text(text.text, root.as_u64()); - // } else { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // } - // } - // (0, _) => { - // self.mutations.edits.push(PushRoot { - // root: root.as_u64(), - // }); - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.stack.create_children(new.children, MountType::Append); - // } - // (_, 0) => { - // self.remove_nodes(old.children, false); - // self.mutations.set_text("", root.as_u64()); - // } - // (_, _) => { - // self.stack.element_stack.push(root); - // self.stack.instructions.push(DiffInstruction::PopElement); - // self.diff_children(old.children, new.children); - // } - // } + match (old.children.len(), new.children.len()) { + (0, 0) => {} + (0, _) => { + let created = self.create_children(new.children); + self.mutations.append_children(created as u32); + } + (_, _) => self.diff_children(old.children, new.children), + }; } fn diff_component_nodes( &mut self, - old_node: &'bump VNode<'bump>, - new_node: &'bump VNode<'bump>, - old: &'bump VComponent<'bump>, - new: &'bump VComponent<'bump>, + old_node: &'b VNode<'b>, + new_node: &'b VNode<'b>, + old: &'b VComponent<'b>, + new: &'b VComponent<'b>, ) { - let scope_addr = old.scope.get().unwrap(); - log::trace!("diff_component_nodes: {:?}", scope_addr); + let scope_addr = old + .scope + .get() + .expect("existing component nodes should have a scope"); if std::ptr::eq(old, new) { - log::trace!("skipping component diff - component is the sames"); return; } // Make sure we're dealing with the same component (by function pointer) if old.user_fc == new.user_fc { - self.stack.scope_stack.push(scope_addr); + self.enter_scope(scope_addr); + { + // Make sure the new component vnode is referencing the right scope id + new.scope.set(Some(scope_addr)); - // Make sure the new component vnode is referencing the right scope id - new.scope.set(Some(scope_addr)); + // make sure the component's caller function is up to date + let scope = self + .scopes + .get_scope(scope_addr) + .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - // make sure the component's caller function is up to date - let scope = self - .scopes - .get_scope(scope_addr) - .unwrap_or_else(|| panic!("could not find {:?}", scope_addr)); - - // take the new props out regardless - // when memoizing, push to the existing scope if memoization happens - let new_props = new.props.borrow_mut().take().unwrap(); - - let should_run = { - if old.can_memoize { - let props_are_the_same = unsafe { - scope - .props - .borrow() - .as_ref() - .unwrap() - .memoize(new_props.as_ref()) - }; - !props_are_the_same || self.force_diff - } else { - true - } - }; - - if should_run { - let _old_props = scope + // take the new props out regardless + // when memoizing, push to the existing scope if memoization happens + let new_props = new .props - .replace(unsafe { std::mem::transmute(Some(new_props)) }); + .borrow_mut() + .take() + .expect("new component props should exist"); - // this should auto drop the previous props - self.scopes.run_scope(scope_addr); - self.mutations.dirty_scopes.insert(scope_addr); + let should_diff = { + if old.can_memoize { + // safety: we trust the implementation of "memoize" + let props_are_the_same = unsafe { + let new_ref = new_props.as_ref(); + scope.props.borrow().as_ref().unwrap().memoize(new_ref) + }; + !props_are_the_same || self.force_diff + } else { + true + } + }; - self.diff_node( - self.scopes.wip_head(scope_addr), - self.scopes.fin_head(scope_addr), - ); - } else { - log::trace!("memoized"); - // memoization has taken place - drop(new_props); - }; + if should_diff { + let _old_props = scope + .props + .replace(unsafe { std::mem::transmute(Some(new_props)) }); - self.stack.scope_stack.pop(); + // this should auto drop the previous props + self.scopes.run_scope(scope_addr); + self.mutations.mark_dirty_scope(scope_addr); + + self.diff_node( + self.scopes.wip_head(scope_addr), + self.scopes.fin_head(scope_addr), + ); + } else { + // memoization has taken place + drop(new_props); + }; + } + self.leave_scope(); } else { - self.stack - .create_node(new_node, MountType::Replace { old: old_node }); + self.replace_node(old_node, new_node); } } - fn diff_fragment_nodes(&mut self, old: &'bump VFragment<'bump>, new: &'bump VFragment<'bump>) { + fn diff_fragment_nodes(&mut self, old: &'b VFragment<'b>, new: &'b VFragment<'b>) { + if std::ptr::eq(old, new) { + return; + } + // This is the case where options or direct vnodes might be used. // In this case, it's faster to just skip ahead to their diff if old.children.len() == 1 && new.children.len() == 1 { - self.diff_node(&old.children[0], &new.children[0]); + if !std::ptr::eq(old, new) { + self.diff_node(&old.children[0], &new.children[0]); + } return; } @@ -766,10 +525,6 @@ impl<'bump> DiffState<'bump> { self.diff_children(old.children, new.children); } - // ============================================= - // Utilities for creating new diff instructions - // ============================================= - // Diff the given set of old and new children. // // The parent must be on top of the change list stack when this function is @@ -785,11 +540,15 @@ impl<'bump> DiffState<'bump> { // // Fragment nodes cannot generate empty children lists, so we can assume that when a list is empty, it belongs only // to an element, and appending makes sense. - fn diff_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + if std::ptr::eq(old, new) { + return; + } + // Remember, fragments can never be empty (they always have a single child) match (old, new) { ([], []) => {} - ([], _) => self.stack.create_children(new, MountType::Append), + ([], _) => self.create_and_append_children(new), (_, []) => self.remove_nodes(old, true), _ => { let new_is_keyed = new[0].key().is_some(); @@ -821,29 +580,21 @@ impl<'bump> DiffState<'bump> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + use std::cmp::Ordering; + // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - for (new, old) in new.iter().zip(old.iter()).rev() { - self.stack.push(DiffInstruction::Diff { new, old }); - } - - use std::cmp::Ordering; match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..], true), - Ordering::Less => { - self.stack.create_children( - &new[old.len()..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); - } - Ordering::Equal => { - // nothing - they're the same size - } + Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Equal => {} + } + + for (new, old) in new.iter().zip(old.iter()) { + self.diff_node(old, new); } } @@ -863,10 +614,10 @@ impl<'bump> DiffState<'bump> { // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739 // // The stack is empty upon entry. - fn diff_keyed_children(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { let mut keys = fxhash::FxHashSet::default(); - let mut assert_unique_keys = |children: &'bump [VNode<'bump>]| { + let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { let key = child.key(); @@ -907,6 +658,7 @@ impl<'bump> DiffState<'bump> { !((old_middle.len() == new_middle.len()) && old_middle.is_empty()), "keyed children must have the same number of children" ); + if new_middle.is_empty() { // remove the old elements self.remove_nodes(old_middle, true); @@ -916,30 +668,15 @@ impl<'bump> DiffState<'bump> { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.stack.create_children( - new_middle, - MountType::InsertBefore { - other_node: foothold, - }, - ); + self.create_and_insert_before(new_middle, foothold); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.stack.create_children( - new_middle, - MountType::InsertAfter { - other_node: foothold, - }, - ); + self.create_and_insert_after(new_middle, foothold); } } else { self.diff_keyed_middle(old_middle, new_middle); @@ -953,9 +690,8 @@ impl<'bump> DiffState<'bump> { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - - old: &'bump [VNode<'bump>], - new: &'bump [VNode<'bump>], + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -964,19 +700,14 @@ impl<'bump> DiffState<'bump> { if old.key() != new.key() { break; } - self.stack.push(DiffInstruction::Diff { old, new }); + self.diff_node(old, new); left_offset += 1; } // If that was all of the old children, then create and append the remaining // new children and we're finished. if left_offset == old.len() { - self.stack.create_children( - &new[left_offset..], - MountType::InsertAfter { - other_node: old.last().unwrap(), - }, - ); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap()); return None; } @@ -1014,7 +745,8 @@ impl<'bump> DiffState<'bump> { // This function will load the appropriate nodes onto the stack and do diffing in place. // // Upon exit from this function, it will be restored to that same self. - fn diff_keyed_middle(&mut self, old: &'bump [VNode<'bump>], new: &'bump [VNode<'bump>]) { + #[allow(clippy::too_many_lines)] + fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -1039,8 +771,8 @@ impl<'bump> DiffState<'bump> { // 0. Debug sanity checks // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + debug_assert_ne!(new.first().map(VNode::key), old.first().map(VNode::key)); + debug_assert_ne!(new.last().map(VNode::key), old.last().map(VNode::key)); // 1. Map the old keys into a numerical ordering based on indices. // 2. Create a map of old key to its index @@ -1072,10 +804,12 @@ impl<'bump> DiffState<'bump> { if shared_keys.is_empty() { if let Some(first_old) = old.get(0) { self.remove_nodes(&old[1..], true); - self.stack - .create_children(new, MountType::Replace { old: first_old }) + let nodes_created = self.create_children(new); + self.replace_inner(first_old, nodes_created); } else { - self.stack.create_children(new, MountType::Append {}); + // I think this is wrong - why are we appending? + // only valid of the if there are no trailing elements + self.create_and_append_children(new); } return; } @@ -1103,33 +837,31 @@ impl<'bump> DiffState<'bump> { lis_sequence.pop(); } - let apply = |new_idx, new_node: &'bump VNode<'bump>, stack: &mut DiffStack<'bump>| { - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - stack.create_node(new_node, MountType::Absorb); - } else { - // this function should never take LIS indices - stack.push(DiffInstruction::PrepareMove { node: new_node }); - stack.push(DiffInstruction::Diff { - new: new_node, - old: &old[old_index], - }); - } - }; + for idx in &lis_sequence { + self.diff_node(&old[new_index_to_old_index[*idx]], &new[*idx]); + } - // add mount instruction for the last items not covered by the lis - let first_lis = *lis_sequence.first().unwrap(); - if first_lis > 0 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[first_lis], - }, - }); + let mut nodes_created = 0; - for (idx, new_node) in new[..first_lis].iter().enumerate().rev() { - apply(idx, new_node, &mut self.stack); + // add mount instruction for the first items not covered by the lis + let last = *lis_sequence.last().unwrap(); + if last < (new.len() - 1) { + for (idx, new_node) in new[(last + 1)..].iter().enumerate() { + let new_idx = idx + last + 1; + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_after( + self.find_last_element(&new[last]).unwrap(), + nodes_created as u32, + ); + nodes_created = 0; } // for each spacing, generate a mount instruction @@ -1137,85 +869,53 @@ impl<'bump> DiffState<'bump> { let mut last = *lis_iter.next().unwrap(); for next in lis_iter { if last - next > 1 { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertBefore { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(next + 1)..last].iter().enumerate().rev() { - apply(idx + next + 1, new_node, &mut self.stack); + for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { + let new_idx = idx + next + 1; + let old_index = new_index_to_old_index[new_idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } + + self.mutations.insert_before( + self.find_first_element(&new[last]).unwrap(), + nodes_created as u32, + ); + + nodes_created = 0; } last = *next; } - // add mount instruction for the first items not covered by the lis - let last = *lis_sequence.last().unwrap(); - if last < (new.len() - 1) { - self.stack.push_nodes_created(0); - self.stack.push(DiffInstruction::Mount { - and: MountType::InsertAfter { - other_node: &new[last], - }, - }); - for (idx, new_node) in new[(last + 1)..].iter().enumerate().rev() { - apply(idx + last + 1, new_node, &mut self.stack); + // add mount instruction for the last items not covered by the lis + let first_lis = *lis_sequence.first().unwrap(); + if first_lis > 0 { + for (idx, new_node) in new[..first_lis].iter().enumerate() { + let old_index = new_index_to_old_index[idx]; + if old_index == u32::MAX as usize { + nodes_created += self.create_node(new_node); + } else { + self.diff_node(&old[old_index], new_node); + nodes_created += self.push_all_nodes(new_node); + } } - } - for idx in lis_sequence.iter().rev() { - self.stack.push(DiffInstruction::Diff { - new: &new[*idx], - old: &old[new_index_to_old_index[*idx]], - }); + self.mutations.insert_before( + self.find_first_element(&new[first_lis]).unwrap(), + nodes_created as u32, + ); } } - // ===================== - // Utilities - // ===================== - - fn find_last_element(&mut self, vnode: &'bump VNode<'bump>) -> Option { - let mut search_node = Some(vnode); - - loop { - match &search_node.take().unwrap() { - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - VNode::Fragment(frag) => { - search_node = frag.children.last(); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - } - } + fn replace_node(&mut self, old: &'b VNode<'b>, new: &'b VNode<'b>) { + let nodes_created = self.create_node(new); + self.replace_inner(old, nodes_created); } - fn find_first_element_id(&mut self, vnode: &'bump VNode<'bump>) -> Option { - let mut search_node = Some(vnode); - - loop { - match &search_node.take().unwrap() { - // the ones that have a direct id - VNode::Fragment(frag) => { - search_node = Some(&frag.children[0]); - } - VNode::Component(el) => { - let scope_id = el.scope.get().unwrap(); - search_node = Some(self.scopes.root_node(scope_id)); - } - VNode::Text(t) => break t.id.get(), - VNode::Element(t) => break t.id.get(), - VNode::Placeholder(t) => break t.id.get(), - } - } - } - - fn replace_node(&mut self, old: &'bump VNode<'bump>, nodes_created: usize) { + fn replace_inner(&mut self, old: &'b VNode<'b>, nodes_created: usize) { match old { VNode::Element(el) => { let id = old @@ -1224,6 +924,7 @@ impl<'bump> DiffState<'bump> { self.mutations.replace_with(id, nodes_created as u32); self.remove_nodes(el.children, false); + self.scopes.collect_garbage(id); } VNode::Text(_) | VNode::Placeholder(_) => { @@ -1232,41 +933,51 @@ impl<'bump> DiffState<'bump> { .unwrap_or_else(|| panic!("broke on {:?}", old)); self.mutations.replace_with(id, nodes_created as u32); + self.scopes.collect_garbage(id); } VNode::Fragment(f) => { - self.replace_node(&f.children[0], nodes_created); + self.replace_inner(&f.children[0], nodes_created); self.remove_nodes(f.children.iter().skip(1), true); } VNode::Component(c) => { - let node = self.scopes.fin_head(c.scope.get().unwrap()); - self.replace_node(node, nodes_created); - + log::trace!("Replacing component {:?}", old); let scope_id = c.scope.get().unwrap(); + let node = self.scopes.fin_head(scope_id); - // we can only remove components if they are actively being diffed - if self.stack.scope_stack.contains(&c.originator) { + self.enter_scope(scope_id); + { + self.replace_inner(node, nodes_created); + + log::trace!("Replacing component x2 {:?}", old); + + let scope = self.scopes.get_scope(scope_id).unwrap(); + c.scope.set(None); + let props = scope.props.take().unwrap(); + c.props.borrow_mut().replace(props); self.scopes.try_remove(scope_id).unwrap(); + + // // we can only remove components if they are actively being diffed + // if self.scope_stack.contains(&c.originator) { + // log::trace!("Removing component {:?}", old); + + // self.scopes.try_remove(scope_id).unwrap(); + // } } + self.leave_scope(); } } } - /// schedules nodes for garbage collection and pushes "remove" to the mutation stack - /// remove can happen whenever - pub(crate) fn remove_nodes( - &mut self, - nodes: impl IntoIterator>, - gen_muts: bool, - ) { - // or cache the vec on the diff machine + pub fn remove_nodes(&mut self, nodes: impl IntoIterator>, gen_muts: bool) { for node in nodes { match node { VNode::Text(t) => { // this check exists because our null node will be removed but does not have an ID if let Some(id) = t.id.get() { self.scopes.collect_garbage(id); + t.id.set(None); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1276,6 +987,7 @@ impl<'bump> DiffState<'bump> { VNode::Placeholder(a) => { let id = a.id.get().unwrap(); self.scopes.collect_garbage(id); + a.id.set(None); if gen_muts { self.mutations.remove(id.as_u64()); @@ -1288,6 +1000,9 @@ impl<'bump> DiffState<'bump> { self.mutations.remove(id.as_u64()); } + self.scopes.collect_garbage(id); + e.id.set(None); + self.remove_nodes(e.children, false); } @@ -1296,16 +1011,124 @@ impl<'bump> DiffState<'bump> { } VNode::Component(c) => { - let scope_id = c.scope.get().unwrap(); - let root = self.scopes.root_node(scope_id); - self.remove_nodes(Some(root), gen_muts); + self.enter_scope(c.scope.get().unwrap()); + { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.remove_nodes([root], gen_muts); - // we can only remove this node if the originator is actively - if self.stack.scope_stack.contains(&c.originator) { + let scope = self.scopes.get_scope(scope_id).unwrap(); + c.scope.set(None); + + let props = scope.props.take().unwrap(); + c.props.borrow_mut().replace(props); self.scopes.try_remove(scope_id).unwrap(); } + self.leave_scope(); } } } } + + fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { + let mut created = 0; + for node in nodes { + created += self.create_node(node); + } + created + } + + fn create_and_append_children(&mut self, nodes: &'b [VNode<'b>]) { + let created = self.create_children(nodes); + self.mutations.append_children(created as u32); + } + + fn create_and_insert_after(&mut self, nodes: &'b [VNode<'b>], after: &'b VNode<'b>) { + let created = self.create_children(nodes); + let last = self.find_last_element(after).unwrap(); + self.mutations.insert_after(last, created as u32); + } + + fn create_and_insert_before(&mut self, nodes: &'b [VNode<'b>], before: &'b VNode<'b>) { + let created = self.create_children(nodes); + let first = self.find_first_element(before).unwrap(); + self.mutations.insert_before(first, created as u32); + } + + fn current_scope(&self) -> ScopeId { + self.scope_stack.last().copied().expect("no current scope") + } + + fn enter_scope(&mut self, scope: ScopeId) { + self.scope_stack.push(scope); + } + + fn leave_scope(&mut self) { + self.scope_stack.pop(); + } + + fn find_last_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + loop { + match &search_node.take().unwrap() { + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + VNode::Fragment(frag) => search_node = frag.children.last(), + VNode::Component(el) => { + let scope_id = el.scope.get().unwrap(); + search_node = Some(self.scopes.root_node(scope_id)); + } + } + } + } + + fn find_first_element(&self, vnode: &'b VNode<'b>) -> Option { + let mut search_node = Some(vnode); + loop { + match &search_node.take().expect("search node to have an ID") { + VNode::Text(t) => break t.id.get(), + VNode::Element(t) => break t.id.get(), + VNode::Placeholder(t) => break t.id.get(), + VNode::Fragment(frag) => search_node = Some(&frag.children[0]), + VNode::Component(el) => { + let scope = el.scope.get().expect("element to have a scope assigned"); + search_node = Some(self.scopes.root_node(scope)); + } + } + } + } + + // recursively push all the nodes of a tree onto the stack and return how many are there + fn push_all_nodes(&mut self, node: &'b VNode<'b>) -> usize { + match node { + VNode::Text(_) | VNode::Placeholder(_) => { + self.mutations.push_root(node.mounted_id()); + 1 + } + + VNode::Fragment(frag) => { + let mut added = 0; + for child in frag.children { + added += self.push_all_nodes(child); + } + added + } + + VNode::Component(c) => { + let scope_id = c.scope.get().unwrap(); + let root = self.scopes.root_node(scope_id); + self.push_all_nodes(root) + } + + VNode::Element(el) => { + let mut num_on_stack = 0; + for child in el.children.iter() { + num_on_stack += self.push_all_nodes(child); + } + self.mutations.push_root(el.id.get().unwrap()); + num_on_stack + 1 + } + } + } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dba4effbb..e81ddca87 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,7 +12,6 @@ pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { - pub(crate) use crate::diff::*; pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index fa96edd1b..bd94d0b71 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -143,6 +143,10 @@ impl<'a> Mutations<'a> { self.edits.push(InsertBefore { n, root }); } + pub(crate) fn append_children(&mut self, n: u32) { + self.edits.push(AppendChildren { many: n }); + } + // Remove Nodes from the dom pub(crate) fn remove(&mut self, id: u64) { self.edits.push(Remove { root: id }); @@ -217,6 +221,10 @@ impl<'a> Mutations<'a> { let name = attribute.name; self.edits.push(RemoveAttribute { name, root }); } + + pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) { + self.dirty_scopes.insert(scope); + } } // refs are only assigned once diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index e30a07001..7a36f58ce 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -4,7 +4,7 @@ //! cheap and *very* fast to construct - building a full tree should be quick. use crate::{ - innerlude::{Element, Properties, Scope, ScopeId, ScopeState}, + innerlude::{ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, AnyEvent, Component, }; @@ -147,13 +147,6 @@ impl<'src> VNode<'src> { } } - pub(crate) fn children(&self) -> &[VNode<'src>] { - match &self { - VNode::Fragment(f) => f.children, - _ => &[], - } - } - // Create an "owned" version of the vnode. pub fn decouple(&self) -> VNode<'src> { match *self { @@ -175,13 +168,21 @@ impl Debug for VNode<'_> { .field("key", &el.key) .field("attrs", &el.attributes) .field("children", &el.children) + .field("id", &el.id) .finish(), VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text), - VNode::Placeholder(_) => write!(s, "VNode::VPlaceholder"), + VNode::Placeholder(t) => write!(s, "VNode::VPlaceholder {{ id: {:?} }}", t.id), VNode::Fragment(frag) => { write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children) } - VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc), + VNode::Component(comp) => s + .debug_struct("VNode::VComponent") + .field("name", &comp.fn_name) + .field("fnptr", &comp.user_fc) + .field("key", &comp.key) + .field("scope", &comp.scope) + .field("originator", &comp.originator) + .finish(), } } } @@ -351,11 +352,18 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` -#[derive(Default)] pub struct EventHandler<'bump, T = ()> { pub callback: RefCell>>, } +impl<'a, T> Default for EventHandler<'a, T> { + fn default() -> Self { + Self { + callback: RefCell::new(None), + } + } +} + impl EventHandler<'_, T> { /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { @@ -377,7 +385,8 @@ pub struct VComponent<'src> { pub originator: ScopeId, pub scope: Cell>, pub can_memoize: bool, - pub user_fc: *const (), + pub user_fc: ComponentPtr, + pub fn_name: &'static str, pub props: RefCell>>, } @@ -542,6 +551,7 @@ impl<'a> NodeFactory<'a> { component: fn(Scope<'a, P>) -> Element, props: P, key: Option, + fn_name: &'static str, ) -> VNode<'a> where P: Properties + 'a, @@ -550,8 +560,9 @@ impl<'a> NodeFactory<'a> { key: key.map(|f| self.raw_text(f).0), scope: Default::default(), can_memoize: P::IS_STATIC, - user_fc: component as *const (), + user_fc: component as ComponentPtr, originator: self.scope.scope_id(), + fn_name, props: RefCell::new(Some(Box::new(VComponentProps { // local_props: RefCell::new(Some(props)), // heap_props: RefCell::new(None), diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 77c648d18..00c3024bf 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -13,7 +13,9 @@ use std::{ rc::Rc, }; -pub(crate) type FcSlot = *const (); +/// for traceability, we use the raw fn pointer to identify the function +/// we also get the component name, but that's not necessarily unique in the app +pub(crate) type ComponentPtr = *mut std::os::raw::c_void; pub(crate) struct Heuristic { hook_arena_size: usize, @@ -28,7 +30,7 @@ pub(crate) struct ScopeArena { pub scope_gen: Cell, pub bump: Bump, pub scopes: RefCell>, - pub heuristics: RefCell>, + pub heuristics: RefCell>, pub free_scopes: RefCell>, pub nodes: RefCell>>, pub tasks: Rc, @@ -82,7 +84,7 @@ impl ScopeArena { pub(crate) fn new_with_key( &self, - fc_ptr: *const (), + fc_ptr: ComponentPtr, vcomp: Box, parent_scope: Option, container: ElementId, @@ -94,7 +96,7 @@ impl ScopeArena { // Get the height of the scope let height = parent_scope - .map(|id| self.get_scope(id).map(|scope| scope.height)) + .map(|id| self.get_scope(id).map(|scope| scope.height + 1)) .flatten() .unwrap_or_default(); @@ -110,31 +112,62 @@ impl ScopeArena { if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { // reuse the old scope let scope = unsafe { &mut *old_scope }; - scope.props.get_mut().replace(vcomp); + + scope.container = container; + scope.our_arena_idx = new_scope_id; scope.parent_scope = parent_scope; scope.height = height; + scope.fnptr = fc_ptr; + scope.props.get_mut().replace(vcomp); scope.subtree.set(subtree); - scope.our_arena_idx = new_scope_id; - scope.container = container; + scope.frames[0].reset(); + scope.frames[1].reset(); + scope.shared_contexts.get_mut().clear(); + scope.items.get_mut().listeners.clear(); + scope.items.get_mut().borrowed_props.clear(); + scope.hook_idx.set(0); + scope.hook_vals.get_mut().clear(); + let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); debug_assert!(any_item.is_none()); } else { // else create a new scope + let (node_capacity, hook_capacity) = self + .heuristics + .borrow() + .get(&fc_ptr) + .map(|h| (h.node_arena_size, h.hook_arena_size)) + .unwrap_or_default(); + self.scopes.borrow_mut().insert( new_scope_id, - self.bump.alloc(ScopeState::new( - height, + self.bump.alloc(ScopeState { container, - new_scope_id, + our_arena_idx: new_scope_id, parent_scope, - vcomp, - self.tasks.clone(), - self.heuristics - .borrow() - .get(&fc_ptr) - .map(|h| (h.node_arena_size, h.hook_arena_size)) - .unwrap_or_default(), - )), + height, + fnptr: fc_ptr, + props: RefCell::new(Some(vcomp)), + frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], + + // todo: subtrees + subtree: Cell::new(0), + is_subtree_root: Cell::new(false), + + generation: 0.into(), + + tasks: self.tasks.clone(), + shared_contexts: Default::default(), + + items: RefCell::new(SelfReferentialItems { + listeners: Default::default(), + borrowed_props: Default::default(), + }), + + hook_arena: Bump::new(), + hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), + hook_idx: Default::default(), + }), ); } @@ -189,8 +222,6 @@ impl ScopeArena { /// This also makes sure that drop order is consistent and predictable. All resources that rely on being dropped will /// be dropped. pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { - log::trace!("Ensuring drop safety for scope {:?}", scope_id); - if let Some(scope) = self.get_scope(scope_id) { let mut items = scope.items.borrow_mut(); @@ -201,7 +232,6 @@ impl ScopeArena { if let Some(scope_id) = comp.scope.get() { self.ensure_drop_safety(scope_id); } - drop(comp.props.take()); }); @@ -217,18 +247,18 @@ impl ScopeArena { // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners - log::trace!("Running scope {:?}", id); self.ensure_drop_safety(id); // todo: we *know* that this is aliased by the contents of the scope itself let scope = unsafe { &mut *self.get_scope_raw(id).expect("could not find scope") }; + log::trace!("running scope {:?} symbol: {:?}", id, scope.fnptr); + // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held // - All children nodes that rely on &mut T are replaced with a new reference scope.hook_idx.set(0); - // book keeping to ensure safety around the borrowed data { // Safety: // - We've dropped all references to the wip bump frame with "ensure_drop_safety" @@ -241,8 +271,6 @@ impl ScopeArena { debug_assert!(items.borrowed_props.is_empty()); } - // safety: this is definitely not dropped - /* If the component returns None, then we fill in a placeholder node. This will wipe what was there. An alternate approach is to leave the Real Dom the same, but that can lead to safety issues and a lot more checks. @@ -284,14 +312,13 @@ impl ScopeArena { while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { - log::trace!("Found valid receiver element"); - let real_el = unsafe { &**el }; + log::debug!("looking for listener on {:?}", real_el); + if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { - log::trace!("Found valid receiver event"); - + log::debug!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled break; @@ -421,6 +448,7 @@ pub struct ScopeState { pub(crate) container: ElementId, pub(crate) our_arena_idx: ScopeId, pub(crate) height: u32, + pub(crate) fnptr: ComponentPtr, // todo: subtrees pub(crate) is_subtree_root: Cell, @@ -449,43 +477,6 @@ pub struct SelfReferentialItems<'a> { // Public methods exposed to libraries and components impl ScopeState { - fn new( - height: u32, - container: ElementId, - our_arena_idx: ScopeId, - parent_scope: Option<*mut ScopeState>, - vcomp: Box, - tasks: Rc, - (node_capacity, hook_capacity): (usize, usize), - ) -> Self { - ScopeState { - container, - our_arena_idx, - parent_scope, - height, - props: RefCell::new(Some(vcomp)), - frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)], - - // todo: subtrees - subtree: Cell::new(0), - is_subtree_root: Cell::new(false), - - generation: 0.into(), - - tasks, - shared_contexts: Default::default(), - - items: RefCell::new(SelfReferentialItems { - listeners: Default::default(), - borrowed_props: Default::default(), - }), - - hook_arena: Bump::new(), - hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)), - hook_idx: Default::default(), - } - } - /// Get the subtree ID that this scope belongs to. /// /// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 4e426caad..a7092104c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,6 +2,7 @@ //! //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. +use crate::diff::DiffState; use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::{future::poll_fn, StreamExt}; @@ -212,7 +213,7 @@ impl VirtualDom { let scopes = ScopeArena::new(channel.0.clone()); scopes.new_with_key( - root as *const _, + root as ComponentPtr, Box::new(VComponentProps { props: root_props, memo: |_a, _b| unreachable!("memo on root will neve be run"), @@ -448,6 +449,7 @@ impl VirtualDom { /// apply_mutations(mutations); /// } /// ``` + #[allow(unused)] pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { let mut committed_mutations = vec![]; @@ -467,36 +469,41 @@ impl VirtualDom { h1.cmp(&h2).reverse() }); + log::debug!("dirty_scopes: {:?}", self.dirty_scopes); + if let Some(scopeid) = self.dirty_scopes.pop() { if !ran_scopes.contains(&scopeid) { ran_scopes.insert(scopeid); self.scopes.run_scope(scopeid); - let (old, new) = (self.scopes.wip_head(scopeid), self.scopes.fin_head(scopeid)); - diff_state.stack.push(DiffInstruction::Diff { new, old }); - diff_state.stack.scope_stack.push(scopeid); + diff_state.diff_scope(scopeid); - let scope = scopes.get_scope(scopeid).unwrap(); - diff_state.stack.element_stack.push(scope.container); + let DiffState { mutations, .. } = diff_state; + + log::debug!("succesffuly resolved scopes {:?}", mutations.dirty_scopes); + for scope in &mutations.dirty_scopes { + self.dirty_scopes.remove(scope); + } + + committed_mutations.push(mutations); + + // todo: pause the diff machine + // if diff_state.work(&mut deadline) { + // let DiffState { mutations, .. } = diff_state; + // for scope in &mutations.dirty_scopes { + // self.dirty_scopes.remove(scope); + // } + // committed_mutations.push(mutations); + // } else { + // // leave the work in an incomplete state + // // + // // todo: we should store the edits and re-apply them later + // // for now, we just dump the work completely (threadsafe) + // return committed_mutations; + // } } } - - if diff_state.work(&mut deadline) { - let DiffState { mutations, .. } = diff_state; - - for scope in &mutations.dirty_scopes { - self.dirty_scopes.remove(scope); - } - - committed_mutations.push(mutations); - } else { - // leave the work in an incomplete state - // - // todo: we should store the edits and re-apply them later - // for now, we just dump the work completely (threadsafe) - return committed_mutations; - } } committed_mutations @@ -524,13 +531,15 @@ impl VirtualDom { let mut diff_state = DiffState::new(&self.scopes); self.scopes.run_scope(scope_id); - diff_state - .stack - .create_node(self.scopes.fin_head(scope_id), MountType::Append); - diff_state.stack.element_stack.push(ElementId(0)); - diff_state.stack.scope_stack.push(scope_id); - diff_state.work(|| false); + diff_state.element_stack.push(ElementId(0)); + diff_state.scope_stack.push(scope_id); + + let node = self.scopes.fin_head(scope_id); + let created = diff_state.create_node(node); + + diff_state.mutations.append_children(created as u32); + self.dirty_scopes.clear(); assert!(self.dirty_scopes.is_empty()); @@ -577,12 +586,11 @@ impl VirtualDom { ); diff_machine.force_diff = true; - diff_machine.stack.push(DiffInstruction::Diff { old, new }); - diff_machine.stack.scope_stack.push(scope_id); - + diff_machine.scope_stack.push(scope_id); let scope = diff_machine.scopes.get_scope(scope_id).unwrap(); - diff_machine.stack.element_stack.push(scope.container); - diff_machine.work(|| false); + diff_machine.element_stack.push(scope.container); + + diff_machine.diff_node(old, new); diff_machine.mutations } @@ -621,10 +629,10 @@ impl VirtualDom { /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.push(DiffInstruction::Diff { new, old }); - machine.stack.element_stack.push(ElementId(0)); - machine.stack.scope_stack.push(ScopeId(0)); - machine.work(|| false); + machine.element_stack.push(ElementId(0)); + machine.scope_stack.push(ScopeId(0)); + machine.diff_node(old, new); + machine.mutations } @@ -643,11 +651,11 @@ impl VirtualDom { /// ``` pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); - machine.stack.element_stack.push(ElementId(0)); - machine - .stack - .create_node(self.render_vnodes(nodes), MountType::Append); - machine.work(|| false); + machine.scope_stack.push(ScopeId(0)); + machine.element_stack.push(ElementId(0)); + let node = self.render_vnodes(nodes); + let created = machine.create_node(node); + machine.mutations.append_children(created as u32); machine.mutations } @@ -672,16 +680,15 @@ impl VirtualDom { let (old, new) = (self.render_vnodes(left), self.render_vnodes(right)); let mut create = DiffState::new(&self.scopes); - create.stack.scope_stack.push(ScopeId(0)); - create.stack.element_stack.push(ElementId(0)); - create.stack.create_node(old, MountType::Append); - create.work(|| false); + create.scope_stack.push(ScopeId(0)); + create.element_stack.push(ElementId(0)); + let created = create.create_node(old); + create.mutations.append_children(created as u32); let mut edit = DiffState::new(&self.scopes); - edit.stack.scope_stack.push(ScopeId(0)); - edit.stack.element_stack.push(ElementId(0)); - edit.stack.push(DiffInstruction::Diff { old, new }); - edit.work(|| false); + edit.scope_stack.push(ScopeId(0)); + edit.element_stack.push(ElementId(0)); + edit.diff_node(old, new); (create.mutations, edit.mutations) } diff --git a/packages/core/tests/.rustfmt.toml b/packages/core/tests/.rustfmt.toml new file mode 100644 index 000000000..70a9483a8 --- /dev/null +++ b/packages/core/tests/.rustfmt.toml @@ -0,0 +1 @@ +struct_lit_width = 80 diff --git a/packages/core/tests/create_dom.rs b/packages/core/tests/create_dom.rs index 044be289c..0cdc37c6d 100644 --- a/packages/core/tests/create_dom.rs +++ b/packages/core/tests/create_dom.rs @@ -36,18 +36,9 @@ fn test_original_diff() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, @@ -81,34 +72,13 @@ fn create() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "Hello, world!" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, - CreateTextNode { - root: 7, - text: "world" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "Hello, world!" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, + CreateTextNode { root: 7, text: "world" }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, @@ -135,32 +105,14 @@ fn create_list() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "hello" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "hello" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "hello" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -185,22 +137,10 @@ fn create_simple() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, AppendChildren { many: 4 }, ] ); @@ -235,36 +175,18 @@ fn create_components() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateElement { - root: 2, - tag: "div" - }, - CreateTextNode { - root: 3, - text: "abc1" - }, + CreateElement { root: 2, tag: "div" }, + CreateTextNode { root: 3, text: "abc1" }, AppendChildren { many: 1 }, CreateElement { root: 4, tag: "p" }, CreateElement { root: 5, tag: "h1" }, - CreateElement { - root: 6, - tag: "div" - }, - CreateTextNode { - root: 7, - text: "abc2" - }, + CreateElement { root: 6, tag: "div" }, + CreateTextNode { root: 7, text: "abc2" }, AppendChildren { many: 1 }, CreateElement { root: 8, tag: "p" }, CreateElement { root: 9, tag: "h1" }, - CreateElement { - root: 10, - tag: "div" - }, - CreateTextNode { - root: 11, - text: "abc3" - }, + CreateElement { root: 10, tag: "div" }, + CreateTextNode { root: 11, text: "abc3" }, AppendChildren { many: 1 }, CreateElement { root: 12, tag: "p" }, AppendChildren { many: 9 }, @@ -285,14 +207,8 @@ fn anchors() { assert_eq!( mutations.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 3 }, AppendChildren { many: 2 }, diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5b98ea80e..6bc9b5596 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] + //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -31,26 +32,14 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello world" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello world" }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] ); - assert_eq!( - change.edits, - [SetText { - text: "Goodbye world", - root: 2 - },] - ); + assert_eq!(change.edits, [SetText { text: "Goodbye world", root: 2 },]); } /// Should result in 3 elements on the stack @@ -67,32 +56,14 @@ fn fragments_create_properly() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - root: 2, - text: "Hello a" - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { root: 2, text: "Hello a" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - root: 4, - text: "Hello b" - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { root: 4, text: "Hello b" }, AppendChildren { many: 1 }, - CreateElement { - root: 5, - tag: "div" - }, - CreateTextNode { - root: 6, - text: "Hello c" - }, + CreateElement { root: 5, tag: "div" }, + CreateTextNode { root: 6, text: "Hello c" }, AppendChildren { many: 1 }, AppendChildren { many: 3 }, ] @@ -116,10 +87,7 @@ fn empty_fragments_create_anchors() { assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, ReplaceWith { m: 1, root: 1 } ] ); @@ -138,29 +106,15 @@ fn empty_fragments_create_many_anchors() { create.edits, [CreatePlaceholder { root: 1 }, AppendChildren { many: 1 }] ); + assert_eq!( change.edits, [ - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, ReplaceWith { m: 5, root: 1 } ] ); @@ -188,32 +142,14 @@ fn empty_fragments_create_anchors_with_many_children() { assert_eq!( change.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "hello: 0", - root: 3 - }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "hello: 0", root: 3 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 4, - }, - CreateTextNode { - text: "hello: 1", - root: 5 - }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hello: 1", root: 5 }, AppendChildren { many: 1 }, - CreateElement { - tag: "div", - root: 6, - }, - CreateTextNode { - text: "hello: 2", - root: 7 - }, + CreateElement { tag: "div", root: 6 }, + CreateTextNode { text: "hello: 2", root: 7 }, AppendChildren { many: 1 }, ReplaceWith { root: 1, m: 3 } ] @@ -236,23 +172,11 @@ fn many_items_become_fragment() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 2 - }, + CreateElement { root: 1, tag: "div" }, + CreateTextNode { text: "hello", root: 2 }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "div" - }, - CreateTextNode { - text: "hello", - root: 4 - }, + CreateElement { root: 3, tag: "div" }, + CreateTextNode { text: "hello", root: 4 }, AppendChildren { many: 1 }, AppendChildren { many: 2 }, ] @@ -315,7 +239,7 @@ fn two_fragments_with_differrent_elements_are_differet() { // replace the divs with new h1s CreateElement { tag: "h1", root: 7 }, ReplaceWith { root: 1, m: 1 }, - CreateElement { tag: "h1", root: 8 }, + CreateElement { tag: "h1", root: 1 }, // notice how 1 gets re-used ReplaceWith { root: 2, m: 1 }, ] ); @@ -339,39 +263,27 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, - CreateElement { - root: 3, - tag: "div" - }, - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, + CreateElement { root: 3, tag: "div" }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, CreateElement { root: 6, tag: "p" }, AppendChildren { many: 6 }, ] ); + + // note: key reuse is always the last node that got used + // slab maintains a linked list, essentially assert_eq!( change.edits, [ Remove { root: 3 }, Remove { root: 4 }, Remove { root: 5 }, - CreateElement { root: 7, tag: "h1" }, - ReplaceWith { root: 1, m: 1 }, - CreateElement { root: 8, tag: "h1" }, + CreateElement { root: 5, tag: "h1" }, // 3 gets reused + ReplaceWith { root: 1, m: 1 }, // 1 gets deleted + CreateElement { root: 1, tag: "h1" }, // 1 gets reused ReplaceWith { root: 2, m: 1 }, ] ); @@ -395,14 +307,8 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( create.edits, [ - CreateElement { - root: 1, - tag: "div" - }, - CreateElement { - root: 2, - tag: "div" - }, + CreateElement { root: 1, tag: "div" }, + CreateElement { root: 2, tag: "div" }, CreateElement { root: 3, tag: "p" }, AppendChildren { many: 3 }, ] @@ -410,18 +316,9 @@ fn two_fragments_with_same_elements_are_differet() { assert_eq!( change.edits, [ - CreateElement { - root: 4, - tag: "div" - }, - CreateElement { - root: 5, - tag: "div" - }, - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 4, tag: "div" }, + CreateElement { root: 5, tag: "div" }, + CreateElement { root: 6, tag: "div" }, InsertAfter { root: 2, n: 3 }, ] ); @@ -628,14 +525,8 @@ fn keyed_diffing_additions() { assert_eq!( change.edits, [ - CreateElement { - root: 6, - tag: "div" - }, - CreateElement { - root: 7, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, + CreateElement { root: 7, tag: "div" }, InsertAfter { n: 2, root: 5 } ] ); @@ -663,14 +554,8 @@ fn keyed_diffing_additions_and_moves_on_ends() { change.edits, [ // create 11, 12 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertAfter { root: 3, n: 2 }, // move 7 to the front PushRoot { root: 4 }, @@ -684,13 +569,13 @@ fn keyed_diffing_additions_and_moves_in_middle() { let dom = new_dom(); let left = rsx!({ - [/**/ 4, 5, 6, 7 /**/].iter().map(|f| { + [/**/ 1, 2, 3, 4 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); let right = rsx!({ - [/**/ 7, 4, 13, 17, 5, 11, 12, 6 /**/].iter().map(|f| { + [/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| { rsx! { div { key: "{f}" }} }) }); @@ -701,26 +586,14 @@ fn keyed_diffing_additions_and_moves_in_middle() { assert_eq!( change.edits, [ - // create 13, 17 - CreateElement { - tag: "div", - root: 5 - }, - CreateElement { - tag: "div", - root: 6 - }, - InsertBefore { root: 2, n: 2 }, - // create 11, 12 - CreateElement { - tag: "div", - root: 7 - }, - CreateElement { - tag: "div", - root: 8 - }, + // create 5, 6 + CreateElement { tag: "div", root: 5 }, + CreateElement { tag: "div", root: 6 }, InsertBefore { root: 3, n: 2 }, + // create 7, 8 + CreateElement { tag: "div", root: 7 }, + CreateElement { tag: "div", root: 8 }, + InsertBefore { root: 2, n: 2 }, // move 7 PushRoot { root: 4 }, InsertBefore { root: 1, n: 1 } @@ -756,16 +629,10 @@ fn controlled_keyed_diffing_out_of_order() { // remove 7 // create 9 and insert before 6 - CreateElement { - root: 5, - tag: "div" - }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -792,10 +659,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { - root: 6, - tag: "div" - }, + CreateElement { root: 6, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 }, diff --git a/packages/core/tests/earlyabort.rs b/packages/core/tests/earlyabort.rs index 7dc2ae4d4..3999a7402 100644 --- a/packages/core/tests/earlyabort.rs +++ b/packages/core/tests/earlyabort.rs @@ -42,14 +42,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - CreateTextNode { - text: "Hello, world!", - root: 2, - }, + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "Hello, world!", root: 2 }, AppendChildren { many: 1 }, AppendChildren { many: 1 }, ] @@ -65,14 +59,8 @@ fn test_early_abort() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "Hello, world!", - root: 4, - }, + CreateElement { tag: "div", root: 1 }, // keys get reused + CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused AppendChildren { many: 1 }, ReplaceWith { root: 3, m: 1 }, ] diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 6c0df4e1e..35058d418 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -1,4 +1,5 @@ #![allow(unused, non_upper_case_globals)] +#![allow(non_snake_case)] //! Tests for the lifecycle of components. use dioxus::prelude::*; @@ -27,12 +28,7 @@ fn manual_diffing() { }; let value = Arc::new(Mutex::new("Hello")); - let mut dom = VirtualDom::new_with_props( - App, - AppProps { - value: value.clone(), - }, - ); + let mut dom = VirtualDom::new_with_props(App, AppProps { value: value.clone() }); let _ = dom.rebuild(); @@ -45,7 +41,7 @@ fn manual_diffing() { #[test] fn events_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let count = cx.use_hook(|_| 0); let inner = match *count { @@ -66,7 +62,7 @@ fn events_generate() { cx.render(inner) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let mut channel = dom.get_scheduler_channel(); assert!(dom.has_work()); @@ -74,28 +70,12 @@ fn events_generate() { assert_eq!( edits.edits, [ - CreateElement { - tag: "div", - root: 1, - }, - NewEventListener { - event_name: "click", - scope: ScopeId(0), - root: 1, - }, - CreateElement { - tag: "div", - root: 2, - }, - CreateTextNode { - text: "nested", - root: 3, - }, + CreateElement { tag: "div", root: 1 }, + NewEventListener { event_name: "click", scope: ScopeId(0), root: 1 }, + CreateElement { tag: "div", root: 2 }, + CreateTextNode { text: "nested", root: 3 }, AppendChildren { many: 1 }, - CreateTextNode { - text: "Click me!", - root: 4, - }, + CreateTextNode { text: "Click me!", root: 4 }, AppendChildren { many: 2 }, AppendChildren { many: 1 }, ] @@ -104,7 +84,7 @@ fn events_generate() { #[test] fn components_generate() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -121,106 +101,84 @@ fn components_generate() { }) }; - static Child: Component = |cx| { + fn Child(cx: Scope) -> Element { + log::debug!("Running child"); cx.render(rsx! { h1 {} }) - }; + } - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); assert_eq!( edits.edits, [ - CreateTextNode { - text: "Text0", - root: 1, - }, + CreateTextNode { text: "Text0", root: 1 }, AppendChildren { many: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { - tag: "div", - root: 2, - }, + CreateElement { tag: "div", root: 2 }, ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "Text2", - root: 3, - }, + CreateTextNode { text: "Text2", root: 1 }, ReplaceWith { root: 2, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // child {} assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 4 }, - ReplaceWith { root: 3, m: 1 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); + // placeholder assert_eq!( - edits.edits, - [CreatePlaceholder { root: 5 }, ReplaceWith { root: 4, m: 1 },] + dom.hard_diff(ScopeId(0)).edits, + [CreatePlaceholder { root: 1 }, ReplaceWith { root: 2, m: 1 },] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 3", - root: 6, - }, - ReplaceWith { root: 5, m: 1 }, + CreateTextNode { text: "text 3", root: 2 }, + ReplaceWith { root: 1, m: 1 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateTextNode { - text: "text 0", - root: 7, - }, - CreateTextNode { - text: "text 1", - root: 8, - }, - ReplaceWith { root: 6, m: 2 }, + CreateTextNode { text: "text 0", root: 1 }, + CreateTextNode { text: "text 1", root: 3 }, + ReplaceWith { root: 2, m: 2 }, ] ); - let edits = dom.hard_diff(ScopeId(0)); assert_eq!( - edits.edits, + dom.hard_diff(ScopeId(0)).edits, [ - CreateElement { tag: "h1", root: 9 }, - ReplaceWith { root: 7, m: 1 }, - Remove { root: 8 }, + CreateElement { tag: "h1", root: 2 }, + ReplaceWith { root: 1, m: 1 }, + Remove { root: 3 }, ] ); } #[test] fn component_swap() { - static App: Component = |cx| { + fn app(cx: Scope) -> Element { let render_phase = cx.use_hook(|_| 0); *render_phase += 1; @@ -296,7 +254,7 @@ fn component_swap() { }) }; - let mut dom = VirtualDom::new(App); + let mut dom = VirtualDom::new(app); let edits = dom.rebuild(); dbg!(&edits); diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index bb4963b71..fe98c83ae 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -1,3 +1,4 @@ +#![allow(non_snake_case)] /* Stress Miri as much as possible. @@ -152,12 +153,7 @@ fn free_works_on_root_props() { } } - let mut dom = new_dom( - app, - Custom { - val: String::from("asd"), - }, - ); + let mut dom = new_dom(app, Custom { val: String::from("asd") }); dom.rebuild(); } @@ -313,3 +309,112 @@ fn leak_thru_children() { dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); dom.work_with_deadline(|| false); } + +#[test] +fn test_pass_thru() { + #[inline_props] + fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + header { + nav { children } + } + }) + } + + fn NavMenu(cx: Scope) -> Element { + rsx!(cx, + NavBrand {} + div { + NavStart {} + NavEnd {} + } + ) + } + + fn NavBrand(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavStart(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + fn NavEnd(cx: Scope) -> Element { + rsx!(cx, div {}) + } + + #[inline_props] + fn MainContainer<'a>( + cx: Scope, + nav: Element<'a>, + body: Element<'a>, + footer: Element<'a>, + ) -> Element { + cx.render(rsx! { + div { + class: "columns is-mobile", + div { + class: "column is-full", + nav, + body, + footer, + } + } + }) + } + + fn app(cx: Scope) -> Element { + let nav = cx.render(rsx! { + NavContainer { + NavMenu {} + } + }); + let body = cx.render(rsx! { + div {} + }); + let footer = cx.render(rsx! { + div {} + }); + + cx.render(rsx! { + MainContainer { + nav: nav, + body: body, + footer: footer, + } + }) + } + + let mut dom = new_dom(app, ()); + let _ = dom.rebuild(); + + for _ in 0..40 { + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + dom.work_with_deadline(|| false); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + dom.work_with_deadline(|| false); + } +} diff --git a/packages/core/tests/passthru.rs b/packages/core/tests/passthru.rs new file mode 100644 index 000000000..948de8b67 --- /dev/null +++ b/packages/core/tests/passthru.rs @@ -0,0 +1,107 @@ +#![allow(unused, non_upper_case_globals)] +#![allow(non_snake_case)] + +//! Diffing Tests +//! +//! These tests only verify that the diffing algorithm works properly for single components. +//! +//! It does not validated that component lifecycles work properly. This is done in another test file. + +use dioxus::{prelude::*, DomEdit, ScopeId}; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +mod test_logging; + +fn new_dom() -> VirtualDom { + const IS_LOGGING_ENABLED: bool = false; + test_logging::set_up_logging(IS_LOGGING_ENABLED); + VirtualDom::new(|cx| rsx!(cx, "hi")) +} + +use DomEdit::*; + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + Child { + Child { + div { + "hi" + } + } + } + } + }) + }; + + #[inline_props] + fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { children }) + }; + + let mut dom = VirtualDom::new(app); + let mut channel = dom.get_scheduler_channel(); + assert!(dom.has_work()); + + let edits = dom.rebuild(); + assert_eq!( + edits.edits, + [ + CreateElement { tag: "div", root: 1 }, + CreateTextNode { text: "hi", root: 2 }, + AppendChildren { many: 1 }, + AppendChildren { many: 1 }, + ] + ) +} + +/// Should push the text node onto the stack and modify it +#[test] +fn nested_passthru_creates_add() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + Child { + "1" + Child { + "2" + Child { + "3" + div { + "hi" + } + } + } + } + }) + }; + + #[inline_props] + fn Child<'a>(cx: Scope, children: Element<'a>) -> Element { + cx.render(rsx! { + children + }) + }; + + let mut dom = VirtualDom::new(app); + let mut channel = dom.get_scheduler_channel(); + assert!(dom.has_work()); + + let edits = dom.rebuild(); + assert_eq!( + edits.edits, + [ + CreateTextNode { text: "1", root: 1 }, + CreateTextNode { text: "2", root: 2 }, + CreateTextNode { text: "3", root: 3 }, + CreateElement { tag: "div", root: 4 }, + CreateTextNode { text: "hi", root: 5 }, + AppendChildren { many: 1 }, + AppendChildren { many: 4 }, + ] + ) +} diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 3f04aa154..18d569914 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -1,6 +1,6 @@ -#![allow(unused, non_upper_case_globals)] +#![allow(unused, non_upper_case_globals, non_snake_case)] -use dioxus::{prelude::*, DomEdit, Mutations}; +use dioxus::{prelude::*, DomEdit, Mutations, SchedulerMsg, ScopeId}; use dioxus_core as dioxus; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; @@ -29,11 +29,102 @@ fn shared_state_test() { assert_eq!( edits, [ - CreateTextNode { - root: 1, - text: "Hello, world!" - }, + CreateTextNode { root: 1, text: "Hello, world!" }, AppendChildren { many: 1 }, ] ); } + +#[test] +fn swap_test() { + struct MySharedState(&'static str); + + fn app(cx: Scope) -> Element { + let val = cx.use_hook(|_| 0); + *val += 1; + + cx.provide_context(MySharedState("world!")); + + let child = match *val % 2 { + 0 => rsx!( + Child1 { + Child1 { } + Child2 { } + } + ), + _ => rsx!( + Child2 { + Child2 { } + Child2 { } + } + ), + }; + + cx.render(rsx!( + Router { + div { child } + } + )) + } + + #[inline_props] + fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> { + cx.render(rsx!(div { children })) + } + + #[inline_props] + fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element { + let shared = cx.consume_context::().unwrap(); + println!("Child1: {}", shared.0); + cx.render(rsx! { + div { + "{shared.0}", + children + } + }) + } + + #[inline_props] + fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element { + let shared = cx.consume_context::().unwrap(); + println!("Child2: {}", shared.0); + cx.render(rsx! { + h1 { + "{shared.0}", + children + } + }) + } + + let mut dom = VirtualDom::new(app); + let Mutations { edits, .. } = dom.rebuild(); + + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); + // dom.work_with_deadline(|| false); + + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); + // dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); + // dom.work_with_deadline(|| false); +} diff --git a/packages/core/tests/vdom_rebuild.rs b/packages/core/tests/vdom_rebuild.rs index 5d1dbcdf8..83b4ff35f 100644 --- a/packages/core/tests/vdom_rebuild.rs +++ b/packages/core/tests/vdom_rebuild.rs @@ -68,15 +68,9 @@ fn conditional_rendering() { mutations.edits, [ CreateElement { root: 1, tag: "h1" }, - CreateTextNode { - root: 2, - text: "hello" - }, + CreateTextNode { root: 2, text: "hello" }, AppendChildren { many: 1 }, - CreateElement { - root: 3, - tag: "span" - }, + CreateElement { root: 3, tag: "span" }, CreateTextNode { root: 4, text: "a" }, AppendChildren { many: 1 }, CreatePlaceholder { root: 5 }, diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 245d05623..65657ab37 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -5,6 +5,76 @@ 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 + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - default asset server + - remove dioxus id on non-event elements + - also hide placeholder node + - wire up both desktop and web + - bool attr white list + - setup a typescript build + +### Bug Fixes + + - custom protocol receiver type + - clippy + - wry pathing + - cursor jumping and use set instead of lsit + - format code + - check `href` null + - add exclusion list + - check `href` null + - prevent `submit` default + +### Commit Statistics + + + + - 26 commits contributed to the release over the course of 11 calendar days. + - 17 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - custom protocol receiver type ([`22308eb`](https://github.comgit//DioxusLabs/dioxus/commit/22308eb26a9ea48b14f5f5abb833aa90a4e3fc40)) + - default asset server ([`430cde7`](https://github.comgit//DioxusLabs/dioxus/commit/430cde7068d308f2783e33d278fd0c0efa659c1b)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - clippy ([`6bc45b1`](https://github.comgit//DioxusLabs/dioxus/commit/6bc45b1c5064a4e2b04d452c52a8167ad179691e)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - wry pathing ([`bad3616`](https://github.comgit//DioxusLabs/dioxus/commit/bad36162af764291f5a031b6233d151f61d745a4)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - also hide placeholder node ([`eb13884`](https://github.comgit//DioxusLabs/dioxus/commit/eb138848ec7a8978f0ed7c717374684d2315dc03)) + - drag and drop support ([`9ae981a`](https://github.comgit//DioxusLabs/dioxus/commit/9ae981a1af4b5474ce16e27e070794d59128c12a)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - cursor jumping and use set instead of lsit ([`be614e6`](https://github.comgit//DioxusLabs/dioxus/commit/be614e6535e6e13e6ff93e9c6a171c1c002e6b01)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - setup a typescript build ([`5bf6c96`](https://github.comgit//DioxusLabs/dioxus/commit/5bf6c96f9fed04de949403202bafcbeadb5d2030)) + - check `href` null ([`2073b40`](https://github.comgit//DioxusLabs/dioxus/commit/2073b400df55f0c6d8bed7371b2313be6c064e6e)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - check `href` null ([`327f901`](https://github.comgit//DioxusLabs/dioxus/commit/327f9015481809d8e5b9e69f26202e8d66dd198e)) + - prevent `submit` default ([`8089023`](https://github.comgit//DioxusLabs/dioxus/commit/8089023a6c3a54957af9c9c05c9dee6088b059ef)) +
+ ## v0.1.5 (2022-01-08) ### Documentation @@ -37,6 +107,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - omg what a dumb mistake - bless up, no more segfaults - wire up event delegator for webview + - link open in browser + - move `rpc` to handler + - `open_browser` bool attribute + - link open in browser + - link open in browser ### Bug Fixes @@ -48,13 +123,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - tags - desktop and mobile - messed up how lifetimes worked, need to render once per component + - error pattern + - format code ### Commit Statistics - - 91 commits contributed to the release over the course of 151 calendar days. - - 78 commits where understood as [conventional](https://www.conventionalcommits.org). + - 98 commits contributed to the release over the course of 151 calendar days. + - 84 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -64,11 +141,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) + - Release dioxus-desktop v0.1.5 ([`cd0dcac`](https://github.comgit//DioxusLabs/dioxus/commit/cd0dcacaf2862f26d29acb21d98f75d41b940e3f)) - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) + - link open in browser ([`46fd6ac`](https://github.comgit//DioxusLabs/dioxus/commit/46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - error pattern ([`62b637f`](https://github.comgit//DioxusLabs/dioxus/commit/62b637f8b0eaf616c49461fa23b9251a79abc147)) + - move `rpc` to handler ([`f006f50`](https://github.comgit//DioxusLabs/dioxus/commit/f006f50317f4b75fac353bc988db057a281ba7f8)) + - `open_browser` bool attribute ([`9e04ce5`](https://github.comgit//DioxusLabs/dioxus/commit/9e04ce5342850d2e0a01dde169807d6f6eb16566)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - format code ([`5233ee9`](https://github.comgit//DioxusLabs/dioxus/commit/5233ee97d9314f7f0e0bdf05c56d2a9e4201a596)) + - link open in browser ([`c737c42`](https://github.comgit//DioxusLabs/dioxus/commit/c737c424b05ad8453e8770a14a0d210fb0c7c2fe)) - Merge pull request #89 from DioxusLabs/jk/simplify-example-run ([`8b6aa8b`](https://github.comgit//DioxusLabs/dioxus/commit/8b6aa8b880b6cb5c95e0c0743aad4e4e74388e05)) + - link open in browser ([`a0f6015`](https://github.comgit//DioxusLabs/dioxus/commit/a0f60152bc7e5866f114ed469809ce8be70d17d4)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - clear warnigns ([`175a6a1`](https://github.comgit//DioxusLabs/dioxus/commit/175a6a199c6738d8d0c7646ba0ec3fc4406c6535)) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index eb9425c1f..6fd277ba4 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-desktop" -version = "0.1.5" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,29 +12,38 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } -argh = "0.1.4" -serde = "1.0.120" -serde_json = "1.0.61" -thiserror = "1.0.23" -log = "0.4.13" -html-escape = "0.2.9" -wry = "0.12.2" -futures-channel = "0.3" -tokio = { version = "1.12.0", features = [ +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } +serde = "1.0.136" +serde_json = "1.0.79" +thiserror = "1.0.30" +log = "0.4.14" +wry = { version = "0.13.1" } +futures-channel = "0.3.21" +tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} -dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" +mime_guess = "2.0.3" +dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } + [features] default = ["tokio_runtime"] tokio_runtime = ["tokio"] +devtool = ["wry/devtool"] +fullscreen = ["wry/fullscreen"] +transparent = ["wry/transparent"] + +tray = ["wry/tray"] +ayatana = ["wry/ayatana"] + [dev-dependencies] +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-hooks = { path = "../hooks" } +# image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs deleted file mode 100644 index 056e7b591..000000000 --- a/packages/desktop/examples/async.rs +++ /dev/null @@ -1,34 +0,0 @@ -use dioxus::prelude::*; -use dioxus_core as dioxus; -use dioxus_core_macro::*; -use dioxus_hooks::*; -use dioxus_html as dioxus_elements; -use std::time::Duration; - -fn main() { - dioxus_desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); - - use_future(&cx, || { - let count = count.for_async(); - async move { - loop { - tokio::time::sleep(Duration::from_millis(1000)).await; - *count.modify() += 1; - } - } - }); - - cx.render(rsx! { - div { - h1 { "High-Five counter: {count}" } - button { - onclick: move |_| count.set(0), - "Click me!" - } - } - }) -} diff --git a/packages/desktop/src/assets/default_icon.bin b/packages/desktop/src/assets/default_icon.bin new file mode 100644 index 000000000..2d4898f0c Binary files /dev/null and b/packages/desktop/src/assets/default_icon.bin differ diff --git a/packages/desktop/src/assets/default_icon.png b/packages/desktop/src/assets/default_icon.png new file mode 100644 index 000000000..d0742ea8a Binary files /dev/null and b/packages/desktop/src/assets/default_icon.png differ diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index ad85cfc0f..c5d6eef50 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use wry::application::window::Icon; use wry::{ application::{ event_loop::EventLoop, @@ -11,14 +12,14 @@ use wry::{ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { - pub window: WindowBuilder, - pub file_drop_handler: Option bool>>, - pub protocos: Vec, + pub(crate) window: WindowBuilder, + pub(crate) file_drop_handler: Option bool>>, + pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } -pub type WryProtocl = ( +pub(crate) type WryProtocol = ( String, Box WryResult + 'static>, ); @@ -31,7 +32,7 @@ impl DesktopConfig { Self { event_handler: None, window, - protocos: Vec::new(), + protocols: Vec::new(), file_drop_handler: None, pre_rendered: None, } @@ -71,11 +72,25 @@ impl DesktopConfig { self } - pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self + pub fn with_custom_protocol(&mut self, name: String, handler: F) -> &mut Self where F: Fn(&HttpRequest) -> WryResult + 'static, { - self.protocos.push((name, Box::new(handler))); + self.protocols.push((name, Box::new(handler))); + self + } + + pub fn with_icon(&mut self, icon: Icon) -> &mut Self { + self.window.window.window_icon = Some(icon); + self + } +} + +impl DesktopConfig { + pub(crate) fn with_default_icon(mut self) -> Self { + let bin: &[u8] = include_bytes!("./assets/default_icon.bin"); + let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed"); + self.window.window.window_icon = Some(rgba); self } } @@ -85,3 +100,31 @@ impl Default for DesktopConfig { Self::new() } } + +// dirty trick, avoid introducing `image` at runtime +// TODO: use serde when `Icon` impl serde +// +// This function should only be enabled when generating new icons. +// +// #[test] +// #[ignore] +// fn prepare_default_icon() { +// use image::io::Reader as ImageReader; +// use image::ImageFormat; +// use std::fs::File; +// use std::io::Cursor; +// use std::io::Write; +// use std::path::PathBuf; +// let png: &[u8] = include_bytes!("default_icon.png"); +// let mut reader = ImageReader::new(Cursor::new(png)); +// reader.set_format(ImageFormat::Png); +// let icon = reader.decode().unwrap(); +// let bin = PathBuf::from(file!()) +// .parent() +// .unwrap() +// .join("default_icon.bin"); +// println!("{:?}", bin); +// let mut file = File::create(bin).unwrap(); +// file.write_all(icon.as_bytes()).unwrap(); +// println!("({}, {})", icon.width(), icon.height()) +// } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs new file mode 100644 index 000000000..8715c74f0 --- /dev/null +++ b/packages/desktop/src/controller.rs @@ -0,0 +1,107 @@ +use crate::desktop_context::DesktopContext; +use crate::user_window_events::UserWindowEvent; +use dioxus_core::*; +use std::{ + collections::{HashMap, VecDeque}, + sync::atomic::AtomicBool, + sync::{Arc, RwLock}, +}; +use wry::{ + self, + application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, + webview::WebView, +}; + +pub(super) struct DesktopController { + pub(super) webviews: HashMap, + pub(super) sender: futures_channel::mpsc::UnboundedSender, + pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, + pub(super) is_ready: Arc, +} + +impl DesktopController { + // Launch the virtualdom on its own thread managed by tokio + // returns the desktop state + pub(super) fn new_on_tokio( + root: Component

, + props: P, + proxy: EventLoopProxy, + ) -> Self { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let pending_edits = edit_queue.clone(); + + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let return_sender = sender.clone(); + + let desktop_context_proxy = proxy.clone(); + std::thread::spawn(move || { + // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async move { + let mut dom = + VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); + + let window_context = DesktopContext::new(desktop_context_proxy); + + dom.base_scope().provide_context(window_context); + + let edits = dom.rebuild(); + + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edits.edits).unwrap()); + + // Make sure the window is ready for any new updates + proxy.send_event(UserWindowEvent::Update).unwrap(); + + loop { + dom.wait_for_work().await; + let mut muts = dom.work_with_deadline(|| false); + + while let Some(edit) = muts.pop() { + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edit.edits).unwrap()); + } + + let _ = proxy.send_event(UserWindowEvent::Update); + } + }) + }); + + Self { + pending_edits, + sender: return_sender, + webviews: HashMap::new(), + is_ready: Arc::new(AtomicBool::new(false)), + quit_app_on_close: true, + } + } + + pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { + self.webviews.remove(&window_id); + + if self.webviews.is_empty() && self.quit_app_on_close { + *control_flow = ControlFlow::Exit; + } + } + + pub(super) fn try_load_ready_webviews(&mut self) { + if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { + let mut queue = self.pending_edits.write().unwrap(); + let (_id, view) = self.webviews.iter_mut().next().unwrap(); + + while let Some(edit) = queue.pop_back() { + view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) + .unwrap(); + } + } + } +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 48c82f5a8..e1b726d73 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,100 +1,118 @@ -use std::cell::RefCell; +use std::rc::Rc; -use dioxus::prelude::Scope; -use dioxus_core as dioxus; -use dioxus_core::{Context, Element, LazyNodes, NodeFactory, Properties}; -use dioxus_core_macro::Props; +use dioxus_core::ScopeState; +use wry::application::event_loop::EventLoopProxy; -/* -This module provides a set of Dioxus components to easily manage windows, tabs, etc. +use crate::user_window_events::UserWindowEvent; +use UserWindowEvent::*; -Windows can be created anywhere in the tree, making them very flexible for things like modals, etc. +type ProxyType = EventLoopProxy; -*/ -pub struct DesktopContext {} +/// Desktop-Window handle api context +/// +/// you can use this context control some window event +/// +/// you can use `cx.consume_context::` to get this context +/// +/// ```rust +/// let desktop = cx.consume_context::().unwrap(); +/// ``` +#[derive(Clone)] +pub struct DesktopContext { + proxy: ProxyType, +} impl DesktopContext { - fn add_window(&mut self) { - // + pub(crate) fn new(proxy: ProxyType) -> Self { + Self { proxy } } - fn close_window(&mut self) { - // + + /// trigger the drag-window event + /// + /// Moves the window with the left mouse button until the button is released. + /// + /// you need use it in `onmousedown` event: + /// ```rust + /// onmousedown: move |_| { desktop.drag_window(); } + /// ``` + pub fn drag(&self) { + let _ = self.proxy.send_event(DragWindow); + } + + /// set window minimize state + pub fn set_minimized(&self, minimized: bool) { + let _ = self.proxy.send_event(Minimize(minimized)); + } + + /// set window maximize state + pub fn set_maximized(&self, maximized: bool) { + let _ = self.proxy.send_event(Maximize(maximized)); + } + + /// toggle window maximize state + pub fn toggle_maximized(&self) { + let _ = self.proxy.send_event(MaximizeToggle); + } + + /// set window visible or not + pub fn set_visible(&self, visible: bool) { + let _ = self.proxy.send_event(Visible(visible)); + } + + /// close window + pub fn close(&self) { + let _ = self.proxy.send_event(CloseWindow); + } + + /// set window to focus + pub fn focus(&self) { + let _ = self.proxy.send_event(FocusWindow); + } + + /// change window to fullscreen + pub fn set_fullscreen(&self, fullscreen: bool) { + let _ = self.proxy.send_event(Fullscreen(fullscreen)); + } + + /// set resizable state + pub fn set_resizable(&self, resizable: bool) { + let _ = self.proxy.send_event(Resizable(resizable)); + } + + /// set the window always on top + pub fn set_always_on_top(&self, top: bool) { + let _ = self.proxy.send_event(AlwaysOnTop(top)); + } + + // set cursor visible or not + pub fn set_cursor_visible(&self, visible: bool) { + let _ = self.proxy.send_event(CursorVisible(visible)); + } + + // set cursor grab + pub fn set_cursor_grab(&self, grab: bool) { + let _ = self.proxy.send_event(CursorGrab(grab)); + } + + /// set window title + pub fn set_title(&self, title: &str) { + let _ = self.proxy.send_event(SetTitle(String::from(title))); + } + + /// change window to borderless + pub fn set_decorations(&self, decoration: bool) { + let _ = self.proxy.send_event(SetDecorations(decoration)); + } + + /// opens DevTool window + pub fn devtool(&self) { + let _ = self.proxy.send_event(DevTool); } } -enum WindowHandlers { - Resized(Box), - Moved(Box), - CloseRequested(Box), - Destroyed(Box), - DroppedFile(Box), - HoveredFile(Box), - HoverFileCancelled(Box), - ReceivedTimeText(Box), - Focused(Box), -} - -#[derive(Props)] -pub struct WebviewWindowProps<'a> { - onclose: &'a dyn FnMut(()), - - onopen: &'a dyn FnMut(()), - - /// focuse me - onfocused: &'a dyn FnMut(()), - - children: Element, -} - -/// A handle to a -/// -/// -/// -/// -/// -/// -/// -/// -/// -pub fn WebviewWindow(cx: Scope) -> Element { - let dtcx = cx.consume_state::>()?; - - cx.use_hook(|_| {}); - - // render the children directly - todo!() - // cx.render(LazyNodes::new(move |f: NodeFactory| { - // f.fragment_from_iter(cx.children()) - // })) -} - -pub struct WindowHandle {} - -/// Get a handle to the current window from inside a component -pub fn use_current_window(cx: Scope) -> Option { - todo!() -} - -#[test] -fn syntax_works() { - use dioxus_core as dioxus; - use dioxus_core::prelude::*; - use dioxus_core_macro::*; - use dioxus_hooks::*; - use dioxus_html as dioxus_elements; - - static App: Component = |cx| { - cx.render(rsx! { - // left window - WebviewWindow { - onclose: move |evt| {} - onopen: move |evt| {} - onfocused: move |evt| {} - - div { - - } - } - }) - }; +/// use this function can get the `DesktopContext` context. +pub fn use_window(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| cx.consume_context::()) + .as_ref() + .unwrap() } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 760bf7662..44126dd63 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,5 +1,4 @@ -//! Convert a serialized event to an event Trigger -//! +//! Convert a serialized event to an event trigger use std::any::Any; use std::sync::Arc; @@ -7,27 +6,49 @@ use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; use dioxus_html::on::*; +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct IpcMessage { + method: String, + params: serde_json::Value, +} + +impl IpcMessage { + pub(crate) fn method(&self) -> &str { + self.method.as_str() + } + + pub(crate) fn params(self) -> serde_json::Value { + self.params + } +} + +pub(crate) fn parse_ipc_message(payload: &str) -> Option { + match serde_json::from_str(payload) { + Ok(message) => Some(message), + Err(e) => { + log::error!("could not parse IPC message, error: {}", e); + None + } + } +} + #[derive(serde::Serialize, serde::Deserialize)] struct ImEvent { event: String, mounted_dom_id: u64, - // scope: u64, contents: serde_json::Value, } pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { - let ims: Vec = serde_json::from_value(val).unwrap(); - let ImEvent { event, mounted_dom_id, contents, - } = ims.into_iter().next().unwrap(); + } = serde_json::from_value(val).unwrap(); - // let scope_id = ScopeId(scope as usize); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_typ(&event); + let name = event_name_from_type(&event); let event = make_synthetic_event(&event, contents); UserEvent { @@ -105,7 +126,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc &'static str { +fn event_name_from_type(typ: &str) -> &'static str { match typ { "copy" => "copy", "cut" => "cut", diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 88fcba12a..292a1ae6a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,22 +1,15 @@ - - - - - - - - - -

-
- - - - + }); + + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 93c175cc6..e84baa034 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,78 +1,30 @@ -//! Dioxus Desktop Renderer -//! -//! Render the Dioxus VirtualDom using the platform's native WebView implementation. -//! -//! # Desktop -//! -//! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. -//! -//! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. -//! -//! -//! ## Getting Set up -//! -//! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: -//! -//! ```shell -//! $ cargo new --bin demo -//! $ cd app -//! ``` -//! -//! Add Dioxus with the `desktop` feature: -//! -//! ```shell -//! $ cargo add dioxus --features desktop -//! ``` -//! -//! Edit your `main.rs`: -//! -//! ```rust -//! // main.rs -//! use dioxus::prelude::*; -//! -//! fn main() { -//! dioxus::desktop::launch(app); -//! } -//! -//! fn app(cx: Scope) -> Element { -//! cx.render(rsx!{ -//! div { -//! "hello world!" -//! } -//! }) -//! } -//! ``` -//! -//! -//! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). -//! -//! ## Future Steps -//! -//! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! +#![doc = include_str!("readme.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub mod cfg; +mod controller; +pub mod desktop_context; pub mod escape; pub mod events; +mod protocol; +mod user_window_events; use cfg::DesktopConfig; +use controller::DesktopController; +pub use desktop_context::use_window; use dioxus_core::*; -use std::{ - collections::{HashMap, VecDeque}, - sync::atomic::AtomicBool, - sync::{Arc, RwLock}, -}; +use events::parse_ipc_message; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowId}, + window::Window, }; pub use wry; pub use wry::application as tao; -use wry::{ - application::event_loop::EventLoopProxy, - webview::RpcRequest, - webview::{WebView, WebViewBuilder}, -}; +use wry::webview::WebViewBuilder; + +use crate::events::trigger_from_serialized; /// Launch the WebView and run the event loop. /// @@ -149,7 +101,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::default(); + let mut cfg = DesktopConfig::default().with_default_icon(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); @@ -174,24 +126,26 @@ pub fn launch_with_props( let mut webview = WebViewBuilder::new(window) .unwrap() + .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_rpc_handler(move |_window: &Window, req: RpcRequest| { - match req.method.as_str() { - "user_event" => { - let event = events::trigger_from_serialized(req.params.unwrap()); - log::trace!("User event: {:?}", event); - sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::Update); - } - "browser_open" => { - let data = req.params.unwrap(); - log::trace!("Open browser: {:?}", data); - if let Some(arr) = data.as_array() { - if let Some(temp) = arr[0].as_object() { + .with_ipc_handler(move |_window: &Window, payload: String| { + parse_ipc_message(&payload) + .map(|message| match message.method() { + "user_event" => { + let event = trigger_from_serialized(message.params()); + log::trace!("User event: {:?}", event); + sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy + .send_event(user_window_events::UserWindowEvent::Update); + } + "browser_open" => { + let data = message.params(); + log::trace!("Open browser: {:?}", data); + if let Some(temp) = data.as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { @@ -200,30 +154,13 @@ pub fn launch_with_props( } } } - } - _ => {} - } - None - }) - .with_custom_protocol(String::from("dioxus"), move |request| { - // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" - // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().replace("dioxus://", ""); - - if path.trim_end_matches('/') == "index.html" { - wry::http::ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) - } else if path.trim_end_matches('/') == "index.html/index.js" { - wry::http::ResponseBuilder::new() - .mimetype("text/javascript") - .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) - } else { - wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(format!("Not found: {}", path).as_bytes().to_vec()) - } + _ => (), + }) + .unwrap_or_else(|| { + log::warn!("invalid IPC message received"); + }); }) + .with_custom_protocol(String::from("dioxus"), protocol::desktop_handler) .with_file_drop_handler(move |window, evet| { file_handler .as_ref() @@ -231,10 +168,32 @@ pub fn launch_with_props( .unwrap_or_default() }); - for (name, handler) in cfg.protocos.drain(..) { + for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) } + if cfg!(debug_assertions) { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_dev_tool(true); + } else { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script( + r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + alert("You've tried to open context menu"); + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + alert("You've tried to open context menu"); + window.event.returnValue = false; + }); + } + "#, + ) + } + desktop.webviews.insert(window_id, webview.build().unwrap()); } @@ -253,11 +212,8 @@ pub fn launch_with_props( _ => {} }, - Event::UserEvent(_evt) => { - // - match _evt { - UserWindowEvent::Update => desktop.try_load_ready_webviews(), - } + Event::UserEvent(user_event) => { + user_window_events::handler(user_event, &mut desktop, control_flow) } Event::MainEventsCleared => {} Event::Resumed => {} @@ -268,98 +224,3 @@ pub fn launch_with_props( } }) } - -pub enum UserWindowEvent { - Update, -} - -pub struct DesktopController { - pub proxy: EventLoopProxy, - pub webviews: HashMap, - pub sender: futures_channel::mpsc::UnboundedSender, - pub pending_edits: Arc>>, - pub quit_app_on_close: bool, - pub is_ready: Arc, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub fn new_on_tokio( - root: Component

, - props: P, - evt: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(RwLock::new(VecDeque::new())); - let pending_edits = edit_queue.clone(); - - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); - let proxy = evt.clone(); - - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = - VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); - - let edits = dom.rebuild(); - - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edits.edits).unwrap()); - - loop { - dom.wait_for_work().await; - let mut muts = dom.work_with_deadline(|| false); - - while let Some(edit) = muts.pop() { - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edit.edits).unwrap()); - } - - let _ = evt.send_event(UserWindowEvent::Update); - } - }) - }); - - Self { - pending_edits, - sender: return_sender, - proxy, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - } - } - - pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - pub fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.write().unwrap(); - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - while let Some(edit) = queue.pop_back() { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } else { - println!("waiting for ready"); - } - } -} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs new file mode 100644 index 000000000..c8f007c1d --- /dev/null +++ b/packages/desktop/src/protocol.rs @@ -0,0 +1,47 @@ +use std::path::Path; +use wry::{ + http::{status::StatusCode, Request, Response, ResponseBuilder}, + Result, +}; + +pub(super) fn desktop_handler(request: &Request) -> Result { + // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". + // For now, we only serve two pieces of content which get included as bytes into the final binary. + let path = request.uri().replace("dioxus://", ""); + + // all assets should be called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { + ResponseBuilder::new() + .mimetype("text/html") + .body(include_bytes!("./index.html").to_vec()) + } else if trimmed == "index.js" { + ResponseBuilder::new() + .mimetype("text/javascript") + .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) + } else { + let path_buf = Path::new(trimmed).canonicalize()?; + let cur_path = Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return ResponseBuilder::new() + .status(StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return ResponseBuilder::new() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = std::fs::read(path_buf)?; + let meta = format!("{}", mime); + + ResponseBuilder::new().mimetype(&meta).body(data) + } +} diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md new file mode 100644 index 000000000..dc034fe67 --- /dev/null +++ b/packages/desktop/src/readme.md @@ -0,0 +1,51 @@ +Dioxus Desktop Renderer + +Render the Dioxus VirtualDom using the platform's native WebView implementation. + +# Desktop + +One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. + +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. + + +## Getting Set up + +Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: + +```shell +$ cargo new --bin demo +$ cd app +``` + +Add Dioxus with the `desktop` feature: + +```shell +$ cargo add dioxus --features desktop +``` + +Edit your `main.rs`: + +```rust +// main.rs +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx!{ + div { + "hello world!" + } + }) +} +``` + + +To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). + +## Future Steps + +Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! \ No newline at end of file diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs new file mode 100644 index 000000000..bde215d48 --- /dev/null +++ b/packages/desktop/src/user_window_events.rs @@ -0,0 +1,73 @@ +use wry::application::event_loop::ControlFlow; +use wry::application::window::Fullscreen as WryFullscreen; + +use crate::controller::DesktopController; + +#[derive(Debug)] +pub(crate) enum UserWindowEvent { + Update, + + CloseWindow, + DragWindow, + FocusWindow, + + Visible(bool), + Minimize(bool), + Maximize(bool), + MaximizeToggle, + Resizable(bool), + AlwaysOnTop(bool), + Fullscreen(bool), + + CursorVisible(bool), + CursorGrab(bool), + + SetTitle(String), + SetDecorations(bool), + + DevTool, +} + +use UserWindowEvent::*; + +pub(super) fn handler( + user_event: UserWindowEvent, + desktop: &mut DesktopController, + control_flow: &mut ControlFlow, +) { + // currently dioxus-desktop supports a single window only, + // so we can grab the only webview from the map; + let webview = desktop.webviews.values().next().unwrap(); + let window = webview.window(); + + match user_event { + Update => desktop.try_load_ready_webviews(), + CloseWindow => *control_flow = ControlFlow::Exit, + DragWindow => { + // if the drag_window has any errors, we don't do anything + window.fullscreen().is_none().then(|| window.drag_window()); + } + Visible(state) => window.set_visible(state), + Minimize(state) => window.set_minimized(state), + Maximize(state) => window.set_maximized(state), + MaximizeToggle => window.set_maximized(!window.is_maximized()), + Fullscreen(state) => { + if let Some(handle) = window.current_monitor() { + window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); + } + } + FocusWindow => window.set_focus(), + Resizable(state) => window.set_resizable(state), + AlwaysOnTop(state) => window.set_always_on_top(state), + + CursorVisible(state) => window.set_cursor_visible(state), + CursorGrab(state) => { + let _ = window.set_cursor_grab(state); + } + + SetTitle(content) => window.set_title(&content), + SetDecorations(state) => window.set_decorations(state), + + DevTool => webview.devtool(), + } +} diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml new file mode 100644 index 000000000..48fe377c7 --- /dev/null +++ b/packages/fermi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fermi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core" } +im-rc = { version = "15.0.0", features = ["serde"] } +log = "0.4.14" + +[dev-dependencies] +closure = "0.3.0" diff --git a/packages/fermi/README.md b/packages/fermi/README.md new file mode 100644 index 000000000..0923aa6f6 --- /dev/null +++ b/packages/fermi/README.md @@ -0,0 +1,92 @@ + +

+

Fermi ⚛

+

+ Atom-based global state management solution for Dioxus +

+
+ + +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + +
+ +----- + +Fermi is a global state management solution for Dioxus that's as easy as `use_state`. + +Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`: + +```rust +static NAME: Atom<&str> = |_| "Dioxus"; +``` + +From anywhere in our app, we can read our the value of our atom: + +```rust +fn NameCard(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + cx.render(rsx!{ h1 { "Hello, {name}"} }) +} +``` + +We can also set the value of our atom, also from anywhere in our app: + +```rust +fn NameCard(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + cx.render(rsx!{ + button { + onclick: move |_| set_name("Fermi"), + "Set name to fermi" + } + }) +} +``` + +It's that simple! + +## Installation +Fermi is currently under construction, so you have to use the `master` branch to get started. + +```rust +[depdencies] +fermi = { git = "https://github.com/dioxuslabs/fermi" } +``` + + +## Running examples + +The examples here use Dioxus Desktop to showcase their functionality. To run an example, use +``` +$ cargo run --example EXAMPLE +``` + +## Features + +Broadly our feature set to required to be released includes: +- [x] Support for Atoms +- [x] Support for AtomRef (for values that aren't clone) +- [ ] Support for Atom Families +- [ ] Support for memoized Selectors +- [ ] Support for memoized SelectorFamilies +- [ ] Support for UseFermiCallback for access to fermi from async diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs new file mode 100644 index 000000000..2b99174b5 --- /dev/null +++ b/packages/fermi/src/atoms/atom.rs @@ -0,0 +1,28 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; + +pub type Atom = fn(AtomBuilder) -> T; +pub struct AtomBuilder; + +impl Readable for Atom { + fn read(&self, _root: AtomRoot) -> Option { + todo!() + } + fn init(&self) -> V { + (*self)(AtomBuilder) + } + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable for Atom { + fn write(&self, _root: AtomRoot, _value: V) { + todo!() + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: Atom<&str> = |_| "hello"; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs new file mode 100644 index 000000000..60fc22bcb --- /dev/null +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; +use im_rc::HashMap as ImMap; + +pub struct AtomFamilyBuilder; +pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; + +impl Readable> for AtomFamily { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> ImMap { + (*self)(AtomFamilyBuilder) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable> for AtomFamily { + fn write(&self, _root: AtomRoot, _value: ImMap) { + todo!() + } +} diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs new file mode 100644 index 000000000..59e412031 --- /dev/null +++ b/packages/fermi/src/atoms/atomref.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable}; +use std::cell::RefCell; + +pub struct AtomRefBuilder; +pub type AtomRef = fn(AtomRefBuilder) -> T; + +impl Readable> for AtomRef { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> RefCell { + RefCell::new((*self)(AtomRefBuilder)) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: AtomRef> = |_| vec![]; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/fermi/src/atoms/selector.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/fermi/src/atoms/selectorfamily.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs new file mode 100644 index 000000000..a7ca9155f --- /dev/null +++ b/packages/fermi/src/callback.rs @@ -0,0 +1,53 @@ +#![allow(clippy::all, unused)] + +use std::rc::Rc; + +use dioxus_core::prelude::*; + +use crate::{AtomRoot, Readable, Writable}; + +#[derive(Clone)] +pub struct CallbackApi { + root: Rc, +} + +impl CallbackApi { + // get the current value of the atom + pub fn get(&self, atom: impl Readable) -> &V { + todo!() + } + + // get the current value of the atom in its RC container + pub fn get_rc(&self, atom: impl Readable) -> &Rc { + todo!() + } + + // set the current value of the atom + pub fn set(&self, atom: impl Writable, value: V) { + todo!() + } +} + +pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi { + todo!() +} + +macro_rules! use_callback { + (&$cx:ident, [$($cap:ident),*], move || $body:expr) => { + move || { + $( + #[allow(unused_mut)] + let mut $cap = $cap.to_owned(); + )* + $cx.spawn($body); + } + }; +} + +#[macro_export] +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs new file mode 100644 index 000000000..da9ad9367 --- /dev/null +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -0,0 +1,62 @@ +use crate::{use_atom_root, AtomId, AtomRef, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +/// +/// +/// +/// +/// +/// +/// +/// +pub fn use_atom_ref(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { + let root = use_atom_root(cx); + + cx.use_hook(|_| { + root.initialize(atom); + UseAtomRef { + ptr: atom.unique_id(), + root: root.clone(), + scope_id: cx.scope_id(), + value: root.register(atom, cx.scope_id()), + } + }) +} + +pub struct UseAtomRef { + ptr: AtomId, + value: Rc>, + root: Rc, + scope_id: ScopeId, +} + +impl UseAtomRef { + pub fn read(&self) -> Ref { + self.value.borrow() + } + + pub fn write(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn write_silent(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn set(&self, new: T) { + self.root.force_update(self.ptr); + self.root.set(self.ptr, new); + } +} + +impl Drop for UseAtomRef { + fn drop(&mut self) { + self.root.unsubscribe(self.ptr, self.scope_id) + } +} diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs new file mode 100644 index 000000000..d2945025b --- /dev/null +++ b/packages/fermi/src/hooks/atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Returns the atom root, initiaizing it at the root of the app if it does not exist. +pub fn use_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(root) => root, + None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs new file mode 100644 index 000000000..edb8aafb8 --- /dev/null +++ b/packages/fermi/src/hooks/init_atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Initializes the atom root and retuns it; +pub fn use_init_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(ctx) => ctx, + None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/read.rs b/packages/fermi/src/hooks/read.rs new file mode 100644 index 000000000..b231c74d3 --- /dev/null +++ b/packages/fermi/src/hooks/read.rs @@ -0,0 +1,36 @@ +use crate::{use_atom_root, AtomId, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::rc::Rc; + +pub fn use_read<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a V { + use_read_rc(cx, f).as_ref() +} + +pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a Rc { + let root = use_atom_root(cx); + + struct UseReadInner { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, + } + + impl Drop for UseReadInner { + fn drop(&mut self) { + self.root.unsubscribe(self.id, self.scope_id) + } + } + + let inner = cx.use_hook(|_| UseReadInner { + value: None, + root: root.clone(), + scope_id: cx.scope_id(), + id: f.unique_id(), + }); + + let value = inner.root.register(f, cx.scope_id()); + + inner.value = Some(value); + inner.value.as_ref().unwrap() +} diff --git a/packages/fermi/src/hooks/set.rs b/packages/fermi/src/hooks/set.rs new file mode 100644 index 000000000..6d77fbf8c --- /dev/null +++ b/packages/fermi/src/hooks/set.rs @@ -0,0 +1,13 @@ +use crate::{use_atom_root, Writable}; +use dioxus_core::ScopeState; +use std::rc::Rc; + +pub fn use_set<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable) -> &'a Rc { + let root = use_atom_root(cx); + cx.use_hook(|_| { + let id = f.unique_id(); + let root = root.clone(); + root.initialize(f); + Rc::new(move |new| root.set(id, new)) as Rc + }) +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs new file mode 100644 index 000000000..8ac4f1ccf --- /dev/null +++ b/packages/fermi/src/lib.rs @@ -0,0 +1,58 @@ +#![doc = include_str!("../README.md")] + +pub mod prelude { + pub use crate::*; +} + +mod callback; +mod root; + +pub use atoms::*; +pub use callback::*; +pub use hooks::*; +pub use root::*; + +mod atoms { + mod atom; + mod atomfamily; + mod atomref; + mod selector; + mod selectorfamily; + + pub use atom::*; + pub use atomfamily::*; + pub use atomref::*; + pub use selector::*; + pub use selectorfamily::*; +} + +pub mod hooks { + mod atom_ref; + mod atom_root; + mod init_atom_root; + mod read; + mod set; + pub use atom_ref::*; + pub use atom_root::*; + pub use init_atom_root::*; + pub use read::*; + pub use set::*; +} + +/// All Atoms are `Readable` - they support reading their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// It is not very useful for your own code, but could be used to build new Atom primitives. +pub trait Readable { + fn read(&self, root: AtomRoot) -> Option; + fn init(&self) -> V; + fn unique_id(&self) -> AtomId; +} + +/// All Atoms are `Writable` - they support writing their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors +pub trait Writable: Readable { + fn write(&self, root: AtomRoot, value: V); +} diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs new file mode 100644 index 000000000..5c07083be --- /dev/null +++ b/packages/fermi/src/root.rs @@ -0,0 +1,103 @@ +use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core::ScopeId; +use im_rc::HashSet; + +use crate::Readable; + +pub type AtomId = *const (); + +pub struct AtomRoot { + pub atoms: RefCell>, + pub update_any: Rc, +} + +pub struct Slot { + pub value: Rc, + pub subscribers: HashSet, +} + +impl AtomRoot { + pub fn new(update_any: Rc) -> Self { + Self { + update_any, + atoms: RefCell::new(HashMap::new()), + } + } + + pub fn initialize(&self, f: impl Readable) { + let id = f.unique_id(); + if self.atoms.borrow().get(&id).is_none() { + self.atoms.borrow_mut().insert( + id, + Slot { + value: Rc::new(f.init()), + subscribers: HashSet::new(), + }, + ); + } + } + + pub fn register(&self, f: impl Readable, scope: ScopeId) -> Rc { + log::trace!("registering atom {:?}", f.unique_id()); + + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.subscribers.insert(scope); + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + let mut subscribers = HashSet::new(); + subscribers.insert(scope); + + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers, + }, + ); + value + } + } + + pub fn set(&self, ptr: AtomId, value: V) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.value = Rc::new(value); + log::trace!("found item with subscribers {:?}", slot.subscribers); + + for scope in &slot.subscribers { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } else { + log::trace!("no atoms found for {:?}", ptr); + } + } + + pub fn unsubscribe(&self, ptr: AtomId, scope: ScopeId) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.subscribers.remove(&scope); + } + } + + // force update of all subscribers + pub fn force_update(&self, ptr: AtomId) { + if let Some(slot) = self.atoms.borrow_mut().get(&ptr) { + for scope in slot.subscribers.iter() { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } + } + + pub fn read(&self, _f: impl Readable) -> &V { + todo!() + } +} diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 07e15ed64..253768068 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -5,6 +5,39 @@ 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 + +### Bug Fixes + + - use_state + - exampels + +### Commit Statistics + + + + - 9 commits contributed to the release over the course of 21 calendar days. + - 3 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - Merge pull request #158 from DioxusLabs/jk/router-onchange ([`08988e1`](https://github.comgit//DioxusLabs/dioxus/commit/08988e1bfec35eadb30e50b5d677dbb36af91b9b)) + - use_state ([`c092bd4`](https://github.comgit//DioxusLabs/dioxus/commit/c092bd43edf1891c427722bc9ca04e11c9359069)) + - exampels ([`a8952a9`](https://github.comgit//DioxusLabs/dioxus/commit/a8952a9ee8d8831fa911cef4e7035293c3a9f88b)) + - Merge branch 'master' into jk/update-hooks ([`5c4bd08`](https://github.comgit//DioxusLabs/dioxus/commit/5c4bd0881bc10572440d9272ceb8ca774431c406)) + - modify usestate to be borrowed ([`58839f4`](https://github.comgit//DioxusLabs/dioxus/commit/58839f47bae3c49cadd4b41da9a5debebb1def99)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.6 (2022-01-08) ### Documentation @@ -52,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 93 commits contributed to the release over the course of 358 calendar days. + - 94 commits contributed to the release over the course of 358 calendar days. - 82 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -63,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 23af30627..12b671d14 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-hooks" -version = "0.1.6" +version = "0.1.7" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for a remote webview instance" @@ -12,8 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version = "^0.1.7" } +dioxus-core = { path = "../../packages/core", version = "^0.1.9" } [dev-dependencies] futures-util = { version = "0.3", default-features = false } +dioxus-core = { path = "../../packages/core", version = "^0.1.9" } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 139e05245..cb4afd297 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,45 +16,25 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -// #[macro_export] -// macro_rules! to_owned { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.to_owned(); -// )*} -// } - -// /// Calls `for_async` on the series of paramters. -// /// -// /// If the type is Clone, then it will be cloned. However, if the type is not `clone` -// /// then it must have a `for_async` method for Rust to lower down into. -// /// -// /// See: how use_state implements `for_async` but *not* through the trait. -// #[macro_export] -// macro_rules! for_async { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.for_async(); -// )*} -// } - -// /// This is a marker trait that uses decoherence. -// /// -// /// It is *not* meant for hooks to actually implement, but rather defer to their -// /// underlying implementation if they *don't* implement the trait. -// /// -// /// -// pub trait AsyncHook { -// type Output; -// fn for_async(self) -> Self::Output; -// } - -// impl AsyncHook for T -// where -// T: ToOwned, -// { -// type Output = T; -// fn for_async(self) -> Self::Output { -// self -// } -// } +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ``` +/// let (data) = use_ref(&cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 9b59601b5..a05e99d7d 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -33,7 +33,7 @@ impl ProvidedStateInner { /// This hook provides some relatively light ergonomics around shared state. /// /// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type -/// ergonimics in a pinch, with zero cost. +/// ergonomics in a pinch, with zero cost. /// /// # Example /// diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 3ddf86e8c..c2537e079 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -1,57 +1,227 @@ +use dioxus_core::ScopeState; use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, }; -use dioxus_core::ScopeState; - -pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef { +/// `use_ref` is a key foundational hook for storing state in Dioxus. +/// +/// It is different that `use_state` in that the value stored is not "immutable". +/// Instead, UseRef is designed to store larger values that will be mutated at will. +/// +/// ## Writing Values +/// +/// Generally, `use_ref` is just a wrapper around a RefCell that tracks mutable +/// writes through the `write` method. Whenever `write` is called, the component +/// that initialized the hook will be marked as "dirty". +/// +/// ```rust +/// let val = use_ref(|| HashMap::::new()); +/// +/// // using `write` will give us a `RefMut` to the inner value, which we can call methods on +/// // This marks the component as "dirty" +/// val.write().insert(1, "hello".to_string()); +/// ``` +/// +/// You can avoid this defualt behavior with `write_silent` +/// +/// ``` +/// // with `write_silent`, the component will not be re-rendered +/// val.write_silent().insert(2, "goodbye".to_string()); +/// ``` +/// +/// ## Reading Values +/// +/// To read values out of the refcell, you can use the `read` method which will retrun a `Ref`. +/// +/// ```rust +/// let map: Ref<_> = val.read(); +/// +/// let item = map.get(&1); +/// ``` +/// +/// To get an &T out of the RefCell, you need to "reborrow" through the Ref: +/// +/// ```rust +/// let read = val.read(); +/// let map = &*read; +/// ``` +/// +/// ## Collections and iteration +/// +/// A common usecase for `use_ref` is to store a large amount of data in a component. +/// Typically this will be a collection like a HashMap or a Vec. To create new +/// elements from the collection, we can use `read()` directly in our rsx!. +/// +/// ```rust +/// rsx!{ +/// val.read().iter().map(|(k, v)| { +/// rsx!{ key: "{k}", value: "{v}" } +/// }) +/// } +/// ``` +/// +/// If you are generating elements outside of `rsx!` then you might need to call +/// "render" inside the iterator. For some cases you might need to collect into +/// a temporary Vec. +/// +/// ```rust +/// let items = val.read().iter().map(|(k, v)| { +/// cx.render(rsx!{ key: "{k}", value: "{v}" }) +/// }); +/// +/// // collect into a Vec +/// +/// let items: Vec = items.collect(); +/// ``` +/// +/// ## Use in Async +/// +/// To access values from a `UseRef` in an async context, you need to detach it +/// from the current scope's lifetime, making it a `'static` value. This is done +/// by simply calling `ToOnwed` or `Clone`. +/// +/// ```rust +/// let val = use_ref(|| HashMap::::new()); +/// +/// cx.spawn({ +/// let val = val.clone(); +/// async move { +/// some_work().await; +/// val.write().insert(1, "hello".to_string()); +/// } +/// }) +/// ``` +/// +/// If you're working with lots of values like UseState and UseRef, you can use the +/// `clone!` macro to make it easier to write the above code. +/// +/// ```rust +/// let val1 = use_ref(|| HashMap::::new()); +/// let val2 = use_ref(|| HashMap::::new()); +/// let val3 = use_ref(|| HashMap::::new()); +/// +/// cx.spawn({ +/// clone![val1, val2, val3]; +/// async move { +/// some_work().await; +/// val.write().insert(1, "hello".to_string()); +/// } +/// }) +/// ``` +pub fn use_ref<'a, T: 'static>( + cx: &'a ScopeState, + initialize_refcell: impl FnOnce() -> T, +) -> &'a UseRef { cx.use_hook(|_| UseRef { - update_callback: cx.schedule_update(), - value: Rc::new(RefCell::new(f())), + update: cx.schedule_update(), + value: Rc::new(RefCell::new(initialize_refcell())), }) } +/// A type created by the [`use_ref`] hook. See its documentation for more details. pub struct UseRef { - update_callback: Rc, + update: Rc, value: Rc>, } -impl UseRef { - pub fn read(&self) -> Ref<'_, T> { - self.value.borrow() - } - - pub fn set(&self, new: T) { - *self.value.borrow_mut() = new; - self.needs_update(); - } - - pub fn read_write(&self) -> (Ref<'_, T>, &Self) { - (self.read(), self) - } - - /// Calling "write" will force the component to re-render - pub fn write(&self) -> RefMut<'_, T> { - self.needs_update(); - self.value.borrow_mut() - } - - /// Allows the ability to write the value without forcing a re-render - pub fn write_silent(&self) -> RefMut<'_, T> { - self.value.borrow_mut() - } - - pub fn needs_update(&self) { - (self.update_callback)(); - } -} - impl Clone for UseRef { fn clone(&self) -> Self { Self { - update_callback: self.update_callback.clone(), + update: self.update.clone(), value: self.value.clone(), } } } + +impl UseRef { + /// Read the value in the RefCell into a `Ref`. If this method is called + /// while other values are still being `read` or `write`, then your app will crash. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + /// during the read calls. + pub fn read(&self) -> Ref<'_, T> { + self.value.borrow() + } + + /// Set the curernt value to `new_value`. This will mark the component as "dirty" + /// + /// This change will propogate immediately, so any other contexts that are + /// using this RefCell will also be affected. If called during an async context, + /// the component will not be re-rendered until the next `.await` call. + pub fn set(&self, new: T) { + *self.value.borrow_mut() = new; + self.needs_update(); + } + + /// Mutably unlock the value in the RefCell. This will mark the component as "dirty" + /// + /// Uses to `write` should be as short as possible. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + /// during the read and write calls. + pub fn write(&self) -> RefMut<'_, T> { + self.needs_update(); + self.value.borrow_mut() + } + + /// Mutably unlock the value in the RefCell. This will not mark the component as dirty. + /// This is useful if you want to do some work without causing the component to re-render. + /// + /// Uses to `write` should be as short as possible. + /// + /// Be very careful when working with this method. If you can, consider using + /// the `with` and `with_mut` methods instead, choosing to render Elements + pub fn write_silent(&self) -> RefMut<'_, T> { + self.value.borrow_mut() + } + + /// Take a reference to the inner value termporarily and produce a new value + /// + /// Note: You can always "reborrow" the value through the RefCell. + /// This method just does it for you automatically. + /// + /// ```rust + /// let val = use_ref(|| HashMap::::new()); + /// + /// + /// // use reborrowing + /// let inner = &*val.read(); + /// + /// // or, be safer and use `with` + /// val.with(|i| println!("{:?}", i)); + /// ``` + pub fn with(&self, immutable_callback: impl FnOnce(&T) -> O) -> O { + immutable_callback(&*self.read()) + } + + /// Take a reference to the inner value termporarily and produce a new value, + /// modifying the original in place. + /// + /// Note: You can always "reborrow" the value through the RefCell. + /// This method just does it for you automatically. + /// + /// ```rust + /// let val = use_ref(|| HashMap::::new()); + /// + /// + /// // use reborrowing + /// let inner = &mut *val.write(); + /// + /// // or, be safer and use `with` + /// val.with_mut(|i| i.insert(1, "hi")); + /// ``` + pub fn with_mut(&self, mutable_callback: impl FnOnce(&mut T) -> O) -> O { + mutable_callback(&mut *self.write()) + } + + /// Call the inner callback to mark the originator component as dirty. + /// + /// This will cause the component to be re-rendered after the current scope + /// has ended or the current async task has been yielded through await. + pub fn needs_update(&self) { + (self.update)(); + } +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs new file mode 100644 index 000000000..7e8892be9 --- /dev/null +++ b/packages/hooks/src/usestate.rs @@ -0,0 +1,346 @@ +#![warn(clippy::pedantic)] + +use dioxus_core::prelude::*; +use std::{ + cell::{RefCell, RefMut}, + fmt::{Debug, Display}, + rc::Rc, +}; + +/// Store state between component renders. +/// +/// ## Dioxus equivalent of useState, designed for Rust +/// +/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// +/// ```ignore +/// const Example: Component = |cx| { +/// let (count, set_count) = use_state(&cx, || 0); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Count: {count}" } +/// button { onclick: move |_| set_count(a - 1), "Increment" } +/// button { onclick: move |_| set_count(a + 1), "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_state<'a, T: 'static>( + cx: &'a ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> (&'a T, &'a UseState) { + let hook = cx.use_hook(move |_| { + let current_val = Rc::new(initial_state_fn()); + let update_callback = cx.schedule_update(); + let slot = Rc::new(RefCell::new(current_val.clone())); + let setter = Rc::new({ + crate::to_owned![update_callback, slot]; + move |new| { + { + let mut slot = slot.borrow_mut(); + + // if there's only one reference (weak or otherwise), we can just swap the values + // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value + if let Some(val) = Rc::get_mut(&mut slot) { + *val = new; + } else { + *slot = Rc::new(new); + } + } + update_callback(); + } + }); + + UseState { + current_val, + update_callback, + setter, + slot, + } + }); + + hook.current_val = hook.slot.borrow().clone(); + + (hook.current_val.as_ref(), hook) +} + +pub struct UseState { + pub(crate) current_val: Rc, + pub(crate) update_callback: Rc, + pub(crate) setter: Rc, + pub(crate) slot: Rc>>, +} + +impl UseState { + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.slot.borrow().clone() + } + + /// Get the `setter` function directly without the `UseState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`UseState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: set_val.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + self.setter.clone() + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// // to increment the value + /// set_value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let set_value = set_value.to_owned(); + /// async move { + /// set_value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + let new_val = { + let current = self.slot.borrow(); + f(current.as_ref()) + }; + (self.setter)(new_val); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// let as_rc = set_value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &Rc { + &self.current_val + } + + /// Mark the component that create this [`UseState`] as dirty, forcing it to re-render. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// // for the component to re-render + /// set_count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + (self.update_callback)(); + } +} + +impl UseState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut slot = self.slot.borrow_mut(); + let mut inner = slot.as_ref().to_owned(); + + apply(&mut inner); + + if let Some(new) = Rc::get_mut(&mut slot) { + *new = inner; + } else { + *slot = Rc::new(inner); + } + + self.needs_update(); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + let mut slot = self.slot.borrow_mut(); + + self.needs_update(); + + if Rc::strong_count(&*slot) > 0 { + *slot = Rc::new(slot.as_ref().to_owned()); + } + + RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } +} + +impl Clone for UseState { + fn clone(&self) -> Self { + UseState { + current_val: self.current_val.clone(), + update_callback: self.update_callback.clone(), + setter: self.setter.clone(), + slot: self.slot.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl PartialEq> for UseState { + fn eq(&self, other: &UseState) -> bool { + // some level of memoization for UseState + Rc::ptr_eq(&self.slot, &other.slot) + } +} + +impl Debug for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.current_val) + } +} + +impl<'a, T> std::ops::Deref for UseState { + type Target = Rc; + + fn deref(&self) -> &Self::Target { + &self.setter + } +} + +#[test] +fn api_makes_sense() { + #[allow(unused)] + fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 0); + + set_val(0); + set_val.modify(|v| v + 1); + let real_current = set_val.current(); + + match val { + 10 => { + set_val(20); + set_val.modify(|v| v + 1); + } + 20 => {} + _ => { + println!("{real_current}"); + } + } + + cx.spawn({ + crate::to_owned![set_val]; + async move { + set_val.modify(|f| f + 1); + } + }); + + cx.render(LazyNodes::new(|f| f.static_text("asd"))) + } +} diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs deleted file mode 100644 index 83acf781e..000000000 --- a/packages/hooks/src/usestate/handle.rs +++ /dev/null @@ -1,215 +0,0 @@ -use super::owned::UseStateOwned; -use std::{ - cell::{Ref, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; - -pub struct UseState<'a, T: 'static>(pub(crate) &'a UseStateOwned); - -impl Copy for UseState<'_, T> {} - -impl<'a, T: 'static> Clone for UseState<'a, T> { - fn clone(&self) -> Self { - UseState(self.0) - } -} - -impl Debug for UseState<'_, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0.current_val) - } -} - -impl<'a, T: 'static> UseState<'a, T> { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.0.update_scheuled.get() { - self.0.update_scheuled.set(true); - (self.0.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.0.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &'a T { - &self.0.current_val - } - - pub fn get_rc(&self) -> &'a Rc { - &self.0.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.0.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.0.wip.borrow_mut() - } - - pub fn classic(self) -> (&'a T, Rc) { - (&self.0.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.0.wip.clone(); - let callback = self.0.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn with(&self, f: impl FnOnce(&mut T)) { - let mut val = self.0.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseStateOwned { - let UseStateOwned { - current_val, - wip, - update_callback, - update_scheuled, - } = self.0; - - UseStateOwned { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl<'a, T: 'static + ToOwned> UseState<'a, T> { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(self) -> RefMut<'a, T> { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.0.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.0.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.0.current_val.as_ref().to_owned() - } -} - -impl<'a, T> std::ops::Deref for UseState<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.current_val) - } -} -impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, T> { - type Output = O; - - fn not(self) -> Self::Output { - !*self.get() - } -} - -/* - -Convenience methods for UseState. - -Note! - -This is not comprehensive. -This is *just* meant to make common operations easier. -*/ - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.0.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.0.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.0.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.0.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.0.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { - fn mul_assign(&mut self, rhs: T) { - self.set(self.0.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseState<'a, T> { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.0.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { - fn div_assign(&mut self, rhs: T) { - self.set(self.0.current_val.div(rhs)); - } -} diff --git a/packages/hooks/src/usestate/mod.rs b/packages/hooks/src/usestate/mod.rs deleted file mode 100644 index cf125c2a4..000000000 --- a/packages/hooks/src/usestate/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod handle; -mod owned; -pub use handle::*; -pub use owned::*; - -use dioxus_core::prelude::*; -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -/// Store state between component renders! -/// -/// ## Dioxus equivalent of useState, designed for Rust -/// -/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and -/// modify state between component renders. When the state is updated, the component will re-render. -/// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// -/// -/// Usage: -/// -/// ```ignore -/// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); -/// -/// cx.render(rsx! { -/// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter += 1, "Increment" } -/// button { onclick: move |_| counter -= 1, "Decrement" } -/// } -/// )) -/// } -/// ``` -pub fn use_state<'a, T: 'static>( - cx: &'a ScopeState, - initial_state_fn: impl FnOnce() -> T, -) -> UseState<'a, T> { - let hook = cx.use_hook(move |_| UseStateOwned { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), - }); - - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - UseState(hook) -} diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs deleted file mode 100644 index a1a6bdb73..000000000 --- a/packages/hooks/src/usestate/owned.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ - cell::{Cell, Ref, RefCell, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; -pub struct UseStateOwned { - // this will always be outdated - pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, - pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, -} - -impl UseStateOwned { - pub fn get(&self) -> Ref> { - self.wip.borrow() - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - (self.update_callback)(); - } - - pub fn modify(&self) -> RefMut> { - (self.update_callback)(); - self.wip.borrow_mut() - } -} - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl Debug for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.current_val) - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} - -impl<'a, T: Copy + Add> Add for UseStateOwned { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.current_val.add(rhs) - } -} - -impl<'a, T: Copy + Add> AddAssign for UseStateOwned { - fn add_assign(&mut self, rhs: T) { - self.set(self.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseStateOwned { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseStateOwned { - fn sub_assign(&mut self, rhs: T) { - self.set(self.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseStateOwned { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseStateOwned { - fn mul_assign(&mut self, rhs: T) { - self.set(self.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseStateOwned { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseStateOwned { - fn div_assign(&mut self, rhs: T) { - self.set(self.current_val.div(rhs)); - } -} diff --git a/packages/html/CHANGELOG.md b/packages/html/CHANGELOG.md index 9648c0c8f..48c7f622c 100644 --- a/packages/html/CHANGELOG.md +++ b/packages/html/CHANGELOG.md @@ -5,6 +5,36 @@ 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 + +### Bug Fixes + + - rustfmt + +### Commit Statistics + + + + - 7 commits contributed to the release over the course of 19 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - rustfmt ([`43e78d5`](https://github.comgit//DioxusLabs/dioxus/commit/43e78d56f72e23ada939f5688a6bd2fdd9b8292d)) + - Add gap and row_gap to style_trait_methods ([`5f4a724`](https://github.comgit//DioxusLabs/dioxus/commit/5f4a72446e63441628e18bb19fd9b2d9f65f9d52)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) +
+ ## v0.1.4 (2022-01-08) ### Documentation @@ -42,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 60 commits contributed to the release over the course of 184 calendar days. + - 61 commits contributed to the release over the course of 184 calendar days. - 52 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -53,8 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** - - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - enable prevent_default everywhere ([`9dff700`](https://github.comgit//DioxusLabs/dioxus/commit/9dff700c220dd9e0da2ee028900e82fd24f9d0dd)) + - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - component pass thru events ([`c439b0a`](https://github.comgit//DioxusLabs/dioxus/commit/c439b0ac7e09f70a04262b7c29938d8c52197b76)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - new versions of everything ([`4ea5c99`](https://github.comgit//DioxusLabs/dioxus/commit/4ea5c990d72b1645724ab0a88ffea2baf28e2835)) diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 3c0f07b9b..293d4c465 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-html" -version = "0.1.4" +version = "0.1.6" authors = ["Jonathan Kelley"] edition = "2018" description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" @@ -11,7 +11,7 @@ documentation = "https://docs.rs/dioxus" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} +dioxus-core = { path = "../core", version = "^0.1.9" } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1a2f36e57..c6b1cb7e0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1047,7 +1047,7 @@ impl input { /// - `text` /// - `time` /// - `url` - /// - `week` + /// - `week` pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("type", val, None, false) } @@ -1100,11 +1100,6 @@ impl label { cx.attr("for", val, None, false) } } -impl a { - pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr("dioxus-prevent-default", val, None, false) - } -} builder_constructors! { // SVG components diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index b9bcecd4f..8067e6314 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -3,6 +3,8 @@ use dioxus_core::exports::bumpalo; use dioxus_core::*; pub mod on { + use std::collections::HashMap; + use super::*; macro_rules! event_directory { ( $( @@ -484,6 +486,7 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 1368f87e3..151a1539b 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -49,6 +49,10 @@ macro_rules! aria_trait_methods { } pub trait GlobalAttributes { + /// Prevent the default action for this element. + /// + /// For more information, see the MDN docs: + /// fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("dioxus-prevent-default", val, None, false) } @@ -603,6 +607,11 @@ pub trait GlobalAttributes { /// Specifies the speed curve of the transition effect. transition_timing_function: "transition-timing-function", + /// The user-select CSS property controls whether the user can select text. + /// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes. + user_select: "user-select", + webkit_user_select: "-webkit-user-select", + /// Sets the vertical positioning of an element relative to the current text baseline. vertical_align: "vertical-align", @@ -685,6 +694,13 @@ pub trait GlobalAttributes { } pub trait SvgAttributes { + /// Prevent the default action for this element. + /// + /// For more information, see the MDN docs: + /// + fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } aria_trait_methods! { accent_height: "accent-height", accumulate: "accumulate", diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml new file mode 100644 index 000000000..1e0f4bb51 --- /dev/null +++ b/packages/interpreter/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "dioxus-interpreter-js" +version = "0.0.0" +edition = "2018" +authors = ["Jonathan Kelley"] +description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus" +keywords = ["dom", "ui", "gui", "react", "wasm"] + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wasm-bindgen = { version = "0.2.79", optional = true } +js-sys = { version = "0.3.56", optional = true } +web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } + + +[features] +default = [] +web = ["wasm-bindgen", "js-sys", "web-sys"] diff --git a/packages/jsinterpreter/README.md b/packages/interpreter/README.md similarity index 99% rename from packages/jsinterpreter/README.md rename to packages/interpreter/README.md index bab57ac16..767fe31c0 100644 --- a/packages/jsinterpreter/README.md +++ b/packages/interpreter/README.md @@ -4,5 +4,4 @@ After diffing old and new trees, the Dioxus VirtualDom produces patches that are In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. - For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/web/src/bindings.rs b/packages/interpreter/src/bindings.rs similarity index 79% rename from packages/web/src/bindings.rs rename to packages/interpreter/src/bindings.rs index 9b0c2ebfb..ecd74870d 100644 --- a/packages/web/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -1,8 +1,10 @@ +#![allow(clippy::unused_unit, non_upper_case_globals)] + use js_sys::Function; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] +#[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { pub type Interpreter; @@ -31,7 +33,7 @@ extern "C" { pub fn Remove(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: u64); #[wasm_bindgen(method)] pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); @@ -49,10 +51,16 @@ extern "C" { pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, root: u64, text: &str); + pub fn SetText(this: &Interpreter, root: u64, text: JsValue); #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + pub fn SetAttribute( + this: &Interpreter, + root: u64, + field: &str, + value: JsValue, + ns: Option<&str>, + ); #[wasm_bindgen(method)] pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); diff --git a/packages/jsinterpreter/interpreter.ts b/packages/interpreter/src/interpreter.js similarity index 68% rename from packages/jsinterpreter/interpreter.ts rename to packages/interpreter/src/interpreter.js index fdd8c671d..76da73752 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/interpreter/src/interpreter.js @@ -1,33 +1,12 @@ - - - - export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); + window.ipc.postMessage(serializeIpcMessage("initialize")); } } - -declare global { - interface Window { - interpreter: Interpreter; - rpc: { call: (method: string, args?: any) => void }; - } -} - - export class Interpreter { - root: Element; - stack: Element[]; - listeners: { [key: string]: number }; - handlers: { [key: string]: (evt: Event) => void }; - lastNodeWasText: boolean; - nodes: Element[]; - - - constructor(root: Element) { + constructor(root) { this.root = root; this.stack = [root]; this.listeners = {}; @@ -35,88 +14,70 @@ export class Interpreter { this.lastNodeWasText = false; this.nodes = [root]; } - top() { return this.stack[this.stack.length - 1]; } - pop() { return this.stack.pop(); } - - PushRoot(root: number) { + PushRoot(root) { const node = this.nodes[root]; this.stack.push(node); } - - AppendChildren(many: number) { + AppendChildren(many) { let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); } } - - ReplaceWith(root_id: number, m: number) { - let root = this.nodes[root_id] as Element; + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); } - - InsertAfter(root: number, n: number) { - let old = this.nodes[root] as Element; + InsertAfter(root, n) { + let old = this.nodes[root]; let new_nodes = this.stack.splice(this.stack.length - n); old.after(...new_nodes); } - - InsertBefore(root: number, n: number) { - let old = this.nodes[root] as Element; + InsertBefore(root, n) { + let old = this.nodes[root]; let new_nodes = this.stack.splice(this.stack.length - n); old.before(...new_nodes); } - - Remove(root: number) { - let node = this.nodes[root] as Element; + Remove(root) { + let node = this.nodes[root]; if (node !== undefined) { node.remove(); } } - - CreateTextNode(text: string, root: number) { + CreateTextNode(text, root) { // todo: make it so the types are okay - const node = document.createTextNode(text) as any as Element; + const node = document.createTextNode(text); this.nodes[root] = node; this.stack.push(node); } - - CreateElement(tag: string, root: number) { + CreateElement(tag, root) { const el = document.createElement(tag); // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; this.stack.push(el); } - - CreateElementNs(tag: string, root: number, ns: string) { + CreateElementNs(tag, root, ns) { let el = document.createElementNS(ns, tag); this.stack.push(el); this.nodes[root] = el; } - - CreatePlaceholder(root: number) { + CreatePlaceholder(root) { let el = document.createElement("pre"); el.hidden = true; this.stack.push(el); this.nodes[root] = el; } - - NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) { + NewEventListener(event_name, root, handler) { const element = this.nodes[root]; element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { this.listeners[event_name] = 0; this.handlers[event_name] = handler; @@ -125,55 +86,46 @@ export class Interpreter { this.listeners[event_name]++; } } - - RemoveEventListener(root: number, event_name: string) { + RemoveEventListener(root, event_name) { const element = this.nodes[root]; element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { this.root.removeEventListener(event_name, this.handlers[event_name]); delete this.listeners[event_name]; delete this.handlers[event_name]; } } - - - SetText(root: number, text: string) { + SetText(root, text) { this.nodes[root].textContent = text; } - - SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - - if (ns == "style") { - + if (ns === "style") { // @ts-ignore - (node as HTMLElement).style[name] = value; - + node.style[name] = value; } else if (ns != null || ns != undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { case "value": - if (value != (node as HTMLInputElement).value) { - (node as HTMLInputElement).value = value; + if (value !== node.value) { + node.value = value; } break; case "checked": - (node as HTMLInputElement).checked = value === "true"; + node.checked = value === "true"; break; case "selected": - (node as HTMLOptionElement).selected = value === "true"; + node.selected = value === "true"; break; case "dangerous_inner_html": node.innerHTML = value; break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { + if (value === "false" && bool_attrs.hasOwnProperty(name)) { node.removeAttribute(name); } else { node.setAttribute(name, value); @@ -181,33 +133,28 @@ export class Interpreter { } } } - RemoveAttribute(root: number, name: string) { - + RemoveAttribute(root, name) { const node = this.nodes[root]; - node.removeAttribute(name); if (name === "value") { - (node as HTMLInputElement).value = ""; - } - - if (name === "checked") { - (node as HTMLInputElement).checked = false; - } - - if (name === "selected") { - (node as HTMLOptionElement).selected = false; + node.value = ""; + } else if (name === "checked") { + node.checked = false; + } else if (name === "selected") { + node.selected = false; + } else if (name === "dangerous_inner_html") { + node.innerHTML = ""; + } else { + node.removeAttribute(name); } } - - handleEdits(edits: DomEdit[]) { + handleEdits(edits) { this.stack.push(this.root); - for (let edit of edits) { this.handleEdit(edit); } } - - handleEdit(edit: DomEdit) { + handleEdit(edit) { switch (edit.type) { case "PushRoot": this.PushRoot(edit.root); @@ -243,44 +190,71 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - - - // this handler is only provided on desktop implementations since this + // this handler is only provided on desktop implementations since this // method is not used by the web implementation - let handler = (event: Event) => { - let target = event.target as Element | null; - + let handler = (event) => { + let target = event.target; if (target != null) { let realId = target.getAttribute(`data-dioxus-id`); + let shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); + if (event.type === "click") { + // todo call prevent default if it's the right type of event + if (shouldPreventDefault !== `onclick`) { + if (target.tagName === "A") { + event.preventDefault(); + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); + } + } + } + + // also prevent buttons from submitting + if (target.tagName === "BUTTON") { + event.preventDefault(); + } + } // walk the tree to find the real element - while (realId == null && target.parentElement != null) { + while (realId == null) { + // we've reached the root we don't want to send an event + if (target.parentElement === null) { + return; + } + target = target.parentElement; realId = target.getAttribute(`data-dioxus-id`); } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + shouldPreventDefault = target.getAttribute( + `dioxus-prevent-default` + ); let contents = serialize_event(event); if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - - if (event.type == "submit") { + if (event.type === "submit") { event.preventDefault(); } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); + if (target.tagName === "FORM") { + for (let x = 0; x < target.elements.length; x++) { + let element = target.elements[x]; + let name = element.getAttribute("name"); + if (name != null) { + if (element.getAttribute("type") === "checkbox") { + // @ts-ignore + contents.values[name] = element.checked ? "true" : "false"; + } else { + // @ts-ignore + contents.values[name] = + element.value ?? element.textContent; } } } @@ -289,12 +263,13 @@ export class Interpreter { if (realId == null) { return; } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); + window.ipc.postMessage( + serializeIpcMessage("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }) + ); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -312,25 +287,21 @@ export class Interpreter { } } - - -function serialize_event(event: Event) { +export function serialize_event(event) { switch (event.type) { case "copy": case "cut": case "past": { return {}; } - case "compositionend": case "compositionstart": case "compositionupdate": { - let { data } = (event as CompositionEvent); + let { data } = event; return { data, }; } - case "keydown": case "keypress": case "keyup": { @@ -345,8 +316,7 @@ function serialize_event(event: Event) { location, repeat, which, - } = (event as KeyboardEvent); - + } = event; return { char_code: charCode, key: key, @@ -361,42 +331,37 @@ function serialize_event(event: Event) { locale: "locale", }; } - case "focus": case "blur": { return {}; } - case "change": { - let target = event.target as HTMLInputElement; + let target = event.target; let value; if (target.type === "checkbox" || target.type === "radio") { value = target.checked ? "true" : "false"; } else { value = target.value ?? target.textContent; } - return { value: value, + values: {}, }; } - case "input": case "invalid": case "reset": case "submit": { - let target = event.target as HTMLFormElement; + let target = event.target; let value = target.value ?? target.textContent; - - if (target.type == "checkbox") { + if (target.type === "checkbox") { value = target.checked ? "true" : "false"; } - return { value: value, + values: {}, }; } - case "click": case "contextmenu": case "doubleclick": @@ -428,8 +393,7 @@ function serialize_event(event: Event) { screenX, screenY, shiftKey, - } = event as MouseEvent; - + } = event; return { alt_key: altKey, button: button, @@ -445,7 +409,6 @@ function serialize_event(event: Event) { shift_key: shiftKey, }; } - case "pointerdown": case "pointermove": case "pointerup": @@ -479,7 +442,7 @@ function serialize_event(event: Event) { twist, pointerType, isPrimary, - } = event as PointerEvent; + } = event; return { alt_key: altKey, button: button, @@ -505,21 +468,14 @@ function serialize_event(event: Event) { is_primary: isPrimary, }; } - case "select": { return {}; } - case "touchcancel": case "touchend": case "touchmove": case "touchstart": { - const { - altKey, - ctrlKey, - metaKey, - shiftKey, - } = event as TouchEvent; + const { altKey, ctrlKey, metaKey, shiftKey } = event; return { // changed_touches: event.changedTouches, // target_touches: event.targetTouches, @@ -530,18 +486,11 @@ function serialize_event(event: Event) { shift_key: shiftKey, }; } - case "scroll": { return {}; } - case "wheel": { - const { - deltaX, - deltaY, - deltaZ, - deltaMode, - } = event as WheelEvent; + const { deltaX, deltaY, deltaZ, deltaMode } = event; return { delta_x: deltaX, delta_y: deltaY, @@ -549,35 +498,24 @@ function serialize_event(event: Event) { delta_mode: deltaMode, }; } - case "animationstart": case "animationend": case "animationiteration": { - const { - animationName, - elapsedTime, - pseudoElement, - } = event as AnimationEvent; + const { animationName, elapsedTime, pseudoElement } = event; return { animation_name: animationName, elapsed_time: elapsedTime, pseudo_element: pseudoElement, }; } - case "transitionend": { - const { - propertyName, - elapsedTime, - pseudoElement, - } = event as TransitionEvent; + const { propertyName, elapsedTime, pseudoElement } = event; return { property_name: propertyName, elapsed_time: elapsedTime, pseudo_element: pseudoElement, }; } - case "abort": case "canplay": case "canplaythrough": @@ -603,17 +541,17 @@ function serialize_event(event: Event) { case "waiting": { return {}; } - case "toggle": { return {}; } - default: { return {}; } } } - +function serializeIpcMessage(method, params = {}) { + return JSON.stringify({ method, params }); +} const bool_attrs = { allowfullscreen: true, allowpaymentrequest: true, @@ -642,39 +580,3 @@ const bool_attrs = { selected: true, truespeed: true, }; - - - -type PushRoot = { type: "PushRoot", root: number }; -type AppendChildren = { type: "AppendChildren", many: number }; -type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; -type InsertAfter = { type: "InsertAfter", root: number, n: number }; -type InsertBefore = { type: "InsertBefore", root: number, n: number }; -type Remove = { type: "Remove", root: number }; -type CreateTextNode = { type: "CreateTextNode", text: string, root: number }; -type CreateElement = { type: "CreateElement", tag: string, root: number }; -type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string }; -type CreatePlaceholder = { type: "CreatePlaceholder", root: number }; -type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number }; -type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number }; -type SetText = { type: "SetText", root: number, text: string }; -type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; -type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; - - -type DomEdit = - PushRoot | - AppendChildren | - ReplaceWith | - InsertAfter | - InsertBefore | - Remove | - CreateTextNode | - CreateElement | - CreateElementNs | - CreatePlaceholder | - NewEventListener | - RemoveEventListener | - SetText | - SetAttribute | - RemoveAttribute; diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs new file mode 100644 index 000000000..076a89197 --- /dev/null +++ b/packages/interpreter/src/lib.rs @@ -0,0 +1,7 @@ +pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); + +#[cfg(feature = "web")] +mod bindings; + +#[cfg(feature = "web")] +pub use bindings::Interpreter; diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js deleted file mode 100644 index ea50ec3d3..000000000 --- a/packages/jsinterpreter/interpreter.js +++ /dev/null @@ -1,499 +0,0 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -export class Interpreter { - root; - stack; - listeners; - handlers; - lastNodeWasText; - nodes; - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - ReplaceWith(root_id, m) { - let root = this.nodes[root_id]; - let els = this.stack.splice(this.stack.length - m); - root.replaceWith(...els); - } - InsertAfter(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - InsertBefore(root, n) { - let old = this.nodes[root]; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - Remove(root) { - let node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - // todo: make it so the types are okay - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreateElement(tag, root) { - const el = document.createElement(tag); - // el.setAttribute("data-dioxus-id", `${root}`); - this.nodes[root] = el; - this.stack.push(el); - } - CreateElementNs(tag, root, ns) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } - else { - this.listeners[event_name]++; - } - } - RemoveEventListener(root, event_name) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; - } - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - const node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } - else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, name) { - const node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - handleEdits(edits) { - this.stack.push(this.root); - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - // walk the tree to find the real element - while (realId == null && target.parentElement != null) { - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - let contents = serialize_event(event); - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - if (event.type == "submit") { - event.preventDefault(); - } - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = target.value ?? target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey, } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode, } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement, } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement, } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json deleted file mode 100644 index f63034044..000000000 --- a/packages/jsinterpreter/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ES2015", - "lib": [ - "es2015", - "es5", - "es6", - "dom" - ], - "strict": true, - } -} diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 284068810..4486eb279 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -5,6 +5,26 @@ 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 + + + + - 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 + + + +
view details + + * **Uncategorized** + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) +
+ ## v0.0.3 (2022-01-08) ### Documentation @@ -28,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 32 commits contributed to the release over the course of 151 calendar days. + - 33 commits contributed to the release over the course of 151 calendar days. - 21 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -39,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml index 373814260..cdf0a5c05 100644 --- a/packages/mobile/Cargo.toml +++ b/packages/mobile/Cargo.toml @@ -12,4 +12,9 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-desktop = { path = "../desktop", version ="^0.1.5"} +dioxus-desktop = { path = "../desktop", version = "^0.1.6" } + +[lib] +doctest = false +# tests suspended until package ready +test = false diff --git a/packages/mobile/Makefile.toml b/packages/mobile/Makefile.toml new file mode 100644 index 000000000..90f08a170 --- /dev/null +++ b/packages/mobile/Makefile.toml @@ -0,0 +1,7 @@ +[tasks.test] +command = "cargo" +args = [ + "test", + "--no-run", +] + diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index 7e257f854..86b157737 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -5,6 +5,54 @@ 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 + +### New Features + + - enable use_router + - connect an onchange listener + - flatten props attrs + +### Bug Fixes + + - attach router listener to subscriber list + - use_route should subscribe to changes to the route + +### Commit Statistics + + + + - 18 commits contributed to the release over the course of 14 calendar days. + - 7 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #169 from DioxusLabs/jk/router-userouter ([`3509602`](https://github.comgit//DioxusLabs/dioxus/commit/3509602c0bcd327a33bc8c95896775e24751da1a)) + - enable use_router ([`e24957f`](https://github.comgit//DioxusLabs/dioxus/commit/e24957fc191476184816fe5dee249f691170d4ae)) + - add docs to router UseRouteListener ([`79e0993`](https://github.comgit//DioxusLabs/dioxus/commit/79e09934aa685d03d6e0b323723bc1cd537d74d9)) + - Merge pull request #166 from DioxusLabs/jk/default-assets-desktop ([`ccbb955`](https://github.comgit//DioxusLabs/dioxus/commit/ccbb955b7b24bd4e1c5aa40e813a2872ae474a69)) + - rustfmt ([`9da46eb`](https://github.comgit//DioxusLabs/dioxus/commit/9da46eb7bc207997ca7779c58fcb2a9645dfa9d0)) + - Make log message in Link component trace level, not debug ([`72c6bb3`](https://github.comgit//DioxusLabs/dioxus/commit/72c6bb3d0b7253f084f7e3bcf55d458cb4adeedb)) + - attach router listener to subscriber list ([`5ee9d6c`](https://github.comgit//DioxusLabs/dioxus/commit/5ee9d6c4348a2f51adac827f715fb138918d1dc6)) + - connect an onchange listener ([`29ed7eb`](https://github.comgit//DioxusLabs/dioxus/commit/29ed7ebece26e9d53925af55f2f34a8fd8241405)) + - use_route should subscribe to changes to the route ([`a21e7d4`](https://github.comgit//DioxusLabs/dioxus/commit/a21e7d4dd168129da06f535f9dc4b1de724617cb)) + - Merge pull request #95 from DioxusLabs/jk/filedragindrop ([`ca0dd4a`](https://github.comgit//DioxusLabs/dioxus/commit/ca0dd4aa7192d483a195d420363f39d771f3e471)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - flatten props attrs ([`d237271`](https://github.comgit//DioxusLabs/dioxus/commit/d2372717bd01fcff50af0572360e3f763d4c869d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) + - Merge pull request #138 from mrxiaozhuox/master ([`8c7473d`](https://github.comgit//DioxusLabs/dioxus/commit/8c7473d1943dd133f388ec36116c9d8295861b97)) + - Add a warning when Link it called outside of a Router context ([`6408058`](https://github.comgit//DioxusLabs/dioxus/commit/64080588d02c54a1d380116cbecdd17de16d2392)) + - Merge pull request #133 from mrxiaozhuox/master ([`887f69d`](https://github.comgit//DioxusLabs/dioxus/commit/887f69d5b47bdcde4fe0eab094c0cd0de23e4f3f)) + - Fix handling of re-renders in the Router ([`81c094e`](https://github.comgit//DioxusLabs/dioxus/commit/81c094ed29fcdc5c6099492dd6ab09a59b79252c)) +
+ ## v0.1.0 (2022-01-08) ### Documentation @@ -33,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 42 commits contributed to the release over the course of 352 calendar days. + - 50 commits contributed to the release over the course of 352 calendar days. - 35 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -44,8 +92,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - More WIP router implementation ([`e06eac1`](https://github.comgit//DioxusLabs/dioxus/commit/e06eac1ce5c6bb0a6680482574d82b16a141a626)) + - Implement UseRoute segment method ([`c9408da`](https://github.comgit//DioxusLabs/dioxus/commit/c9408da7310423b1676fab6d41635f9a8000d89e)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) + - Implement router matching for path parameters ([`f8a7e1c`](https://github.comgit//DioxusLabs/dioxus/commit/f8a7e1cd8255fd7d0116384247e0e305bb73bf3d)) + - Commit WIP on router ([`3c6142f`](https://github.comgit//DioxusLabs/dioxus/commit/3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923)) + - Add more trace messages to the RouterService code ([`3a5b417`](https://github.comgit//DioxusLabs/dioxus/commit/3a5b417ad1639a31e9aac9f41b05d8b3f074b128)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) + - Fix typo in RouterService struct's "registered_routes" field name ([`d367e0f`](https://github.comgit//DioxusLabs/dioxus/commit/d367e0f89f1b31484bde42efaf10c3531aed1d64)) + - Add title prop to Link ([`e22ba5b`](https://github.comgit//DioxusLabs/dioxus/commit/e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0)) - add prevent default attribute and upgrade router ([`427b126`](https://github.comgit//DioxusLabs/dioxus/commit/427b126bc17336d5d14d56eb7fddb8e07752495f)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) - bump all versions ([`4f92ba4`](https://github.comgit//DioxusLabs/dioxus/commit/4f92ba41602d706449c1bddabd49829873ee72eb)) diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 032dbe3bf..221372b50 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-router" -version = "0.1.0" +version = "0.1.1" edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" license = "MIT/Apache-2.0" @@ -11,13 +11,13 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", default-features = false } -dioxus-html = { path = "../html", version ="^0.1.4", default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} +dioxus-core = { path = "../core", version = "^0.1.9", default-features = false } +dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +# url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ @@ -43,9 +43,18 @@ web = ["web-sys", "gloo", "js-sys", "wasm-bindgen"] desktop = [] mobile = [] derive = [] +wasm_test = [] [dev-dependencies] console_error_panic_hook = "0.1.7" dioxus-web = { path = "../web" } log = "0.4.14" wasm-logger = "0.2.0" +wasm-bindgen-test = "0.3" +gloo-utils = "0.1.2" + +[dev-dependencies.web-sys] +version = "0.3" +features = [ + "Document", +] diff --git a/packages/router/Makefile.toml b/packages/router/Makefile.toml new file mode 100644 index 000000000..42b7f5c9a --- /dev/null +++ b/packages/router/Makefile.toml @@ -0,0 +1,10 @@ +[tasks.test-with-browser] +extend = "core::wasm-pack-base" +command = "wasm-pack" +args = [ + "test", + "@@split(DIOXUS_TEST_FLAGS, )", + "--", + "--features", + "${DIOXUS_TEST_FEATURES}", +] \ No newline at end of file diff --git a/packages/router/README.md b/packages/router/README.md index a6dc5027b..6fbc652eb 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows ```rust fn app() { cx.render(rsx! { - Routes { + Router { Route { to: "/", Component {} }, Route { to: "/blog", Blog {} }, Route { to: "/blog/:id", BlogPost {} }, diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple.rs index 4aebafb84..4afb122ae 100644 --- a/packages/router/examples/simple.rs +++ b/packages/router/examples/simple.rs @@ -1,8 +1,9 @@ +#![allow(non_snake_case)] + use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; use dioxus_router::*; -use serde::{Deserialize, Serialize}; fn main() { console_error_panic_hook::set_once(); @@ -25,26 +26,17 @@ static APP: Component = |cx| { fn Home(cx: Scope) -> Element { cx.render(rsx! { - div { - h1 { "Home" } - } + h1 { "Home" } }) } fn BlogList(cx: Scope) -> Element { cx.render(rsx! { - div { - - } + div { "Blog List" } }) } fn BlogPost(cx: Scope) -> Element { let id = use_route(&cx).segment::("id")?; - - cx.render(rsx! { - div { - - } - }) + cx.render(rsx! { div { "{id:?}" } }) } diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 4c0ecb996..b2b69108f 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -34,6 +34,17 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] title: Option<&'a str>, + #[props(default = true)] + autodetect: bool, + + /// Is this link an external link? + #[props(default = false)] + external: bool, + + /// New tab? + #[props(default = false)] + new_tab: bool, + children: Element<'a>, #[props(default)] @@ -41,17 +52,38 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - log::debug!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { + let LinkProps { + to, + href, + class, + id, + title, + autodetect, + external, + new_tab, + children, + .. + } = cx.props; + + let is_http = to.starts_with("http") || to.starts_with("https"); + let outerlink = (*autodetect && is_http) || *external; + + let prevent_default = if outerlink { "" } else { "onclick" }; + return cx.render(rsx! { a { - href: "{cx.props.to}", - class: format_args!("{}", cx.props.class.unwrap_or("")), - id: format_args!("{}", cx.props.id.unwrap_or("")), - title: format_args!("{}", cx.props.title.unwrap_or("")), - - prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to), + href: "{to}", + class: format_args!("{}", class.unwrap_or("")), + id: format_args!("{}", id.unwrap_or("")), + title: format_args!("{}", title.unwrap_or("")), + prevent_default: "{prevent_default}", + target: format_args!("{}", if *new_tab { "_blank" } else { "" }), + onclick: move |_| { + if !outerlink { + service.push_route(to); + } + }, &cx.props.children } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 129953eea..e4dd2f9c8 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,7 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(ctx) => ctx.total_route.to_string(), None => cx.props.to.to_string(), }; - log::trace!("total route for {} is {}", cx.props.to, total_route); + // log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { @@ -48,7 +48,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(RouteInner {}) }); - log::trace!("Checking route {}", cx.props.to); + // log::trace!("Checking route {}", cx.props.to); if router_root.should_render(cx.scope_id()) { cx.render(rsx!(&cx.props.children)) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 3d9f0c83a..c117c6650 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -11,18 +11,26 @@ use crate::RouterService; pub struct RouterProps<'a> { children: Element<'a>, - #[props(default, strip_option)] - onchange: Option<&'a dyn Fn(&'a str)>, + #[props(default)] + onchange: EventHandler<'a, String>, } #[allow(non_snake_case)] pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { - cx.use_hook(|_| { + log::debug!("running router {:?}", cx.scope_id()); + let svc = cx.use_hook(|_| { let update = cx.schedule_update_any(); cx.provide_context(RouterService::new(update, cx.scope_id())) }); - cx.render(rsx!( - div { &cx.props.children } - )) + let any_pending = svc.pending_events.borrow().len() > 0; + svc.pending_events.borrow_mut().clear(); + + if any_pending { + let location = svc.current_location(); + let path = location.path(); + cx.props.onchange.call(path.to_string()); + } + + cx.render(rsx!(&cx.props.children)) } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 97dd6c048..4de6dff12 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,4 +1,4 @@ -use dioxus_core::ScopeState; +use dioxus_core::{ScopeId, ScopeState}; use gloo::history::{HistoryResult, Location}; use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; @@ -8,6 +8,7 @@ use crate::RouterService; /// This struct provides is a wrapper around the internal router /// implementation, with methods for getting information about the current /// route. +#[derive(Clone)] pub struct UseRoute { router: Rc, } @@ -74,10 +75,40 @@ impl UseRoute { /// This hook provides access to information about the current location in the /// context of a [`Router`]. If this function is called outside of a `Router` /// component it will panic. -pub fn use_route(cx: &ScopeState) -> UseRoute { - let router = cx - .consume_context::() - .expect("Cannot call use_route outside the scope of a Router component") - .clone(); - UseRoute { router } +pub fn use_route(cx: &ScopeState) -> &UseRoute { + &cx.use_hook(|_| { + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component"); + + router.subscribe_onchange(cx.scope_id()); + + UseRouteListener { + router: UseRoute { router }, + scope: cx.scope_id(), + } + }) + .router +} + +// The entire purpose of this struct is to unubscribe this component when it is unmounted. +// The UseRoute can be cloned into async contexts, so we can't rely on its drop to unubscribe. +// Instead, we hide the drop implementation on this private type exclusive to the hook, +// and reveal our cached version of UseRoute to the component. +struct UseRouteListener { + router: UseRoute, + scope: ScopeId, +} +impl Drop for UseRouteListener { + fn drop(&mut self) { + self.router.router.unsubscribe_onchange(self.scope) + } +} + +/// This hook provides access to the `RouterService` for the app. +pub fn use_router(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| { + cx.consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + }) } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index b6ffb5971..309cef905 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -8,22 +8,9 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { +//! //! } -//! -//! -//! -//! -//! //! ``` -//! -//! -//! -//! -//! -//! -//! -//! -//! mod hooks { mod use_route; diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index 7afc1316a..ec59a4703 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,9 +1,4 @@ pub trait RouterProvider { fn get_current_route(&self) -> String; - fn subscribe_to_route_changes(&self, callback: Box); -} - -enum RouteChange { - LinkTo(String), - Back(String), + fn listen(&self, callback: Box); } diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 00642414f..a4ab4dc0d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,21 +1,33 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, Ref, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, rc::Rc, }; use dioxus_core::ScopeId; +use crate::platform::RouterProvider; + pub struct RouterService { pub(crate) regen_route: Rc, - history: Rc>, + pub(crate) pending_events: Rc>>, slots: Rc>>, + onchange_listeners: Rc>>, root_found: Rc>>, cur_path_params: Rc>>, + + // history: Rc, + history: Rc>, listener: HistoryListener, } +pub enum RouteEvent { + Change, + Pop, + Push, +} + enum RouteSlot { Routes { // the partial route @@ -35,58 +47,66 @@ impl RouterService { let location = history.location(); let path = location.path(); + let onchange_listeners = Rc::new(RefCell::new(HashSet::new())); let slots: Rc>> = Default::default(); - - let _slots = slots.clone(); - + let pending_events: Rc>> = Default::default(); let root_found = Rc::new(Cell::new(None)); - let regen = regen_route.clone(); - let _root_found = root_found.clone(); - let listener = history.listen(move || { - _root_found.set(None); - // checking if the route is valid is cheap, so we do it - for (slot, root) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?} for root '{}'", slot, root); - regen(*slot); + + let listener = history.listen({ + let pending_events = pending_events.clone(); + let regen_route = regen_route.clone(); + let root_found = root_found.clone(); + let slots = slots.clone(); + let onchange_listeners = onchange_listeners.clone(); + move || { + root_found.set(None); + // checking if the route is valid is cheap, so we do it + for (slot, root) in slots.borrow_mut().iter().rev() { + regen_route(*slot); + } + + for listener in onchange_listeners.borrow_mut().iter() { + regen_route(*listener); + } + + // also regenerate the root + regen_route(root_scope); + + pending_events.borrow_mut().push(RouteEvent::Change) } }); Self { + listener, root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, + pending_events, + onchange_listeners, cur_path_params: Rc::new(RefCell::new(HashMap::new())), - listener, } } pub fn push_route(&self, route: &str) { - log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { let clean = clean_route(route); - log::trace!("Registered route '{}' with scope id {:?}", clean, scope); self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { - log::trace!("Should render scope id {:?}?", scope); if let Some(root_id) = self.root_found.get() { - log::trace!(" we already found a root with scope id {:?}", root_id); if root_id == scope { - log::trace!(" yes - it's a match"); return true; } - log::trace!(" no - it's not a match"); return false; } let location = self.history.borrow().location(); let path = location.path(); - log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -95,23 +115,15 @@ impl RouterService { // fallback logic match root { Some((id, route)) => { - log::trace!( - " matched given scope id {:?} with route root '{}'", - scope, - route, - ); if let Some(params) = route_matches_path(route, path) { - log::trace!(" and it matches the current path '{}'", path); self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { - log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(Some(*id)); true } else { - log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } @@ -127,6 +139,14 @@ impl RouterService { pub fn current_path_params(&self) -> Ref> { self.cur_path_params.borrow() } + + pub fn subscribe_onchange(&self, id: ScopeId) { + self.onchange_listeners.borrow_mut().insert(id); + } + + pub fn unsubscribe_onchange(&self, id: ScopeId) { + self.onchange_listeners.borrow_mut().remove(&id); + } } fn clean_route(route: String) -> String { @@ -147,36 +167,20 @@ fn route_matches_path(route: &str, path: &str) -> Option let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); - log::trace!( - " checking route pieces {:?} vs path pieces {:?}", - route_pieces, - path_pieces, - ); - if route_pieces.len() != path_pieces.len() { - log::trace!(" the routes are different lengths"); return None; } let mut matches = HashMap::new(); for (i, r) in route_pieces.iter().enumerate() { - log::trace!(" checking route piece '{}' vs path", r); // If this is a parameter then it matches as long as there's // _any_thing in that spot in the path. if r.starts_with(':') { - log::trace!( - " route piece '{}' starts with a colon so it matches anything", - r, - ); let param = &r[1..]; matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } - log::trace!( - " route piece '{}' must be an exact match for path piece '{}'", - r, - path_pieces[i], - ); + if path_pieces[i] != *r { return None; } diff --git a/packages/router/tests/route.rs b/packages/router/tests/route.rs new file mode 100644 index 000000000..89b7a64fa --- /dev/null +++ b/packages/router/tests/route.rs @@ -0,0 +1,63 @@ +#![cfg(target_arch = "wasm32")] + +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; +use dioxus_router::*; +use gloo_utils::document; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn simple_test() { + fn main() { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + dioxus_web::launch(APP); + } + + static APP: Component = |cx| { + cx.render(rsx! { + Router { + onchange: move |route| log::info!("route changed to {}", route), + Route { to: "/", Home {} } + Route { to: "blog" + Route { to: "/", BlogList {} } + Route { to: ":id", BlogPost {} } + } + } + }) + }; + + fn Home(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Home" } + } + }) + } + + fn BlogList(cx: Scope) -> Element { + cx.render(rsx! { + div { + + } + }) + } + + fn BlogPost(cx: Scope) -> Element { + let id = use_route(&cx).segment::("id")?; + + cx.render(rsx! { + div { + + } + }) + } + + main(); + + let element = gloo_utils::document(); +} diff --git a/packages/router/webdriver.json b/packages/router/webdriver.json new file mode 100644 index 000000000..81413f24b --- /dev/null +++ b/packages/router/webdriver.json @@ -0,0 +1,11 @@ +{ + "moz:firefoxOptions": { + "binary": "/usr/bin/firefox", + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + } + } + \ No newline at end of file diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml new file mode 100644 index 000000000..4cad9064c --- /dev/null +++ b/packages/rsx/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dioxus-rsx" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.8" +proc-macro-error = "1.0.4" +proc-macro2 = { version = "1.0.6" } +quote = "1.0" +syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/core-macro/src/rsx/component.rs b/packages/rsx/src/component.rs similarity index 96% rename from packages/core-macro/src/rsx/component.rs rename to packages/rsx/src/component.rs index 5257022a4..17006e6ac 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/rsx/src/component.rs @@ -23,10 +23,10 @@ use syn::{ }; pub struct Component { - name: syn::Path, - body: Vec, - children: Vec, - manual_props: Option, + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, } impl Parse for Component { @@ -135,11 +135,14 @@ impl ToTokens for Component { None => quote! { None }, }; + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + tokens.append_all(quote! { __cx.component( #name, #builder, #key_token, + #fn_name ) }) } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/rsx/src/element.rs similarity index 92% rename from packages/core-macro/src/rsx/element.rs rename to packages/rsx/src/element.rs index ebc401b6e..fa8fc9f51 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/rsx/src/element.rs @@ -11,12 +11,11 @@ use syn::{ // Parse the VNode::Element type // ======================================= pub struct Element { - name: Ident, - key: Option, - attributes: Vec, - listeners: Vec, - children: Vec, - _is_static: bool, + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, } impl Parse for Element { @@ -28,7 +27,6 @@ impl Parse for Element { syn::braced!(content in stream); let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; @@ -54,6 +52,7 @@ impl Parse for Element { }); } else { let value = content.parse::()?; + attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, @@ -82,7 +81,7 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - listeners.push(ElementAttrNamed { + attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::EventTokens { name, @@ -182,7 +181,6 @@ impl Parse for Element { name: el_name, attributes, children, - listeners, _is_static: false, }) } @@ -193,14 +191,21 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - let listeners = &self.listeners; - let attr = &self.attributes; - let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; + let listeners = self + .attributes + .iter() + .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + + let attr = self + .attributes + .iter() + .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + tokens.append_all(quote! { __cx.element( dioxus_elements::#name, @@ -213,29 +218,28 @@ impl ToTokens for Element { } } -enum ElementAttr { - // attribute: "valuee {}" +pub enum ElementAttr { + /// attribute: "valuee {}" AttrText { name: Ident, value: LitStr }, - // attribute: true, + /// attribute: true, AttrExpression { name: Ident, value: Expr }, - // "attribute": "value {}" + /// "attribute": "value {}" CustomAttrText { name: LitStr, value: LitStr }, - // "attribute": true, + /// "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - // // onclick: move |_| {} + // /// onclick: move |_| {} // EventClosure { name: Ident, closure: ExprClosure }, - - // onclick: {} + /// onclick: {} EventTokens { name: Ident, tokens: Expr }, } -struct ElementAttrNamed { - el_name: Ident, - attr: ElementAttr, +pub struct ElementAttrNamed { + pub el_name: Ident, + pub attr: ElementAttr, } impl ToTokens for ElementAttrNamed { diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/rsx/src/lib.rs similarity index 96% rename from packages/core-macro/src/rsx/mod.rs rename to packages/rsx/src/lib.rs index 5ff5d85f7..e20d80dec 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/rsx/src/lib.rs @@ -15,6 +15,8 @@ mod component; mod element; mod node; +pub mod pretty; + // Re-export the namespaces into each other pub use component::*; pub use element::*; @@ -29,8 +31,8 @@ use syn::{ }; pub struct CallBody { - custom_context: Option, - roots: Vec, + pub custom_context: Option, + pub roots: Vec, } impl Parse for CallBody { diff --git a/packages/core-macro/src/rsx/node.rs b/packages/rsx/src/node.rs similarity index 95% rename from packages/core-macro/src/rsx/node.rs rename to packages/rsx/src/node.rs index d5d701767..eeee421b0 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/rsx/src/node.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, + token, Attribute, Expr, LitStr, Result, Token, }; /* @@ -20,6 +20,7 @@ pub enum BodyNode { Component(Component), Text(LitStr), RawExpr(Expr), + Meta(Attribute), } impl Parse for BodyNode { @@ -79,6 +80,7 @@ impl ToTokens for BodyNode { BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), + BodyNode::Meta(_) => {} } } } diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs new file mode 100644 index 000000000..26ad54006 --- /dev/null +++ b/packages/rsx/src/pretty.rs @@ -0,0 +1 @@ +//! pretty printer for rsx! diff --git a/packages/ssr/CHANGELOG.md b/packages/ssr/CHANGELOG.md index 73cee67b0..43e1960c1 100644 --- a/packages/ssr/CHANGELOG.md +++ b/packages/ssr/CHANGELOG.md @@ -5,6 +5,34 @@ 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 + +### Bug Fixes + + - ssr respects bool attrs + +### Commit Statistics + + + + - 5 commits contributed to the release over the course of 21 calendar days. + - 1 commit where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d)) + - Merge pull request #108 from DioxusLabs/jk/fstring-component-fields ([`f4132d1`](https://github.comgit//DioxusLabs/dioxus/commit/f4132d1874f7495049fac23ba0a022ac137ad74f)) + - ssr respects bool attrs ([`255f58a`](https://github.comgit//DioxusLabs/dioxus/commit/255f58af63e002339b02bf91c5b85cc5ec917428)) + - Enable clippy ([`b6903bf`](https://github.comgit//DioxusLabs/dioxus/commit/b6903bf558bc7a3d0fe6794a137c44fca0957d11)) +
+ ## v0.1.2 (2022-01-08) ### Documentation @@ -47,7 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - 84 commits contributed to the release over the course of 357 calendar days. + - 85 commits contributed to the release over the course of 357 calendar days. - 75 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages @@ -58,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 754fbbb78..02f51f453 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-ssr" -version = "0.1.2" +version = "0.1.3" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus render-to-string" @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } [dev-dependencies] diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index b81747e95..3c27f3f94 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -5,6 +5,60 @@ 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 + +### Documentation + + - fix web doc example and use &mut for builders everywhere + +### New Features + + - add panic hook by default + - remove dioxus id on non-event elements + - wire up both desktop and web + - bool attr white list + +### Bug Fixes + + - webconfig should take &mut self + - format code + - ssr + hydration event listeners + - add exclusion list + +### Commit Statistics + + + + - 17 commits contributed to the release over the course of 21 calendar days. + - 10 commits where understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' where seen in commit messages + +### Commit Details + + + +
view details + + * **Uncategorized** + - webconfig should take &mut self ([`ae676d9`](https://github.comgit//DioxusLabs/dioxus/commit/ae676d9d816fcbf88d110157124443a5b3aef9eb)) + - fix web doc example and use &mut for builders everywhere ([`a239d2b`](https://github.comgit//DioxusLabs/dioxus/commit/a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63)) + - add panic hook by default ([`1406c90`](https://github.comgit//DioxusLabs/dioxus/commit/1406c9020bb3102635737dc2d13e5b3171499a18)) + - Merge pull request #111 from DioxusLabs/jk/props-attrs ([`0369fe7`](https://github.comgit//DioxusLabs/dioxus/commit/0369fe72fb247409da300a54ef11ba9155d0efb3)) + - Merge pull request #113 from DioxusLabs/jk/desktop-cursor-jump ([`20a2940`](https://github.comgit//DioxusLabs/dioxus/commit/20a29409b22510b001fdbee349724adb7b44d401)) + - remove dioxus id on non-event elements ([`95e93ed`](https://github.comgit//DioxusLabs/dioxus/commit/95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36)) + - feat(events:focus): add missing `onfocusin` event ([`007d06d`](https://github.comgit//DioxusLabs/dioxus/commit/007d06d602f1adfaa51c87ec89b2afe90d8cdef9)) + - feat(example:todomvc): add editing support ([`9849f68`](https://github.comgit//DioxusLabs/dioxus/commit/9849f68f257200fac511c048bfb1a076243b86d3)) + - Merge pull request #101 from alexkirsz/ci ([`29bf424`](https://github.comgit//DioxusLabs/dioxus/commit/29bf424b0976b95ff645bb128d0e758cf0186614)) + - Merge pull request #139 from DioxusLabs/jk/provide-context-any ([`70f2ef4`](https://github.comgit//DioxusLabs/dioxus/commit/70f2ef43db5b6737bd9bcbfc1aa21c834ce4b395)) + - Merge branch 'master' into jk/unify ([`824defa`](https://github.comgit//DioxusLabs/dioxus/commit/824defa2dbcc16d66588b3976699d89b65a8a068)) + - wire up both desktop and web ([`05331dd`](https://github.comgit//DioxusLabs/dioxus/commit/05331ddd8033f6997d4916179b62f4d62f832988)) + - format code ([`9256161`](https://github.comgit//DioxusLabs/dioxus/commit/92561612c727e73356d7d36e16af39aacf02a56d)) + - ssr + hydration event listeners ([`c1a5d4e`](https://github.comgit//DioxusLabs/dioxus/commit/c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1)) + - web now links against the js interprter code ([`10db6ad`](https://github.comgit//DioxusLabs/dioxus/commit/10db6ad65bd23dabf60d0afbe6ff82cda8220a8b)) + - bool attr white list ([`8f4aa84`](https://github.comgit//DioxusLabs/dioxus/commit/8f4aa84f1a4f2443b34d81ee42490564e168de53)) + - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) +
+ ## v0.0.4 (2022-01-08) ### Documentation @@ -84,7 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 203 commits contributed to the release over the course of 358 calendar days. - - 190 commits where understood as [conventional](https://www.conventionalcommits.org). + - 189 commits where understood as [conventional](https://www.conventionalcommits.org). - 0 issues like '(#ID)' where seen in commit messages ### Commit Details @@ -94,10 +148,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
view details * **Uncategorized** + - Release dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`b804c69`](https://github.comgit//DioxusLabs/dioxus/commit/b804c691d5ade4776390bb3d334cc9cd8efa4a49)) + - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Release dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`a36dab7`](https://github.comgit//DioxusLabs/dioxus/commit/a36dab7f45920acd8535a69b4aa3695f3bb92111)) - Release dioxus-core v0.1.7, dioxus-core-macro v0.1.6, dioxus-html v0.1.4, dioxus-desktop v0.1.5, dioxus-hooks v0.1.6, dioxus-mobile v0.0.3, dioxus-router v0.1.0, dioxus-ssr v0.1.2, dioxus-web v0.0.4, dioxus v0.1.7 ([`40d1f85`](https://github.comgit//DioxusLabs/dioxus/commit/40d1f85d0c3e2c9fd23c08840cca9f459d4e4307)) - - add exclusion list ([`2123228`](https://github.comgit//DioxusLabs/dioxus/commit/21232285d9d84168d9003969ddd254fc22951e4b)) - - handle bool attrs properly ([`8d685f4`](https://github.comgit//DioxusLabs/dioxus/commit/8d685f40b7e0ef6521c60310d8687291e9b9c48a)) - Merge pull request #74 from mrxiaozhuox/master ([`47056fd`](https://github.comgit//DioxusLabs/dioxus/commit/47056fda4577bcbdaa2a6f63d82eec876e5a5aee)) - Merge pull request #80 from DioxusLabs/jk/router2dotoh ([`cdc2d8e`](https://github.comgit//DioxusLabs/dioxus/commit/cdc2d8ec6d123245c2ea5f6d10af02b6a6833994)) - memoize dom in the prescence of identical components ([`cb2782b`](https://github.comgit//DioxusLabs/dioxus/commit/cb2782b4bb34cdaadfff590bfee930ae3ac6536c)) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index a5befe7b3..333838398 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-web" -version = "0.0.4" +version = "0.0.5" authors = ["Jonathan Kelley"] edition = "2018" description = "Dioxus VirtualDOM renderer for the web browser using websys" @@ -11,26 +11,25 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version = "^0.1.7" } -dioxus-html = { path = "../html", version = "^0.1.4" } -js-sys = "0.3" -wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } -lazy_static = "1.4.0" -wasm-bindgen-futures = "0.4.20" +dioxus-core = { path = "../core", version = "^0.1.9" } +dioxus-html = { path = "../html", version = "^0.1.6" } +js-sys = "0.3.56" +wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } +wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" -wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen-test = "0.3.21" -once_cell = "1.8" -async-channel = "1.6.1" -anyhow = "1.0" -gloo-timers = { version = "0.2.1", features = ["futures"] } -futures-util = "0.3.15" +once_cell = "1.9.0" +anyhow = "1.0.53" +gloo-timers = { version = "0.2.3", features = ["futures"] } +futures-util = "0.3.19" smallstr = "0.2.0" +dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } +serde-wasm-bindgen = "0.4.2" +futures-channel = "0.3.21" [dependencies.web-sys] -version = "0.3.51" +version = "0.3.56" features = [ "Comment", "Attr", @@ -41,6 +40,7 @@ features = [ "HtmlInputElement", "HtmlSelectElement", "HtmlTextAreaElement", + "HtmlFormElement", "EventTarget", "HtmlCollection", "Node", @@ -75,5 +75,7 @@ panic_hook = ["console_error_panic_hook"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } -wasm-bindgen-test = "0.3.28" +wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } +wasm-logger = "0.2.0" + diff --git a/packages/web/examples/hydrate.rs b/packages/web/examples/hydrate.rs index c78f4036a..3097db2e7 100644 --- a/packages/web/examples/hydrate.rs +++ b/packages/web/examples/hydrate.rs @@ -1,8 +1,6 @@ -use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; -use wasm_bindgen_test::wasm_bindgen_test; use web_sys::window; fn app(cx: Scope) -> Element { diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ffbef9f4b..b2eb65d08 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,8 +7,8 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use crate::bindings::Interpreter; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use dioxus_interpreter_js::Interpreter; use js_sys::Function; use std::{any::Any, rc::Rc, sync::Arc}; use wasm_bindgen::{closure::Closure, JsCast}; @@ -28,17 +28,57 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type let callback: Box = Box::new(move |event: &web_sys::Event| { - if let Ok(synthetic_event) = decode_trigger(event) { + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let decoded: anyhow::Result = loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone(), target.clone()), + element: Some(ElementId(id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }); + } + Some(Err(e)) => { + break Err(e.into()); + } + None => { + // walk the tree upwards until we actually find an event target + if let Some(parent) = target.parent_element() { + target = parent; + } else { + break Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event( + event.clone(), + target.clone(), + ), + element: None, + scope_id: None, + priority: dioxus_core::EventPriority::Low, + }); + } + } + } + }; + + if let Ok(synthetic_event) = decoded { // Try to prevent default if the attribute is set - if let Some(target) = event.target() { - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); } } } @@ -47,7 +87,12 @@ impl WebsysDom { } }); - let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + // a match here in order to avoid some error during runtime browser test + let document = load_document(); + let root = match document.get_element_by_id(&cfg.rootname) { + Some(root) => root, + None => document.create_element("body").ok().unwrap(), + }; Self { interpreter: Interpreter::new(root.clone()), @@ -65,9 +110,7 @@ impl WebsysDom { DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n), DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n), DomEdit::Remove { root } => self.interpreter.Remove(root), - DomEdit::CreateTextNode { text, root } => { - self.interpreter.CreateTextNode(text, root) - } + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), DomEdit::CreateElementNs { tag, root, ns } => { self.interpreter.CreateElementNs(tag, root, ns) @@ -83,15 +126,27 @@ impl WebsysDom { DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } - DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), + + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) + } + + DomEdit::CreateTextNode { text, root } => { + let text = serde_wasm_bindgen::to_value(text).unwrap(); + self.interpreter.CreateTextNode(text, root) + } + DomEdit::SetText { root, text } => { + let text = serde_wasm_bindgen::to_value(text).unwrap(); + self.interpreter.SetText(root, text) + } DomEdit::SetAttribute { root, field, value, ns, - } => self.interpreter.SetAttribute(root, field, value, ns), - DomEdit::RemoveAttribute { root, name } => { - self.interpreter.RemoveAttribute(root, name) + } => { + let value = serde_wasm_bindgen::to_value(value).unwrap(); + self.interpreter.SetAttribute(root, field, value, ns) } } } @@ -107,7 +162,10 @@ unsafe impl Sync for DioxusWebsysEvent {} // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { +fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> Arc { use dioxus_html::on::*; use dioxus_html::KeyCode; @@ -140,9 +198,6 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { - let evt: &web_sys::Event = event.dyn_ref().unwrap(); - - let target: web_sys::EventTarget = evt.target().unwrap(); let value: String = (&target) .dyn_ref() .map(|input: &web_sys::HtmlInputElement| { @@ -178,7 +233,38 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc() { + let elements = form.elements(); + for x in 0..elements.length() { + let element = elements.item(x).unwrap(); + if let Some(name) = element.get_attribute("name") { + let value: String = (&element) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => input.value() + } + }) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value())) + .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value())) + .or_else(|| target.dyn_ref::().unwrap().text_content()) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + values.insert(name, value); + } + } + } + + Arc::new(FormData { value, values }) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" @@ -270,53 +356,11 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(MediaData {}), "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), } } -/// This function decodes a websys event and produces an EventTrigger -/// With the websys implementation, we attach a unique key to the nodes -fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - let typ = event.type_(); - - loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }); - } - Some(Err(e)) => { - return Err(e.into()); - } - None => { - // walk the tree upwards until we actually find an event target - if let Some(parent) = target.parent_element() { - target = parent; - } else { - return Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: None, - scope_id: None, - priority: dioxus_core::EventPriority::Low, - }); - } - } - } - } -} - pub(crate) fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index f4266a7c8..90fe84d04 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,7 +61,6 @@ pub use dioxus_core as dioxus; use dioxus_core::prelude::Component; use futures_util::FutureExt; -pub(crate) mod bindings; mod cache; mod cfg; mod dom; @@ -212,7 +211,7 @@ pub async fn run_with_props(root: Component, root_props: T websys_dom.apply_edits(edits.edits); } - let work_loop = ric_raf::RafLoop::new(); + let mut work_loop = ric_raf::RafLoop::new(); loop { log::trace!("waiting for work"); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 2ac0668b9..89819fff0 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -7,6 +7,7 @@ //! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see //! if RIC is available. +use futures_util::StreamExt; use gloo_timers::future::TimeoutFuture; use js_sys::Function; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; @@ -14,21 +15,21 @@ use web_sys::{window, Window}; pub(crate) struct RafLoop { window: Window, - ric_receiver: async_channel::Receiver, - raf_receiver: async_channel::Receiver<()>, + ric_receiver: futures_channel::mpsc::UnboundedReceiver, + raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>, ric_closure: Closure, raf_closure: Closure, } impl RafLoop { pub fn new() -> Self { - let (raf_sender, raf_receiver) = async_channel::unbounded(); + let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded(); let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { - raf_sender.try_send(()).unwrap() + raf_sender.unbounded_send(()).unwrap() })); - let (ric_sender, ric_receiver) = async_channel::unbounded(); + let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded(); let has_idle_callback = { let bo = window().unwrap().dyn_into::().unwrap(); @@ -45,7 +46,7 @@ impl RafLoop { 10 }; - ric_sender.try_send(time_remaining).unwrap() + ric_sender.unbounded_send(time_remaining).unwrap() })); // execute the polyfill for safari @@ -64,16 +65,16 @@ impl RafLoop { } } /// waits for some idle time and returns a timeout future that expires after the idle time has passed - pub async fn wait_for_idle_time(&self) -> TimeoutFuture { + pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); - let deadline = self.ric_receiver.recv().await.unwrap(); + let deadline = self.ric_receiver.next().await.unwrap(); TimeoutFuture::new(deadline) } - pub async fn wait_for_raf(&self) { + pub async fn wait_for_raf(&mut self) { let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); - self.raf_receiver.recv().await.unwrap(); + self.raf_receiver.next().await.unwrap(); } } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index b0ecc6c51..b8a034a5b 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -1,4 +1,3 @@ -use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_core_macro::*; use dioxus_html as dioxus_elements; diff --git a/src/lib.rs b/src/lib.rs index d7b479a07..488b1ee9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,6 +340,9 @@ pub use dioxus_web as web; #[cfg(feature = "desktop")] pub use dioxus_desktop as desktop; +#[cfg(feature = "fermi")] +pub use fermi; + // #[cfg(feature = "mobile")] // pub use dioxus_mobile as mobile; @@ -350,7 +353,7 @@ pub mod events { pub mod prelude { pub use dioxus_core::prelude::*; - pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props, Routable}; + pub use dioxus_core_macro::{format_args_f, inline_props, rsx, Props}; pub use dioxus_elements::{GlobalAttributes, SvgAttributes}; pub use dioxus_hooks::*; pub use dioxus_html as dioxus_elements; diff --git a/tests/fermi.rs b/tests/fermi.rs new file mode 100644 index 000000000..1149539f0 --- /dev/null +++ b/tests/fermi.rs @@ -0,0 +1,46 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::*; + +#[test] +fn test_fermi() { + let mut app = VirtualDom::new(App); + app.rebuild(); +} + +static TITLE: Atom = |_| "".to_string(); +static USERS: AtomFamily = |_| Default::default(); + +fn App(cx: Scope) -> Element { + cx.render(rsx!( + Leaf { id: 0 } + Leaf { id: 1 } + Leaf { id: 2 } + )) +} + +#[derive(Debug, PartialEq, Props)] +struct LeafProps { + id: u32, +} + +fn Leaf(cx: Scope) -> Element { + let _user = use_read(&cx, TITLE); + let _user = use_read(&cx, USERS); + + rsx!(cx, div { + button { + onclick: move |_| {}, + "Start" + } + button { + onclick: move |_| {}, + "Stop" + } + button { + onclick: move |_| {}, + "Reverse" + } + }) +}