diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..464189e01 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace. +[env] +DIOXUS_FRONT_ISSUER_URL = "" +DIOXUS_FRONT_CLIENT_ID = "" +DIOXUS_FRONT_URL = "" diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index 4cd33f47e..544c375a2 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -36,6 +36,8 @@ jobs: toolchain: ${{ matrix.platform.toolchain }} targets: ${{ matrix.platform.target }} + - uses: ilammy/setup-nasm@v1 + # Setup the Github Actions Cache for the CLI package - name: Setup cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml index 642216f0f..523a24067 100644 --- a/.github/workflows/docs stable.yml +++ b/.github/workflows/docs stable.yml @@ -33,7 +33,7 @@ jobs: # cd fermi && mdbook build -d ../nightly/fermi && cd .. - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.4.3 + uses: JamesIves/github-pages-deploy-action@v4.5.0 with: branch: gh-pages # The branch the action should deploy to. folder: docs/nightly # The folder the action should deploy. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 88542aea8..7701209c1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: # cd fermi && mdbook build -d ../nightly/fermi && cd .. - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.4.3 + uses: JamesIves/github-pages-deploy-action@v4.5.0 with: branch: gh-pages # The branch the action should deploy to. folder: docs/nightly # The folder the action should deploy. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 037525557..f0b66b951 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,9 +36,15 @@ jobs: if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - uses: actions/checkout@v4 @@ -48,11 +54,17 @@ jobs: if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev + - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev - uses: davidB/rust-cargo-make@v1 - uses: browser-actions/setup-firefox@latest - uses: jetli/wasm-pack-action@v0.4.0 @@ -63,9 +75,15 @@ jobs: if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 + - uses: ilammy/setup-nasm@v1 - run: rustup component add rustfmt - uses: actions/checkout@v4 - run: cargo fmt --all -- --check @@ -74,9 +92,15 @@ jobs: if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 + - uses: ilammy/setup-nasm@v1 - run: sudo apt-get update - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev - run: rustup component add clippy @@ -86,6 +110,10 @@ jobs: matrix_test: runs-on: ${{ matrix.platform.os }} env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} strategy: matrix: @@ -125,7 +153,7 @@ jobs: steps: - uses: actions/checkout@v4 - + - uses: ilammy/setup-nasm@v1 - name: install stable uses: dtolnay/rust-toolchain@master with: @@ -136,11 +164,16 @@ jobs: if: ${{ matrix.platform.cross == true }} uses: taiki-e/install-action@cross - - uses: Swatinem/rust-cache@v2 + - uses: mozilla-actions/sccache-action@v0.0.3 with: workspaces: core -> ../target save-if: ${{ matrix.features.key == 'all' }} + - name: Install rustfmt + run: rustup component add rustfmt + + - uses: actions/checkout@v4 + - name: test run: | ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 4a467b9b2..6c8a260b3 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -26,8 +26,8 @@ env: RUST_BACKTRACE: 1 # Change to specific Rust release to pin rust_stable: stable - rust_nightly: nightly-2022-11-03 - rust_clippy: 1.65.0 + rust_nightly: nightly-2023-11-16 + rust_clippy: 1.70.0 # When updating this, also update: # - README.md # - tokio/README.md @@ -70,6 +70,7 @@ jobs: run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV - uses: actions/checkout@v4 + - uses: ilammy/setup-nasm@v1 - name: Install Rust ${{ env.rust_nightly }} uses: dtolnay/rust-toolchain@master with: @@ -86,8 +87,7 @@ jobs: # working-directory: tokio env: - # todo: disable memory leaks ignore - MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks + MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields PROPTEST_CASES: 10 # Cache the global cargo directory, but NOT the local `target` directory which diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 500d41564..6b1b5acf0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,7 +20,8 @@ jobs: steps: # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: ilammy/setup-nasm@v1 + - uses: actions/setup-node@v4 with: node-version: 16 - name: Install Rust @@ -43,7 +44,7 @@ jobs: # args: --path packages/cli - name: Run Playwright tests run: npx playwright test - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report diff --git a/.gitignore b/.gitignore index b8025753e..364e8d0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /dist Cargo.lock .DS_Store +/examples/assets/test_video.mp4 .vscode/* !.vscode/settings.json diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b056a9590..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,171 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## Unreleased - -### Commit Statistics - - - - - 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 - - - tests - -### Commit Statistics - - - - - 1 commit contributed to the release over the course of 2 calendar days. - - 1 commit where understood as [conventional](https://www.conventionalcommits.org). - - 0 issues like '(#ID)' where seen in commit messages - -### Commit Details - - - -
view details - - * **Uncategorized** - - tests ([`bd341f5`](https://github.comgit//DioxusLabs/dioxus/commit/bd341f5571580cdf5e495379b49ca988fd9211c3)) -
- -## v0.1.1 (2021-12-15) - -### Documentation - - - update local examples and docs to support new syntaxes - - improve docs - - big updates to the reference - - lib.rs docs - - move around examples - -### New Features - - - it compiles once more - - massage lifetimes - - amazingly awesome error handling - - svgs working in webview - - mvoe away from compound context - - more suspended nodes! - - add aria - - enable arbitrary body in rsx! macro - -## v0.1.0 (2021-12-15) - -### Documentation - - - update local examples and docs to support new syntaxes - - improve docs - - big updates to the reference - - lib.rs docs - - move around examples - -### New Features - - - it compiles once more - - massage lifetimes - - amazingly awesome error handling - - svgs working in webview - - mvoe away from compound context - - more suspended nodes! - - add aria - - enable arbitrary body in rsx! macro - -## v0.0.1 (2022-01-03) - -### Documentation - - - improve docs - - update local examples and docs to support new syntaxes - - big updates to the reference - - lib.rs docs - - move around examples - -### New Features - - - it compiles once more - - massage lifetimes - - amazingly awesome error handling - - svgs working in webview - - mvoe away from compound context - - more suspended nodes! - - add aria - - enable arbitrary body in rsx! macro - -### Commit Statistics - - - - - 40 commits contributed to the release over the course of 193 calendar days. - - 38 commits where understood as [conventional](https://www.conventionalcommits.org). - - 0 issues like '(#ID)' where seen in commit messages - -### Commit Details - - - -
view details - - * **Uncategorized** - - remove runner on hook and then update docs ([`d156045`](https://github.comgit//DioxusLabs/dioxus/commit/d1560450bac55f9566e00e00ea405bd1c70b57e5)) - - polish some more things ([`1496102`](https://github.comgit//DioxusLabs/dioxus/commit/14961023f927b3a8bde83cfc7883aa8bfcca9e85)) - - upgrade hooks ([`b3ac2ee`](https://github.comgit//DioxusLabs/dioxus/commit/b3ac2ee3f76549cd1c7b6f9eee7e3382b07d873c)) - - docs ([`8814977`](https://github.comgit//DioxusLabs/dioxus/commit/8814977eeebe06748a3b9677a8070e42a037ebd7)) - - prepare to change our fragment pattern. Add some more docs ([`2c3a046`](https://github.comgit//DioxusLabs/dioxus/commit/2c3a0464264fa11e8100df025d863931f9606cdb)) - - it compiles once more ([`8acdd2e`](https://github.comgit//DioxusLabs/dioxus/commit/8acdd2ea830b995b608d8bac2ef527db8d40e662)) - - some docs and suspense ([`93d4b8c`](https://github.comgit//DioxusLabs/dioxus/commit/93d4b8ca7c1b133e5dba2a8dc9a310dbe1357001)) - - docs and router ([`a5f05d7`](https://github.comgit//DioxusLabs/dioxus/commit/a5f05d73acc0e47b05cff64a373482519414bc7c)) - - Merge branch 'master' into jk/remove_node_safety ([`db00047`](https://github.comgit//DioxusLabs/dioxus/commit/db0004758c77331cc3b93ea8cf227c060028e12e)) - - improve docs ([`7800744`](https://github.comgit//DioxusLabs/dioxus/commit/78007445f944f259170307d840e0f16242b7b4b6)) - - Various typos/grammar/rewording ([`5747e00`](https://github.comgit//DioxusLabs/dioxus/commit/5747e00b27b1b69c4f9c2820e7e78030feaff71e)) - - bubbling in progress ([`a21020e`](https://github.comgit//DioxusLabs/dioxus/commit/a21020ea575e467ba0d608737269fe1b0792dba7)) - - update local examples and docs to support new syntaxes ([`4de16c4`](https://github.comgit//DioxusLabs/dioxus/commit/4de16c4779648e591b3869b5df31271ae603c812)) - - massage lifetimes ([`9726a06`](https://github.comgit//DioxusLabs/dioxus/commit/9726a065b0d4fb1ede5b53a2ddd58c855e51539f)) - - major cleanups to scheduler ([`2933e4b`](https://github.comgit//DioxusLabs/dioxus/commit/2933e4bc11b3074c2bde8d76ec55364fca841988)) - - threadsafe ([`82953f2`](https://github.comgit//DioxusLabs/dioxus/commit/82953f2ac37913f83a822333acd0c47e20777d31)) - - move macro crate out of core ([`7bdad1e`](https://github.comgit//DioxusLabs/dioxus/commit/7bdad1e2e6f67e74c9f67dde2150140cf8a090e8)) - - amazingly awesome error handling ([`4a72b31`](https://github.comgit//DioxusLabs/dioxus/commit/4a72b3140bd244da602deada1eeecded65ff5848)) - - some ideas ([`05c909f`](https://github.comgit//DioxusLabs/dioxus/commit/05c909f320765aec1bf4c1c55ca59ffd5525a2c7)) - - big updates to the reference ([`583fdfa`](https://github.comgit//DioxusLabs/dioxus/commit/583fdfa5618e11d660985b97e570d4503be2ff49)) - - docs, html! macro, more ([`caf772c`](https://github.comgit//DioxusLabs/dioxus/commit/caf772cf249d2f56c8d0b0fa2737ad48e32c6e82)) - - cleanup workspace ([`8f0bb5d`](https://github.comgit//DioxusLabs/dioxus/commit/8f0bb5dc5bfa3e775af567c4b569622cdd932af1)) - - svgs working in webview ([`3bedcb9`](https://github.comgit//DioxusLabs/dioxus/commit/3bedcb93cacec5bdf134adc38ff02eadbf96c1c6)) - - mvoe away from compound context ([`a2c7d17`](https://github.comgit//DioxusLabs/dioxus/commit/a2c7d17b0595769f60bc1c2bbf7cbe32cec37486)) - - more suspended nodes! ([`de9f61b`](https://github.comgit//DioxusLabs/dioxus/commit/de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2)) - - add aria ([`4091846`](https://github.comgit//DioxusLabs/dioxus/commit/4091846934b4b3b2bc03d3ca8aaf7712aebd4e36)) - - more examples ([`56e7eb8`](https://github.comgit//DioxusLabs/dioxus/commit/56e7eb83a97ebd6d5bcd23464cfb9d718e5ac26d)) - - more refactor for async ([`975fa56`](https://github.comgit//DioxusLabs/dioxus/commit/975fa566f9809f8fa2bb0bdb07fbfc7f855dcaeb)) - - enable arbitrary body in rsx! macro ([`7aec40d`](https://github.comgit//DioxusLabs/dioxus/commit/7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2)) - - move CLI into its own "studio" app ([`fd79335`](https://github.comgit//DioxusLabs/dioxus/commit/fd7933561fe81922e4d5d77f6ac3b6f19efb5a90)) - - move some examples around ([`98a0933`](https://github.comgit//DioxusLabs/dioxus/commit/98a09339fd3190799ea4dd316908f0a53fdf2413)) - - fix issues with lifetimes ([`a38a81e`](https://github.comgit//DioxusLabs/dioxus/commit/a38a81e1290375cae685f7c49d3745e4298fab26)) - - more examples ([`11f89e5`](https://github.comgit//DioxusLabs/dioxus/commit/11f89e5d338d14a7aeece0a6275c24ae65913ce7)) - - lib.rs docs ([`bf21c82`](https://github.comgit//DioxusLabs/dioxus/commit/bf21c82de04e25daee60a06232b9a16b640508f2)) - - rename ctx to cx ([`81382e7`](https://github.comgit//DioxusLabs/dioxus/commit/81382e7044fb3dba61d4abb1e6086b7b29143116)) - - move around examples ([`70cd46d`](https://github.comgit//DioxusLabs/dioxus/commit/70cd46dbb2a689ae2d512e142b8aee9c80798430)) - - start moving events to rc ([`b9ff95f`](https://github.comgit//DioxusLabs/dioxus/commit/b9ff95fa12c46365fe73b64a4926a506d5da2342)) - - rename recoil to atoms ([`36ea39a`](https://github.comgit//DioxusLabs/dioxus/commit/36ea39ae30aa3f1fb2d718c0fdf08850c6bfd3ac)) - - more examples and docs ([`7fbaf69`](https://github.comgit//DioxusLabs/dioxus/commit/7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9)) - - docs ([`f5683a2`](https://github.comgit//DioxusLabs/dioxus/commit/f5683a23464992ecace463a61414795b5a2c58c8)) -
- diff --git a/Cargo.toml b/Cargo.toml index e6b07eb8c..7ef10d612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "packages/extension", "packages/router", "packages/html", + "packages/html-internal-macro", "packages/hooks", "packages/web", "packages/ssr", @@ -41,6 +42,7 @@ members = [ "examples/tailwind", "examples/PWA-example", "examples/query_segments_demo", + "examples/openid_connect_demo", # Playwright tests "playwright-tests/liveview", "playwright-tests/web", @@ -49,7 +51,7 @@ members = [ exclude = ["examples/mobile_demo"] [workspace.package] -version = "0.4.2" +version = "0.4.3" # dependencies that are shared across packages [workspace.dependencies] @@ -58,7 +60,8 @@ dioxus-core = { path = "packages/core", version = "0.4.2" } dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0" } dioxus-router = { path = "packages/router", version = "0.4.1" } dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" } -dioxus-html = { path = "packages/html", version = "0.4.0" } +dioxus-html = { path = "packages/html", default-features = false, version = "0.4.0" } +dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0" } dioxus-hooks = { path = "packages/hooks", version = "0.4.0" } dioxus-web = { path = "packages/web", version = "0.4.0" } dioxus-ssr = { path = "packages/ssr", version = "0.4.0" } @@ -76,7 +79,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" } dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" } rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" } dioxus-signals = { path = "packages/signals" } -generational-box = { path = "packages/generational-box" } +generational-box = { path = "packages/generational-box", version = "0.4.3" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" } @@ -87,7 +90,7 @@ slab = "0.4.2" futures-channel = "0.3.21" futures-util = { version = "0.3", default-features = false } rustc-hash = "1.1.0" -wasm-bindgen = "0.2.87" +wasm-bindgen = "0.2.88" html_parser = "0.7.0" thiserror = "1.0.40" prettyplease = { package = "prettier-please", version = "0.2", features = [ @@ -98,7 +101,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [ # It is not meant to be published, but is used so "cargo run --example XYZ" works properly [package] name = "dioxus-examples" -version = "0.0.0" +version = "0.4.3" authors = ["Jonathan Kelley"] edition = "2021" description = "Top level crate for the Dioxus repository" @@ -132,3 +135,8 @@ fern = { version = "0.6.0", features = ["colored"] } env_logger = "0.10.0" simple_logger = "4.0.0" thiserror = { workspace = true } + + +[dependencies] +tracing-subscriber = "0.3.17" +http-range = "0.1.5" diff --git a/Makefile.toml b/Makefile.toml index 6f331b98f..98af226aa 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -24,12 +24,64 @@ script = [ ] script_runner = "@duckscript" +[tasks.format] +command = "cargo" +args = ["fmt", "--all"] + +[tasks.check] +command = "cargo" +args = ["check", "--workspace", "--examples", "--tests"] + +[tasks.clippy] +command = "cargo" +args = [ + "clippy", + "--workspace", + "--examples", + "--tests", + "--", + "-D", + "warnings", +] + +[tasks.tidy] +category = "Formatting" +dependencies = ["format", "check", "clippy"] +description = "Format and Check workspace" + +[tasks.install-miri] +toolchain = "nightly" +install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" } +private = true + +[tasks.miri-native] +command = "cargo" +toolchain = "nightly" +dependencies = ["install-miri"] +args = [ + "miri", + "test", + "--package", + "dioxus-native-core", + "--test", + "miri_native", +] + +[tasks.miri-stress] +command = "cargo" +toolchain = "nightly" +dependencies = ["install-miri"] +args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"] + +[tasks.miri] +dependencies = ["miri-native", "miri-stress"] + [tasks.tests] 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} +env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] } +run_task = { name = ["test-flow", "test-with-browser"], fork = true } [tasks.build] command = "cargo" @@ -42,10 +94,26 @@ private = true [tasks.test] dependencies = ["build"] command = "cargo" -args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"] +args = [ + "test", + "--lib", + "--bins", + "--tests", + "--examples", + "--workspace", + "--exclude", + "dioxus-router", + "--exclude", + "dioxus-desktop", + "--exclude", + "dioxus-mobile", +] private = true [tasks.test-with-browser] -env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] } +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [ + "**/packages/router", + "**/packages/desktop", +] } private = true workspace = true diff --git a/README.md b/README.md index 5b6a99ad3..65cd08263 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,9 @@ So... Dioxus is great, but why won't it work for me? ## Contributing +- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing). - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). -- Join the discord and ask questions! +- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions! diff --git a/examples/README.md b/examples/README.md index 93530ba30..a20ffe60e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -139,7 +139,6 @@ Missing Features Missing examples - Shared state - Root-less element groups -- Spread props - Custom elements - Component Children: Pass children into child components - Render To string: Render a mounted virtualdom to a string diff --git a/examples/all_events.rs b/examples/all_events.rs index 594a736d5..3f57ec747 100644 --- a/examples/all_events.rs +++ b/examples/all_events.rs @@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element { }; cx.render(rsx! ( - div { - style: "{CONTAINER_STYLE}", + div { style: "{CONTAINER_STYLE}", div { style: "{RECT_STYLE}", // focusing is necessary to catch keyboard events @@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element { onmousemove: move |event| log_event(Event::MouseMove(event)), onclick: move |event| log_event(Event::MouseClick(event)), - ondblclick: move |event| log_event(Event::MouseDoubleClick(event)), + ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)), onmousedown: move |event| log_event(Event::MouseDown(event)), onmouseup: move |event| log_event(Event::MouseUp(event)), @@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element { "Hover, click, type or scroll to see the info down below" } - div { - events.read().iter().map(|event| rsx!( div { "{event:?}" } )) - }, - }, + div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) } + } )) } diff --git a/examples/button.rs b/examples/button.rs deleted file mode 100644 index f001ed531..000000000 --- a/examples/button.rs +++ /dev/null @@ -1,18 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - dioxus_desktop::launch(app); -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - button { - onclick: |_| async move { - println!("hello, desktop!"); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - println!("goodbye, desktop!"); - }, - "hello, desktop!" - } - }) -} diff --git a/examples/calculator.rs b/examples/calculator.rs index bc7f13459..2f8d4ee5f 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -6,13 +6,13 @@ This calculator version uses React-style state management. All state is held as use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; use dioxus::prelude::*; -use dioxus_desktop::{Config, WindowBuilder}; +use dioxus_desktop::{Config, LogicalSize, WindowBuilder}; fn main() { let config = Config::new().with_window( WindowBuilder::default() .with_title("Calculator") - .with_inner_size(dioxus_desktop::LogicalSize::new(300.0, 500.0)), + .with_inner_size(LogicalSize::new(300.0, 500.0)), ); dioxus_desktop::launch_cfg(app, config); @@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element { div { id: "wrapper", div { class: "app", div { class: "calculator", + tabindex: "0", onkeydown: handle_key_down_event, div { class: "calculator-display", val.to_string() } div { class: "calculator-keypad", diff --git a/examples/clock.rs b/examples/clock.rs index f9c1892d9..ba47d994a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element { use_future!(cx, || async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + tokio::time::sleep(std::time::Duration::from_millis(10)).await; count += 1; println!("current: {count}"); } diff --git a/examples/compose.rs b/examples/compose.rs index 3ee6d36ae..133192281 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -1,7 +1,6 @@ //! This example shows how to create a popup window and send data back to the parent window. use dioxus::prelude::*; -use dioxus_desktop::use_window; use futures_util::StreamExt; fn main() { @@ -9,7 +8,6 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = use_window(cx); let emails_sent = use_ref(cx, Vec::new); let tx = use_coroutine(cx, |mut rx: UnboundedReceiver| { @@ -27,14 +25,8 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { - let dom = VirtualDom::new_with_props(compose, ComposeProps { - app_tx: tx.clone() - }); - - // this returns a weak reference to the other window - // Be careful not to keep a strong reference to the other window or it will never be dropped - // and the window will never close. - window.new_window(dom, Default::default()); + let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() }); + dioxus_desktop::window().new_window(dom, Default::default()); }, "Click to compose a new email" } @@ -57,7 +49,6 @@ struct ComposeProps { fn compose(cx: Scope) -> Element { let user_input = use_state(cx, String::new); - let window = use_window(cx); cx.render(rsx! { div { @@ -66,17 +57,12 @@ fn compose(cx: Scope) -> Element { button { onclick: move |_| { cx.props.app_tx.send(user_input.get().clone()); - window.close(); + dioxus_desktop::window().close(); }, "Click to send" } - input { - oninput: move |e| { - user_input.set(e.value.clone()); - }, - value: "{user_input}" - } + input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" } } }) } diff --git a/examples/counter.rs b/examples/counter.rs index f51083a95..1de77dc38 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element { input { value: "{counter}", oninput: move |e| { - if let Ok(value) = e.value.parse::() { + if let Ok(value) = e.value().parse::() { counters.make_mut()[i] = value; } } diff --git a/examples/crm.rs b/examples/crm.rs index d2cd98118..8af49b401 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element { rel: "stylesheet", href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css", integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5", - crossorigin: "anonymous", + crossorigin: "anonymous" } - style { " + style { + " .red {{ background-color: rgb(202, 60, 60) !important; }} - " } + " + } h1 { "Dioxus CRM Example" } @@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element { cx.render(rsx! { h2 { "List of Clients" } - Link { - to: Route::ClientAdd {}, - class: "pure-button pure-button-primary", - "Add Client" - } - Link { - to: Route::Settings {}, - class: "pure-button", - "Settings" - } + Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" } + Link { to: Route::Settings {}, class: "pure-button", "Settings" } clients.read().iter().map(|client| rsx! { div { @@ -87,8 +81,6 @@ fn ClientAdd(cx: Scope) -> Element { let last_name = use_state(cx, String::new); let description = use_state(cx, String::new); - let navigator = use_navigator(cx); - cx.render(rsx! { h2 { "Add new Client" } @@ -96,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element { class: "pure-form pure-form-aligned", onsubmit: move |_| { let mut clients = clients.write(); - - clients.push(Client { - first_name: first_name.to_string(), - last_name: last_name.to_string(), - description: description.to_string(), - }); - - navigator.push(Route::ClientList {}); + clients + .push(Client { + first_name: first_name.to_string(), + last_name: last_name.to_string(), + description: description.to_string(), + }); + dioxus_router::router().push(Route::ClientList {}); }, fieldset { - div { - class: "pure-control-group", - label { - "for": "first_name", - "First Name" - } + div { class: "pure-control-group", + label { "for": "first_name", "First Name" } input { id: "first_name", "type": "text", placeholder: "First Name…", required: "", value: "{first_name}", - oninput: move |e| first_name.set(e.value.clone()) + oninput: move |e| first_name.set(e.value()) } } - div { - class: "pure-control-group", - label { - "for": "last_name", - "Last Name" - } + div { class: "pure-control-group", + label { "for": "last_name", "Last Name" } input { id: "last_name", "type": "text", placeholder: "Last Name…", required: "", value: "{last_name}", - oninput: move |e| last_name.set(e.value.clone()) + oninput: move |e| last_name.set(e.value()) } } - div { - class: "pure-control-group", - label { - "for": "description", - "Description" - } + div { class: "pure-control-group", + label { "for": "description", "Description" } textarea { id: "description", placeholder: "Description…", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| description.set(e.value()) } } - div { - class: "pure-controls", - button { - "type": "submit", - class: "pure-button pure-button-primary", - "Save" - } - Link { - to: Route::ClientList {}, - class: "pure-button pure-button-primary red", - "Cancel" - } + div { class: "pure-controls", + button { "type": "submit", class: "pure-button pure-button-primary", "Save" } + Link { to: Route::ClientList {}, class: "pure-button pure-button-primary red", "Cancel" } } } - - } }) } @@ -189,10 +157,6 @@ fn Settings(cx: Scope) -> Element { "Remove all Clients" } - Link { - to: Route::ClientList {}, - class: "pure-button", - "Go back" - } + Link { to: Route::ClientList {}, class: "pure-button", "Go back" } }) } diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 1253efba2..7eb9db375 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element { p { "This should show an image:" } - img { src: "examples/assets/logo.png" } + img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() } } }) } diff --git a/examples/dynamic_asset.rs b/examples/dynamic_asset.rs new file mode 100644 index 000000000..51dcfac3a --- /dev/null +++ b/examples/dynamic_asset.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; +use dioxus_desktop::{use_asset_handler, wry::http::Response}; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + use_asset_handler(cx, "logos", |request, response| { + // Note that the "logos" prefix is stripped from the URI + // + // However, the asset is absolute to its "virtual folder" - meaning it starts with a leading slash + if request.uri().path() != "/logo.png" { + return; + } + + response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec())); + }); + + cx.render(rsx! { + div { + img { + src: "/logos/logo.png" + } + } + }) +} diff --git a/examples/error_handle.rs b/examples/error_handle.rs index d1fb57933..21b16ec45 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -1,4 +1,4 @@ -use dioxus::prelude::*; +use dioxus::{core::CapturedError, prelude::*}; fn main() { dioxus_desktop::launch(App); @@ -6,30 +6,25 @@ fn main() { #[component] fn App(cx: Scope) -> Element { - let val = use_state(cx, || "0.0001"); - - let num = match val.parse::() { - Err(_) => return cx.render(rsx!("Parsing failed")), - Ok(num) => num, - }; - cx.render(rsx! { - h1 { "The parsed value is {num}" } - button { - onclick: move |_| val.set("invalid"), - "Set an invalid number" + ErrorBoundary { + handle_error: |error: CapturedError| rsx! {"Found error {error}"}, + DemoC { + x: 1 + } } - (0..5).map(|i| rsx! { - DemoC { x: i } - }) }) } #[component] fn DemoC(cx: Scope, x: i32) -> Element { + let result = Err("Error"); + + result.throw()?; + cx.render(rsx! { h1 { - "asdasdasdasd {x}" + "{x}" } }) } diff --git a/examples/eval.rs b/examples/eval.rs index 54959df69..65c8acbeb 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -5,26 +5,21 @@ fn main() { } fn app(cx: Scope) -> Element { - let eval_provider = use_eval(cx); - - let future = use_future(cx, (), |_| { - to_owned![eval_provider]; - async move { - let eval = eval_provider( - r#" + let future = use_future(cx, (), |_| async move { + let eval = eval( + r#" dioxus.send("Hi from JS!"); let msg = await dioxus.recv(); console.log(msg); return "hello world"; "#, - ) - .unwrap(); + ) + .unwrap(); - eval.send("Hi from Rust!".into()).unwrap(); - let res = eval.recv().await.unwrap(); - println!("{:?}", eval.await); - res - } + eval.send("Hi from Rust!".into()).unwrap(); + let res = eval.recv().await.unwrap(); + println!("{:?}", eval.await); + res }); match future.value() { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 88b37789d..f0534cbf8 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -18,13 +18,14 @@ fn main() { ); } +const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css")); + fn app(cx: Scope) -> Element { let files = use_ref(cx, Files::new); cx.render(rsx! { div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } - style { include_str!("./assets/fileexplorer.css") } header { i { class: "material-icons icon-menu", "menu" } h1 { "Files: ", files.read().current() } diff --git a/examples/file_upload.rs b/examples/file_upload.rs index f5deab173..2af49f848 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element { r#type: "checkbox", checked: "{enable_directory_upload}", oninput: move |evt| { - enable_directory_upload.set(evt.value.parse().unwrap()); + enable_directory_upload.set(evt.value().parse().unwrap()); }, }, "Enable directory upload" @@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element { onchange: |evt| { to_owned![files_uploaded]; async move { - if let Some(file_engine) = &evt.files { + if let Some(file_engine) = &evt.files() { let files = file_engine.files(); for file_name in files { sleep(std::time::Duration::from_secs(1)).await; diff --git a/examples/form.rs b/examples/form.rs index a7de142f3..897ffc710 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element { div { h1 { "Form" } form { - onsubmit: move |ev| println!("Submitted {:?}", ev.values), - oninput: move |ev| println!("Input {:?}", ev.values), + 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" } diff --git a/examples/login_form.rs b/examples/login_form.rs index 5ee47e4b5..3e8220aef 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -8,33 +8,30 @@ fn main() { } fn app(cx: Scope) -> Element { - let onsubmit = move |evt: FormEvent| { - cx.spawn(async move { - let resp = reqwest::Client::new() - .post("http://localhost:8080/login") - .form(&[ - ("username", &evt.values["username"]), - ("password", &evt.values["password"]), - ]) - .send() - .await; + let onsubmit = move |evt: FormEvent| async move { + let resp = reqwest::Client::new() + .post("http://localhost:8080/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!"), + 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 - you need a login server running on localhost:8080.") - } + //Handle any errors from the fetch here + Err(_err) => { + println!("Login failed - you need a login server running on localhost:8080.") } - }); + } }; cx.render(rsx! { h1 { "Login" } - form { - onsubmit: onsubmit, + form { onsubmit: onsubmit, input { r#type: "text", id: "username", name: "username" } label { "Username" } br {} diff --git a/examples/mobile_demo/.gitignore b/examples/mobile_demo/.gitignore index 419513ee1..e1e084c4b 100644 --- a/examples/mobile_demo/.gitignore +++ b/examples/mobile_demo/.gitignore @@ -2,7 +2,7 @@ target/ **/*.rs.bk -# tauri-mobile +# cargo-mobile2 .cargo/ /gen diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml index d22269a0c..e980774e7 100644 --- a/examples/mobile_demo/Cargo.toml +++ b/examples/mobile_demo/Cargo.toml @@ -35,7 +35,7 @@ frameworks = ["WebKit"] [dependencies] anyhow = "1.0.56" log = "0.4.11" -wry = "0.28.0" +wry = "0.35.0" dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop", features = [ "tokio_runtime", diff --git a/examples/mobile_demo/README.md b/examples/mobile_demo/README.md index 7ea4303b8..178b2f733 100644 --- a/examples/mobile_demo/README.md +++ b/examples/mobile_demo/README.md @@ -4,7 +4,7 @@ Right now, Dioxus supports mobile targets including iOS and Android. However, our tooling is not mature enough to include the build commands directly. -This project was generated using [tauri-mobile](https://github.com/tauri-apps/tauri-mobile). We have yet to integrate this generation into the Dioxus-CLI. The open issue for this is [#1157](https://github.com/DioxusLabs/dioxus/issues/1157). +This project was generated using [cargo-mobile2](https://github.com/tauri-apps/cargo-mobile2). We have yet to integrate this generation into the Dioxus-CLI. The open issue for this is [#1157](https://github.com/DioxusLabs/dioxus/issues/1157). ## Running on iOS diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index e2caeba80..c7ad2c603 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -5,14 +5,12 @@ fn main() { } fn app(cx: Scope) -> Element { - let window = dioxus_desktop::use_window(cx); - cx.render(rsx! { div { button { onclick: move |_| { let dom = VirtualDom::new(popup); - window.new_window(dom, Default::default()); + dioxus_desktop::window().new_window(dom, Default::default()); }, "New Window" } diff --git a/examples/openid_connect_demo/.gitignore b/examples/openid_connect_demo/.gitignore new file mode 100644 index 000000000..919c8ee55 --- /dev/null +++ b/examples/openid_connect_demo/.gitignore @@ -0,0 +1,3 @@ +/target +/dist +.env diff --git a/examples/openid_connect_demo/Cargo.toml b/examples/openid_connect_demo/Cargo.toml new file mode 100644 index 000000000..2a1abe173 --- /dev/null +++ b/examples/openid_connect_demo/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "openid_auth_demo" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +console_error_panic_hook = "0.1" +dioxus-logger = "0.4.1" +dioxus = { path = "../../packages/dioxus", version = "*" } +dioxus-router = { path = "../../packages/router", version = "*" } +dioxus-web = { path = "../../packages/web", version = "*" } +fermi = { path = "../../packages/fermi", version = "*" } +form_urlencoded = "1.2.0" +gloo-storage = "0.3.0" +log = "0.4" +openidconnect = "3.4.0" +reqwest = "0.11.20" +serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0.105" +thiserror = "1.0.48" +uuid = "1.4" +web-sys = { version = "0.3", features = ["Request", "Document"] } diff --git a/examples/openid_connect_demo/Dioxus.toml b/examples/openid_connect_demo/Dioxus.toml new file mode 100644 index 000000000..483dfe957 --- /dev/null +++ b/examples/openid_connect_demo/Dioxus.toml @@ -0,0 +1,47 @@ +[application] + +# dioxus project name +name = "OpenID Connect authentication demo" + +# default platfrom +# you can also use `dioxus serve/build --platform XXX` to use other platform +# value: web | desktop +default_platform = "web" + +# Web `build` & `serve` dist path +out_dir = "dist" + +# resource (static) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "OpenID Connect authentication demo" + +[web.watcher] + +index_on_404 = true + +watch_path = ["src"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = [] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] + +[application.plugins] + +available = true + +required = [] diff --git a/examples/openid_connect_demo/README.md b/examples/openid_connect_demo/README.md new file mode 100644 index 000000000..e16467ddf --- /dev/null +++ b/examples/openid_connect_demo/README.md @@ -0,0 +1,13 @@ +# OpenID Connect example to show how to authenticate an user + +The environment variables in `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)). + +Once they are set, you can run `dx serve` + +### Environment variables summary + +```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url + +```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id + +```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080` \ No newline at end of file diff --git a/examples/openid_connect_demo/src/constants.rs b/examples/openid_connect_demo/src/constants.rs new file mode 100644 index 000000000..0a0b4950a --- /dev/null +++ b/examples/openid_connect_demo/src/constants.rs @@ -0,0 +1,2 @@ +pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token"; +pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request"; diff --git a/examples/openid_connect_demo/src/errors.rs b/examples/openid_connect_demo/src/errors.rs new file mode 100644 index 000000000..30005c1d8 --- /dev/null +++ b/examples/openid_connect_demo/src/errors.rs @@ -0,0 +1,20 @@ +use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Discovery error: {0}")] + OpenIdConnect( + #[from] openidconnect::DiscoveryError>, + ), + #[error("Parsing error: {0}")] + Parse(#[from] url::ParseError), + #[error("Request token error: {0}")] + RequestToken( + #[from] + RequestTokenError< + openidconnect::reqwest::Error, + StandardErrorResponse, + >, + ), +} diff --git a/examples/openid_connect_demo/src/main.rs b/examples/openid_connect_demo/src/main.rs new file mode 100644 index 000000000..7ed8db8a9 --- /dev/null +++ b/examples/openid_connect_demo/src/main.rs @@ -0,0 +1,60 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use fermi::*; +use gloo_storage::{LocalStorage, Storage}; +use log::LevelFilter; +pub(crate) mod constants; +pub(crate) mod errors; +pub(crate) mod model; +pub(crate) mod oidc; +pub(crate) mod props; +pub(crate) mod router; +pub(crate) mod storage; +pub(crate) mod views; +use oidc::{AuthRequestState, AuthTokenState}; +use router::Route; + +use dioxus_router::prelude::*; + +use crate::{ + constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN}, + oidc::ClientState, +}; +pub static FERMI_CLIENT: fermi::AtomRef = AtomRef(|_| ClientState::default()); + +// An option is required to prevent the component from being constantly refreshed +pub static FERMI_AUTH_TOKEN: fermi::AtomRef> = AtomRef(|_| None); +pub static FERMI_AUTH_REQUEST: fermi::AtomRef> = AtomRef(|_| None); + +pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL"); +pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID"); +pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL"); + +fn App(cx: Scope) -> Element { + use_init_atom_root(cx); + + // Retrieve the value stored in the browser's storage + let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN) + .ok() + .unwrap_or(AuthTokenState::default()); + let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN); + if fermi_auth_token.read().is_none() { + *fermi_auth_token.write() = Some(stored_auth_token); + } + + let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST) + .ok() + .unwrap_or(AuthRequestState::default()); + let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST); + if fermi_auth_request.read().is_none() { + *fermi_auth_request.write() = Some(stored_auth_request); + } + render! { Router:: {} } +} + +fn main() { + dioxus_logger::init(LevelFilter::Info).expect("failed to init logger"); + console_error_panic_hook::set_once(); + log::info!("starting app"); + dioxus_web::launch(App); +} diff --git a/examples/openid_connect_demo/src/model/mod.rs b/examples/openid_connect_demo/src/model/mod.rs new file mode 100644 index 000000000..68a79f5c0 --- /dev/null +++ b/examples/openid_connect_demo/src/model/mod.rs @@ -0,0 +1 @@ +pub(crate) mod user; diff --git a/examples/openid_connect_demo/src/model/user.rs b/examples/openid_connect_demo/src/model/user.rs new file mode 100644 index 000000000..b1005fdd2 --- /dev/null +++ b/examples/openid_connect_demo/src/model/user.rs @@ -0,0 +1,7 @@ +use uuid::Uuid; + +#[derive(PartialEq)] +pub struct User { + pub id: Uuid, + pub name: String, +} diff --git a/examples/openid_connect_demo/src/oidc.rs b/examples/openid_connect_demo/src/oidc.rs new file mode 100644 index 000000000..7fa96c14b --- /dev/null +++ b/examples/openid_connect_demo/src/oidc.rs @@ -0,0 +1,125 @@ +use openidconnect::{ + core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse}, + reqwest::async_http_client, + url::Url, + AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl, + LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError, + StandardErrorResponse, +}; +use serde::{Deserialize, Serialize}; + +use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID}; + +#[derive(Clone, Debug, Default)] +pub struct ClientState { + pub oidc_client: Option, +} + +/// State that holds the nonce and authorization url and the nonce generated to log in an user +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct AuthRequestState { + pub auth_request: Option, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct AuthRequest { + pub nonce: Nonce, + pub authorize_url: String, +} + +/// State the tokens returned once the user is authenticated +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct AuthTokenState { + /// Token used to identify the user + pub id_token: Option, + /// Token used to refresh the tokens if they expire + pub refresh_token: Option, +} + +pub fn email( + client: CoreClient, + id_token: CoreIdToken, + nonce: Nonce, +) -> Result { + match id_token.claims(&client.id_token_verifier(), &nonce) { + Ok(claims) => Ok(claims.clone().email().unwrap().to_string()), + Err(error) => Err(error), + } +} + +pub fn authorize_url(client: CoreClient) -> AuthRequest { + let (authorize_url, _csrf_state, nonce) = client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(openidconnect::Scope::new("email".to_string())) + .add_scope(openidconnect::Scope::new("profile".to_string())) + .url(); + AuthRequest { + authorize_url: authorize_url.to_string(), + nonce, + } +} + +pub async fn init_provider_metadata() -> Result { + let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?; + Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?) +} + +pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> { + let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string()); + let provider_metadata = init_provider_metadata().await?; + let client_secret = None; + let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?; + + Ok(( + client_id.clone(), + CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret) + .set_redirect_uri(redirect_url), + )) +} + +///TODO: Add pkce_pacifier +pub async fn token_response( + oidc_client: CoreClient, + code: String, +) -> Result { + // let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + Ok(oidc_client + .exchange_code(AuthorizationCode::new(code.clone())) + // .set_pkce_verifier(pkce_verifier) + .request_async(async_http_client) + .await?) +} + +pub async fn exchange_refresh_token( + oidc_client: CoreClient, + refresh_token: RefreshToken, +) -> Result< + CoreTokenResponse, + RequestTokenError< + openidconnect::reqwest::Error, + StandardErrorResponse, + >, +> { + oidc_client + .exchange_refresh_token(&refresh_token) + .request_async(async_http_client) + .await +} + +pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result { + let provider_metadata = init_provider_metadata().await?; + let end_session_url = provider_metadata + .additional_metadata() + .clone() + .end_session_endpoint + .unwrap(); + let logout_request: LogoutRequest = LogoutRequest::from(end_session_url); + Ok(logout_request + .set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string())) + .set_id_token_hint(&id_token_hint) + .http_get_url()) +} diff --git a/examples/openid_connect_demo/src/props/client.rs b/examples/openid_connect_demo/src/props/client.rs new file mode 100644 index 000000000..2e02111e1 --- /dev/null +++ b/examples/openid_connect_demo/src/props/client.rs @@ -0,0 +1,20 @@ +use dioxus::prelude::*; +use openidconnect::{core::CoreClient, ClientId}; + +#[derive(Props, Clone, Debug)] +pub struct ClientProps { + pub client: CoreClient, + pub client_id: ClientId, +} + +impl PartialEq for ClientProps { + fn eq(&self, other: &Self) -> bool { + self.client_id == other.client_id + } +} + +impl ClientProps { + pub fn new(client_id: ClientId, client: CoreClient) -> Self { + ClientProps { client_id, client } + } +} diff --git a/examples/openid_connect_demo/src/props/mod.rs b/examples/openid_connect_demo/src/props/mod.rs new file mode 100644 index 000000000..1d3313153 --- /dev/null +++ b/examples/openid_connect_demo/src/props/mod.rs @@ -0,0 +1 @@ +pub(crate) mod client; diff --git a/examples/openid_connect_demo/src/router.rs b/examples/openid_connect_demo/src/router.rs new file mode 100644 index 000000000..0bb80afbb --- /dev/null +++ b/examples/openid_connect_demo/src/router.rs @@ -0,0 +1,17 @@ +use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound}; +use dioxus::prelude::*; +use dioxus_router::prelude::*; + +#[derive(Routable, Clone)] +pub enum Route { + #[layout(AuthHeader)] + #[route("/")] + Home {}, + + // https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments + #[route("/login?:query_string")] + Login { query_string: String }, + #[end_layout] + #[route("/:..route")] + NotFound { route: Vec }, +} diff --git a/examples/openid_connect_demo/src/storage.rs b/examples/openid_connect_demo/src/storage.rs new file mode 100644 index 000000000..402740cdf --- /dev/null +++ b/examples/openid_connect_demo/src/storage.rs @@ -0,0 +1,38 @@ +use fermi::UseAtomRef; +use gloo_storage::{LocalStorage, Storage}; +use serde::{Deserialize, Serialize}; + +use crate::{ + constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN}, + oidc::{AuthRequestState, AuthTokenState}, +}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct StorageEntry { + pub key: String, + pub value: T, +} + +pub trait PersistentWrite { + fn persistent_set(atom_ref: &UseAtomRef>, entry: Option); +} + +impl PersistentWrite for AuthTokenState { + fn persistent_set( + atom_ref: &UseAtomRef>, + entry: Option, + ) { + *atom_ref.write() = entry.clone(); + LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap(); + } +} + +impl PersistentWrite for AuthRequestState { + fn persistent_set( + atom_ref: &UseAtomRef>, + entry: Option, + ) { + *atom_ref.write() = entry.clone(); + LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap(); + } +} diff --git a/examples/openid_connect_demo/src/views/header.rs b/examples/openid_connect_demo/src/views/header.rs new file mode 100644 index 000000000..1f2c6e057 --- /dev/null +++ b/examples/openid_connect_demo/src/views/header.rs @@ -0,0 +1,250 @@ +use crate::{ + oidc::{ + authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url, + AuthRequestState, AuthTokenState, ClientState, + }, + props::client::ClientProps, + router::Route, + storage::PersistentWrite, + FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT, +}; +use dioxus::prelude::*; +use dioxus_router::prelude::{Link, Outlet}; +use fermi::*; +use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse}; + +#[component] +pub fn LogOut(cx: Scope) -> Element { + let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN); + let fermi_auth_token_read = fermi_auth_token.read().clone(); + let log_out_url_state = use_state(cx, || None::>>); + cx.render(match fermi_auth_token_read { + Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() { + Some(id_token) => match log_out_url_state.get() { + Some(log_out_url_result) => match log_out_url_result { + Some(uri) => match uri { + Ok(uri) => { + rsx! { + Link { + onclick: move |_| { + { + AuthTokenState::persistent_set( + fermi_auth_token, + Some(AuthTokenState::default()), + ); + } + }, + to: uri.to_string(), + "Log out" + } + } + } + Err(error) => { + rsx! { + div { format!{"Failed to load disconnection url: {:?}", error} } + } + } + }, + None => { + rsx! { div { "Loading... Please wait" } } + } + }, + None => { + let logout_url_task = move || { + cx.spawn({ + let log_out_url_state = log_out_url_state.to_owned(); + async move { + let logout_url = log_out_url(id_token).await; + let logout_url_option = Some(logout_url); + log_out_url_state.set(Some(logout_url_option)); + } + }) + }; + logout_url_task(); + rsx! { div{"Loading log out url... Please wait"}} + } + }, + None => { + rsx! {{}} + } + }, + None => { + rsx! {{}} + } + }) +} + +#[component] +pub fn RefreshToken(cx: Scope) -> Element { + let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN); + let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST); + let fermi_auth_token_read = fermi_auth_token.read().clone(); + cx.render(match fermi_auth_token_read { + Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token { + Some(refresh_token) => { + let fermi_auth_token = fermi_auth_token.to_owned(); + let fermi_auth_request = fermi_auth_request.to_owned(); + let client = cx.props.client.clone(); + let exchange_refresh_token_spawn = move || { + cx.spawn({ + async move { + let exchange_refresh_token = + exchange_refresh_token(client, refresh_token).await; + match exchange_refresh_token { + Ok(response_token) => { + AuthTokenState::persistent_set( + &fermi_auth_token, + Some(AuthTokenState { + id_token: response_token.id_token().cloned(), + refresh_token: response_token.refresh_token().cloned(), + }), + ); + } + Err(_error) => { + AuthTokenState::persistent_set( + &fermi_auth_token, + Some(AuthTokenState::default()), + ); + AuthRequestState::persistent_set( + &fermi_auth_request, + Some(AuthRequestState::default()), + ); + } + } + } + }) + }; + exchange_refresh_token_spawn(); + rsx! { div { "Refreshing session, please wait" } } + } + None => { + rsx! { div { "Id token expired and no refresh token found" } } + } + }, + None => { + rsx! {{}} + } + }) +} + +#[component] +pub fn LoadClient(cx: Scope) -> Element { + let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await }); + let fermi_client: &UseAtomRef = use_atom_ref(cx, &FERMI_CLIENT); + cx.render(match init_client_future.value() { + Some(client_props) => match client_props { + Ok((client_id, client)) => { + *fermi_client.write() = ClientState { + oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())), + }; + rsx! { + div { "Client successfully loaded" } + Outlet:: {} + } + } + Err(error) => { + rsx! { + div { format!{"Failed to load client: {:?}", error} } + log::info!{"Failed to load client: {:?}", error}, + Outlet:: {} + } + } + }, + None => { + rsx! { + div { + div { "Loading client, please wait" } + Outlet:: {} + } + } + } + }) +} + +#[component] +pub fn AuthHeader(cx: Scope) -> Element { + let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN); + let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST); + let fermi_client: &UseAtomRef = use_atom_ref(cx, &FERMI_CLIENT); + let client = fermi_client.read().oidc_client.clone(); + let auth_request_read = fermi_auth_request.read().clone(); + let auth_token_read = auth_token.read().clone(); + cx.render(match (client, auth_request_read, auth_token_read) { + // We have everything we need to attempt to authenticate the user + (Some(client_props), Some(auth_request), Some(auth_token)) => { + match auth_request.auth_request { + Some(auth_request) => { + match auth_token.id_token { + Some(id_token) => { + match email( + client_props.client.clone(), + id_token.clone(), + auth_request.nonce.clone(), + ) { + Ok(email) => { + rsx! { + div { + div { email } + LogOut { client_id: client_props.client_id, client: client_props.client } + Outlet:: {} + } + } + } + // Id token failed to be decoded + Err(error) => match error { + // Id token failed to be decoded because it expired, we refresh it + openidconnect::ClaimsVerificationError::Expired(_message) => { + log::info!("Token expired"); + rsx! { + div { + RefreshToken {client_id: client_props.client_id, client: client_props.client} + Outlet:: {} + } + } + } + // Other issue with token decoding + _ => { + log::info!("Other issue with token"); + rsx! { + div { + div { error.to_string() } + Outlet:: {} + } + } + } + }, + } + } + // User is not logged in + None => { + rsx! { + div { + Link { to: auth_request.authorize_url.clone(), "Log in" } + Outlet:: {} + } + } + } + } + } + None => { + let auth_request = authorize_url(client_props.client); + AuthRequestState::persistent_set( + fermi_auth_request, + Some(AuthRequestState { + auth_request: Some(auth_request), + }), + ); + rsx! { div { "Loading nonce" } } + } + } + } + // Client is not initialized yet, we need it for everything + (None, _, _) => { + rsx! { LoadClient {} } + } + // We need everything loaded before doing anything + (_client, _auth_request, _auth_token) => { + rsx! {{}} + } + }) +} diff --git a/examples/openid_connect_demo/src/views/home.rs b/examples/openid_connect_demo/src/views/home.rs new file mode 100644 index 000000000..f39695302 --- /dev/null +++ b/examples/openid_connect_demo/src/views/home.rs @@ -0,0 +1,5 @@ +use dioxus::prelude::*; + +pub fn Home(cx: Scope) -> Element { + render! { div { "Hello world" } } +} diff --git a/examples/openid_connect_demo/src/views/login.rs b/examples/openid_connect_demo/src/views/login.rs new file mode 100644 index 000000000..2583503de --- /dev/null +++ b/examples/openid_connect_demo/src/views/login.rs @@ -0,0 +1,86 @@ +use crate::{ + oidc::{token_response, AuthRequestState, AuthTokenState}, + router::Route, + storage::PersistentWrite, + DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT, +}; +use dioxus::prelude::*; +use dioxus_router::prelude::{Link, NavigationTarget}; +use fermi::*; +use openidconnect::{OAuth2TokenResponse, TokenResponse}; + +#[component] +pub fn Login(cx: Scope, query_string: String) -> Element { + let fermi_client = use_atom_ref(cx, &FERMI_CLIENT); + let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN); + let home_url: NavigationTarget = DIOXUS_FRONT_URL.parse().unwrap(); + let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST); + let client = fermi_client.read().oidc_client.clone(); + let auth_token_read = fermi_auth_token.read().clone(); + cx.render(match (client, auth_token_read) { + (Some(client_props), Some(auth_token_read)) => { + match (auth_token_read.id_token, auth_token_read.refresh_token) { + (Some(_id_token), Some(_refresh_token)) => { + rsx! { + div { "Sign in successful" } + Link { to: home_url, "Go back home" } + } + } + // If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value + (None, Some(_)) | (Some(_), None) => { + rsx! { + div { "Error while attempting to log in" } + Link { + to: home_url, + onclick: move |_| { + AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default())); + AuthRequestState::persistent_set( + fermi_auth_request, + Some(AuthRequestState::default()), + ); + }, + "Go back home" + } + } + } + (None, None) => { + let mut query_pairs = form_urlencoded::parse(query_string.as_bytes()); + let code_pair = query_pairs.find(|(key, _value)| key == "code"); + match code_pair { + Some((_key, code)) => { + let auth_code = code.to_string(); + let token_response_spawn = move ||{ + cx.spawn({ + let fermi_auth_token = fermi_auth_token.to_owned(); + async move { + let token_response_result = token_response(client_props.client, auth_code).await; + match token_response_result{ + Ok(token_response) => { + let id_token = token_response.id_token().unwrap(); + AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState { + id_token: Some(id_token.clone()), + refresh_token: token_response.refresh_token().cloned() + })); + } + Err(error) => { + log::warn!{"{error}"}; + } + } + } + }) + }; + token_response_spawn(); + rsx!{ div {} } + } + None => { + rsx! { div { "No code provided" } } + } + } + } + } + } + (_, _) => { + rsx! {{}} + } + }) +} diff --git a/examples/openid_connect_demo/src/views/mod.rs b/examples/openid_connect_demo/src/views/mod.rs new file mode 100644 index 000000000..1b8e08a58 --- /dev/null +++ b/examples/openid_connect_demo/src/views/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod header; +pub(crate) mod home; +pub(crate) mod login; +pub(crate) mod not_found; diff --git a/examples/openid_connect_demo/src/views/not_found.rs b/examples/openid_connect_demo/src/views/not_found.rs new file mode 100644 index 000000000..6321dc9fc --- /dev/null +++ b/examples/openid_connect_demo/src/views/not_found.rs @@ -0,0 +1,7 @@ +use dioxus::prelude::*; + +#[component] +pub fn NotFound(cx: Scope, route: Vec) -> Element { + let routes = route.join(""); + render! {rsx! {div{routes}}} +} diff --git a/examples/optional_props.rs b/examples/optional_props.rs index c5652a2e7..8108d437d 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element { a: "asd".to_string(), c: "asd".to_string(), d: Some("asd".to_string()), + e: Some("asd".to_string()), + } + Button { + a: "asd".to_string(), + b: "asd".to_string(), + c: "asd".to_string(), + d: Some("asd".to_string()), e: "asd".to_string(), } + Button { + a: "asd".to_string(), + c: "asd".to_string(), + d: Some("asd".to_string()), + } }) } diff --git a/examples/overlay.rs b/examples/overlay.rs index 0afd830be..a4b9e7bfe 100644 --- a/examples/overlay.rs +++ b/examples/overlay.rs @@ -1,13 +1,11 @@ use dioxus::prelude::*; -use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder}; +use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder}; fn main() { dioxus_desktop::launch_cfg(app, make_config()); } fn app(cx: Scope) -> Element { - let window = use_window(cx); - cx.render(rsx! { div { width: "100%", @@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element { width: "100%", height: "10px", background_color: "black", - onmousedown: move |_| window.drag(), + onmousedown: move |_| dioxus_desktop::window().drag(), } "This is an overlay!" diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index a6c35d17a..2f6cc2803 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -21,7 +21,7 @@ use dioxus::events::*; use dioxus::html::input_data::keyboard_types::Key; use dioxus::html::MouseEvent; use dioxus::prelude::*; -use dioxus_desktop::wry::application::dpi::LogicalSize; +use dioxus_desktop::tao::dpi::LogicalSize; use dioxus_desktop::{Config, WindowBuilder}; fn main() { diff --git a/examples/query_segments_demo/Cargo.toml b/examples/query_segments_demo/Cargo.toml index 4a7d784c7..ee9f49c70 100644 --- a/examples/query_segments_demo/Cargo.toml +++ b/examples/query_segments_demo/Cargo.toml @@ -2,6 +2,7 @@ name = "query_segments_demo" version = "0.1.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/examples/query_segments_demo/src/main.rs b/examples/query_segments_demo/src/main.rs index d9f5f83ef..3303aeecd 100644 --- a/examples/query_segments_demo/src/main.rs +++ b/examples/query_segments_demo/src/main.rs @@ -14,29 +14,35 @@ use dioxus_router::prelude::*; #[derive(Routable, Clone)] #[rustfmt::skip] enum Route { - // segments that start with ?: are query segments - #[route("/blog?:query_params")] + // segments that start with ?:.. are query segments that capture the entire query + #[route("/blog?:..query_params")] BlogPost { // You must include query segments in child variants - query_params: BlogQuerySegments, + query_params: ManualBlogQuerySegments, + }, + // segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax + #[route("/autoblog?:name&:surname")] + AutomaticBlogPost { + name: String, + surname: String, }, } #[derive(Debug, Clone, PartialEq)] -struct BlogQuerySegments { +struct ManualBlogQuerySegments { name: String, surname: String, } /// The display impl needs to display the query in a way that can be parsed: -impl Display for BlogQuerySegments { +impl Display for ManualBlogQuerySegments { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "name={}&surname={}", self.name, self.surname) } } -/// The query segment is anything that implements https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html. You can implement that trait for a struct if you want to parse multiple query parameters. -impl FromQuery for BlogQuerySegments { +/// The query segment is anything that implements . You can implement that trait for a struct if you want to parse multiple query parameters. +impl FromQuery for ManualBlogQuerySegments { fn from_query(query: &str) -> Self { let mut name = None; let mut surname = None; @@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments { } #[component] -fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element { +fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element { render! { div{"This is your blogpost with a query segment:"} div{format!("{:?}", query_params)} } } +#[component] +fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element { + render! { + div{"This is your blogpost with a query segment:"} + div{format!("name={}&surname={}", name, surname)} + } +} + #[component] fn App(cx: Scope) -> Element { render! { Router::{} } diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index f9ecb4939..317288d55 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element { let formatting = "formatting!"; let formatting_tuple = ("a", "b"); let lazy_fmt = format_args!("lazily formatted text"); + let asd = 123; cx.render(rsx! { div { // Elements @@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element { // pass simple rust expressions in class: lazy_fmt, id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), + class: "asd", + class: "{asd}", + // if statements can be used to conditionally render attributes + class: if formatting.contains("form") { "{asd}" }, div { class: { const WORD: &str = "expressions"; diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 37271f160..487aa3fe3 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element { fn DataView(cx: Scope, id: usize) -> Element { let cool_data = use_shared_state::(cx).unwrap(); - let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone()); + let oninput = |e: FormEvent| cool_data.write().set(*id, e.value()); let cool_data = cool_data.read(); let my_data = &cool_data.view(id).unwrap(); diff --git a/examples/signals.rs b/examples/signals.rs index bc627fa8f..0faac0af4 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -6,11 +6,17 @@ fn main() { } fn app(cx: Scope) -> Element { + let running = dioxus_signals::use_signal(cx, || true); let mut count = dioxus_signals::use_signal(cx, || 0); + let saved_values = dioxus_signals::use_signal(cx, || vec![0.to_string()]); + // Signals can be used in async functions without an explicit clone since they're 'static and Copy + // Signals are backed by a runtime that is designed to deeply integrate with Dioxus apps use_future!(cx, || async move { loop { - count += 1; + if running.value() { + count += 1; + } tokio::time::sleep(Duration::from_millis(400)).await; } }); @@ -19,9 +25,25 @@ fn app(cx: Scope) -> Element { h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| running.toggle(), "Toggle counter" } + button { onclick: move |_| saved_values.push(count.value().to_string()), "Save this value" } + button { onclick: move |_| saved_values.write().clear(), "Clear saved values" } + // We can do boolean operations on the current signal value if count.value() > 5 { rsx!{ h2 { "High five!" } } } + + // We can cleanly map signals with iterators + for value in saved_values.read().iter() { + h3 { "Saved value: {value}" } + } + + // We can also use the signal value as a slice + if let [ref first, .., ref last] = saved_values.read().as_slice() { + rsx! { li { "First and last: {first}, {last}" } } + } else { + rsx! { "No saved values" } + } }) } diff --git a/examples/spread.rs b/examples/spread.rs new file mode 100644 index 000000000..45bdc90af --- /dev/null +++ b/examples/spread.rs @@ -0,0 +1,36 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + let _ = dom.rebuild(); + let html = dioxus_ssr::render(&dom); + + println!("{}", html); +} + +fn app(cx: Scope) -> Element { + render! { + Component { + width: "10px", + extra_data: "hello{1}", + extra_data2: "hello{2}", + height: "10px", + left: 1 + } + } +} + +#[component] +fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { + render! { + audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" } + } +} + +#[derive(Props)] +struct Props<'a> { + #[props(extends = GlobalAttributes)] + attributes: Vec>, + extra_data: &'a str, + extra_data2: &'a str, +} diff --git a/examples/streams.rs b/examples/streams.rs new file mode 100644 index 000000000..005f73f53 --- /dev/null +++ b/examples/streams.rs @@ -0,0 +1,33 @@ +use dioxus::prelude::*; +use dioxus_signals::use_signal; +use futures_util::{future, stream, Stream, StreamExt}; +use std::time::Duration; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_signal(cx, || 10); + + use_future(cx, (), |_| async move { + let mut stream = some_stream(); + + while let Some(second) = stream.next().await { + count.set(second); + } + }); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + }) +} + +fn some_stream() -> std::pin::Pin>> { + Box::pin( + stream::once(future::ready(0)).chain(stream::iter(1..).then(|second| async move { + tokio::time::sleep(Duration::from_secs(1)).await; + second + })), + ) +} diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml index 5c2fbe666..26ac202f1 100644 --- a/examples/tailwind/Cargo.toml +++ b/examples/tailwind/Cargo.toml @@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop" } [target.'cfg(target_arch = "wasm32")'.dependencies] -dioxus-web = { path = "../../packages/web" } \ No newline at end of file +dioxus-web = { path = "../../packages/web" } diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml index b440d6a69..b7e78ff7f 100644 --- a/examples/tailwind/Dioxus.toml +++ b/examples/tailwind/Dioxus.toml @@ -30,7 +30,7 @@ watch_path = ["src", "public"] [web.resource] # CSS style file -style = ["/tailwind.css"] +style = [] # Javascript code file script = [] diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md index 552ab402f..95d01f408 100644 --- a/examples/tailwind/README.md +++ b/examples/tailwind/README.md @@ -7,7 +7,7 @@ This example shows how an app might be styled with TailwindCSS. 1. Install the Dioxus CLI: ```bash -cargo install --git https://github.com/DioxusLabs/cli +cargo install dioxus-cli ``` 2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm diff --git a/examples/tailwind/dist/tailwind3531548035813279582.css b/examples/tailwind/dist/tailwind3531548035813279582.css new file mode 100644 index 000000000..2196a9412 --- /dev/null +++ b/examples/tailwind/dist/tailwind3531548035813279582.css @@ -0,0 +1 @@ +*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mb-16{margin-bottom:4rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-5{margin-right:1.25rem}.mt-4{margin-top:1rem}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-5\/6{width:83.3333%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-2{padding:.5rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-24{padding-top:6rem;padding-bottom:6rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.leading-relaxed{line-height:1.625}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}@media (width>=640px){.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (width>=768px){.md\:mb-0{margin-bottom:0}.md\:ml-auto{margin-left:auto}.md\:mt-0{margin-top:0}.md\:w-1\/2{width:50%}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:pr-16{padding-right:4rem}.md\:text-left{text-align:left}}@media (width>=1024px){.lg\:inline-block{display:inline-block}.lg\:w-full{width:100%}.lg\:max-w-lg{max-width:32rem}.lg\:flex-grow{flex-grow:1}.lg\:pr-24{padding-right:6rem}} \ No newline at end of file diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs index 2dbb183c9..10124e47c 100644 --- a/examples/tailwind/src/main.rs +++ b/examples/tailwind/src/main.rs @@ -2,21 +2,23 @@ use dioxus::prelude::*; +const _STYLE: &str = mg!(file("./public/tailwind.css")); + fn main() { #[cfg(not(target_arch = "wasm32"))] - dioxus_desktop::launch_cfg( - app, - dioxus_desktop::Config::new() - .with_custom_head(r#""#.to_string()), - ); + dioxus_desktop::launch(app); #[cfg(target_arch = "wasm32")] dioxus_web::launch(app); } pub fn app(cx: Scope) -> Element { + let grey_background = true; cx.render(rsx!( div { - header { class: "text-gray-400 bg-gray-900 body-font", + header { + class: "text-gray-400 body-font", + // you can use optional attributes to optionally apply a tailwind class + class: if grey_background { "bg-gray-900" }, 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", StacksIcon {} diff --git a/examples/textarea.rs b/examples/textarea.rs index df684aa85..62afc45bb 100644 --- a/examples/textarea.rs +++ b/examples/textarea.rs @@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element { rows: "10", cols: "80", value: "{model}", - oninput: move |e| model.set(e.value.clone()), + oninput: move |e| model.set(e.value().clone()), } }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 4b25507a5..cdf41aaa0 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -7,6 +7,8 @@ fn main() { dioxus_desktop::launch(app); } +const _STYLE: &str = mg!(file("./examples/assets/todomvc.css")); + #[derive(PartialEq, Eq, Clone, Copy)] pub enum FilterState { All, @@ -24,8 +26,6 @@ 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 todo_id = use_state(cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos @@ -47,45 +47,10 @@ pub fn app(cx: Scope<()>) -> Element { let show_clear_completed = todos.values().any(|todo| todo.checked); - let selected = |state| { - if *filter == state { - "selected" - } else { - "false" - } - }; - cx.render(rsx! { section { class: "todoapp", - style { include_str!("./assets/todomvc.css") } - header { class: "header", - h1 {"todos"} - input { - class: "new-todo", - placeholder: "What needs to be done?", - value: "{draft}", - autofocus: "true", - oninput: move |evt| { - draft.set(evt.value.clone()); - }, - onkeydown: move |evt| { - if evt.key() == Key::Enter && !draft.is_empty() { - todos.make_mut().insert( - **todo_id, - TodoItem { - id: **todo_id, - checked: false, - contents: draft.to_string(), - }, - ); - *todo_id.make_mut() += 1; - draft.set("".to_string()); - } - } - } - } - section { - class: "main", + TodoHeader { todos: todos } + section { class: "main", if !todos.is_empty() { rsx! { input { @@ -111,43 +76,58 @@ pub fn app(cx: Scope<()>) -> Element { })) } (!todos.is_empty()).then(|| rsx!( - footer { class: "footer", - span { class: "todo-count", - strong {"{active_todo_count} "} - span {"{active_todo_text} left"} - } - ul { class: "filters", - for (state, state_text, url) in [ - (FilterState::All, "All", "#/"), - (FilterState::Active, "Active", "#/active"), - (FilterState::Completed, "Completed", "#/completed"), - ] { - li { - a { - href: url, - class: selected(state), - onclick: move |_| filter.set(state), - prevent_default: "onclick", - state_text - } - } - } - } - show_clear_completed.then(|| rsx!( - button { - class: "clear-completed", - onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked), - "Clear completed" - } - )) + ListFooter { + active_todo_count: active_todo_count, + active_todo_text: active_todo_text, + show_clear_completed: show_clear_completed, + todos: todos, + filter: filter, } )) } } - footer { class: "info", - p { "Double-click to edit a todo" } - p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} - p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} + PageFooter {} + }) +} + +#[derive(Props)] +pub struct TodoHeaderProps<'a> { + todos: &'a UseState>, +} + +pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element { + let draft = use_state(cx, || "".to_string()); + let todo_id = use_state(cx, || 0); + + cx.render(rsx! { + header { class: "header", + h1 { "todos" } + input { + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + autofocus: "true", + oninput: move |evt| { + draft.set(evt.value().clone()); + }, + onkeydown: move |evt| { + if evt.key() == Key::Enter && !draft.is_empty() { + cx.props + .todos + .make_mut() + .insert( + **todo_id, + TodoItem { + id: **todo_id, + checked: false, + contents: draft.to_string(), + }, + ); + *todo_id.make_mut() += 1; + draft.set("".to_string()); + } + } + } } }) } @@ -167,8 +147,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { let editing = if **is_editing { "editing" } else { "" }; cx.render(rsx!{ - li { - class: "{completed} {editing}", + li { class: "{completed} {editing}", div { class: "view", input { class: "toggle", @@ -176,26 +155,28 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { id: "cbg-{todo.id}", checked: "{todo.checked}", oninput: move |evt| { - cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.todos.make_mut()[&cx.props.id].checked = evt.value().parse().unwrap(); } } label { r#for: "cbg-{todo.id}", - ondblclick: move |_| is_editing.set(true), + ondoubleclick: move |_| is_editing.set(true), prevent_default: "onclick", "{todo.contents}" } button { class: "destroy", - onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); }, - prevent_default: "onclick", + onclick: move |_| { + cx.props.todos.make_mut().remove(&todo.id); + }, + prevent_default: "onclick" } } is_editing.then(|| rsx!{ input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value(), autofocus: "true", onfocusout: move |_| is_editing.set(false), onkeydown: move |evt| { @@ -209,3 +190,76 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { } }) } + +#[derive(Props)] +pub struct ListFooterProps<'a> { + todos: &'a UseState>, + active_todo_count: usize, + active_todo_text: &'a str, + show_clear_completed: bool, + filter: &'a UseState, +} + +pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element { + let active_todo_count = cx.props.active_todo_count; + let active_todo_text = cx.props.active_todo_text; + + let selected = |state| { + if *cx.props.filter == state { + "selected" + } else { + "false" + } + }; + + cx.render(rsx! { + footer { class: "footer", + span { class: "todo-count", + strong { "{active_todo_count} " } + span { "{active_todo_text} left" } + } + ul { class: "filters", + for (state , state_text , url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), +] { + li { + a { + href: url, + class: selected(state), + onclick: move |_| cx.props.filter.set(state), + prevent_default: "onclick", + state_text + } + } + } + } + if cx.props.show_clear_completed { + cx.render(rsx! { + button { + class: "clear-completed", + onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked), + "Clear completed" + } + }) + } + } + }) +} + +pub fn PageFooter(cx: Scope) -> Element { + cx.render(rsx! { + footer { class: "info", + p { "Double-click to edit a todo" } + p { + "Created by " + a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } + } + p { + "Part of " + a { href: "http://todomvc.com", "TodoMVC" } + } + } + }) +} diff --git a/examples/video_stream.rs b/examples/video_stream.rs new file mode 100644 index 000000000..c1e0c3c49 --- /dev/null +++ b/examples/video_stream.rs @@ -0,0 +1,188 @@ +use dioxus::prelude::*; +use dioxus_desktop::wry::http; +use dioxus_desktop::wry::http::Response; +use dioxus_desktop::{use_asset_handler, AssetRequest}; +use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; +use std::{io::SeekFrom, path::PathBuf}; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncSeekExt; +use tokio::io::AsyncWriteExt; + +const VIDEO_PATH: &str = "./examples/assets/test_video.mp4"; + +fn main() { + let video_file = PathBuf::from(VIDEO_PATH); + if !video_file.exists() { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + println!("Downloading video file..."); + let video_url = + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; + let mut response = reqwest::get(video_url).await.unwrap(); + let mut file = tokio::fs::File::create(&video_file).await.unwrap(); + while let Some(chunk) = response.chunk().await.unwrap() { + file.write_all(&chunk).await.unwrap(); + } + }); + } + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + use_asset_handler(cx, "videos", move |request, responder| { + // Using dioxus::spawn works, but is slower than a dedicated thread + tokio::task::spawn(async move { + let video_file = PathBuf::from(VIDEO_PATH); + let mut file = tokio::fs::File::open(&video_file).await.unwrap(); + + match get_stream_response(&mut file, &request).await { + Ok(response) => responder.respond(response), + Err(err) => eprintln!("Error: {}", err), + } + }); + }); + + render! { + div { + video { + src: "/videos/test_video.mp4", + autoplay: true, + controls: true, + width: 640, + height: 480 + } + } + } +} + +/// This was taken from wry's example +async fn get_stream_response( + asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync), + request: &AssetRequest, +) -> Result>, Box> { + // get stream length + let len = { + let old_pos = asset.stream_position().await?; + let len = asset.seek(SeekFrom::End(0)).await?; + asset.seek(SeekFrom::Start(old_pos)).await?; + len + }; + + let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4"); + + // if the webview sent a range header, we need to send a 206 in return + // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. + let http_response = if let Some(range_header) = request.headers().get("range") { + let not_satisfiable = || { + ResponseBuilder::new() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![]) + }; + + // parse range header + let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) { + ranges + .iter() + // map the output back to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return Ok(not_satisfiable()?); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return Ok(not_satisfiable()?); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + // allocate a buf with a suitable capacity + let mut buf = Vec::with_capacity(bytes_to_read as usize); + // seek the file to the starting byte + asset.seek(SeekFrom::Start(start)).await?; + // read the needed bytes + asset.take(bytes_to_read).read_to_end(&mut buf).await?; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } else { + let mut buf = Vec::new(); + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = format!("{:x}", rand::random::()); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}\r\n"); + + resp = resp.header( + CONTENT_TYPE, + format!("multipart/byteranges; boundary={boundary}"), + ); + + for (end, start) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes()).await?; + + // write the needed headers `Content-Type` and `Content-Range` + buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes()) + .await?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) + .await?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes()).await?; + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + let mut local_buf = vec![0_u8; bytes_to_read as usize]; + asset.seek(SeekFrom::Start(start)).await?; + asset.read_exact(&mut local_buf).await?; + buf.extend_from_slice(&local_buf); + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes()).await?; + + resp.body(buf) + } + } else { + resp = resp.header(CONTENT_LENGTH, len); + let mut buf = Vec::with_capacity(len as usize); + asset.read_to_end(&mut buf).await?; + resp.body(buf) + }; + + http_response.map_err(Into::into) +} diff --git a/examples/window_focus.rs b/examples/window_focus.rs index 79a5e5044..612e38d6d 100644 --- a/examples/window_focus.rs +++ b/examples/window_focus.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; +use dioxus_desktop::tao::event::Event as WryEvent; use dioxus_desktop::tao::event::WindowEvent; use dioxus_desktop::use_wry_event_handler; -use dioxus_desktop::wry::application::event::Event as WryEvent; use dioxus_desktop::{Config, WindowCloseBehaviour}; fn main() { diff --git a/examples/window_zoom.rs b/examples/window_zoom.rs index e45d71be2..840c12215 100644 --- a/examples/window_zoom.rs +++ b/examples/window_zoom.rs @@ -1,12 +1,10 @@ use dioxus::prelude::*; -use dioxus_desktop::use_window; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { - let window = use_window(cx); let level = use_state(cx, || 1.0); cx.render(rsx! { @@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element { r#type: "number", value: "{level}", oninput: |e| { - if let Ok(new_zoom) = e.value.parse::() { + if let Ok(new_zoom) = e.value().parse::() { level.set(new_zoom); - window.webview.zoom(new_zoom); + dioxus_desktop::window().webview.zoom(new_zoom); } } } diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 5272d661b..73da2ff14 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -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| contents.set(e.value()), } } }) diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..e5c50616d --- /dev/null +++ b/flake.lock @@ -0,0 +1,247 @@ +{ + "nodes": { + "crane": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1696384830, + "narHash": "sha256-j8ZsVqzmj5sOm5MW9cqwQJUZELFFwOislDmqDDEMl6k=", + "owner": "ipetkov", + "repo": "crane", + "rev": "f2143cd27f8bd09ee4f0121336c65015a2a0a19c", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696267196, + "narHash": "sha256-AAQ/2sD+0D18bb8hKuEEVpHUYD1GmO2Uh/taFamn6XQ=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "4f910c9827911b1ec2bf26b5a062cd09f8d89f85", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1696343447, + "narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697009197, + "narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1696019113, + "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1681358109, + "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay_2", + "systems": "systems_3" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "crane", + "flake-utils" + ], + "nixpkgs": [ + "crane", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1696299134, + "narHash": "sha256-RS77cAa0N+Sfj5EmKbm5IdncNXaBCE1BSSQvUE8exvo=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "611ccdceed92b4d94ae75328148d84ee4a5b462d", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1697076655, + "narHash": "sha256-NcCtVUOd0X81srZkrdP8qoA1BMsPdO2tGtlZpsGijeU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "aa7584f5bbf5947716ad8ec14eccc0334f0d28f0", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..7f151a660 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default"; + + rust-overlay.url = "github:oxalica/rust-overlay"; + crane.url = "github:ipetkov/crane"; + crane.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + ]; + }; + rustBuildInputs = [ + pkgs.openssl + pkgs.libiconv + pkgs.pkg-config + ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [ + IOKit + Carbon + WebKit + Security + Cocoa + ]); + + # This is useful when building crates as packages + # Note that it does require a `Cargo.lock` which this repo does not have + # craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + in + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.rust-overlay.overlays.default + ]; + }; + + devShells.default = pkgs.mkShell { + name = "dioxus-dev"; + buildInputs = rustBuildInputs; + nativeBuildInputs = [ + # Add shell dependencies here + rustToolchain + ]; + shellHook = '' + # For rust-analyzer 'hover' tooltips to work. + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + ''; + }; + }; + }; +} diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index fea5a4a3f..f19a7d8dc 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -8,13 +8,14 @@ use std::fmt::{Result, Write}; use dioxus_rsx::IfmtInput; -use crate::write_ifmt; +use crate::{indent::IndentOptions, write_ifmt}; /// The output buffer that tracks indent and string #[derive(Debug, Default)] pub struct Buffer { pub buf: String, - pub indent: usize, + pub indent_level: usize, + pub indent: IndentOptions, } impl Buffer { @@ -31,16 +32,16 @@ impl Buffer { } pub fn tab(&mut self) -> Result { - self.write_tabs(self.indent) + self.write_tabs(self.indent_level) } pub fn indented_tab(&mut self) -> Result { - self.write_tabs(self.indent + 1) + self.write_tabs(self.indent_level + 1) } pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result { for _ in 0..num { - write!(self.buf, " ")? + write!(self.buf, "{}", self.indent.indent_str())? } Ok(()) } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index dbc336401..48077fa16 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,6 +49,7 @@ impl Writer<'_> { attributes, children, brace, + .. } = el; /* @@ -66,7 +67,7 @@ impl Writer<'_> { // check if we have a lot of attributes let attr_len = self.is_short_attrs(attributes); - let is_short_attr_list = (attr_len + self.out.indent * 4) < 80; + let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80; let children_len = self.is_short_children(children); let is_small_children = children_len.is_some(); @@ -86,7 +87,7 @@ impl Writer<'_> { // if we have few children and few attributes, make it a one-liner if is_short_attr_list && is_small_children { - if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 { + if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 { opt_level = ShortOptimization::Oneliner; } else { opt_level = ShortOptimization::PropsOnTop; @@ -165,7 +166,7 @@ impl Writer<'_> { fn write_attributes( &mut self, - attributes: &[ElementAttrNamed], + attributes: &[AttributeType], key: &Option, sameline: bool, ) -> Result { @@ -185,11 +186,11 @@ impl Writer<'_> { } while let Some(attr) = attr_iter.next() { - self.out.indent += 1; + self.out.indent_level += 1; if !sameline { - self.write_comments(attr.attr.start())?; + self.write_comments(attr.start())?; } - self.out.indent -= 1; + self.out.indent_level -= 1; if !sameline { self.out.indented_tabbed_line()?; @@ -209,12 +210,34 @@ impl Writer<'_> { Ok(()) } - fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result { - match &attr.attr { - ElementAttr::AttrText { name, value } => { - write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?; + fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result { + match attr { + ElementAttrName::BuiltIn(name) => { + write!(self.out, "{}", name)?; } - ElementAttr::AttrExpression { name, value } => { + ElementAttrName::Custom(name) => { + write!(self.out, "{}", name.to_token_stream())?; + } + } + + Ok(()) + } + + fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result { + match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + write!( + self.out, + "if {condition} {{ ", + condition = prettyplease::unparse_expr(condition), + )?; + self.write_attribute_value(value)?; + write!(self.out, " }}")?; + } + ElementAttrValue::AttrLiteral(value) => { + write!(self.out, "{value}", value = ifmt_to_string(value))?; + } + ElementAttrValue::AttrExpr(value) => { let out = prettyplease::unparse_expr(value); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); @@ -222,9 +245,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -237,22 +260,7 @@ impl Writer<'_> { } } } - - ElementAttr::CustomAttrText { name, value } => { - write!( - self.out, - "{name}: {value}", - name = name.to_token_stream(), - value = ifmt_to_string(value) - )?; - } - - ElementAttr::CustomAttrExpression { name, value } => { - let out = prettyplease::unparse_expr(value); - write!(self.out, "{}: {}", name.to_token_stream(), out)?; - } - - ElementAttr::EventTokens { name, tokens } => { + ElementAttrValue::EventTokens(tokens) => { let out = self.retrieve_formatted_expr(tokens).to_string(); let mut lines = out.split('\n').peekable(); @@ -261,9 +269,9 @@ impl Writer<'_> { // a one-liner for whatever reason // Does not need a new line if lines.peek().is_none() { - write!(self.out, "{name}: {first}")?; + write!(self.out, "{first}")?; } else { - writeln!(self.out, "{name}: {first}")?; + writeln!(self.out, "{first}")?; while let Some(line) = lines.next() { self.out.indented_tab()?; @@ -281,6 +289,28 @@ impl Writer<'_> { Ok(()) } + fn write_attribute(&mut self, attr: &AttributeType) -> Result { + match attr { + AttributeType::Named(attr) => self.write_named_attribute(attr), + AttributeType::Spread(attr) => self.write_spread_attribute(attr), + } + } + + fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result { + self.write_attribute_name(&attr.attr.name)?; + write!(self.out, ": ")?; + self.write_attribute_value(&attr.attr.value)?; + + Ok(()) + } + + fn write_spread_attribute(&mut self, attr: &Expr) -> Result { + write!(self.out, "..")?; + write!(self.out, "{}", prettyplease::unparse_expr(attr))?; + + Ok(()) + } + // make sure the comments are actually relevant to this element. // test by making sure this element is the primary element on this line pub fn current_span_is_primary(&self, location: Span) -> bool { @@ -398,14 +428,14 @@ impl Writer<'_> { for idx in start.line..end.line { let line = &self.src[idx]; if line.trim().starts_with("//") { - for _ in 0..self.out.indent + 1 { + for _ in 0..self.out.indent_level + 1 { write!(self.out, " ")? } writeln!(self.out, "{}", line.trim()).unwrap(); } } - for _ in 0..self.out.indent { + for _ in 0..self.out.indent_level { write!(self.out, " ")? } diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index b0257f8c3..b7a93a4fc 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -29,7 +29,7 @@ impl Writer<'_> { let first_line = &self.src[start.line - 1]; write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?; - let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4; + let prev_block_indent_level = self.out.indent.count_indents(first_line); for (id, line) in self.src[start.line..end.line].iter().enumerate() { writeln!(self.out)?; @@ -43,9 +43,9 @@ impl Writer<'_> { }; // trim the leading whitespace - let previous_indent = crate::leading_whitespaces(line) / 4; + let previous_indent = self.out.indent.count_indents(line); let offset = previous_indent.saturating_sub(prev_block_indent_level); - let required_indent = self.out.indent + offset; + let required_indent = self.out.indent_level + offset; self.out.write_tabs(required_indent)?; let line = line.trim_start(); diff --git a/packages/autofmt/src/indent.rs b/packages/autofmt/src/indent.rs new file mode 100644 index 000000000..2cce7cf1e --- /dev/null +++ b/packages/autofmt/src/indent.rs @@ -0,0 +1,108 @@ +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum IndentType { + Spaces, + Tabs, +} + +#[derive(Debug, Clone)] +pub struct IndentOptions { + width: usize, + indent_string: String, +} + +impl IndentOptions { + pub fn new(typ: IndentType, width: usize) -> Self { + assert_ne!(width, 0, "Cannot have an indent width of 0"); + Self { + width, + indent_string: match typ { + IndentType::Tabs => "\t".into(), + IndentType::Spaces => " ".repeat(width), + }, + } + } + + /// Gets a string containing one indent worth of whitespace + pub fn indent_str(&self) -> &str { + &self.indent_string + } + + /// Computes the line length in characters, counting tabs as the indent width. + pub fn line_length(&self, line: &str) -> usize { + line.chars() + .map(|ch| if ch == '\t' { self.width } else { 1 }) + .sum() + } + + /// Estimates how many times the line has been indented. + pub fn count_indents(&self, mut line: &str) -> usize { + let mut indent = 0; + while !line.is_empty() { + // Try to count tabs + let num_tabs = line.chars().take_while(|ch| *ch == '\t').count(); + if num_tabs > 0 { + indent += num_tabs; + line = &line[num_tabs..]; + continue; + } + + // Try to count spaces + let num_spaces = line.chars().take_while(|ch| *ch == ' ').count(); + if num_spaces >= self.width { + // Intentionally floor here to take only the amount of space that matches an indent + let num_space_indents = num_spaces / self.width; + indent += num_space_indents; + line = &line[num_space_indents * self.width..]; + continue; + } + + // Line starts with either non-indent characters or an unevent amount of spaces, + // so no more indent remains. + break; + } + indent + } +} + +impl Default for IndentOptions { + fn default() -> Self { + Self::new(IndentType::Spaces, 4) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn count_indents() { + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"), + 0 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"), + 1 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"), + 2 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"), + 2 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"), + 2 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"), + 2 + ); + assert_eq!( + IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"), + 2 + ); + } +} diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 8ec33f0f0..0c3fe6bb8 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + use std::fmt::{Display, Write}; use crate::writer::*; @@ -12,8 +16,11 @@ mod collect_macros; mod component; mod element; mod expr; +mod indent; mod writer; +pub use indent::{IndentOptions, IndentType}; + /// A modification to the original file to be applied by an IDE /// /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes. @@ -43,7 +50,7 @@ pub struct FormattedBlock { /// back to the file precisely. /// /// Nested blocks of RSX will be handled automatically -pub fn fmt_file(contents: &str) -> Vec { +pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec { let mut formatted_blocks = Vec::new(); let parsed = syn::parse_file(contents).unwrap(); @@ -57,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec { } let mut writer = Writer::new(contents); + writer.out.indent = indent; // Don't parse nested macros let mut end_span = LineColumn { column: 0, line: 0 }; @@ -72,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec { let rsx_start = macro_path.span().start(); - writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4; + writer.out.indent_level = writer + .out + .indent + .count_indents(writer.src[rsx_start.line - 1]); write_body(&mut writer, &body); @@ -155,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { buf.consume() } -pub fn fmt_block(block: &str, indent_level: usize) -> Option { +pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option { let body = syn::parse_str::(block).unwrap(); let mut buf = Writer::new(block); - buf.out.indent = indent_level; + buf.out.indent = indent; + buf.out.indent_level = indent_level; write_body(&mut buf, &body); @@ -226,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f let display = DisplayIfmt(input); write!(writable, "{}", display) } - -pub fn leading_whitespaces(input: &str) -> usize { - input - .chars() - .map_while(|c| match c { - ' ' => Some(1), - '\t' => Some(4), - _ => None, - }) - .sum() -} diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 59f135592..d569cdb9f 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,4 @@ -use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop}; +use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; use std::{ @@ -96,11 +96,11 @@ impl<'a> Writer<'a> { // Push out the indent level and write each component, line by line pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result { - self.out.indent += 1; + self.out.indent_level += 1; self.write_body_no_indent(children)?; - self.out.indent -= 1; + self.out.indent_level -= 1; Ok(()) } @@ -132,12 +132,45 @@ impl<'a> Writer<'a> { Ok(()) } - pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize { + pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize { + match value { + ElementAttrValue::AttrOptionalExpr { condition, value } => { + let condition_len = self.retrieve_formatted_expr(condition).len(); + let value_len = self.attr_value_len(value); + + condition_len + value_len + 6 + } + ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), + ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), + ElementAttrValue::EventTokens(tokens) => { + let location = Location::new(tokens.span().start()); + + let len = if let std::collections::hash_map::Entry::Vacant(e) = + self.cached_formats.entry(location) + { + let formatted = prettyplease::unparse_expr(tokens); + let len = if formatted.contains('\n') { + 10000 + } else { + formatted.len() + }; + e.insert(formatted); + len + } else { + self.cached_formats[&location].len() + }; + + len + } + } + } + + pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize { let mut total = 0; for attr in attributes { - if self.current_span_is_primary(attr.attr.start()) { - 'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() { + if self.current_span_is_primary(attr.start()) { + 'line: for line in self.src[..attr.start().start().line - 1].iter().rev() { match (line.trim().starts_with("//"), line.is_empty()) { (true, _) => return 100000, (_, true) => continue 'line, @@ -146,40 +179,25 @@ impl<'a> Writer<'a> { } } - total += match &attr.attr { - ElementAttr::AttrText { value, name } => { - ifmt_to_string(value).len() + name.span().line_length() + 6 - } - ElementAttr::AttrExpression { name, value } => { - value.span().line_length() + name.span().line_length() + 6 - } - ElementAttr::CustomAttrText { value, name } => { - ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6 - } - ElementAttr::CustomAttrExpression { name, value } => { - name.to_token_stream().to_string().len() + value.span().line_length() + 6 - } - ElementAttr::EventTokens { tokens, name } => { - let location = Location::new(tokens.span().start()); - - let len = if let std::collections::hash_map::Entry::Vacant(e) = - self.cached_formats.entry(location) - { - let formatted = prettyplease::unparse_expr(tokens); - let len = if formatted.contains('\n') { - 10000 - } else { - formatted.len() - }; - e.insert(formatted); - len - } else { - self.cached_formats[&location].len() + match attr { + AttributeType::Named(attr) => { + let name_len = match &attr.attr.name { + dioxus_rsx::ElementAttrName::BuiltIn(name) => { + let name = name.to_string(); + name.len() + } + dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2, }; - - len + name.span().line_length() + 6 + total += name_len; + total += self.attr_value_len(&attr.attr.value); + } + AttributeType::Spread(expr) => { + let expr_len = self.retrieve_formatted_expr(expr).len(); + total += expr_len + 3; } }; + + total += 6; } total @@ -218,7 +236,7 @@ impl<'a> Writer<'a> { } } -trait SpanLength { +pub(crate) trait SpanLength { fn line_length(&self) -> usize; } impl SpanLength for Span { diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 8145b0e8e..9431d700e 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -12,7 +12,7 @@ macro_rules! twoway { #[test] fn $name() { let src = include_str!(concat!("./samples/", stringify!($name), ".rsx")); - let formatted = dioxus_autofmt::fmt_file(src); + let formatted = dioxus_autofmt::fmt_file(src, Default::default()); let out = dioxus_autofmt::apply_formats(src, formatted); // normalize line endings let out = out.replace("\r", ""); diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index a0e573974..a3bfd1388 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -33,7 +33,7 @@ rsx! { } // No children, minimal props - img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" } + img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" } // One level compression div { diff --git a/packages/autofmt/tests/wrong.rs b/packages/autofmt/tests/wrong.rs index 5a0fb8a87..06a0f00d1 100644 --- a/packages/autofmt/tests/wrong.rs +++ b/packages/autofmt/tests/wrong.rs @@ -1,10 +1,12 @@ +use dioxus_autofmt::{IndentOptions, IndentType}; + macro_rules! twoway { - ($val:literal => $name:ident) => { + ($val:literal => $name:ident ($indent:expr)) => { #[test] fn $name() { let src_right = include_str!(concat!("./wrong/", $val, ".rsx")); let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx")); - let formatted = dioxus_autofmt::fmt_file(src_wrong); + let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent); let out = dioxus_autofmt::apply_formats(src_wrong, formatted); // normalize line endings @@ -16,8 +18,11 @@ macro_rules! twoway { }; } -twoway!("comments" => comments); +twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4))); +twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4))); -twoway!("multi" => multi); +twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4))); +twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4))); -twoway!("multiexpr" => multiexpr); +twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4))); +twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4))); diff --git a/packages/autofmt/tests/wrong/comments.rsx b/packages/autofmt/tests/wrong/comments-4sp.rsx similarity index 100% rename from packages/autofmt/tests/wrong/comments.rsx rename to packages/autofmt/tests/wrong/comments-4sp.rsx diff --git a/packages/autofmt/tests/wrong/comments.wrong.rsx b/packages/autofmt/tests/wrong/comments-4sp.wrong.rsx similarity index 100% rename from packages/autofmt/tests/wrong/comments.wrong.rsx rename to packages/autofmt/tests/wrong/comments-4sp.wrong.rsx diff --git a/packages/autofmt/tests/wrong/comments-tab.rsx b/packages/autofmt/tests/wrong/comments-tab.rsx new file mode 100644 index 000000000..4c05e347e --- /dev/null +++ b/packages/autofmt/tests/wrong/comments-tab.rsx @@ -0,0 +1,7 @@ +rsx! { + div { + // Comments + class: "asdasd", + "hello world" + } +} diff --git a/packages/autofmt/tests/wrong/comments-tab.wrong.rsx b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx new file mode 100644 index 000000000..eac96642d --- /dev/null +++ b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx @@ -0,0 +1,5 @@ +rsx! { + div { + // Comments + class: "asdasd", "hello world" } +} diff --git a/packages/autofmt/tests/wrong/multi.rsx b/packages/autofmt/tests/wrong/multi-4sp.rsx similarity index 100% rename from packages/autofmt/tests/wrong/multi.rsx rename to packages/autofmt/tests/wrong/multi-4sp.rsx diff --git a/packages/autofmt/tests/wrong/multi.wrong.rsx b/packages/autofmt/tests/wrong/multi-4sp.wrong.rsx similarity index 100% rename from packages/autofmt/tests/wrong/multi.wrong.rsx rename to packages/autofmt/tests/wrong/multi-4sp.wrong.rsx diff --git a/packages/autofmt/tests/wrong/multi-tab.rsx b/packages/autofmt/tests/wrong/multi-tab.rsx new file mode 100644 index 000000000..1fb85c3d0 --- /dev/null +++ b/packages/autofmt/tests/wrong/multi-tab.rsx @@ -0,0 +1,3 @@ +fn app(cx: Scope) -> Element { + cx.render(rsx! { div { "hello world" } }) +} diff --git a/packages/autofmt/tests/wrong/multi-tab.wrong.rsx b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx new file mode 100644 index 000000000..d818f9535 --- /dev/null +++ b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx @@ -0,0 +1,5 @@ +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div {"hello world" } + }) +} diff --git a/packages/autofmt/tests/wrong/multiexpr.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx similarity index 100% rename from packages/autofmt/tests/wrong/multiexpr.rsx rename to packages/autofmt/tests/wrong/multiexpr-4sp.rsx diff --git a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx similarity index 100% rename from packages/autofmt/tests/wrong/multiexpr.wrong.rsx rename to packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.rsx new file mode 100644 index 000000000..1fc3401c4 --- /dev/null +++ b/packages/autofmt/tests/wrong/multiexpr-tab.rsx @@ -0,0 +1,8 @@ +fn ItWroks() { + cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", + left, + right + } + }) +} diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx new file mode 100644 index 000000000..073107541 --- /dev/null +++ b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx @@ -0,0 +1,5 @@ +fn ItWroks() { + cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + }) +} diff --git a/packages/check/Cargo.toml b/packages/check/Cargo.toml index 90422780a..52b98cc44 100644 --- a/packages/check/Cargo.toml +++ b/packages/check/Cargo.toml @@ -11,13 +11,10 @@ keywords = ["dom", "ui", "gui", "react"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-rsx = { workspace = true } proc-macro2 = { version = "1.0.6", features = ["span-locations"] } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] } -serde = { version = "1.0.136", features = ["derive"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] } -prettyplease = { workspace = true } [dev-dependencies] indoc = "2.0.3" diff --git a/packages/check/README.md b/packages/check/README.md index 0bb6ce0a4..6375a8316 100644 --- a/packages/check/README.md +++ b/packages/check/README.md @@ -6,7 +6,7 @@ [![Discord chat][discord-badge]][discord-url] [crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg -[crates-url]: https://crates.io/crates/dioxus-autofmt +[crates-url]: https://crates.io/crates/dioxus-check [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg [mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE [actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg @@ -16,7 +16,7 @@ [Website](https://dioxuslabs.com) | [Guides](https://dioxuslabs.com/learn/0.4/) | -[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) | +[API Docs](https://docs.rs/dioxus-check) | [Chat](https://discord.gg/XgGxMSkvUM) ## Overview diff --git a/packages/check/src/lib.rs b/packages/check/src/lib.rs index 3870fb610..43a164514 100644 --- a/packages/check/src/lib.rs +++ b/packages/check/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + mod check; mod issues; mod metadata; diff --git a/packages/cli/.github/workflows/build.yml b/packages/cli/.github/workflows/build.yml deleted file mode 100644 index f0765e951..000000000 --- a/packages/cli/.github/workflows/build.yml +++ /dev/null @@ -1,31 +0,0 @@ -# .github/workflows/build.yml - -on: - release: - types: [created] - -jobs: - release: - name: release ${{ matrix.target }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - archive: tar.gz tar.xz - - target: x86_64-unknown-linux-musl - archive: tar.gz tar.xz - - target: x86_64-apple-darwin - archive: tar.gz tar.xz - - target: x86_64-pc-windows-gnu - archive: zip - - steps: - - uses: actions/checkout@master - - name: Compile and release - uses: rust-build/rust-build.action@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUSTTARGET: ${{ matrix.target }} - ARCHIVE_TYPES: ${{ matrix.archive }} diff --git a/packages/cli/.github/workflows/docs.yml b/packages/cli/.github/workflows/docs.yml deleted file mode 100644 index d8765cac8..000000000 --- a/packages/cli/.github/workflows/docs.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: github pages - -on: - push: - paths: - - docs/** - branches: - - master - -jobs: - deploy: - runs-on: ubuntu-20.04 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - steps: - - uses: actions/checkout@v2 - - - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 - with: - mdbook-version: '0.4.10' - # mdbook-version: 'latest' - - - run: cd docs && mdbook build - - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.2.3 - with: - branch: gh-pages # The branch the action should deploy to. - folder: docs/book # The folder the action should deploy. - target-folder: docs/nightly/cli - repository-name: dioxuslabs/docsite - clean: false - token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now diff --git a/packages/cli/.github/workflows/main.yml b/packages/cli/.github/workflows/main.yml deleted file mode 100644 index b22f78eae..000000000 --- a/packages/cli/.github/workflows/main.yml +++ /dev/null @@ -1,52 +0,0 @@ -on: [push, pull_request] - -name: Rust CI - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v1 - - run: cargo check - - test: - name: Test Suite - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v1 - - run: cargo test - - fmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v1 - - run: rustup component add rustfmt - - run: cargo fmt --all -- --check - - # clippy: - # name: Clippy - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 - # - uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: stable - # override: true - # - uses: Swatinem/rust-cache@v1 - # - run: rustup component add clippy - # - uses: actions-rs/cargo@v1 - # with: - # command: clippy - # args: -- -D warnings diff --git a/packages/cli/Cargo.lock b/packages/cli/Cargo.lock deleted file mode 100644 index 707a02b6e..000000000 --- a/packages/cli/Cargo.lock +++ /dev/null @@ -1,4778 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aes" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.10", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.2.10", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "aligned" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a21b9440a626c7fc8573a9e3d3a06b75c7c97754c2949bc7857b90353ca655" -dependencies = [ - "as-slice", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" - -[[package]] -name = "anstyle-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "anymap2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "async-compression" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" -dependencies = [ - "brotli", - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "axum" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.13.1", - "bitflags", - "bytes", - "futures-util", - "headers", - "http", - "http-body", - "hyper", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sha-1", - "sync_wrapper", - "tokio", - "tokio-tungstenite", - "tower", - "tower-http 0.3.5", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "mime", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide 0.6.2", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -dependencies = [ - "byteorder", - "safemem", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "binary-install" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bc5f8c50dd6a80d0b303ddab79f42ddcb52fd43d68107ecf622c551fd4cd4" -dependencies = [ - "curl", - "dirs 1.0.5", - "failure", - "flate2", - "hex 0.3.2", - "is_executable", - "siphasher", - "tar", - "zip 0.5.13", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "memchr", -] - -[[package]] -name = "bstr" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" -dependencies = [ - "memchr", - "once_cell", - "regex-automata", - "serde", -] - -[[package]] -name = "btoi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "bumpslab" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5816c875b50b9866d759fa24d46159dccab0d7942c0ccbfd700b4f45dd961e" -dependencies = [ - "bumpalo", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "camino" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-generate" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b2f627381dc7523340c606559dddf6083cb2e6134368381da5778638f906d8" -dependencies = [ - "anyhow", - "clap", - "console", - "dialoguer", - "env_logger", - "git2", - "gix-config", - "heck 0.4.1", - "home", - "ignore", - "indicatif", - "liquid", - "liquid-core", - "liquid-derive", - "liquid-lib", - "log", - "names", - "paste", - "path-absolutize", - "regex", - "remove_dir_all", - "rhai", - "sanitize-filename", - "semver", - "serde", - "tempfile", - "thiserror", - "toml 0.7.5", - "walkdir", -] - -[[package]] -name = "cargo-platform" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "cargo_toml" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c3ff59e3b7d24630206bb63a73af65da4ed5df1f76ee84dfafb9fee2ba60e" -dependencies = [ - "serde", - "serde_derive", - "toml 0.5.11", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-expr" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbc13bf6290a6b202cc3efb36f7ec2b739a80634215630c8053a313edf6abef" -dependencies = [ - "smallvec", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" -dependencies = [ - "anstream", - "anstyle", - "bitflags", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "clap_lex" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - -[[package]] -name = "colored" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "console" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.45.0", -] - -[[package]] -name = "const-random" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" -dependencies = [ - "getrandom 0.2.10", - "once_cell", - "proc-macro-hack", - "tiny-keccak", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "convert_case" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctrlc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" -dependencies = [ - "nix", - "windows-sys 0.48.0", -] - -[[package]] -name = "curl" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2 0.4.9", - "winapi", -] - -[[package]] -name = "curl-sys" -version = "0.4.63+curl-8.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "winapi", -] - -[[package]] -name = "cvt" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "darling" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "darling_macro" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "data-encoding" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" - -[[package]] -name = "dialoguer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" -dependencies = [ - "console", - "shell-words", - "tempfile", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "dioxus-autofmt" -version = "0.3.0" -source = "git+https://github.com/DioxusLabs/dioxus#11c9abcf7ce731ccb4a44c52de383c090ab319af" -dependencies = [ - "dioxus-rsx", - "prettier-please", - "proc-macro2", - "quote", - "serde", - "syn 1.0.109", -] - -[[package]] -name = "dioxus-cli" -version = "0.3.1" -dependencies = [ - "anyhow", - "atty", - "axum", - "binary-install", - "cargo-generate", - "cargo_metadata", - "cargo_toml", - "chrono", - "clap", - "colored 2.0.0", - "convert_case", - "ctrlc", - "dioxus-autofmt", - "dioxus-core", - "dioxus-html", - "dioxus-rsx", - "dirs 4.0.0", - "fern", - "flate2", - "fs_extra", - "futures", - "gitignore", - "headers", - "html_parser", - "hyper", - "hyper-rustls 0.23.2", - "indicatif", - "lazy_static", - "log", - "mlua", - "notify", - "open", - "proc-macro2", - "regex", - "reqwest", - "rsx-rosetta", - "serde", - "serde_json", - "subprocess", - "syn 1.0.109", - "tar", - "thiserror", - "tokio", - "toml 0.5.11", - "toml_edit", - "tower", - "tower-http 0.2.5", - "walkdir", - "wasm-bindgen-cli-support", - "zip 0.6.6", -] - -[[package]] -name = "dioxus-core" -version = "0.3.3" -source = "git+https://github.com/DioxusLabs/dioxus#11c9abcf7ce731ccb4a44c52de383c090ab319af" -dependencies = [ - "bumpalo", - "bumpslab", - "futures-channel", - "futures-util", - "indexmap 1.9.3", - "log", - "longest-increasing-subsequence", - "rustc-hash", - "serde", - "slab", - "smallbox", -] - -[[package]] -name = "dioxus-html" -version = "0.3.1" -source = "git+https://github.com/DioxusLabs/dioxus#11c9abcf7ce731ccb4a44c52de383c090ab319af" -dependencies = [ - "async-trait", - "dioxus-core", - "dioxus-rsx", - "enumset", - "euclid", - "keyboard-types", - "serde", - "serde-value", - "serde_repr", -] - -[[package]] -name = "dioxus-rsx" -version = "0.0.3" -source = "git+https://github.com/DioxusLabs/dioxus#11c9abcf7ce731ccb4a44c52de383c090ab319af" -dependencies = [ - "dioxus-core", - "internment", - "krates", - "proc-macro2", - "quote", - "serde", - "syn 1.0.109", -] - -[[package]] -name = "dirs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" -dependencies = [ - "libc", - "redox_users 0.3.5", - "winapi", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users 0.4.3", - "winapi", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-as-inner" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "enumset" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e875f1719c16de097dee81ed675e2d9bb63096823ed3f0ca827b7dea3028bbbb" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "equivalent" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "euclid" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" -dependencies = [ - "num-traits", - "serde", -] - -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "colored 1.9.3", - "log", -] - -[[package]] -name = "filetime" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.2.16", - "windows-sys 0.48.0", -] - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide 0.7.1", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_at" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15550ecca96ea332ec143fb450701074143b70d358e50b32b1f847ccff2e1cf7" -dependencies = [ - "aligned", - "cfg-if", - "cvt", - "libc", - "nix", - "windows-sys 0.48.0", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "gimli" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" - -[[package]] -name = "git2" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "openssl-probe", - "openssl-sys", - "url", -] - -[[package]] -name = "gitignore" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d051488d9a601181a9b90c9ad8ae7e8251d642ddd2463008f2f5019d255bd89" -dependencies = [ - "glob", -] - -[[package]] -name = "gix-actor" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc22b0cdc52237667c301dd7cdc6ead8f8f73c9f824e9942c8ebd6b764f6c0bf" -dependencies = [ - "bstr 1.5.0", - "btoi", - "gix-date", - "itoa", - "nom", - "thiserror", -] - -[[package]] -name = "gix-config" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbad5ce54a8fc997acc50febd89ec80fa6e97cb7f8d0654cb229936407489d8" -dependencies = [ - "bstr 1.5.0", - "gix-config-value", - "gix-features 0.28.1", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "log", - "memchr", - "nom", - "once_cell", - "smallvec", - "thiserror", - "unicode-bom", -] - -[[package]] -name = "gix-config-value" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09154c0c8677e4da0ec35e896f56ee3e338e741b9599fae06075edd83a4081c" -dependencies = [ - "bitflags", - "bstr 1.5.0", - "gix-path", - "libc", - "thiserror", -] - -[[package]] -name = "gix-date" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96271912ce39822501616f177dea7218784e6c63be90d5f36322ff3a722aae2" -dependencies = [ - "bstr 1.5.0", - "itoa", - "thiserror", - "time 0.3.22", -] - -[[package]] -name = "gix-features" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b76f9a80f6dd7be66442ae86e1f534effad9546676a392acc95e269d0c21c22" -dependencies = [ - "gix-hash 0.10.4", - "libc", - "sha1_smol", - "walkdir", -] - -[[package]] -name = "gix-features" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf69b0f5c701cc3ae22d3204b671907668f6437ca88862d355eaf9bc47a4f897" -dependencies = [ - "gix-hash 0.11.3", - "libc", -] - -[[package]] -name = "gix-fs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b37a1832f691fdc09910bd267f9a2e413737c1f9ec68c6e31f9e802616278a9" -dependencies = [ - "gix-features 0.29.0", -] - -[[package]] -name = "gix-glob" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e43efd776bc543f46f0fd0ca3d920c37af71a764a16f2aebd89765e9ff2993" -dependencies = [ - "bitflags", - "bstr 1.5.0", -] - -[[package]] -name = "gix-hash" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a258595457bc192d1f1c59d0d168a1e34e2be9b97a614e14995416185de41a7" -dependencies = [ - "hex 0.4.3", - "thiserror", -] - -[[package]] -name = "gix-hash" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dd58cdbe7ffa4032fc111864c80d5f8cecd9a2c9736c97ae7e5be834188272" -dependencies = [ - "hex 0.4.3", - "thiserror", -] - -[[package]] -name = "gix-lock" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c693d7f05730fa74a7c467150adc7cea393518410c65f0672f80226b8111555" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror", -] - -[[package]] -name = "gix-object" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df068db9180ee935fbb70504848369e270bdcb576b05c0faa8b9fd3b86fc017" -dependencies = [ - "bstr 1.5.0", - "btoi", - "gix-actor", - "gix-features 0.28.1", - "gix-hash 0.10.4", - "gix-validate", - "hex 0.4.3", - "itoa", - "nom", - "smallvec", - "thiserror", -] - -[[package]] -name = "gix-path" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32370dce200bb951df013e03dff35b4233fc7a89458642b047629b91734a7e19" -dependencies = [ - "bstr 1.5.0", - "thiserror", -] - -[[package]] -name = "gix-ref" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e909396ed3b176823991ccc391c276ae2a015e54edaafa3566d35123cfac9d" -dependencies = [ - "gix-actor", - "gix-features 0.28.1", - "gix-hash 0.10.4", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-validate", - "memmap2", - "nom", - "thiserror", -] - -[[package]] -name = "gix-sec" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ffa5bf0772f9b01de501c035b6b084cf9b8bb07dec41e3afc6a17336a65f47" -dependencies = [ - "bitflags", - "dirs 4.0.0", - "gix-path", - "libc", - "windows 0.43.0", -] - -[[package]] -name = "gix-tempfile" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71a0d32f34e71e86586124225caefd78dabc605d0486de580d717653addf182" -dependencies = [ - "gix-fs", - "libc", - "once_cell", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-utils" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca284c260845bc0724050aec59c7a596407678342614cdf5a1d69e044f29a36" -dependencies = [ - "fastrand", -] - -[[package]] -name = "gix-validate" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d092b594c8af00a3a31fe526d363ee8a51a6f29d8496cdb991ed2f01ec0ec13" -dependencies = [ - "bstr 1.5.0", - "thiserror", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" -dependencies = [ - "aho-corasick 0.7.20", - "bstr 1.5.0", - "fnv", - "log", - "regex", -] - -[[package]] -name = "h2" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 1.9.3", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "headers" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" -dependencies = [ - "base64 0.13.1", - "bitflags", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - -[[package]] -name = "html_parser" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec016cabcf7c9c48f9d5fdc6b03f273585bfce640a0f47a69552039e92b1959a" -dependencies = [ - "pest", - "pest_derive", - "serde", - "serde_derive", - "serde_json", - "thiserror", -] - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.4.9", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" -dependencies = [ - "http", - "hyper", - "log", - "rustls 0.20.8", - "rustls-native-certs", - "tokio", - "tokio-rustls 0.23.4", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" -dependencies = [ - "http", - "hyper", - "rustls 0.21.2", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows 0.48.0", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "ignore" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" -dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" -dependencies = [ - "equivalent", - "hashbrown 0.14.0", -] - -[[package]] -name = "indicatif" -version = "0.17.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "internment" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161079c3ad892faa215fcfcf3fd7a6a3c9288df2b06a2c2bad7fbfad4f01d69d" -dependencies = [ - "hashbrown 0.12.3", - "parking_lot", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.3", - "widestring", - "windows-sys 0.48.0", - "winreg 0.50.0", -] - -[[package]] -name = "ipnet" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" - -[[package]] -name = "iri-string" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0f7638c1e223529f1bfdc48c8b133b9e0b434094d1d28473161ee48b235f78" -dependencies = [ - "nom", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "is_executable" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d553b8abc8187beb7d663e34c065ac4570b273bc9511a50e940e99409c577" -dependencies = [ - "winapi", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "keyboard-types" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" -dependencies = [ - "bitflags", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kqueue" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "krates" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c43a6cba1c201dfe81a943c89fa5c9140b34993e0c027f542c80b92e319a7" -dependencies = [ - "cargo_metadata", - "cfg-expr", - "petgraph", - "semver", -] - -[[package]] -name = "kstring" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" -dependencies = [ - "serde", - "static_assertions", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "libgit2-sys" -version = "0.15.2+1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" -dependencies = [ - "cc", - "libc", - "libssh2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", -] - -[[package]] -name = "libssh2-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "liquid" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f68ae1011499ae2ef879f631891f21c78e309755f4a5e483c4a8f12e10b609" -dependencies = [ - "doc-comment", - "liquid-core", - "liquid-derive", - "liquid-lib", - "serde", -] - -[[package]] -name = "liquid-core" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e0724dfcaad5cfb7965ea0f178ca0870b8d7315178f4a7179f5696f7f04d5f" -dependencies = [ - "anymap2", - "itertools", - "kstring", - "liquid-derive", - "num-traits", - "pest", - "pest_derive", - "regex", - "serde", - "time 0.3.22", -] - -[[package]] -name = "liquid-derive" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2fb41a9bb4257a3803154bdf7e2df7d45197d1941c9b1a90ad815231630721" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "liquid-lib" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a17e273a6fb1fb6268f7a5867ddfd0bd4683c7e19b51084f3d567fad4348c0" -dependencies = [ - "itertools", - "liquid-core", - "once_cell", - "percent-encoding", - "regex", - "time 0.3.22", - "unicode-segmentation", -] - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "longest-increasing-subsequence" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" - -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "lua-src" -version = "546.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb00c1380f1b4b4928dd211c07301ffa34872a239e590bd3219d9e5b213face" -dependencies = [ - "cc", -] - -[[package]] -name = "luajit-src" -version = "210.4.5+resty2cf5186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b7992a40e602786272d84c6f2beca44a588ededcfd57b48ec6f82008a7cb97" -dependencies = [ - "cc", -] - -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "matchit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "mlua" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577" -dependencies = [ - "bstr 0.2.17", - "cc", - "futures-core", - "futures-task", - "futures-util", - "lua-src", - "luajit-src", - "mlua_derive", - "num-traits", - "once_cell", - "pkg-config", - "rustc-hash", -] - -[[package]] -name = "mlua_derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9214e60d3cf1643013b107330fcd374ccec1e4ba1eef76e7e5da5e8202e71c0" -dependencies = [ - "itertools", - "once_cell", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", -] - -[[package]] -name = "names" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" -dependencies = [ - "rand", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nix" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" -dependencies = [ - "bitflags", - "cfg-if", - "libc", - "static_assertions", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "normpath" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "notify" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" -dependencies = [ - "bitflags", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "mio", - "serde", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "object" -version = "0.30.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "open" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openssl" -version = "0.10.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-float" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets 0.48.0", -] - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "path-absolutize" -version = "3.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1d4993b16f7325d90c18c3c6a3327db7808752db8d208cea0acee0abd52c52" -dependencies = [ - "path-dedot", -] - -[[package]] -name = "path-dedot" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" -dependencies = [ - "once_cell", -] - -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "pest" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "pest_meta" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "petgraph" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" -dependencies = [ - "fixedbitset", - "indexmap 1.9.3", -] - -[[package]] -name = "pin-project" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "portable-atomic" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "prettier-please" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be31b7957122175fcf33c6d8f54489a5262176020bf096026a86b308b7fa5b23" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.10", -] - -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" -dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" -dependencies = [ - "aho-corasick 1.0.2", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "remove_dir_all" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23895cfadc1917fed9c6ed76a8c2903615fa3704f7493ff82b364c6540acc02b" -dependencies = [ - "aligned", - "cfg-if", - "cvt", - "fs_at", - "lazy_static", - "libc", - "normpath", - "windows-sys 0.45.0", -] - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls 0.24.0", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.2", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.24.1", - "tokio-util", - "tower-service", - "trust-dns-resolver", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", - "winreg 0.10.1", -] - -[[package]] -name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", -] - -[[package]] -name = "rhai" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd29fa1f740be6dc91982013957e08c3c4232d7efcfe19e12da87d50bad47758" -dependencies = [ - "ahash 0.8.3", - "bitflags", - "instant", - "num-traits", - "rhai_codegen", - "smallvec", - "smartstring", -] - -[[package]] -name = "rhai_codegen" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db74e3fdd29d969a0ec1f8e79171a6f0f71d0429293656901db382d248c4c021" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rsx-rosetta" -version = "0.3.0" -source = "git+https://github.com/DioxusLabs/dioxus#11c9abcf7ce731ccb4a44c52de383c090ab319af" -dependencies = [ - "convert_case", - "dioxus-autofmt", - "dioxus-rsx", - "html_parser", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64 0.13.1", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.37.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustls" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.100.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "sanitize-filename" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "schannel" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" -dependencies = [ - "windows-sys 0.42.0", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "security-framework" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" -dependencies = [ - "serde", -] - -[[package]] -name = "serde" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "serde_json" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "serde_spanned" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - -[[package]] -name = "sha2" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "siphasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallbox" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4679d6eef28b85020158619fc09769de89e90886c5de7157587d87cb72648faa" - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "subprocess" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "tar" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" -dependencies = [ - "itoa", - "libc", - "num_threads", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" -dependencies = [ - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" -dependencies = [ - "autocfg", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.4.9", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.2", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" -dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba3f3efabf7fb41fae8534fc20a817013dd1c12cb45441efb6c82e6556b4cd8" -dependencies = [ - "async-compression", - "base64 0.13.1", - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "httpdate", - "iri-string", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "trust-dns-proto" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.2.3", - "ipnet", - "lazy_static", - "rand", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lazy_static", - "lru-cache", - "parking_lot", - "resolv-conf", - "smallvec", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "tungstenite" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" -dependencies = [ - "base64 0.13.1", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand", - "sha-1", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-bom" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ec69f541d875b783ca40184d655f2927c95f0bffd486faa83cd3ac3529ec32" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna 0.4.0", - "percent-encoding", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "walrus" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb08e48cde54c05f363d984bb54ce374f49e242def9468d2e1b6c2372d291f8" -dependencies = [ - "anyhow", - "id-arena", - "leb128", - "log", - "walrus-macro", - "wasmparser", -] - -[[package]] -name = "walrus-macro" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.22", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-cli-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21c60239a09bf9bab8dfa752be4e6c637db22296b9ded493800090448692da9" -dependencies = [ - "anyhow", - "base64 0.9.3", - "log", - "rustc-demangle", - "serde_json", - "tempfile", - "unicode-ident", - "walrus", - "wasm-bindgen-externref-xform", - "wasm-bindgen-multi-value-xform", - "wasm-bindgen-shared", - "wasm-bindgen-threads-xform", - "wasm-bindgen-wasm-conventions", - "wasm-bindgen-wasm-interpreter", -] - -[[package]] -name = "wasm-bindgen-externref-xform" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafbe1984f67cc12645f12ab65e6145e8ddce1ab265d0be58435f25bb0ce2608" -dependencies = [ - "anyhow", - "walrus", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-multi-value-xform" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581419e3995571a1d2d066e360ca1c0c09da097f5a53c98e6f00d96eddaf0ffe" -dependencies = [ - "anyhow", - "walrus", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "wasm-bindgen-threads-xform" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05d272073981137e8426cf2a6830d43d1f84f988a050b2f8b210f0e266b8983" -dependencies = [ - "anyhow", - "walrus", - "wasm-bindgen-wasm-conventions", -] - -[[package]] -name = "wasm-bindgen-wasm-conventions" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9c65b1ff5041ea824ca24c519948aec16fb6611c617d601623c0657dfcd47b" -dependencies = [ - "anyhow", - "walrus", -] - -[[package]] -name = "wasm-bindgen-wasm-interpreter" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5c796220738ab5d44666f37205728a74141c0039d1166bcf8110b26bafaa1e" -dependencies = [ - "anyhow", - "log", - "walrus", - "wasm-bindgen-wasm-conventions", -] - -[[package]] -name = "wasm-streams" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.77.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe3d5405e9ea6c1317a656d6e0820912d8b7b3607823a7596117c8f666daf6f" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winnow" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "xattr" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" -dependencies = [ - "libc", -] - -[[package]] -name = "zeroize" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" - -[[package]] -name = "zip" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" -dependencies = [ - "byteorder", - "bzip2", - "crc32fast", - "flate2", - "thiserror", - "time 0.1.45", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2", - "sha1", - "time 0.3.22", - "zstd", -] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index f325430aa..37dcb6e48 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-cli" -version = "0.4.1" +version = "0.4.3" authors = ["Jonathan Kelley"] edition = "2021" description = "CLI tool for developing, testing, and publishing Dioxus apps" @@ -22,18 +22,15 @@ serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" toml = "0.5.8" fs_extra = "1.2.0" -cargo_toml = "0.11.4" +cargo_toml = "0.16.0" futures = "0.3.21" notify = { version = "5.0.0-pre.16", features = ["serde"] } html_parser = { workspace = true } -binary-install = "0.0.2" -convert_case = "0.5.0" cargo_metadata = "0.15.0" -tokio = { version = "1.16.1", features = ["full"] } +tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] } atty = "0.2.14" -regex = "1.5.4" chrono = "0.4.19" -anyhow = "1.0.53" +anyhow = "1" hyper = "0.14.17" hyper-rustls = "0.23.2" indicatif = "0.17.5" @@ -59,7 +56,6 @@ tar = "0.4.38" zip = "0.6.2" tower = "0.4.12" syn = { version = "2.0", features = ["full", "extra-traits"] } -proc-macro2 = { version = "1.0", features = ["span-locations"] } lazy_static = "1.4.0" # plugin packages @@ -71,14 +67,15 @@ mlua = { version = "0.8.1", features = [ "macros", ], optional = true } ctrlc = "3.2.3" -gitignore = "1.0.7" open = "4.1.0" cargo-generate = "0.18" toml_edit = "0.19.11" # bundling -tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] } -tauri-utils = "=1.4.*" +tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] } +tauri-utils = "=1.5.*" + +manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } @@ -88,6 +85,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-hot-reload = { workspace = true } interprocess-docfix = { version = "1.2.2" } +gitignore = "1.0.8" [features] default = [] diff --git a/packages/cli/README.md b/packages/cli/README.md index d868b1c9d..2325d6784 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,14 +4,14 @@ The **dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running. -It handles all building, bundling, development and publishing to simplify development. +It handles building, bundling, development and publishing to simplify development. ## Installation ### Install the stable version (recommended) -``` -cargo install dioxus-cli --locked +```shell +cargo install dioxus-cli ``` ### Install the latest development build through git @@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio However, this is not fully tested. That means you're probably going to have more bugs despite having the latest bug fixes. -``` +```shell cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli ``` @@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). ### Install from local folder -``` +```shell cargo install --path . --debug ``` @@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu Alternatively, you can specify the template path: -``` +```shell dx create hello --template gh:dioxuslabs/dioxus-template ``` diff --git a/packages/cli/docs/.gitignore b/packages/cli/docs/.gitignore deleted file mode 100644 index 7585238ef..000000000 --- a/packages/cli/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -book diff --git a/packages/cli/docs/book.toml b/packages/cli/docs/book.toml deleted file mode 100644 index a1e57f634..000000000 --- a/packages/cli/docs/book.toml +++ /dev/null @@ -1,6 +0,0 @@ -[book] -authors = ["YuKun Liu"] -language = "en" -multilingual = false -src = "src" -title = "Dioxus CLI" diff --git a/packages/cli/docs/src/SUMMARY.md b/packages/cli/docs/src/SUMMARY.md deleted file mode 100644 index 98a66ae21..000000000 --- a/packages/cli/docs/src/SUMMARY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Summary - -- [Introduction](./introduction.md) -- [Installation](./installation.md) -- [Create a project](./creating.md) -- [Configure a project](./configure.md) -- [Plugin development](./plugin/README.md) - - [API.Log](plugin/interface/log.md) - - [API.Command](plugin/interface/command.md) - - [API.OS](plugin/interface/os.md) - - [API.Directories](plugin/interface/dirs.md) - - [API.Network](plugin/interface/network.md) - - [API.Path](plugin/interface/path.md) \ No newline at end of file diff --git a/packages/cli/docs/src/configure.md b/packages/cli/docs/src/configure.md deleted file mode 100644 index 3b9f6189b..000000000 --- a/packages/cli/docs/src/configure.md +++ /dev/null @@ -1,197 +0,0 @@ -# Configure Project - -This chapter will teach you how to configure the CLI with the `Dioxus.toml` file. -There's an [example](#config-example) which has comments to describe individual keys. -You can copy that or view this documentation for a more complete learning experience. - -"🔒" indicates a mandatory item. Some headers are mandatory, but none of the keys inside them are. It might look weird, but it's normal. Simply don't include any keys. - -## Structure - -Each header has it's TOML form directly under it. - -### Application 🔒 - -```toml -[application] -``` - -Application-wide configuration. Applies to both web and desktop. - -1. **name** 🔒 - Project name & title. - ```toml - name = "my_project" - ``` - -2. **default_platform** 🔒 - The platform this project targets - ```toml - # Currently supported platforms: web, desktop - default_platform = "web" - ``` - -3. **out_dir** - The directory to place the build artifacts from `dx build` or `dx serve` into. This is also where the `assets` directory will be copied into. - ```toml - out_dir = "dist" - ``` - -4. **asset_dir** - The directory with your static assets. The CLI will automatically copy these assets into the **out_dir** after a build/serve. - ```toml - asset_dir = "public" - ``` - -5. **sub_package** - The sub package in the workspace to build by default. - ```toml - sub_package = "my-crate" - ``` - -### Web.App 🔒 - -```toml -[web.app] -``` - -Web-specific configuration. - -1. **title** - The title of the web page. - ```toml - # HTML title tag content - title = "project_name" - ``` - -2. **base_path** - The base path to build the application for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on GitHub Pages. - ```toml - # The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served - base_path = "my_application" - ``` - -### Web.Watcher ✍ - -```toml -[web.watcher] -``` - -Development server configuration. - -1. **reload_html** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt - ```toml - reload_html = true - ``` - -2. **watch_path** - The files & directories to monitor for changes - ```toml - watch_path = ["src", "public"] - ``` - -3. **index_on_404** - If enabled, Dioxus will serve the root page when a route is not found. -*This is needed when serving an application that uses the router*. -However, when serving your app using something else than Dioxus (e.g. GitHub Pages), you will have to check how to configure it on that platform. -In GitHub Pages, you can make a copy of `index.html` named `404.html` in the same directory. - ```toml - index_on_404 = true - ``` - -### Web.Resource 🔒 - -```toml -[web.resource] -``` - -Static resource configuration. - -1. **style** - CSS files to include in your application. - ```toml - style = [ - # Include from public_dir. - "./assets/style.css", - # Or some asset from online cdn. - "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css" - ] - ``` - -2. **script** - JavaScript files to include in your application. - ```toml - script = [ - # Include from asset_dir. - "./public/index.js", - # Or from an online CDN. - "https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js" - ] - ``` - -### Web.Resource.Dev 🔒 - -```toml -[web.resource.dev] -``` - -This is the same as [`[web.resource]`](#webresource-), but it only works in development servers. -For example, if you want to include a file in a `dx serve` server, but not a `dx serve --release` server, put it here. - -### Web.Proxy - -```toml -[[web.proxy]] -``` - -Configuration related to any proxies your application requires during development. Proxies will forward requests to a new service. - -1. **backend** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404 - ```toml - backend = "http://localhost:8000/api/" - ``` - This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is currently not supported). - -## Config example - -This includes all fields, mandatory or not. - -```toml -[application] - -# App name -name = "project_name" - -# The Dioxus platform to default to -default_platform = "web" - -# `build` & `serve` output path -out_dir = "dist" - -# The static resource path -asset_dir = "public" - -[web.app] - -# HTML title tag content -title = "project_name" - -[web.watcher] - -# When watcher is triggered, regenerate the `index.html` -reload_html = true - -# Which files or dirs will be monitored -watch_path = ["src", "public"] - -# Include style or script assets -[web.resource] - -# CSS style file -style = [] - -# Javascript code file -script = [] - -[web.resource.dev] - -# Same as [web.resource], but for development servers - -# CSS style file -style = [] - -# JavaScript files -script = [] - -[[web.proxy]] -backend = "http://localhost:8000/api/" -``` diff --git a/packages/cli/docs/src/creating.md b/packages/cli/docs/src/creating.md deleted file mode 100644 index 4c594dfd8..000000000 --- a/packages/cli/docs/src/creating.md +++ /dev/null @@ -1,37 +0,0 @@ -# Create a Project - -Once you have the Dioxus CLI installed, you can use it to create your own project! - -## Initializing a default project - -First, run the `dx create` command to create a new project: -``` -dx create hello-dioxus -``` - -> It will clone this [template](https://github.com/DioxusLabs/dioxus-template). -> This default template is used for `web` platform application. -> -> You can choose to create your project from a different template by passing the `template` argument: -> ``` -> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template -> ``` - -Next, navigate into your new project: - -``` -cd hello-dioxus -``` - -> Make sure the WASM target is installed before running the projects. -> You can install the WASM target for rust using rustup: -> ``` -> rustup target add wasm32-unknown-unknown -> ``` - -Finally, serve your project: -``` -dx serve -``` - -By default, the CLI serves your website at [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/). diff --git a/packages/cli/docs/src/installation.md b/packages/cli/docs/src/installation.md deleted file mode 100644 index 22803be61..000000000 --- a/packages/cli/docs/src/installation.md +++ /dev/null @@ -1,23 +0,0 @@ -# Installation - -## Install the latest development build through git - -To get the latest bug fixes and features, you can install the development version from git. - -``` -cargo install --git https://github.com/Dioxuslabs/cli -``` - -This will download `Dioxus-CLI` source from GitHub master branch, -and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). - -## Install stable through `crates.io` - -The published version of the Dioxus CLI is updated less often, but is more stable than the git version. - -``` -cargo install dioxus-cli --locked -``` - -Run `dx --help` for a list of all the available commands. -Furthermore, you can run `dx --help` to get help with a specific command. diff --git a/packages/cli/docs/src/introduction.md b/packages/cli/docs/src/introduction.md deleted file mode 100644 index 8b10b0546..000000000 --- a/packages/cli/docs/src/introduction.md +++ /dev/null @@ -1,18 +0,0 @@ -# Introduction - -The 📦✨ **Dioxus CLI** is a tool to help get Dioxus projects off the ground. - -## Features - -* Build and pack a Dioxus project -* `html` to `rsx` conversion tool -* Hot Reload for `web` platform -* Create a Dioxus project from `git` repo -* And more! - \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/README.md b/packages/cli/docs/src/plugin/README.md deleted file mode 100644 index fe7375618..000000000 --- a/packages/cli/docs/src/plugin/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# CLI Plugin development - -**IMPORTANT: Ignore this documentation. Plugins are yet to be released and chances are it won't work for you. This is just what plugins *could* look like.** - -In the past we used `dx tool` to use and install tools, but it was a flawed system. -Tools were hard-coded by us, but people want more tools than we could code, so this plugin system was made to let -anyone develop plugins and use them in Dioxus projects. - -Plugin resources: -* [Source code](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli/src/plugin) -* [Unofficial Dioxus plugin community](https://github.com/DioxusPluginCommunity). Contains certain plugins you can use right now. - -### Why Lua? - -We chose Lua `5.4` to be the plugin developing language, -because it's extremely lightweight, embeddable and easy to learn. -We installed Lua into the CLI, so you don't need to do it yourself. - -Lua resources: -* [Official website](https://www.lua.org/). You can basically find everything here. -* [Awesome Lua](https://github.com/LewisJEllis/awesome-lua). Additional resources (such as Lua plugins for your favorite IDE), and other *awesome* tools! - - -## Creating a plugin - -A plugin is just an `init.lua` file. -You can include other files using `dofile(path)`. -You need to have a plugin and a manager instance, which you can get using `require`: -```lua -local plugin = require("plugin") -local manager = require("manager") -``` - -You need to set some `manager` fields and then initialize the plugin: -```lua -manager.name = "My first plugin" -manager.repository = "https://github.com/john-doe/my-first-plugin" -- The repository URL. -manager.author = "John Doe " -manager.version = "0.1.0" -plugin.init(manager) -``` - -You also need to return the `manager`, which basically represents your plugin: -```lua --- Your code here. --- End of file. - -manager.serve.interval = 1000 -return manager -``` - -And you're ready to go. Now, go and have a look at the stuff below and the API documentation. - -### Plugin info - -You will encounter this type in the events below. The keys are as follows: -* `name: string` - The name of the plugin. -* `repository: string` - The plugin repository URL. -* `author: string` - The author of the plugin. -* `version: string` - The plugin version. - -### Event management - -The plugin library has certain events that you can subscribe to. - -* `manager.on_init` - Triggers the first time the plugin is loaded. -* `manager.build.on_start(info)` - Triggers before the build process. E.g., before `dx build`. -* `manager.build.on_finish(info)` - Triggers after the build process. E.g., after `dx build`. -* `manager.serve.on_start(info)` - Triggers before the serving process. E.g., before `dx serve`. -* `manager.serve.on_rebuild_start(info)` - Triggers before the server rebuilds the web with hot reload. -* `manager.serve.on_rebuild_end(info)` - Triggers after the server rebuilds the web with hot reload. -* `manager.serve.on_shutdown` - Triggers when the server is shutdown. E.g., when the `dx serve` process is terminated. - -To subscribe to an event, you simply need to assign it to a function: - -```lua -manager.build.on_start = function (info) - log.info("[plugin] Build starting: " .. info.name) -end -``` - -### Plugin template - -```lua -package.path = library_dir .. "/?.lua" - -local plugin = require("plugin") -local manager = require("manager") - --- deconstruct api functions -local log = plugin.log - --- plugin information -manager.name = "Hello Dixous Plugin" -manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin" -manager.author = "YuKun Liu " -manager.version = "0.0.1" - --- init manager info to plugin api -plugin.init(manager) - -manager.on_init = function () - -- when the first time plugin been load, this function will be execute. - -- system will create a `dcp.json` file to verify init state. - log.info("[plugin] Start to init plugin: " .. manager.name) -end - ----@param info BuildInfo -manager.build.on_start = function (info) - -- before the build work start, system will execute this function. - log.info("[plugin] Build starting: " .. info.name) -end - ----@param info BuildInfo -manager.build.on_finish = function (info) - -- when the build work is done, system will execute this function. - log.info("[plugin] Build finished: " .. info.name) -end - ----@param info ServeStartInfo -manager.serve.on_start = function (info) - -- this function will after clean & print to run, so you can print some thing. - log.info("[plugin] Serve start: " .. info.name) -end - ----@param info ServeRebuildInfo -manager.serve.on_rebuild = function (info) - -- this function will after clean & print to run, so you can print some thing. - local files = plugin.tool.dump(info.changed_files) - log.info("[plugin] Serve rebuild: '" .. files .. "'") -end - -manager.serve.on_shutdown = function () - log.info("[plugin] Serve shutdown") -end - -manager.serve.interval = 1000 - -return manager -``` diff --git a/packages/cli/docs/src/plugin/interface/command.md b/packages/cli/docs/src/plugin/interface/command.md deleted file mode 100644 index 2882e5153..000000000 --- a/packages/cli/docs/src/plugin/interface/command.md +++ /dev/null @@ -1,21 +0,0 @@ -# Command Functions - -You can use command functions to execute code and scripts. - -Type definition: -``` -Stdio: "Inherit" | "Piped" | "Null" -``` - -### `exec(commands: [string], stdout: Stdio, stderr: Stdio)` - -You can use this function to run some commands on the current system. - -```lua -local cmd = plugin.command - -manager.test = function () - cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"}) -end -``` -> Warning: This function doesn't catch exceptions. \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/dirs.md b/packages/cli/docs/src/plugin/interface/dirs.md deleted file mode 100644 index 9984802ff..000000000 --- a/packages/cli/docs/src/plugin/interface/dirs.md +++ /dev/null @@ -1,30 +0,0 @@ -# Dirs Functions - -Dirs functions are for getting various directory paths. Not to be confused with `plugin.path`. - -### `plugin_dir() -> string` - -Get the plugin's root directory path. - -```lua -local path = plugin.dirs.plugin_dir() --- example: ~/Development/DioxusCli/plugin/test-plugin/ -``` - -### `bin_dir() -> string` - -Get the plugin's binary directory path. Put binary files like `tailwind-cli` or `sass-cli` in this directory. - -```lua -local path = plugin.dirs.bin_dir() --- example: ~/Development/DioxusCli/plugin/test-plugin/bin/ -``` - -### `temp_dir() -> string` - -Get the plugin's temporary directory path. Put any temporary files here. - -```lua -local path = plugin.dirs.bin_dir() --- example: ~/Development/DioxusCli/plugin/test-plugin/temp/ -``` \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/log.md b/packages/cli/docs/src/plugin/interface/log.md deleted file mode 100644 index ee437b77a..000000000 --- a/packages/cli/docs/src/plugin/interface/log.md +++ /dev/null @@ -1,48 +0,0 @@ -# Log Functions - -You can use log functions to print various logging information. - -### `trace(info: string)` - -Print trace log info. - -```lua -local log = plugin.log -log.trace("trace information") -``` - -### `debug(info: string)` - -Print debug log info. - -```lua -local log = plugin.log -log.debug("debug information") -``` - -### `info(info: string)` - -Print info log info. - -```lua -local log = plugin.log -log.info("info information") -``` - -### `warn(info: string)` - -Print warning log info. - -```lua -local log = plugin.log -log.warn("warn information") -``` - -### `error(info: string)` - -Print error log info. - -```lua -local log = plugin.log -log.error("error information") -``` \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/network.md b/packages/cli/docs/src/plugin/interface/network.md deleted file mode 100644 index 566bc5073..000000000 --- a/packages/cli/docs/src/plugin/interface/network.md +++ /dev/null @@ -1,37 +0,0 @@ -# Network Functions - -You can use Network functions to download & read some data from the internet. - -### `download_file(url: string, path: string) -> boolean` - -Downloads a file from the specified URL, -and returns a `boolean` that represents the download status (true: success, false: failure). - -You need to pass a target URL and a local path (where you want to save this file). - -```lua --- this file will download to plugin temp directory -local status = plugin.network.download_file( - "http://xxx.com/xxx.zip", - plugin.dirs.temp_dir() -) -if status != true then - log.error("Download Failed") -end -``` - -### `clone_repo(url: string, path: string) -> boolean` - -Clone a repository from the given URL into the given path. -Returns a `boolean` that represents the clone status (true: success, false: failure). -The system executing this function must have git installed. - -```lua -local status = plugin.network.clone_repo( - "http://github.com/mrxiaozhuox/dioxus-starter", - plugin.dirs.bin_dir() -) -if status != true then - log.error("Clone Failed") -end -``` \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/os.md b/packages/cli/docs/src/plugin/interface/os.md deleted file mode 100644 index 84f4697b8..000000000 --- a/packages/cli/docs/src/plugin/interface/os.md +++ /dev/null @@ -1,11 +0,0 @@ -# OS Functions - -OS functions are for getting system information. - -### `current_platform() -> string ("windows" | "macos" | "linux")` - -Get the current OS platform. - -```lua -local platform = plugin.os.current_platform() -``` \ No newline at end of file diff --git a/packages/cli/docs/src/plugin/interface/path.md b/packages/cli/docs/src/plugin/interface/path.md deleted file mode 100644 index a5bee1eb9..000000000 --- a/packages/cli/docs/src/plugin/interface/path.md +++ /dev/null @@ -1,38 +0,0 @@ -# Path Functions - -You can use path functions to perform operations on valid path strings. - -### `join(path: string, extra: string) -> string` - - -Extend a path; you can extend both directory and file paths. - -```lua -local current_path = "~/hello/dioxus" -local new_path = plugin.path.join(current_path, "world") --- new_path = "~/hello/dioxus/world" -``` - -### `parent(path: string) -> string` - -Return the parent path of the specified path. The parent path is always a directory. - -```lua -local current_path = "~/hello/dioxus" -local new_path = plugin.path.parent(current_path) --- new_path = "~/hello/" -``` - -### `exists(path: string) -> boolean` - -Check if the specified path exists, as either a file or a directory. - -### `is_file(path: string) -> boolean` - -Check if the specified path is a file. - -### `is_dir(path: string) -> boolean` - -Check if the specified path is a directory. \ No newline at end of file diff --git a/packages/cli/examples/plugin/init.lua b/packages/cli/examples/plugin/init.lua deleted file mode 100644 index 7d6b710ef..000000000 --- a/packages/cli/examples/plugin/init.lua +++ /dev/null @@ -1,18 +0,0 @@ -local Api = require("./interface") -local log = Api.log; - -local manager = { - name = "Dioxus-CLI Plugin Demo", - repository = "http://github.com/DioxusLabs/cli", - author = "YuKun Liu ", -} - -manager.onLoad = function () - log.info("plugin loaded.") -end - -manager.onStartBuild = function () - log.warn("system start to build") -end - -return manager \ No newline at end of file diff --git a/packages/cli/examples/plugin/interface.lua b/packages/cli/examples/plugin/interface.lua deleted file mode 100644 index bc6c91cb1..000000000 --- a/packages/cli/examples/plugin/interface.lua +++ /dev/null @@ -1,25 +0,0 @@ -local interface = {} - -if plugin_logger ~= nil then - interface.log = plugin_logger -else - interface.log = { - trace = function (info) - print("trace: " .. info) - end, - debug = function (info) - print("debug: " .. info) - end, - info = function (info) - print("info: " .. info) - end, - warn = function (info) - print("warn: " .. info) - end, - error = function (info) - print("error: " .. info) - end, - } -end - -return interface \ No newline at end of file diff --git a/packages/cli/src/assets/autoreload.js b/packages/cli/src/assets/autoreload.js index fffb3e26c..cc8fef166 100644 --- a/packages/cli/src/assets/autoreload.js +++ b/packages/cli/src/assets/autoreload.js @@ -1,5 +1,5 @@ // Dioxus-CLI -// https://github.com/DioxusLabs/cli +// https://github.com/DioxusLabs/dioxus/tree/master/packages/cli (function () { var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -22,4 +22,4 @@ } }; ws.onclose = reload_upon_connect; -})() \ No newline at end of file +})() diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml index 6386fb76e..892a6cdf8 100644 --- a/packages/cli/src/assets/dioxus.toml +++ b/packages/cli/src/assets/dioxus.toml @@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust" index_on_404 = true -watch_path = ["src"] +watch_path = ["src", "examples"] # include `assets` in web platform [web.resource] diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index c4d1bde6f..6d1ccea05 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -2,31 +2,35 @@ use crate::{ config::{CrateConfig, ExecutableType}, error::{Error, Result}, tools::Tool, - DioxusConfig, }; use cargo_metadata::{diagnostic::Diagnostic, Message}; use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use manganis_cli_support::AssetManifestExt; use serde::Serialize; use std::{ fs::{copy, create_dir_all, File}, - io::Read, + io::{Read, Write}, panic, path::PathBuf, time::Duration, }; use wasm_bindgen_cli_support::Bindgen; +lazy_static! { + static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new(); +} + #[derive(Serialize, Debug, Clone)] pub struct BuildResult { pub warnings: Vec, pub elapsed_time: u128, } -#[allow(unused)] -pub fn build(config: &CrateConfig, quiet: bool) -> Result { +pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result { // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) // [2] Generate the appropriate build folders - // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm + // [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm // [4] Wasm-opt the .wasm file with whatever optimizations need to be done // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary // [6] Link up the html page to the wasm module @@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .. } = config; + let _gaurd = WebAssetConfigDropGuard::new(); + // start to build the assets let ignore_files = build_assets(config)?; @@ -48,14 +54,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { // [1] Build the .wasm module log::info!("🚅 Running build command..."); - let cmd = subprocess::Exec::cmd("cargo"); - let cmd = cmd + + let wasm_check_command = std::process::Command::new("rustup") + .args(["show"]) + .output()?; + let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap(); + if !wasm_check_output.contains("wasm32-unknown-unknown") { + log::info!("wasm32-unknown-unknown target not detected, installing.."); + let _ = std::process::Command::new("rustup") + .args(["target", "add", "wasm32-unknown-unknown"]) + .output()?; + } + + let cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", target_dir) .cwd(crate_dir) .arg("build") .arg("--target") .arg("wasm32-unknown-unknown") - .arg("--message-format=json") - .arg("--quiet"); + .arg("--message-format=json"); let cmd = if config.release { cmd.arg("--release") @@ -65,7 +82,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { let cmd = if config.verbose { cmd.arg("--verbose") } else { - cmd + cmd.arg("--quiet") }; let cmd = if config.custom_profile.is_some() { @@ -82,6 +99,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { cmd }; + let cmd = cmd.args(&config.cargo_args); + let cmd = match executable { ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), @@ -239,19 +258,28 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } + if !skip_assets { + process_assets(config)?; + } + Ok(BuildResult { warnings: warning_messages, elapsed_time: t_start.elapsed().as_millis(), }) } -pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { +pub fn build_desktop( + config: &CrateConfig, + _is_serve: bool, + skip_assets: bool, +) -> Result { log::info!("🚅 Running build [Desktop] command..."); let t_start = std::time::Instant::now(); let ignore_files = build_assets(config)?; let mut cmd = subprocess::Exec::cmd("cargo") + .env("CARGO_TARGET_DIR", &config.target_dir) .cwd(&config.crate_dir) .arg("build") .arg("--message-format=json"); @@ -261,6 +289,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result cmd.arg("--bin").arg(name), crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), @@ -290,12 +328,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { file_name = name.clone(); - config.target_dir.join(release_type).join(name) + config + .target_dir + .join(target_platform) + .join(release_type) + .join(name) } crate::ExecutableType::Example(name) => { file_name = name.clone(); config .target_dir + .join(target_platform) .join(release_type) .join("examples") .join(name) @@ -312,7 +355,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result Result Result Result<()> { + let manifest = config.asset_manifest(); + let mut file = File::create(config.out_dir.join("__assets_head.html"))?; + file.write_all(manifest.head().as_bytes())?; + Ok(()) +} + fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { let mut warning_messages: Vec = vec![]; - let pb = ProgressBar::new_spinner(); + let mut pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(200)); + pb = PROGRESS_BARS.add(pb); pb.set_style( ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}") .unwrap() @@ -378,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { ); pb.set_message("💼 Waiting to start build the project..."); - struct StopSpinOnDrop(ProgressBar); - - impl Drop for StopSpinOnDrop { - fn drop(&mut self) { - self.0.finish_and_clear(); - } - } - let stdout = cmd.detached().stream_stdout()?; let reader = std::io::BufReader::new(stdout); @@ -421,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { std::process::exit(1); } } - _ => (), // Unknown message + _ => { + // Unknown message + } } } Ok(warning_messages) } -pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { +pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String { + let _gaurd = WebAssetConfigDropGuard::new(); + let crate_root = crate::cargo::crate_root().unwrap(); let custom_html_file = crate_root.join("index.html"); let mut html = if custom_html_file.is_file() { @@ -442,14 +496,14 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { String::from(include_str!("./assets/index.html")) }; - let resouces = config.web.resource.clone(); + let resources = config.dioxus_config.web.resource.clone(); - let mut style_list = resouces.style.unwrap_or_default(); - let mut script_list = resouces.script.unwrap_or_default(); + let mut style_list = resources.style.unwrap_or_default(); + let mut script_list = resources.script.unwrap_or_default(); if serve { - let mut dev_style = resouces.dev.style.clone().unwrap_or_default(); - let mut dev_script = resouces.dev.script.unwrap_or_default(); + let mut dev_style = resources.dev.style.clone().unwrap_or_default(); + let mut dev_script = resources.dev.script.unwrap_or_default(); style_list.append(&mut dev_style); script_list.append(&mut dev_script); } @@ -462,13 +516,18 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { )) } if config + .dioxus_config .application .tools .clone() .unwrap_or_default() .contains_key("tailwindcss") { - style_str.push_str("\n"); + style_str.push_str("\n"); + } + if !skip_assets { + let manifest = config.asset_manifest(); + style_str.push_str(&manifest.head()); } replace_or_insert_before("{style_include}", &style_str, " String { ); } - let base_path = match &config.web.app.base_path { + let base_path = match &config.dioxus_config.web.app.base_path { Some(path) => path, None => ".", }; - let app_name = &config.application.name; + let app_name = &config.dioxus_config.application.name; // Check if a script already exists if html.contains("{app_name}") && html.contains("{base_path}") { html = html.replace("{app_name}", app_name); @@ -519,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { } let title = config + .dioxus_config .web .app .title @@ -689,34 +749,41 @@ fn build_assets(config: &CrateConfig) -> Result> { Ok(result) } -// use binary_install::{Cache, Download}; +/// Process any assets collected from the binary +fn process_assets(config: &CrateConfig) -> anyhow::Result<()> { + let manifest = config.asset_manifest(); -// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a -// /// precompiled binary. -// /// -// /// Returns `Some` if a binary was found or it was successfully downloaded. -// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't -// /// have precompiled binaries. Returns an error if we failed to download the -// /// binary. -// pub fn find_wasm_opt( -// cache: &Cache, -// install_permitted: bool, -// ) -> Result { -// // First attempt to look up in PATH. If found assume it works. -// if let Ok(path) = which::which("wasm-opt") { -// PBAR.info(&format!("found wasm-opt at {:?}", path)); + let static_asset_output_dir = PathBuf::from( + config + .dioxus_config + .web + .app + .base_path + .clone() + .unwrap_or_default(), + ); + let static_asset_output_dir = config.out_dir.join(static_asset_output_dir); -// match path.as_path().parent() { -// Some(path) => return Ok(install::Status::Found(Download::at(path))), -// None => {} -// } -// } + manifest.copy_static_assets_to(static_asset_output_dir)?; -// let version = "version_78"; -// Ok(install::download_prebuilt( -// &install::Tool::WasmOpt, -// cache, -// version, -// install_permitted, -// )?) -// } + Ok(()) +} + +pub(crate) struct WebAssetConfigDropGuard; + +impl WebAssetConfigDropGuard { + pub fn new() -> Self { + // Set up the collect asset config + manganis_cli_support::Config::default() + .with_assets_serve_location("/") + .save(); + Self {} + } +} + +impl Drop for WebAssetConfigDropGuard { + fn drop(&mut self) { + // Reset the config + manganis_cli_support::Config::default().save(); + } +} diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index f436ab4c4..812a4c325 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -1,3 +1,4 @@ +use dioxus_autofmt::{IndentOptions, IndentType}; use futures::{stream::FuturesUnordered, StreamExt}; use std::{fs, path::Path, process::exit}; @@ -26,16 +27,19 @@ pub struct Autoformat { impl Autoformat { // Todo: autoformat the entire crate pub async fn autoformat(self) -> Result<()> { + let Autoformat { check, raw, file } = self; + // Default to formatting the project - if self.raw.is_none() && self.file.is_none() { - if let Err(e) = autoformat_project(self.check).await { + if raw.is_none() && file.is_none() { + if let Err(e) = autoformat_project(check).await { eprintln!("error formatting project: {}", e); exit(1); } } - if let Some(raw) = self.raw { - if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) { + if let Some(raw) = raw { + let indent = indentation_for(".")?; + if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) { println!("{}", inner); } else { // exit process with error @@ -45,33 +49,90 @@ impl Autoformat { } // Format single file - if let Some(file) = self.file { - let file_content = fs::read_to_string(&file); - - match file_content { - Ok(s) => { - let edits = dioxus_autofmt::fmt_file(&s); - let out = dioxus_autofmt::apply_formats(&s, edits); - match fs::write(&file, out) { - Ok(_) => { - println!("formatted {}", file); - } - Err(e) => { - eprintln!("failed to write formatted content to file: {}", e); - } - } - } - Err(e) => { - eprintln!("failed to open file: {}", e); - exit(1); - } - } + if let Some(file) = file { + refactor_file(file)?; } Ok(()) } } +fn refactor_file(file: String) -> Result<(), Error> { + let indent = indentation_for(".")?; + let file_content = if file == "-" { + let mut contents = String::new(); + std::io::stdin().read_to_string(&mut contents)?; + Ok(contents) + } else { + fs::read_to_string(&file) + }; + let Ok(s) = file_content else { + eprintln!("failed to open file: {}", file_content.unwrap_err()); + exit(1); + }; + let edits = dioxus_autofmt::fmt_file(&s, indent); + let out = dioxus_autofmt::apply_formats(&s, edits); + + if file == "-" { + print!("{}", out); + } else if let Err(e) = fs::write(&file, out) { + eprintln!("failed to write formatted content to file: {e}",); + } else { + println!("formatted {}", file); + } + + Ok(()) +} + +fn get_project_files(config: &CrateConfig) -> Vec { + let mut files = vec![]; + + let gitignore_path = config.crate_dir.join(".gitignore"); + if gitignore_path.is_file() { + let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap(); + if let Ok(git_files) = gitigno.included_files() { + let git_files = git_files + .into_iter() + .filter(|f| f.ends_with(".rs") && !is_target_dir(f)); + files.extend(git_files) + }; + } else { + collect_rs_files(&config.crate_dir, &mut files); + } + + files +} + +fn is_target_dir(file: &Path) -> bool { + let stripped = if let Ok(cwd) = std::env::current_dir() { + file.strip_prefix(cwd).unwrap_or(file) + } else { + file + }; + if let Some(first) = stripped.components().next() { + first.as_os_str() == "target" + } else { + false + } +} + +async fn format_file( + path: impl AsRef, + indent: IndentOptions, +) -> Result { + let contents = tokio::fs::read_to_string(&path).await?; + + let edits = dioxus_autofmt::fmt_file(&contents, indent); + let len = edits.len(); + + if !edits.is_empty() { + let out = dioxus_autofmt::apply_formats(&contents, edits); + tokio::fs::write(path, out).await?; + } + + Ok(len) +} + /// Read every .rs file accessible when considering the .gitignore and try to format it /// /// Runs using Tokio for multithreading, so it should be really really fast @@ -80,42 +141,27 @@ impl Autoformat { async fn autoformat_project(check: bool) -> Result<()> { let crate_config = crate::CrateConfig::new(None)?; - let mut files_to_format = vec![]; - collect_rs_files(&crate_config.crate_dir, &mut files_to_format); + let files_to_format = get_project_files(&crate_config); + + if files_to_format.is_empty() { + return Ok(()); + } + + let indent = indentation_for(&files_to_format[0])?; let counts = files_to_format .into_iter() - .filter(|file| { - if file.components().any(|f| f.as_os_str() == "target") { - return false; - } - - true - }) .map(|path| async { - let _path = path.clone(); - let res = tokio::spawn(async move { - let contents = tokio::fs::read_to_string(&path).await?; - - let edits = dioxus_autofmt::fmt_file(&contents); - let len = edits.len(); - - if !edits.is_empty() { - let out = dioxus_autofmt::apply_formats(&contents, edits); - tokio::fs::write(&path, out).await?; - } - - Ok(len) as Result - }) - .await; + let path_clone = path.clone(); + let res = tokio::spawn(format_file(path, indent.clone())).await; match res { Err(err) => { - eprintln!("error formatting file: {}\n{err}", _path.display()); + eprintln!("error formatting file: {}\n{err}", path_clone.display()); None } Ok(Err(err)) => { - eprintln!("error formatting file: {}\n{err}", _path.display()); + eprintln!("error formatting file: {}\n{err}", path_clone.display()); None } Ok(Ok(res)) => Some(res), @@ -125,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> { .collect::>() .await; - let files_formatted: usize = counts - .into_iter() - .map(|f| match f { - Some(res) => res, - _ => 0, - }) - .sum(); + let files_formatted: usize = counts.into_iter().flatten().sum(); if files_formatted > 0 && check { eprintln!("{} files needed formatting", files_formatted); @@ -141,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> { Ok(()) } -fn collect_rs_files(folder: &Path, files: &mut Vec) { - let Ok(folder) = folder.read_dir() else { +fn indentation_for(file_or_dir: impl AsRef) -> Result { + let out = std::process::Command::new("cargo") + .args(["fmt", "--", "--print-config", "current"]) + .arg(file_or_dir.as_ref()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output()?; + if !out.status.success() { + return Err(Error::CargoError("cargo fmt failed".into())); + } + + let config = String::from_utf8_lossy(&out.stdout); + + let hard_tabs = config + .lines() + .find(|line| line.starts_with("hard_tabs ")) + .and_then(|line| line.split_once('=')) + .map(|(_, value)| value.trim() == "true") + .ok_or_else(|| { + Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into()) + })?; + let tab_spaces = config + .lines() + .find(|line| line.starts_with("tab_spaces ")) + .and_then(|line| line.split_once('=')) + .map(|(_, value)| value.trim().parse::()) + .ok_or_else(|| { + Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into()) + })? + .map_err(|_| { + Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into()) + })?; + + Ok(IndentOptions::new( + if hard_tabs { + IndentType::Tabs + } else { + IndentType::Spaces + }, + tab_spaces, + )) +} + +fn collect_rs_files(folder: &impl AsRef, files: &mut Vec) { + if is_target_dir(folder.as_ref()) { + return; + } + let Ok(folder) = folder.as_ref().read_dir() else { return; }; - // load the gitignore - for entry in folder { let Ok(entry) = entry else { continue; }; - let path = entry.path(); - if path.is_dir() { collect_rs_files(&path, files); } - if let Some(ext) = path.extension() { - if ext == "rs" { + if ext == "rs" && !is_target_dir(&path) { files.push(path); } } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 7fe29f8ce..d8032f034 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,6 +1,8 @@ -use crate::cfg::Platform; #[cfg(feature = "plugin")] use crate::plugin::PluginManager; +use crate::server::fullstack::FullstackServerEnvGuard; +use crate::server::fullstack::FullstackWebEnvGuard; +use crate::{cfg::Platform, WebAssetConfigDropGuard}; use super::*; @@ -13,23 +15,26 @@ pub struct Build { } impl Build { - pub fn build(self, bin: Option) -> Result<()> { + pub fn build(self, bin: Option, target_dir: Option<&std::path::Path>) -> Result<()> { let mut crate_config = crate::CrateConfig::new(bin)?; + if let Some(target_dir) = target_dir { + crate_config.target_dir = target_dir.to_path_buf(); + } // change the release state. crate_config.with_release(self.build.release); crate_config.with_verbose(self.build.verbose); if self.build.example.is_some() { - crate_config.as_example(self.build.example.unwrap()); + crate_config.as_example(self.build.example.clone().unwrap()); } if self.build.profile.is_some() { - crate_config.set_profile(self.build.profile.unwrap()); + crate_config.set_profile(self.build.profile.clone().unwrap()); } if self.build.features.is_some() { - crate_config.set_features(self.build.features.unwrap()); + crate_config.set_features(self.build.features.clone().unwrap()); } let platform = self @@ -37,19 +42,56 @@ impl Build { .platform .unwrap_or(crate_config.dioxus_config.application.default_platform); - #[cfg(feature = "plugin")] - let _ = PluginManager::on_build_start(&crate_config, &platform); + if let Some(target) = self.build.target.clone() { + crate_config.set_target(target); + } + + crate_config.set_cargo_args(self.build.cargo_args.clone()); + + // #[cfg(feature = "plugin")] + // let _ = PluginManager::on_build_start(&crate_config, &platform); match platform { Platform::Web => { - crate::builder::build(&crate_config, true)?; + crate::builder::build(&crate_config, false, self.build.skip_assets)?; } Platform::Desktop => { - crate::builder::build_desktop(&crate_config, false)?; + crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?; + } + Platform::Fullstack => { + // Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary + let _config = WebAssetConfigDropGuard::new(); + { + let mut web_config = crate_config.clone(); + let _gaurd = FullstackWebEnvGuard::new(&self.build); + let web_feature = self.build.client_feature; + let features = &mut web_config.features; + match features { + Some(features) => { + features.push(web_feature); + } + None => web_config.features = Some(vec![web_feature]), + }; + crate::builder::build(&crate_config, false, self.build.skip_assets)?; + } + { + let mut desktop_config = crate_config.clone(); + let desktop_feature = self.build.server_feature; + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let _gaurd = + FullstackServerEnvGuard::new(self.build.force_debug, self.build.release); + crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?; + } } } - let temp = gen_page(&crate_config.dioxus_config, false); + let temp = gen_page(&crate_config, false, self.build.skip_assets); let mut file = std::fs::File::create( crate_config @@ -66,8 +108,8 @@ impl Build { )?; file.write_all(temp.as_bytes())?; - #[cfg(feature = "plugin")] - let _ = PluginManager::on_build_finish(&crate_config, &platform); + // #[cfg(feature = "plugin")] + // let _ = PluginManager::on_build_finish(&crate_config, &platform); Ok(()) } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 0d4fb23da..aaa823373 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -76,8 +76,14 @@ impl Bundle { crate_config.set_profile(self.build.profile.unwrap()); } + if let Some(target) = &self.build.target { + crate_config.set_target(target.to_string()); + } + + crate_config.set_cargo_args(self.build.cargo_args); + // build the desktop app - build_desktop(&crate_config, false)?; + build_desktop(&crate_config, false, false)?; // copy the binary to the out dir let package = crate_config.manifest.package.unwrap(); @@ -128,14 +134,27 @@ impl Bundle { } } + // Add all assets from collect assets to the bundle + { + let config = manganis_cli_support::Config::current(); + let location = config.assets_serve_location().to_string(); + let location = format!("./{}", location); + println!("Adding assets from {} to bundle", location); + if let Some(resources) = &mut bundle_settings.resources { + resources.push(location); + } else { + bundle_settings.resources = Some(vec![location]); + } + } + let mut settings = SettingsBuilder::new() .project_out_directory(crate_config.out_dir) .package_settings(PackageSettings { product_name: crate_config.dioxus_config.application.name.clone(), - version: package.version, - description: package.description.unwrap_or_default(), - homepage: package.homepage, - authors: Some(package.authors), + version: package.version().to_string(), + description: package.description().unwrap_or_default().to_string(), + homepage: Some(package.homepage().unwrap_or_default().to_string()), + authors: Some(Vec::from(package.authors())), default_run: Some(crate_config.dioxus_config.application.name.clone()), }) .binaries(binaries) @@ -148,6 +167,11 @@ impl Bundle { .collect(), ); } + + if let Some(target) = &self.build.target { + settings = settings.target(target.to_string()); + } + let settings = settings.build(); // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567) @@ -156,9 +180,9 @@ impl Bundle { tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{ #[cfg(target_os = "macos")] - panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); + panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); #[cfg(not(target_os = "macos"))] - panic!("Failed to bundle project: {}", err); + panic!("Failed to bundle project: {:#?}", err); }); Ok(()) diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 2a441a1c8..62212e574 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -6,15 +6,16 @@ use super::*; /// Config options for the build system. #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsBuild { - /// The index HTML file to drive the bundling process [default: index.html] - #[arg(long)] - pub target: Option, - /// Build in release mode [default: false] #[clap(long)] #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -32,17 +33,53 @@ pub struct ConfigOptsBuild { #[clap(long, value_enum)] pub platform: Option, + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// The feature to use for the client in a fullstack app [default: "web"] + #[clap(long, default_value_t = { "web".to_string() })] + pub client_feature: String, + + /// The feature to use for the server in a fullstack app [default: "ssr"] + #[clap(long, default_value_t = { "ssr".to_string() })] + pub server_feature: String, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, +} + +impl From for ConfigOptsBuild { + fn from(serve: ConfigOptsServe) -> Self { + Self { + target: serve.target, + release: serve.release, + verbose: serve.verbose, + example: serve.example, + profile: serve.profile, + platform: serve.platform, + features: serve.features, + client_feature: serve.client_feature, + server_feature: serve.server_feature, + skip_assets: serve.skip_assets, + force_debug: serve.force_debug, + cargo_args: serve.cargo_args, + } + } } #[derive(Clone, Debug, Default, Deserialize, Parser)] pub struct ConfigOptsServe { - /// The index HTML file to drive the bundling process [default: index.html] - #[arg(short, long)] - pub target: Option, - /// Port of dev server #[clap(long)] #[clap(default_value_t = 8080)] @@ -62,6 +99,11 @@ pub struct ConfigOptsServe { #[serde(default)] pub release: bool, + /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false] + #[clap(long)] + #[serde(default)] + pub force_debug: bool, + // Use verbose output [default: false] #[clap(long)] #[serde(default)] @@ -71,7 +113,7 @@ pub struct ConfigOptsServe { #[clap(long)] pub profile: Option, - /// Build platform: support Web & Desktop [default: "default_platform"] + /// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"] #[clap(long, value_enum)] pub platform: Option, @@ -89,6 +131,27 @@ pub struct ConfigOptsServe { /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub skip_assets: bool, + + /// The feature to use for the client in a fullstack app [default: "web"] + #[clap(long, default_value_t = { "web".to_string() })] + pub client_feature: String, + + /// The feature to use for the server in a fullstack app [default: "ssr"] + #[clap(long, default_value_t = { "ssr".to_string() })] + pub server_feature: String, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] @@ -99,6 +162,9 @@ pub enum Platform { #[clap(name = "desktop")] #[serde(rename = "desktop")] Desktop, + #[clap(name = "fullstack")] + #[serde(rename = "fullstack")] + Fullstack, } /// Config options for the bundling system. @@ -129,4 +195,12 @@ pub struct ConfigOptsBundle { /// Space separated list of features to activate #[clap(long)] pub features: Option>, + + /// Rustc platform triple + #[clap(long)] + pub target: Option, + + /// Extra arguments passed to cargo build + #[clap(last = true)] + pub cargo_args: Vec, } diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index 5fed1e1a6..9a0f0d4fb 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -28,6 +28,12 @@ impl Clean { remove_dir_all(crate_config.crate_dir.join(&out_dir))?; } + let fullstack_out_dir = crate_config.crate_dir.join(".dioxus"); + + if fullstack_out_dir.is_dir() { + remove_dir_all(fullstack_out_dir)?; + } + Ok(()) } } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 433d611b7..1bb24ecdb 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -12,6 +12,7 @@ pub struct Serve { impl Serve { pub async fn serve(self, bin: Option) -> Result<()> { let mut crate_config = crate::CrateConfig::new(bin)?; + let serve_cfg = self.serve.clone(); // change the relase state. crate_config.with_hot_reload(self.serve.hot_reload); @@ -34,6 +35,12 @@ impl Serve { // Subdirectories don't work with the server crate_config.dioxus_config.web.app.base_path = None; + if let Some(target) = self.serve.target { + crate_config.set_target(target); + } + + crate_config.set_cargo_args(self.serve.cargo_args); + let platform = self .serve .platform @@ -42,21 +49,29 @@ impl Serve { match platform { cfg::Platform::Web => { // generate dev-index page - Serve::regen_dev_page(&crate_config)?; + Serve::regen_dev_page(&crate_config, self.serve.skip_assets)?; // start the develop server - server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) - .await?; + server::web::startup( + self.serve.port, + crate_config.clone(), + self.serve.open, + self.serve.skip_assets, + ) + .await?; } cfg::Platform::Desktop => { - server::desktop::startup(crate_config.clone()).await?; + server::desktop::startup(crate_config.clone(), &serve_cfg).await?; + } + cfg::Platform::Fullstack => { + server::fullstack::startup(crate_config.clone(), &serve_cfg).await?; } } Ok(()) } - pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { - let serve_html = gen_page(&crate_config.dioxus_config, true); + pub fn regen_dev_page(crate_config: &CrateConfig, skip_assets: bool) -> Result<()> { + let serve_html = gen_page(crate_config, true, skip_assets); let dist_path = crate_config.crate_dir.join( crate_config diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 0654dc375..b220fdf37 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,4 +1,6 @@ use crate::{cfg::Platform, error::Result}; +use manganis_cli_support::AssetManifest; +use manganis_cli_support::AssetManifestExt; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -105,7 +107,7 @@ impl Default for DioxusConfig { }, proxy: Some(vec![]), watcher: WebWatcherConfig { - watch_path: Some(vec![PathBuf::from("src")]), + watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]), reload_html: Some(false), index_on_404: Some(true), }, @@ -211,6 +213,8 @@ pub struct CrateConfig { pub verbose: bool, pub custom_profile: Option, pub features: Option>, + pub target: Option, + pub cargo_args: Vec, } #[derive(Debug, Clone)] @@ -278,6 +282,8 @@ impl CrateConfig { let verbose = false; let custom_profile = None; let features = None; + let target = None; + let cargo_args = vec![]; Ok(Self { out_dir, @@ -294,9 +300,18 @@ impl CrateConfig { custom_profile, features, verbose, + target, + cargo_args, }) } + pub fn asset_manifest(&self) -> AssetManifest { + AssetManifest::load_from_path( + self.crate_dir.join("Cargo.toml"), + self.workspace_dir.join("Cargo.lock"), + ) + } + pub fn as_example(&mut self, example_name: String) -> &mut Self { self.executable = ExecutableType::Example(example_name); self @@ -331,6 +346,16 @@ impl CrateConfig { self.features = Some(features); self } + + pub fn set_target(&mut self, target: String) -> &mut Self { + self.target = Some(target); + self + } + + pub fn set_cargo_args(&mut self, cargo_args: Vec) -> &mut Self { + self.cargo_args = cargo_args; + self + } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -525,6 +550,7 @@ impl From for tauri_bundler::NsisSettings { display_language_selector: val.display_language_selector, custom_language_files: None, template: None, + compression: None, } } } diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 84b9d4b71..d577b2b40 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -29,6 +29,9 @@ pub enum Error { #[error("Cargo Error: {0}")] CargoError(String), + #[error("Couldn't retrieve cargo metadata")] + CargoMetadata(#[source] cargo_metadata::Error), + #[error("{0}")] CustomError(String), diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index c067ab148..b27cf55a9 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -1,3 +1,7 @@ +#![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 const DIOXUS_CLI_VERSION: &str = "0.4.1"; pub mod builder; diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 48c2a2788..31d088737 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -28,7 +28,19 @@ pub fn set_up_logging() { message = message, )); }) - .level(log::LevelFilter::Info) + .level(match std::env::var("DIOXUS_LOG") { + Ok(level) => match level.to_lowercase().as_str() { + "error" => log::LevelFilter::Error, + "warn" => log::LevelFilter::Warn, + "info" => log::LevelFilter::Info, + "debug" => log::LevelFilter::Debug, + "trace" => log::LevelFilter::Trace, + _ => { + panic!("Invalid log level: {}", level) + } + }, + Err(_) => log::LevelFilter::Info, + }) .chain(std::io::stdout()) .apply() .unwrap(); diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index fe860a0e6..0695d7ac8 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager; use Commands::*; -fn get_bin(bin: Option) -> Result> { - const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace."; +fn get_bin(bin: Option) -> Result { + let metadata = cargo_metadata::MetadataCommand::new() + .exec() + .map_err(Error::CargoMetadata)?; + let package = if let Some(bin) = bin { + metadata + .workspace_packages() + .into_iter() + .find(|p| p.name == bin) + .ok_or(format!("no such package: {}", bin)) + .map_err(Error::CargoError)? + } else { + metadata + .root_package() + .ok_or("no root package?".into()) + .map_err(Error::CargoError)? + }; - if let Some(ref bin) = bin { - let manifest = cargo_toml::Manifest::from_path("./Cargo.toml") - .map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?; + let crate_dir = package + .manifest_path + .parent() + .ok_or("couldn't take parent dir".into()) + .map_err(Error::CargoError)?; - if let Some(workspace) = manifest.workspace { - for item in workspace.members.iter() { - let path = PathBuf::from(item); - - if !path.exists() { - continue; - } - - if !path.is_dir() { - continue; - } - - if path.ends_with(bin.clone()) { - return Ok(Some(path)); - } - } - } else { - return Err(Error::CargoError(ERR_MESSAGE.to_string())); - } - } - - // If the bin exists but we couldn't find it - if bin.is_some() { - return Err(Error::CargoError( - "The specified bin does not exist.".to_string(), - )); - } - - Ok(None) + Ok(crate_dir.into()) } #[tokio::main] @@ -53,34 +42,36 @@ async fn main() -> anyhow::Result<()> { set_up_logging(); - let bin = get_bin(args.bin)?; + let bin = get_bin(args.bin); - let _dioxus_config = DioxusConfig::load(bin.clone()) + if let Ok(bin) = &bin { + let _dioxus_config = DioxusConfig::load(Some(bin.clone())) .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))? .unwrap_or_else(|| { log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config"); DioxusConfig::default() }); - #[cfg(feature = "plugin")] - PluginManager::init(_dioxus_config.plugin) - .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; + #[cfg(feature = "plugin")] + PluginManager::init(_dioxus_config.plugin) + .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; + } match args.action { Translate(opts) => opts .translate() .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), - Build(opts) => opts - .build(bin.clone()) + Build(opts) if bin.is_ok() => opts + .build(Some(bin.unwrap().clone()), None) .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), - Clean(opts) => opts - .clean(bin.clone()) + Clean(opts) if bin.is_ok() => opts + .clean(Some(bin.unwrap().clone())) .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), - Serve(opts) => opts - .serve(bin.clone()) + Serve(opts) if bin.is_ok() => opts + .serve(Some(bin.unwrap().clone())) .await .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), @@ -92,8 +83,8 @@ async fn main() -> anyhow::Result<()> { .config() .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), - Bundle(opts) => opts - .bundle(bin.clone()) + Bundle(opts) if bin.is_ok() => opts + .bundle(Some(bin.unwrap().clone())) .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)), #[cfg(feature = "plugin")] @@ -118,5 +109,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } + _ => Err(anyhow::anyhow!(bin.unwrap_err())), } } diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 91811857d..34f85a312 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -1,11 +1,12 @@ +use crate::server::Platform; use crate::{ + cfg::ConfigOptsServe, server::{ output::{print_console_info, PrettierOptions}, setup_file_watcher, }, BuildResult, CrateConfig, Result, }; - use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; @@ -21,7 +22,14 @@ use plugin::PluginManager; use super::HotReloadState; -pub async fn startup(config: CrateConfig) -> Result<()> { +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + startup_with_platform::(config, serve).await +} + +pub(crate) async fn startup_with_platform( + config: CrateConfig, + serve_cfg: &ConfigOptsServe, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -43,8 +51,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> { let hot_reload_tx = broadcast::channel(100).0; - clear_paths(); - Some(HotReloadState { messages: hot_reload_tx.clone(), file_map: file_map.clone(), @@ -53,15 +59,18 @@ pub async fn startup(config: CrateConfig) -> Result<()> { false => None, }; - serve(config, hot_reload_state).await?; + serve::

(config, serve_cfg, hot_reload_state).await?; Ok(()) } /// Start the server without hot reload -pub async fn serve(config: CrateConfig, hot_reload_state: Option) -> Result<()> { - let (child, first_build_result) = start_desktop(&config)?; - let currently_running_child: RwLock = RwLock::new(child); +async fn serve( + config: CrateConfig, + serve: &ConfigOptsServe, + hot_reload_state: Option, +) -> Result<()> { + let platform = RwLock::new(P::start(&config, serve)?); log::info!("🚀 Starting development server..."); @@ -70,14 +79,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option let _watcher = setup_file_watcher( { let config = config.clone(); - - move || { - let mut current_child = currently_running_child.write().unwrap(); - current_child.kill()?; - let (child, result) = start_desktop(&config)?; - *current_child = child; - Ok(result) - } + move || platform.write().unwrap().rebuild(&config) }, &config, None, @@ -85,19 +87,9 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option ) .await?; - // Print serve info - print_console_info( - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - None, - ); - match hot_reload_state { Some(hot_reload_state) => { + // The open interprocess sockets start_desktop_hot_reload(hot_reload_state).await?; } None => { @@ -109,7 +101,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option } async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> { - match LocalSocketListener::bind("@dioxusin") { + let metadata = cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .unwrap(); + let target_dir = metadata.target_directory.as_std_path(); + let path = target_dir.join("dioxusin"); + clear_paths(&path); + match LocalSocketListener::bind(path) { Ok(local_socket_stream) => { let aborted = Arc::new(Mutex::new(false)); // States @@ -121,30 +120,41 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() let file_map = hot_reload_state.file_map.clone(); let channels = channels.clone(); let aborted = aborted.clone(); - let _ = local_socket_stream.set_nonblocking(true); move || { loop { - if let Ok(mut connection) = local_socket_stream.accept() { - // send any templates than have changed before the socket connected - let templates: Vec<_> = { - file_map - .lock() - .unwrap() - .map - .values() - .filter_map(|(_, template_slot)| *template_slot) - .collect() - }; - for template in templates { - if !send_msg( - HotReloadMsg::UpdateTemplate(template), - &mut connection, - ) { - continue; + //accept() will block the thread when local_socket_stream is in blocking mode (default) + match local_socket_stream.accept() { + Ok(mut connection) => { + // send any templates than have changed before the socket connected + let templates: Vec<_> = { + file_map + .lock() + .unwrap() + .map + .values() + .filter_map(|(_, template_slot)| *template_slot) + .collect() + }; + for template in templates { + if !send_msg( + HotReloadMsg::UpdateTemplate(template), + &mut connection, + ) { + continue; + } + } + channels.lock().unwrap().push(connection); + println!("Connected to hot reloading 🚀"); + } + Err(err) => { + let error_string = err.to_string(); + // Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set + let display_error = err.kind() != std::io::ErrorKind::WouldBlock + && !error_string.contains("Waiting for a process"); + if display_error { + println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err); } } - channels.lock().unwrap().push(connection); - println!("Connected to hot reloading 🚀"); } if *aborted.lock().unwrap() { break; @@ -174,17 +184,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<() Ok(()) } -fn clear_paths() { - if cfg!(target_os = "macos") { +fn clear_paths(file_socket_path: &std::path::Path) { + if cfg!(unix) { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open // We check if the file socket is already open from an old session and then delete it - let paths = ["./dioxusin", "./@dioxusin"]; - for path in paths { - let path = std::path::PathBuf::from(path); - if path.exists() { - let _ = std::fs::remove_file(path); - } + + if file_socket_path.exists() { + let _ = std::fs::remove_file(file_socket_path); } } } @@ -203,9 +210,9 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { } } -pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { +fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> { // Run the desktop application - let result = crate::builder::build_desktop(config, true)?; + let result = crate::builder::build_desktop(config, true, skip_assets)?; match &config.executable { crate::ExecutableType::Binary(name) @@ -215,9 +222,58 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { if cfg!(windows) { file.set_extension("exe"); } - let child = Command::new(file.to_str().unwrap()).spawn()?; + let active = "DIOXUS_ACTIVE"; + let child = RAIIChild( + Command::new(file.to_str().unwrap()) + .env(active, "true") + .spawn()?, + ); Ok((child, result)) } } } + +pub(crate) struct DesktopPlatform { + currently_running_child: RAIIChild, + skip_assets: bool, +} + +impl Platform for DesktopPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result { + let (child, first_build_result) = start_desktop(config, serve.skip_assets)?; + + log::info!("🚀 Starting development server..."); + + // Print serve info + print_console_info( + config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + None, + ); + + Ok(Self { + currently_running_child: child, + skip_assets: serve.skip_assets, + }) + } + + fn rebuild(&mut self, config: &CrateConfig) -> Result { + self.currently_running_child.0.kill()?; + let (child, result) = start_desktop(config, self.skip_assets)?; + self.currently_running_child = child; + Ok(result) + } +} + +struct RAIIChild(Child); + +impl Drop for RAIIChild { + fn drop(&mut self) { + let _ = self.0.kill(); + } +} diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs new file mode 100644 index 000000000..fa9c77459 --- /dev/null +++ b/packages/cli/src/server/fullstack/mod.rs @@ -0,0 +1,161 @@ +use crate::{ + cfg::{ConfigOptsBuild, ConfigOptsServe}, + CrateConfig, Result, WebAssetConfigDropGuard, +}; + +use super::{desktop, Platform}; + +pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> { + desktop::startup_with_platform::(config, serve).await +} + +fn start_web_build_thread( + config: &CrateConfig, + serve: &ConfigOptsServe, +) -> std::thread::JoinHandle> { + let serve = serve.clone(); + let target_directory = config.crate_dir.join(".dioxus").join("web"); + std::fs::create_dir_all(&target_directory).unwrap(); + std::thread::spawn(move || build_web(serve, &target_directory)) +} + +struct FullstackPlatform { + serve: ConfigOptsServe, + desktop: desktop::DesktopPlatform, + _config: WebAssetConfigDropGuard, +} + +impl Platform for FullstackPlatform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized, + { + let thread_handle = start_web_build_thread(config, serve); + + let mut desktop_config = config.clone(); + let desktop_feature = serve.server_feature.clone(); + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let config = WebAssetConfigDropGuard::new(); + let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + + Ok(Self { + desktop, + serve: serve.clone(), + _config: config, + }) + } + + fn rebuild(&mut self, crate_config: &CrateConfig) -> Result { + let thread_handle = start_web_build_thread(crate_config, &self.serve); + let result = { + let mut desktop_config = crate_config.clone(); + let desktop_feature = self.serve.server_feature.clone(); + let features = &mut desktop_config.features; + match features { + Some(features) => { + features.push(desktop_feature); + } + None => desktop_config.features = Some(vec![desktop_feature]), + }; + let _gaurd = FullstackServerEnvGuard::new(self.serve.force_debug, self.serve.release); + self.desktop.rebuild(&desktop_config) + }; + thread_handle + .join() + .map_err(|_| anyhow::anyhow!("Failed to join thread"))??; + result + } +} + +fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Result<()> { + let mut web_config: ConfigOptsBuild = serve.into(); + let web_feature = web_config.client_feature.clone(); + let features = &mut web_config.features; + match features { + Some(features) => { + features.push(web_feature); + } + None => web_config.features = Some(vec![web_feature]), + }; + web_config.platform = Some(crate::cfg::Platform::Web); + + let _gaurd = FullstackWebEnvGuard::new(&web_config); + crate::cli::build::Build { build: web_config }.build(None, Some(target_directory)) +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackWebEnvGuard { + old_rustflags: Option, +} + +impl FullstackWebEnvGuard { + pub fn new(serve: &ConfigOptsBuild) -> Self { + Self { + old_rustflags: (!serve.force_debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if serve.release { + "" + } else { + " -C debug-assertions" + }; + + std::env::set_var( + "RUSTFLAGS", + format!( + "{old_rustflags} -C debuginfo=none -C strip=debuginfo{debug_assertions}" + ), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackWebEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} + +// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default +// This reduces a hello world from ~40MB to ~2MB +pub(crate) struct FullstackServerEnvGuard { + old_rustflags: Option, +} + +impl FullstackServerEnvGuard { + pub fn new(debug: bool, release: bool) -> Self { + Self { + old_rustflags: (!debug).then(|| { + let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default(); + let debug_assertions = if release { "" } else { " -C debug-assertions" }; + + std::env::set_var( + "RUSTFLAGS", + format!("{old_rustflags} -C opt-level=2 {debug_assertions}"), + ); + old_rustflags + }), + } + } +} + +impl Drop for FullstackServerEnvGuard { + fn drop(&mut self) { + if let Some(old_rustflags) = self.old_rustflags.take() { + std::env::set_var("RUSTFLAGS", old_rustflags); + } + } +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index c6073d787..895d9e51e 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -1,4 +1,4 @@ -use crate::{BuildResult, CrateConfig, Result}; +use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result}; use cargo_metadata::diagnostic::Diagnostic; use dioxus_core::Template; @@ -14,6 +14,7 @@ use tokio::sync::broadcast::{self}; mod output; use output::*; pub mod desktop; +pub mod fullstack; pub mod web; /// Sets up a file watcher @@ -32,7 +33,7 @@ async fn setup_file_watcher Result + Send + 'static>( .watcher .watch_path .clone() - .unwrap_or_else(|| vec![PathBuf::from("src")]); + .unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]); let watcher_config = config.clone(); let mut watcher = notify::recommended_watcher(move |info: notify::Result| { @@ -55,6 +56,16 @@ async fn setup_file_watcher Result + Send + 'static>( break; } + // Workaround for notify and vscode-like editor: + // when edit & save a file in vscode, there will be two notifications, + // the first one is a file with empty content. + // filter the empty file notification to avoid false rebuild during hot-reload + if let Ok(metadata) = fs::metadata(path) { + if metadata.len() == 0 { + continue; + } + } + match rsx_file_map.update_rsx(path, &config.crate_dir) { Ok(UpdateResult::UpdatedRsx(msgs)) => { messages.extend(msgs); @@ -121,16 +132,23 @@ async fn setup_file_watcher Result + Send + 'static>( .unwrap(); for sub_path in allow_watch_path { - watcher - .watch( - &config.crate_dir.join(sub_path), - notify::RecursiveMode::Recursive, - ) - .unwrap(); + if let Err(err) = watcher.watch( + &config.crate_dir.join(sub_path), + notify::RecursiveMode::Recursive, + ) { + log::error!("Failed to watch path: {}", err); + } } Ok(watcher) } +pub(crate) trait Platform { + fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + where + Self: Sized; + fn rebuild(&mut self, config: &CrateConfig) -> Result; +} + #[derive(Clone)] pub struct HotReloadState { pub messages: broadcast::Sender>, diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index ce0f55064..4e148b0f1 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -22,17 +22,20 @@ pub fn print_console_info( options: PrettierOptions, web_info: Option, ) { - if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { - "cls" - } else { - "clear" - }) - .output() - { - print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); - } else { - // Try ANSI-Escape characters - print!("\x1b[2J\x1b[H"); + // Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs + if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() { + if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { + "cls" + } else { + "clear" + }) + .output() + { + print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); + } else { + // Try ANSI-Escape characters + print!("\x1b[2J\x1b[H"); + } } let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); @@ -129,7 +132,7 @@ pub fn print_console_info( log::warn!( "{}", format!( - "There were {} warning messages during the build.", + "There were {} warning messages during the build. Run `cargo check` to see them.", options.warnings.len() - 1 ) .yellow() diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 0e1413597..896d08643 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -11,6 +11,7 @@ use axum::{ body::{Full, HttpBody}, extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, http::{ + self, header::{HeaderName, HeaderValue}, Method, Response, StatusCode, }, @@ -47,7 +48,12 @@ struct WsReloadState { update: broadcast::Sender<()>, } -pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { +pub async fn startup( + port: u16, + config: CrateConfig, + start_browser: bool, + skip_assets: bool, +) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); let _ = ctrlc::set_handler(move || { @@ -79,7 +85,15 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res false => None, }; - serve(ip, port, config, start_browser, hot_reload_state).await?; + serve( + ip, + port, + config, + start_browser, + skip_assets, + hot_reload_state, + ) + .await?; Ok(()) } @@ -90,9 +104,10 @@ pub async fn serve( port: u16, config: CrateConfig, start_browser: bool, + skip_assets: bool, hot_reload_state: Option, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, true)?; + let first_build_result = crate::builder::build(&config, false, skip_assets)?; log::info!("🚀 Starting development server..."); @@ -105,7 +120,7 @@ pub async fn serve( { let config = config.clone(); let reload_tx = reload_tx.clone(); - move || build(&config, &reload_tx) + move || build(&config, &reload_tx, skip_assets) }, &config, Some(WebServerInfo { @@ -262,7 +277,7 @@ async fn setup_router( .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) .and_then( move |response: Response| async move { - let response = if file_service_config + let mut response = if file_service_config .dioxus_config .web .watcher @@ -290,6 +305,13 @@ async fn setup_router( } else { response.map(|body| body.boxed()) }; + let headers = response.headers_mut(); + headers.insert( + http::header::CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + ); + headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache")); + headers.insert(http::header::EXPIRES, HeaderValue::from_static("0")); Ok(response) }, ) @@ -412,8 +434,8 @@ async fn ws_handler( }) } -fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(config, true)?; +fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result { + let result = builder::build(config, true, skip_assets)?; // change the websocket reload state to true; // the page will auto-reload. if config @@ -423,7 +445,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { .reload_html .unwrap_or(false) { - let _ = Serve::regen_dev_page(config); + let _ = Serve::regen_dev_page(config, skip_assets); } let _ = reload_tx.send(()); Ok(result) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index d928d5fec..ae3dc9cdd 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,10 +15,12 @@ proc-macro = true [dependencies] proc-macro2 = { version = "1.0" } quote = "1.0" -syn = { version = "2.0", features = ["full", "extra-traits"] } +syn = { version = "2.0", features = ["full", "extra-traits", "visit"] } dioxus-rsx = { workspace = true } dioxus-core = { workspace = true } constcat = "0.3.0" +convert_case = "^0.6.0" +prettyplease = "0.2.15" # testing [dev-dependencies] diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md index 31b0cca90..497c7c587 100644 --- a/packages/core-macro/README.md +++ b/packages/core-macro/README.md @@ -23,9 +23,12 @@ `dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include: -- The `rsx!` macro that underpins templates and node creation -- The `inline_props` macro transforms function arguments into an auto-derived struct -- The `format_args_f` macro which allows f-string formatting with support for expressions +- The `rsx!` macro that underpins templates and node creation. +- The `component` attribute macro denotes a function as a Dioxus component. Currently, this: + - Transforms function arguments into an auto-derived struct. + - Ensures that your component name uses PascalCase. + - Probably more stuff in the future. This macro allows us to have a way of distinguishing functions and components, which can be quite handy. +- The `format_args_f` macro which allows f-string formatting with support for expressions. ## Contributing diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs index e43b1819f..994e51709 100644 --- a/packages/core-macro/src/component_body_deserializers/component.rs +++ b/packages/core-macro/src/component_body_deserializers/component.rs @@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn { block: parse_quote! { { #[warn(non_snake_case)] + #[allow(clippy::inline_always)] #[inline(always)] #inner_comp_fn #inner_comp_ident (#cx_pat) @@ -57,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput { fn to_tokens(&self, tokens: &mut TokenStream2) { let comp_fn = &self.comp_fn; let props_struct = &self.props_struct; + let fn_ident = &comp_fn.sig.ident; + let doc = format!("Properties for the [`{fn_ident}`] component."); tokens.append_all(quote! { + #[doc = #doc] #props_struct #[allow(non_snake_case)] #comp_fn diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs index 8e4581c19..d51132623 100644 --- a/packages/core-macro/src/component_body_deserializers/inline_props.rs +++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs @@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput { impl DeserializerArgs for InlinePropsDeserializerArgs { fn to_output(&self, component_body: &ComponentBody) -> Result { Ok(InlinePropsDeserializerOutput { - comp_fn: Self::get_function(component_body), - props_struct: Self::get_props_struct(component_body), + comp_fn: get_function(component_body), + props_struct: get_props_struct(component_body), }) } } -impl InlinePropsDeserializerArgs { - fn get_props_struct(component_body: &ComponentBody) -> ItemStruct { - let ComponentBody { item_fn, .. } = component_body; - let ItemFn { vis, sig, .. } = item_fn; - let Signature { - inputs, - ident: fn_ident, - generics, - .. - } = sig; +fn get_props_struct(component_body: &ComponentBody) -> ItemStruct { + let ComponentBody { item_fn, .. } = component_body; + let ItemFn { vis, sig, .. } = item_fn; + let Signature { + inputs, + ident: fn_ident, + generics, + .. + } = sig; - // Skip first arg since that's the context - let struct_fields = inputs.iter().skip(1).map(move |f| { - match f { - FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing - FnArg::Typed(pt) => { - let arg_pat = &pt.pat; // Pattern (identifier) - let arg_colon = &pt.colon_token; - let arg_ty = &pt.ty; // Type - let arg_attrs = &pt.attrs; // Attributes + // Skip first arg since that's the context + let struct_fields = inputs.iter().skip(1).map(move |f| { + match f { + FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing + FnArg::Typed(pt) => { + let arg_pat = &pt.pat; // Pattern (identifier) + let arg_colon = &pt.colon_token; + let arg_ty = &pt.ty; // Type + let arg_attrs = &pt.attrs; // Attributes - quote! { - #(#arg_attrs) - * - #vis #arg_pat #arg_colon #arg_ty - } + quote! { + #(#arg_attrs) + * + #vis #arg_pat #arg_colon #arg_ty } } - }); - - let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); - - let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { - Some(lt) - } else { - None - }; - - let struct_attrs = if first_lifetime.is_some() { - quote! { #[derive(Props)] } - } else { - quote! { #[derive(Props, PartialEq)] } - }; - - let struct_generics = if first_lifetime.is_some() { - let struct_generics: Punctuated = component_body - .item_fn - .sig - .generics - .params - .iter() - .map(|it| match it { - GenericParam::Type(tp) => { - let mut tp = tp.clone(); - tp.bounds.push(parse_quote!( 'a )); - - GenericParam::Type(tp) - } - _ => it.clone(), - }) - .collect(); - - quote! { <#struct_generics> } - } else { - quote! { #generics } - }; - - parse_quote! { - #struct_attrs - #[allow(non_camel_case_types)] - #vis struct #struct_ident #struct_generics - { - #(#struct_fields),* - } } - } + }); - fn get_function(component_body: &ComponentBody) -> ItemFn { - let ComponentBody { - item_fn, - cx_pat_type, - .. - } = component_body; - let ItemFn { - attrs: fn_attrs, - vis, - sig, - block: fn_block, - } = item_fn; - let Signature { - inputs, - ident: fn_ident, - generics, - output: fn_output, - asyncness, - .. - } = sig; - let Generics { where_clause, .. } = generics; + let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); - let cx_pat = &cx_pat_type.pat; - let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); + let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { + Some(lt) + } else { + None + }; - // Skip first arg since that's the context - let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f { - FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. - FnArg::Typed(t) => Some(&t.pat), - }); + let struct_attrs = if first_lifetime.is_some() { + quote! { #[derive(Props)] } + } else { + quote! { #[derive(Props, PartialEq)] } + }; - let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { - Some(lt) - } else { - None - }; + let struct_generics = if first_lifetime.is_some() { + let struct_generics: Punctuated = component_body + .item_fn + .sig + .generics + .params + .iter() + .map(|it| match it { + GenericParam::Type(tp) => { + let mut tp = tp.clone(); + tp.bounds.push(parse_quote!( 'a )); - let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime { - (quote! { #lt, }, generics.clone()) - } else { - let lifetime: LifetimeParam = parse_quote! { 'a }; + GenericParam::Type(tp) + } + _ => it.clone(), + }) + .collect(); - let mut fn_generics = generics.clone(); - fn_generics - .params - .insert(0, GenericParam::Lifetime(lifetime.clone())); + quote! { <#struct_generics> } + } else { + quote! { #generics } + }; - (quote! { #lifetime, }, fn_generics) - }; - - let generics_no_bounds = { - let mut generics = generics.clone(); - generics.params = generics - .params - .iter() - .map(|it| match it { - GenericParam::Type(tp) => { - let mut tp = tp.clone(); - tp.bounds.clear(); - - GenericParam::Type(tp) - } - _ => it.clone(), - }) - .collect(); - - generics - }; - - parse_quote! { - #(#fn_attrs)* - #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output - #where_clause - { - let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props; - #fn_block - } + parse_quote! { + #struct_attrs + #[allow(non_camel_case_types)] + #vis struct #struct_ident #struct_generics + { + #(#struct_fields),* } } } + +fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec { + if inputs.len() <= 1 { + return Vec::new(); + } + + let arg_docs = inputs + .iter() + .filter_map(|f| match f { + FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. + FnArg::Typed(pt) => { + let arg_doc = pt + .attrs + .iter() + .filter_map(|attr| { + // TODO: Error reporting + // Check if the path of the attribute is "doc" + if !is_attr_doc(attr) { + return None; + }; + + let Meta::NameValue(meta_name_value) = &attr.meta else { + return None; + }; + + let Expr::Lit(doc_lit) = &meta_name_value.value else { + return None; + }; + + let Lit::Str(doc_lit_str) = &doc_lit.lit else { + return None; + }; + + Some(doc_lit_str.value()) + }) + .fold(String::new(), |mut doc, next_doc_line| { + doc.push('\n'); + doc.push_str(&next_doc_line); + doc + }); + + Some(( + &pt.pat, + &pt.ty, + pt.attrs.iter().find_map(|attr| { + if attr.path() != &parse_quote!(deprecated) { + return None; + } + + let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); + + match res { + Err(e) => panic!("{}", e.to_string()), + Ok(v) => Some(v), + } + }), + arg_doc, + )) + } + }) + .collect::>(); + + let mut props_docs = Vec::with_capacity(5); + let props_def_link = fn_ident.to_string() + "Props"; + let header = + format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*"); + + props_docs.push(parse_quote! { + #[doc = #header] + }); + + for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs { + let arg_name = arg_name.into_token_stream().to_string(); + let arg_type = crate::utils::format_type_string(arg_type); + + let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n') + .replace("\n\n", "

"); + let prop_def_link = format!("{props_def_link}::{arg_name}"); + let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`"); + + if let Some(deprecation) = deprecation { + arg_doc.push_str("

👎 Deprecated"); + + if let Some(since) = deprecation.since { + arg_doc.push_str(&format!(" since {since}")); + } + + if let Some(note) = deprecation.note { + let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " "); + let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " "); + + arg_doc.push_str(&format!(": {note}")); + } + + arg_doc.push_str("

"); + + if !input_arg_doc.is_empty() { + arg_doc.push_str("
"); + } + } + + if !input_arg_doc.is_empty() { + arg_doc.push_str(&format!("

{input_arg_doc}

")); + } + + props_docs.push(parse_quote! { + #[doc = #arg_doc] + }); + } + + props_docs +} + +fn get_function(component_body: &ComponentBody) -> ItemFn { + let ComponentBody { + item_fn, + cx_pat_type, + .. + } = component_body; + let ItemFn { + attrs: fn_attrs, + vis, + sig, + block: fn_block, + } = item_fn; + let Signature { + inputs, + ident: fn_ident, + generics, + output: fn_output, + asyncness, + .. + } = sig; + let Generics { where_clause, .. } = generics; + + let cx_pat = &cx_pat_type.pat; + let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); + + // Skip first arg since that's the context + let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f { + FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. + FnArg::Typed(pt) => Some(&pt.pat), + }); + + let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { + Some(lt) + } else { + None + }; + + let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime { + (quote! { #lt, }, generics.clone()) + } else { + let lifetime: LifetimeParam = parse_quote! { 'a }; + + let mut fn_generics = generics.clone(); + fn_generics + .params + .insert(0, GenericParam::Lifetime(lifetime.clone())); + + (quote! { #lifetime, }, fn_generics) + }; + + let generics_no_bounds = { + let mut generics = generics.clone(); + generics.params = generics + .params + .iter() + .map(|it| match it { + GenericParam::Type(tp) => { + let mut tp = tp.clone(); + tp.bounds.clear(); + + GenericParam::Type(tp) + } + _ => it.clone(), + }) + .collect(); + + generics + }; + + let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect()); + + parse_quote! { + #(#fn_attrs)* + #(#props_docs)* + #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output + #where_clause + { + let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props; + #fn_block + } + } +} + +/// Checks if the attribute is a `#[doc]` attribute. +fn is_attr_doc(attr: &Attribute) -> bool { + attr.path() == &parse_quote!(doc) +} + +fn keep_up_to_n_consecutive_chars( + input: &str, + n_of_consecutive_chars_allowed: usize, + target_char: char, +) -> String { + let mut output = String::new(); + let mut prev_char: Option = None; + let mut consecutive_count = 0; + + for c in input.chars() { + match prev_char { + Some(prev) if c == target_char && prev == target_char => { + if consecutive_count < n_of_consecutive_chars_allowed { + output.push(c); + consecutive_count += 1; + } + } + _ => { + output.push(c); + prev_char = Some(c); + consecutive_count = 1; + } + } + } + + output +} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index d91c72e39..4686bc797 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + use proc_macro::TokenStream; use quote::ToTokens; use rsx::RenderCallBody; @@ -8,6 +12,7 @@ use syn::{parse_macro_input, Path, Token}; mod component_body; mod component_body_deserializers; mod props; +mod utils; // mod rsx; use crate::component_body::ComponentBody; diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index c1931e497..fff8bcbf1 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .included_fields() .map(|f| struct_info.field_impl(f)) .collect::, _>>()?; + let extends = struct_info + .extend_fields() + .map(|f| struct_info.extends_impl(f)) + .collect::, _>>()?; let fields = quote!(#(#fields)*).into_iter(); let required_fields = struct_info .included_fields() @@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { #builder_creation #conversion_helper #( #fields )* + #( #extends )* #( #required_fields )* #build_method } @@ -167,8 +172,8 @@ mod field_info { use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; - use syn::Expr; use syn::{parse::Error, punctuated::Punctuated}; + use syn::{Expr, Path}; use super::util::{ expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix, @@ -199,6 +204,13 @@ mod field_info { ); } + // extended field is automatically empty + if !builder_attr.extends.is_empty() { + builder_attr.default = Some( + syn::parse(quote!(::core::default::Default::default()).into()).unwrap(), + ); + } + // auto detect optional let strip_option_auto = builder_attr.strip_option || !builder_attr.ignore_option @@ -243,10 +255,6 @@ mod field_info { } .into() } - - pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> { - type_from_inside_option(self.ty, check_option_name) - } } #[derive(Debug, Default, Clone)] @@ -257,6 +265,7 @@ mod field_info { pub auto_into: bool, pub strip_option: bool, pub ignore_option: bool, + pub extends: Vec, } impl FieldBuilderAttr { @@ -309,6 +318,17 @@ mod field_info { let name = expr_to_single_string(&assign.left) .ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?; match name.as_str() { + "extends" => { + if let syn::Expr::Path(path) = *assign.right { + self.extends.push(path.path); + Ok(()) + } else { + Err(Error::new_spanned( + assign.right, + "Expected simple identifier", + )) + } + } "default" => { self.default = Some(*assign.right); Ok(()) @@ -363,6 +383,11 @@ mod field_info { Ok(()) } + "extend" => { + self.extends.push(path.path); + Ok(()) + } + _ => { macro_rules! handle_fields { ( $( $flag:expr, $field:ident, $already:expr; )* ) => { @@ -466,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s } mod struct_info { + use convert_case::{Case, Casing}; use proc_macro2::TokenStream; use quote::quote; use syn::parse::Error; use syn::punctuated::Punctuated; - use syn::Expr; + use syn::spanned::Spanned; + use syn::visit::Visit; + use syn::{parse_quote, Expr, Ident}; use super::field_info::{FieldBuilderAttr, FieldInfo}; use super::util::{ @@ -493,7 +521,46 @@ mod struct_info { impl<'a> StructInfo<'a> { pub fn included_fields(&self) -> impl Iterator> { - self.fields.iter().filter(|f| !f.builder_attr.skip) + self.fields + .iter() + .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty()) + } + + pub fn extend_fields(&self) -> impl Iterator> { + self.fields + .iter() + .filter(|f| !f.builder_attr.extends.is_empty()) + } + + fn extend_lifetime(&self) -> syn::Result> { + let first_extend = self.extend_fields().next(); + + match first_extend { + Some(f) => { + struct VisitFirstLifetime(Option); + + impl Visit<'_> for VisitFirstLifetime { + fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) { + if self.0.is_none() { + self.0 = Some(lifetime.clone()); + } + } + } + + let name = f.name; + let mut visitor = VisitFirstLifetime(None); + + visitor.visit_type(f.ty); + + visitor.0.ok_or_else(|| { + syn::Error::new_spanned( + name, + "Unable to find lifetime for extended field. Please specify it manually", + ) + }).map(Some) + } + None => Ok(None), + } } pub fn new( @@ -540,7 +607,17 @@ mod struct_info { // Therefore, we will generate code that shortcircuits the "comparison" in memoization let are_there_generics = !self.generics.params.is_empty(); - let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + let extend_lifetime = self.extend_lifetime()?; + + let generics = self.generics.clone(); + let (_, ty_generics, where_clause) = generics.split_for_impl(); + let impl_generics = self.modify_generics(|g| { + if extend_lifetime.is_none() { + g.params.insert(0, parse_quote!('__bump)); + } + }); + let (impl_generics, _, _) = impl_generics.split_for_impl(); + let (_, b_initial_generics, _) = self.generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), ); @@ -548,21 +625,19 @@ mod struct_info { g.params.insert(0, all_fields_param.clone()); }); let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type())); - let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| { + let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| { args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into())); }); - let phantom_generics = self.generics.params.iter().map(|param| match param { + let phantom_generics = self.generics.params.iter().filter_map(|param| match param { syn::GenericParam::Lifetime(lifetime) => { let lifetime = &lifetime.lifetime; - quote!(::core::marker::PhantomData<&#lifetime ()>) + Some(quote!(::core::marker::PhantomData<&#lifetime ()>)) } syn::GenericParam::Type(ty) => { let ty = &ty.ident; - quote!(::core::marker::PhantomData<#ty>) - } - syn::GenericParam::Const(_cnst) => { - quote!() + Some(quote!(::core::marker::PhantomData<#ty>)) } + syn::GenericParam::Const(_cnst) => None, }); let builder_method_doc = match self.builder_attr.builder_method_doc { Some(ref doc) => quote!(#doc), @@ -609,8 +684,7 @@ Finally, call `.build()` to create the instance of `{name}`. quote!(#[doc(hidden)]) }; - let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) = - b_generics.split_for_impl(); + let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl(); let mut b_generics_where: syn::WhereClause = syn::parse2(quote! { where TypedBuilderFields: Clone })?; @@ -630,12 +704,39 @@ Finally, call `.build()` to create the instance of `{name}`. false => quote! { true }, }; + let extend_fields = self.extend_fields().map(|f| { + let name = f.name; + let ty = f.ty; + quote!(#name: #ty) + }); + let extend_fields_value = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: Vec::new()) + }); + let has_extend_fields = self.extend_fields().next().is_some(); + let take_bump = if has_extend_fields { + quote!(bump: _cx.bump(),) + } else { + quote!() + }; + let bump_field = if has_extend_fields { + quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,) + } else { + quote!() + }; + let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new( + "'__bump", + proc_macro2::Span::call_site(), + )); + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] - #[allow(dead_code)] - #vis fn builder() -> #builder_name #generics_with_empty { + #[allow(dead_code, clippy::type_complexity)] + #vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty { #builder_name { + #(#extend_fields_value,)* + #take_bump fields: #empties_tuple, _phantom: ::core::default::Default::default(), } @@ -646,26 +747,19 @@ Finally, call `.build()` to create the instance of `{name}`. #builder_type_doc #[allow(dead_code, non_camel_case_types, non_snake_case)] #vis struct #builder_name #b_generics { + #(#extend_fields,)* + #bump_field fields: #all_fields_param, _phantom: (#( #phantom_generics ),*), } - impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where { - fn clone(&self) -> Self { - Self { - fields: self.fields.clone(), - _phantom: ::core::default::Default::default(), - } - } - } - - impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics + impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics #b_generics_where_extras_predicates { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; - fn builder() -> Self::Builder { - #name::builder() + fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder { + #name::builder(_cx) } unsafe fn memoize(&self, other: &Self) -> bool { #can_memoize @@ -700,7 +794,147 @@ Finally, call `.build()` to create the instance of `{name}`. }) } + pub fn extends_impl(&self, field: &FieldInfo) -> Result { + let StructInfo { + ref builder_name, .. + } = *self; + + let field_name = field.name; + + let descructuring = self.included_fields().map(|f| { + if f.ordinal == field.ordinal { + quote!(_) + } else { + let name = f.name; + quote!(#name) + } + }); + let reconstructing = self.included_fields().map(|f| f.name); + + // Add the bump lifetime to the generics + let mut ty_generics: Vec = self + .generics + .params + .iter() + .map(|generic_param| match generic_param { + syn::GenericParam::Type(type_param) => { + let ident = type_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + syn::GenericParam::Lifetime(lifetime_def) => { + syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone()) + } + syn::GenericParam::Const(const_param) => { + let ident = const_param.ident.clone(); + syn::parse(quote!(#ident).into()).unwrap() + } + }) + .collect(); + let mut target_generics_tuple = empty_type_tuple(); + let mut ty_generics_tuple = empty_type_tuple(); + let generics = self.modify_generics(|g| { + let index_after_lifetime_in_generics = g + .params + .iter() + .filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_))) + .count(); + for f in self.included_fields() { + if f.ordinal == field.ordinal { + ty_generics_tuple.elems.push_value(empty_type()); + target_generics_tuple + .elems + .push_value(f.tuplized_type_ty_param()); + } else { + g.params + .insert(index_after_lifetime_in_generics, f.generic_ty_param()); + let generic_argument: syn::Type = f.type_ident(); + ty_generics_tuple.elems.push_value(generic_argument.clone()); + target_generics_tuple.elems.push_value(generic_argument); + } + ty_generics_tuple.elems.push_punct(Default::default()); + target_generics_tuple.elems.push_punct(Default::default()); + } + }); + let mut target_generics = ty_generics.clone(); + let index_after_lifetime_in_generics = target_generics + .iter() + .filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_))) + .count(); + target_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(target_generics_tuple.into()), + ); + ty_generics.insert( + index_after_lifetime_in_generics, + syn::GenericArgument::Type(ty_generics_tuple.into()), + ); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let forward_extended_fields = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: self.#name) + }); + + let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned( + field_name, + "Unable to find lifetime for extended field. Please specify it manually", + ))?; + + let extends_impl = field.builder_attr.extends.iter().map(|path| { + let name_str = path_to_single_string(path).unwrap(); + let camel_name = name_str.to_case(Case::UpperCamel); + let marker_name = Ident::new( + format!("{}Extension", &camel_name).as_str(), + path.span(), + ); + quote! { + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {} + } + }); + + Ok(quote! { + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause { + fn push_attribute( + mut self, + name: &#extend_lifetime str, + ns: Option<&'static str>, + attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>, + volatile: bool + ) -> Self { + let ( #(#descructuring,)* ) = self.fields; + self.#field_name.push( + ::dioxus::core::Attribute::new( + name, + { + use ::dioxus::prelude::IntoAttributeValue; + attr.into_value(self.bump) + }, + ns, + volatile, + ) + ); + #builder_name { + #(#forward_extended_fields,)* + bump: self.bump, + fields: ( #(#reconstructing,)* ), + _phantom: self._phantom, + } + } + } + + #(#extends_impl)* + }) + } + pub fn field_impl(&self, field: &FieldInfo) -> Result { + let FieldInfo { + name: field_name, .. + } = field; + if *field_name == "key" { + return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys")); + } let StructInfo { ref builder_name, .. } = *self; @@ -720,6 +954,7 @@ Finally, call `.build()` to create the instance of `{name}`. ty: field_type, .. } = field; + // Add the bump lifetime to the generics let mut ty_generics: Vec = self .generics .params @@ -782,31 +1017,16 @@ Finally, call `.build()` to create the instance of `{name}`. None => quote!(), }; - // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of - // nesting is different so we have to do this little dance. - let arg_type = if field.builder_attr.strip_option { - field.type_from_inside_option(false).ok_or_else(|| { - Error::new_spanned( - field_type, - "can't `strip_option` - field is not `Option<...>`", + let arg_type = field_type; + let (arg_type, arg_expr) = + if field.builder_attr.auto_into || field.builder_attr.strip_option { + ( + quote!(impl ::core::convert::Into<#arg_type>), + quote!(#field_name.into()), ) - })? - } else { - field_type - }; - let (arg_type, arg_expr) = if field.builder_attr.auto_into { - ( - quote!(impl ::core::convert::Into<#arg_type>), - quote!(#field_name.into()), - ) - } else { - (quote!(#arg_type), quote!(#field_name)) - }; - let arg_expr = if field.builder_attr.strip_option { - quote!(Some(#arg_expr)) - } else { - arg_expr - }; + } else { + (quote!(#arg_type), quote!(#field_name)) + }; let repeated_fields_error_type_name = syn::Ident::new( &format!( @@ -818,14 +1038,27 @@ Finally, call `.build()` to create the instance of `{name}`. ); let repeated_fields_error_message = format!("Repeated field {field_name}"); + let forward_extended_fields = self.extend_fields().map(|f| { + let name = f.name; + quote!(#name: self.#name) + }); + let forward_bump = if self.extend_fields().next().is_some() { + quote!(bump: self.bump,) + } else { + quote!() + }; + Ok(quote! { #[allow(dead_code, non_camel_case_types, missing_docs)] impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause { #doc + #[allow(clippy::type_complexity)] pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > { let #field_name = (#arg_expr,); let ( #(#descructuring,)* ) = self.fields; #builder_name { + #(#forward_extended_fields,)* + #forward_bump fields: ( #(#reconstructing,)* ), _phantom: self._phantom, } @@ -840,6 +1073,7 @@ Finally, call `.build()` to create the instance of `{name}`. #[deprecated( note = #repeated_fields_error_message )] + #[allow(clippy::type_complexity)] pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > { self } @@ -858,6 +1092,7 @@ Finally, call `.build()` to create the instance of `{name}`. name: ref field_name, .. } = field; + // Add a bump lifetime to the generics let mut builder_generics: Vec = self .generics .params @@ -1025,7 +1260,9 @@ Finally, call `.build()` to create the instance of `{name}`. // reordering based on that, but for now this much simpler thing is a reasonable approach. let assignments = self.fields.iter().map(|field| { let name = &field.name; - if let Some(ref default) = field.builder_attr.default { + if !field.builder_attr.extends.is_empty() { + quote!(let #name = self.#name;) + } else if let Some(ref default) = field.builder_attr.default { if field.builder_attr.skip { quote!(let #name = #default;) } else { diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs new file mode 100644 index 000000000..7e066f773 --- /dev/null +++ b/packages/core-macro/src/utils.rs @@ -0,0 +1,129 @@ +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{parse_quote, Expr, Lit, Meta, Token, Type}; + +const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:"; +const FORMATTED_TYPE_END: &str = "= todo!();"; + +/// Attempts to convert the given literal to a string. +/// Converts ints and floats to their base 10 counterparts. +/// +/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`] +/// and the byte string could not be converted to UTF-8. +pub fn lit_to_string(lit: Lit) -> Option { + match lit { + Lit::Str(l) => Some(l.value()), + Lit::ByteStr(l) => String::from_utf8(l.value()).ok(), + Lit::Byte(l) => Some(String::from(l.value() as char)), + Lit::Char(l) => Some(l.value().to_string()), + Lit::Int(l) => Some(l.base10_digits().to_string()), + Lit::Float(l) => Some(l.base10_digits().to_string()), + Lit::Bool(l) => Some(l.value().to_string()), + Lit::Verbatim(_) => None, + _ => None, + } +} + +pub fn format_type_string(ty: &Type) -> String { + let ty_unformatted = ty.into_token_stream().to_string(); + let ty_unformatted = ty_unformatted.trim(); + + // This should always be valid syntax. + // Not Rust code, but syntax, which is the only thing that `syn` cares about. + let Ok(file_unformatted) = syn::parse_file(&format!( + "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}" + )) else { + return ty_unformatted.to_string(); + }; + + let file_formatted = prettyplease::unparse(&file_unformatted); + + let file_trimmed = file_formatted.trim(); + let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START); + let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END); + let ty_formatted = end_removed.trim(); + + ty_formatted.to_string() +} + +/// Represents the `#[deprecated]` attribute. +/// +/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct. +#[derive(Default)] +pub struct DeprecatedAttribute { + pub since: Option, + pub note: Option, +} + +impl DeprecatedAttribute { + /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute. + pub fn from_meta(meta: &Meta) -> syn::Result { + if meta.path() != &parse_quote!(deprecated) { + return Err(syn::Error::new( + meta.span(), + "attribute path is not `deprecated`", + )); + } + + match &meta { + Meta::Path(_) => Ok(Self::default()), + Meta::NameValue(name_value) => { + let Expr::Lit(expr_lit) = &name_value.value else { + return Err(syn::Error::new( + name_value.span(), + "literal in `deprecated` value must be a string", + )); + }; + + Ok(Self { + since: None, + note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()), + }) + } + Meta::List(list) => { + let parsed = list.parse_args::()?; + + Ok(Self { + since: parsed.since.map(|s| s.trim().to_string()), + note: parsed.note.map(|s| s.trim().to_string()), + }) + } + } + } +} + +mod kw { + use syn::custom_keyword; + custom_keyword!(since); + custom_keyword!(note); +} + +struct DeprecatedAttributeArgsParser { + since: Option, + note: Option, +} + +impl Parse for DeprecatedAttributeArgsParser { + fn parse(input: ParseStream) -> syn::Result { + let mut since: Option = None; + let mut note: Option = None; + + if input.peek(kw::since) { + input.parse::()?; + input.parse::()?; + + since = lit_to_string(input.parse()?); + } + + if input.peek(Token![,]) && input.peek2(kw::note) { + input.parse::()?; + input.parse::()?; + input.parse::()?; + + note = lit_to_string(input.parse()?); + } + + Ok(Self { since, note }) + } +} diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index ce7c6bccf..6cc7b3ad3 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -36,6 +36,7 @@ serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } +dioxus-html = { workspace = true, features = ["serialize"] } pretty_assertions = "1.3.0" rand = "0.8.5" dioxus-ssr = { workspace = true } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index b7f768755..ffdd4656b 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,62 +13,50 @@ use crate::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub(crate) struct ElementRef { +/// An Element that can be bubbled to's unique identifier. +/// +/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `BubbleId` will be reused for a new component. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct VNodeId(pub usize); + +#[derive(Debug, Clone, Copy)] +pub struct ElementRef { // the pathway of the real element inside the template - pub path: ElementPath, + pub(crate) path: ElementPath, // The actual template - pub template: Option>>, + pub(crate) template: VNodeId, // The scope the element belongs to - pub scope: ScopeId, + pub(crate) scope: ScopeId, } #[derive(Clone, Copy, Debug)] -pub enum ElementPath { - Deep(&'static [u8]), - Root(usize), -} - -impl ElementRef { - pub(crate) fn none() -> Self { - Self { - template: None, - path: ElementPath::Root(0), - scope: ScopeId::ROOT, - } - } +pub struct ElementPath { + pub(crate) path: &'static [u8], } impl VirtualDom { - pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { - self.next_reference(template, ElementPath::Deep(path)) + pub(crate) fn next_element(&mut self) -> ElementId { + ElementId(self.elements.insert(None)) } - pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { - self.next_reference(template, ElementPath::Root(path)) - } + pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId { + let new_id = VNodeId(self.element_refs.insert(Some(unsafe { + std::mem::transmute::, _>(vnode.into()) + }))); - pub(crate) fn next_null(&mut self) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); + // Set this id to be dropped when the scope is rerun + if let Some(scope) = self.runtime.current_scope_id() { + self.scopes[scope.0] + .element_refs_to_drop + .borrow_mut() + .push(new_id); + } - entry.insert(ElementRef::none()); - ElementId(id) - } - - fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId { - let entry = self.elements.vacant_entry(); - let id = entry.key(); - let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT); - - entry.insert(ElementRef { - // We know this is non-null because it comes from a reference - template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }), - path, - scope, - }); - ElementId(id) + new_id } pub(crate) fn reclaim(&mut self, el: ElementId) { @@ -76,7 +64,7 @@ impl VirtualDom { .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); } - pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> { if el.0 == 0 { panic!( "Cannot reclaim the root element - {:#?}", @@ -84,12 +72,12 @@ impl VirtualDom { ); } - self.elements.try_remove(el.0) + self.elements.try_remove(el.0).map(|_| ()) } - pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { - let node: *const VNode = node as *const _; - self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) { + self.element_refs[id.0] = + Some(unsafe { std::mem::transmute::, _>(vnode.into()) }); } // Drop a scope and all its children @@ -101,6 +89,15 @@ impl VirtualDom { id, }); + // Remove all VNode ids from the scope + for id in self.scopes[id.0] + .element_refs_to_drop + .borrow_mut() + .drain(..) + { + self.element_refs.try_remove(id.0); + } + self.ensure_drop_safety(id); if recursive { @@ -145,14 +142,25 @@ impl VirtualDom { } /// Descend through the tree, removing any borrowed props and listeners - pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) { let scope = &self.scopes[scope_id.0]; + { + // Drop all element refs that could be invalidated when the component was rerun + let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut(); + let element_refs_slab = &mut self.element_refs; + for element_ref in element_refs.drain(..) { + if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) { + *element_ref = None; + } + } + } + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children - let mut props = scope.borrowed_props.borrow_mut(); - props.drain(..).for_each(|comp| { + let props = { scope.borrowed_props.borrow_mut().clone() }; + for comp in props { let comp = unsafe { &*comp }; match comp.scope.get() { Some(child) if child != scope_id => self.ensure_drop_safety(child), @@ -161,20 +169,16 @@ impl VirtualDom { if let Ok(mut props) = comp.props.try_borrow_mut() { *props = None; } - }); + } + let scope = &self.scopes[scope_id.0]; + scope.borrowed_props.borrow_mut().clear(); // Now that all the references are gone, we can safely drop our own references in our listeners. - let mut listeners = scope.attributes_to_drop.borrow_mut(); + let mut listeners = scope.attributes_to_drop_before_render.borrow_mut(); listeners.drain(..).for_each(|listener| { let listener = unsafe { &*listener }; - match &listener.value { - AttributeValue::Listener(l) => { - _ = l.take(); - } - AttributeValue::Any(a) => { - _ = a.take(); - } - _ => (), + if let AttributeValue::Listener(l) = &listener.value { + _ = l.take(); } }); } @@ -182,18 +186,12 @@ impl VirtualDom { impl ElementPath { pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool { - match *self { - ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()], - ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8, - } + small.len() <= self.path.len() && *small == &self.path[..small.len()] } } impl PartialEq<&[u8]> for ElementPath { fn eq(&self, other: &&[u8]) -> bool { - match *self { - ElementPath::Deep(deep) => deep.eq(*other), - ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, - } + self.path.eq(*other) } } diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index 0fe7b3867..6927bd8bc 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -1,10 +1,16 @@ use crate::nodes::RenderReturn; +use crate::{Attribute, AttributeValue, VComponent}; use bumpalo::Bump; +use std::cell::RefCell; use std::cell::{Cell, UnsafeCell}; pub(crate) struct BumpFrame { pub bump: UnsafeCell, pub node: Cell<*const RenderReturn<'static>>, + + // The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset. + pub(crate) attributes_to_drop_before_reset: RefCell>>, + pub(crate) props_to_drop_before_reset: RefCell>>, } impl BumpFrame { @@ -13,6 +19,8 @@ impl BumpFrame { Self { bump: UnsafeCell::new(bump), node: Cell::new(std::ptr::null()), + attributes_to_drop_before_reset: Default::default(), + props_to_drop_before_reset: Default::default(), } } @@ -31,8 +39,38 @@ impl BumpFrame { unsafe { &*self.bump.get() } } - #[allow(clippy::mut_from_ref)] - pub(crate) unsafe fn bump_mut(&self) -> &mut Bump { - unsafe { &mut *self.bump.get() } + pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) { + self.attributes_to_drop_before_reset + .borrow_mut() + .push(attribute); + } + + /// Reset the bump allocator and drop all the attributes and props that were allocated in it. + /// + /// # Safety + /// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called. + pub(crate) unsafe fn reset(&self) { + let mut attributes = self.attributes_to_drop_before_reset.borrow_mut(); + attributes.drain(..).for_each(|attribute| { + let attribute = unsafe { &*attribute }; + if let AttributeValue::Any(l) = &attribute.value { + _ = l.take(); + } + }); + let mut props = self.props_to_drop_before_reset.borrow_mut(); + props.drain(..).for_each(|prop| { + let prop = unsafe { &*prop }; + _ = prop.props.borrow_mut().take(); + }); + unsafe { + let bump = &mut *self.bump.get(); + bump.reset(); + } + } +} + +impl Drop for BumpFrame { + fn drop(&mut self) { + unsafe { self.reset() } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index a61a8cace..8602b93bd 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,5 +1,8 @@ use crate::any_props::AnyProps; -use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText}; +use crate::innerlude::{ + AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent, + VPlaceholder, VText, +}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; @@ -94,6 +97,9 @@ impl<'b> VirtualDom { nodes_mut.resize(len, ElementId::default()); }; + // Set this node id + node.stable_id.set(Some(self.next_vnode_ref(node))); + // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways self.register_template(node.template.get()); @@ -181,15 +187,30 @@ impl<'b> VirtualDom { use DynamicNode::*; match &template.dynamic_nodes[idx] { node @ Component { .. } | node @ Fragment(_) => { - self.create_dynamic_node(template, node, idx) + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.create_dynamic_node(template_ref, node) } - Placeholder(VPlaceholder { id }) => { - let id = self.set_slot(template, id, idx); + Placeholder(VPlaceholder { id, parent }) => { + let template_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + parent.set(Some(template_ref)); + let id = self.set_slot(id); self.mutations.push(CreatePlaceholder { id }); 1 } Text(VText { id, value }) => { - let id = self.set_slot(template, id, idx); + let id = self.set_slot(id); self.create_static_text(value, id); 1 } @@ -205,7 +226,7 @@ impl<'b> VirtualDom { }); } - /// We write all the descndent data for this element + /// We write all the descendent data for this element /// /// Elements can contain other nodes - and those nodes can be dynamic or static /// @@ -265,7 +286,14 @@ impl<'b> VirtualDom { .map(|sorted_index| dynamic_nodes[sorted_index].0); for idx in reversed_iter { - let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx); + let boundary_ref = ElementRef { + path: ElementPath { + path: template.template.get().node_paths[idx], + }, + template: template.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]); if m > 0 { // The path is one shorter because the top node is the root let path = &template.template.get().node_paths[idx][1..]; @@ -279,15 +307,15 @@ impl<'b> VirtualDom { attrs: &mut Peekable>, root_idx: u8, root: ElementId, - node: &VNode, + node: &'b VNode<'b>, ) { while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) { - let id = self.assign_static_node_as_dynamic(path, root, node, attr_id); + let id = self.assign_static_node_as_dynamic(path, root); loop { - self.write_attribute(&node.dynamic_attrs[attr_id], id); + self.write_attribute_type(node, &node.dynamic_attrs[attr_id], attr_id, id); // Only push the dynamic attributes forward if they match the current path (same element) match attrs.next_if(|(_, p)| *p == path) { @@ -298,15 +326,44 @@ impl<'b> VirtualDom { } } - fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) { + fn write_attribute_type( + &mut self, + vnode: &'b VNode<'b>, + attribute: &'b MountedAttribute<'b>, + idx: usize, + id: ElementId, + ) { // Make sure we set the attribute's associated id attribute.mounted_element.set(id); + match &attribute.ty { + AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id), + AttributeType::Many(attribute) => { + for attribute in *attribute { + self.write_attribute(vnode, attribute, idx, id); + } + } + } + } + pub(crate) fn write_attribute( + &mut self, + vnode: &'b VNode<'b>, + attribute: &'b crate::Attribute<'b>, + idx: usize, + id: ElementId, + ) { // Safety: we promise not to re-alias this text later on after committing it to the mutation let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) }; match &attribute.value { AttributeValue::Listener(_) => { + let path = &vnode.template.get().attr_paths[idx]; + let element_ref = ElementRef { + path: ElementPath { path }, + template: vnode.stable_id().unwrap(), + scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)), + }; + self.elements[id.0] = Some(element_ref); self.mutations.push(NewEventListener { // all listeners start with "on" name: &unbounded_name[2..], @@ -330,7 +387,7 @@ impl<'b> VirtualDom { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root - let this_id = self.next_root(template, root_idx); + let this_id = self.next_element(); template.root_ids.borrow_mut()[root_idx] = this_id; self.mutations.push(LoadTemplate { @@ -353,8 +410,6 @@ impl<'b> VirtualDom { &mut self, path: &'static [u8], this_id: ElementId, - template: &VNode, - attr_id: usize, ) -> ElementId { if path.len() == 1 { return this_id; @@ -362,7 +417,7 @@ impl<'b> VirtualDom { // if attribute is on a root node, then we've already created the element // Else, it's deep in the template and we should create a new id for it - let id = self.next_element(template, template.template.get().attr_paths[attr_id]); + let id = self.next_element(); self.mutations.push(Mutation::AssignId { path: &path[1..], @@ -405,6 +460,7 @@ impl<'b> VirtualDom { #[allow(unused_mut)] pub(crate) fn register_template(&mut self, mut template: Template<'static>) { let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + let byte_index = byte_index.parse::().unwrap(); // First, check if we've already seen this template if self @@ -439,27 +495,21 @@ impl<'b> VirtualDom { pub(crate) fn create_dynamic_node( &mut self, - template: &'b VNode<'b>, + parent: ElementRef, node: &'b DynamicNode<'b>, - idx: usize, ) -> usize { use DynamicNode::*; match node { - Text(text) => self.create_dynamic_text(template, text, idx), - Placeholder(place) => self.create_placeholder(place, template, idx), - Component(component) => self.create_component_node(template, component), - Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), + Text(text) => self.create_dynamic_text(parent, text), + Placeholder(place) => self.create_placeholder(place, parent), + Component(component) => self.create_component_node(Some(parent), component), + Fragment(frag) => self.create_children(*frag, Some(parent)), } } - fn create_dynamic_text( - &mut self, - template: &'b VNode<'b>, - text: &'b VText<'b>, - idx: usize, - ) -> usize { + fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize { // Allocate a dynamic element reference for this text node - let new_id = self.next_element(template, template.template.get().node_paths[idx]); + let new_id = self.next_element(); // Make sure the text node is assigned to the correct element text.id.set(Some(new_id)); @@ -470,7 +520,7 @@ impl<'b> VirtualDom { // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], value, }); @@ -481,18 +531,20 @@ impl<'b> VirtualDom { pub(crate) fn create_placeholder( &mut self, placeholder: &VPlaceholder, - template: &'b VNode<'b>, - idx: usize, + parent: ElementRef, ) -> usize { // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.get().node_paths[idx]); + let id = self.next_element(); // Make sure the text node is assigned to the correct element placeholder.id.set(Some(id)); + // Assign the placeholder's parent + placeholder.parent.set(Some(parent)); + // Assign the ID to the existing node in the template self.mutations.push(AssignId { - path: &template.template.get().node_paths[idx][1..], + path: &parent.path.path[1..], id, }); @@ -502,7 +554,7 @@ impl<'b> VirtualDom { pub(super) fn create_component_node( &mut self, - template: &'b VNode<'b>, + parent: Option, component: &'b VComponent<'b>, ) -> usize { use RenderReturn::*; @@ -514,8 +566,11 @@ impl<'b> VirtualDom { match unsafe { self.run_scope(scope).extend_lifetime_ref() } { // Create the component's root element - Ready(t) => self.create_scope(scope, t), - Aborted(t) => self.mount_aborted(template, t), + Ready(t) => { + self.assign_boundary_ref(parent, t); + self.create_scope(scope, t) + } + Aborted(t) => self.mount_aborted(t, parent), } } @@ -531,20 +586,17 @@ impl<'b> VirtualDom { .unwrap_or_else(|| component.scope.get().unwrap()) } - fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { - let id = self.next_element(parent, &[]); + fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option) -> usize { + let id = self.next_element(); self.mutations.push(Mutation::CreatePlaceholder { id }); placeholder.id.set(Some(id)); + placeholder.parent.set(parent); + 1 } - fn set_slot( - &mut self, - template: &'b VNode<'b>, - slot: &'b Cell>, - id: usize, - ) -> ElementId { - let id = self.next_element(template, template.template.get().node_paths[id]); + fn set_slot(&mut self, slot: &'b Cell>) -> ElementId { + let id = self.next_element(); slot.set(Some(id)); id } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 74017db0f..a116aee4d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,7 +1,10 @@ use crate::{ any_props::AnyProps, arena::ElementId, - innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText}, + innerlude::{ + AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, + VPlaceholder, VText, + }, mutations::Mutation, nodes::RenderReturn, nodes::{DynamicNode, VNode}, @@ -39,19 +42,27 @@ impl<'b> VirtualDom { (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), // Just move over the placeholder - (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + (Aborted(l), Aborted(r)) => { + r.id.set(l.id.get()); + r.parent.set(l.parent.get()) + } // Placeholder becomes something // We should also clear the error now - (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + (Aborted(l), Ready(r)) => self.replace_placeholder( + l, + [r], + l.parent.get().expect("root node should not be none"), + ), }; } self.runtime.scope_stack.borrow_mut().pop(); } fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { - let id = self.next_null(); + let id = self.next_element(); p.id.set(Some(id)); + p.parent.set(l.parent.get()); self.mutations.push(Mutation::CreatePlaceholder { id }); let pre_edits = self.mutations.edits.len(); @@ -81,12 +92,24 @@ impl<'b> VirtualDom { if let Some(&template) = map.get(&byte_index) { right_template.template.set(template); if template != left_template.template.get() { - return self.replace(left_template, [right_template]); + let parent = left_template.parent.take(); + return self.replace(left_template, [right_template], parent); } } } } + // Copy over the parent + { + right_template.parent.set(left_template.parent.get()); + } + + // Update the bubble id pointer + right_template.stable_id.set(left_template.stable_id.get()); + if let Some(bubble_id) = right_template.stable_id.get() { + self.set_template(bubble_id, right_template); + } + // If the templates are the same, we don't need to do anything, nor do we want to if templates_are_the_same(left_template, right_template) { return; @@ -103,18 +126,54 @@ impl<'b> VirtualDom { .dynamic_attrs .iter() .zip(right_template.dynamic_attrs.iter()) - .for_each(|(left_attr, right_attr)| { + .enumerate() + .for_each(|(idx, (left_attr, right_attr))| { // Move over the ID from the old to the new - right_attr - .mounted_element - .set(left_attr.mounted_element.get()); + let mounted_id = left_attr.mounted_element.get(); + right_attr.mounted_element.set(mounted_id); - // We want to make sure anything that gets pulled is valid - self.update_template(left_attr.mounted_element.get(), right_template); + match (&left_attr.ty, &right_attr.ty) { + (AttributeType::Single(left), AttributeType::Single(right)) => { + self.diff_attribute(left, right, mounted_id) + } + (AttributeType::Many(left), AttributeType::Many(right)) => { + let mut left_iter = left.iter().peekable(); + let mut right_iter = right.iter().peekable(); - // If the attributes are different (or volatile), we need to update them - if left_attr.value != right_attr.value || left_attr.volatile { - self.update_attribute(right_attr, left_attr); + loop { + match (left_iter.peek(), right_iter.peek()) { + (Some(left), Some(right)) => { + // check which name is greater + match left.name.cmp(right.name) { + std::cmp::Ordering::Less => self.remove_attribute( + left.name, + left.namespace, + mounted_id, + ), + std::cmp::Ordering::Greater => self.write_attribute( + right_template, + right, + idx, + mounted_id, + ), + std::cmp::Ordering::Equal => { + self.diff_attribute(left, right, mounted_id) + } + } + } + (Some(_), None) => { + let left = left_iter.next().unwrap(); + self.remove_attribute(left.name, left.namespace, mounted_id) + } + (None, Some(_)) => { + let right = right_iter.next().unwrap(); + self.write_attribute(right_template, right, idx, mounted_id) + } + (None, None) => break, + } + } + } + _ => unreachable!("The macro should never generate this case"), } }); @@ -123,8 +182,16 @@ impl<'b> VirtualDom { .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) - .for_each(|(left_node, right_node)| { - self.diff_dynamic_node(left_node, right_node, right_template); + .enumerate() + .for_each(|(dyn_node_idx, (left_node, right_node))| { + let current_ref = ElementRef { + template: right_template.stable_id().unwrap(), + path: ElementPath { + path: left_template.template.get().node_paths[dyn_node_idx], + }, + scope: self.runtime.scope_stack.borrow().last().copied().unwrap(), + }; + self.diff_dynamic_node(left_node, right_node, current_ref); }); // Make sure the roots get transferred over while we're here @@ -135,14 +202,17 @@ impl<'b> VirtualDom { right.push(element); } } + } - let root_ids = right_template.root_ids.borrow(); - - // Update the node refs - for i in 0..root_ids.len() { - if let Some(root_id) = root_ids.get(i) { - self.update_template(*root_id, right_template); - } + fn diff_attribute( + &mut self, + left_attr: &'b Attribute<'b>, + right_attr: &'b Attribute<'b>, + id: ElementId, + ) { + // If the attributes are different (or volatile), we need to update them + if left_attr.value != right_attr.value || left_attr.volatile { + self.update_attribute(right_attr, left_attr, id); } } @@ -150,25 +220,45 @@ impl<'b> VirtualDom { &mut self, left_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>, - node: &'b VNode<'b>, + parent: ElementRef, ) { match (left_node, right_node) { - (Text(left), Text(right)) => self.diff_vtext(left, right, node), - (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), - (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), - (Component(left), Component(right)) => self.diff_vcomponent(left, right, node), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), - (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), + (Text(left), Text(right)) => self.diff_vtext(left, right), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent), + (Placeholder(left), Placeholder(right)) => { + right.id.set(left.id.get()); + right.parent.set(left.parent.get()); + }, + (Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent), + (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; } - fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) { + fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) { + let name = unsafe { std::mem::transmute(name) }; + let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None; + let value = unsafe { std::mem::transmute(value) }; + self.mutations.push(Mutation::SetAttribute { + id, + ns, + name, + value, + }); + } + + fn update_attribute( + &mut self, + right_attr: &'b Attribute<'b>, + left_attr: &'b Attribute<'b>, + id: ElementId, + ) { let name = unsafe { std::mem::transmute(left_attr.name) }; let value: BorrowedAttributeValue<'b> = (&right_attr.value).into(); let value = unsafe { std::mem::transmute(value) }; self.mutations.push(Mutation::SetAttribute { - id: left_attr.mounted_element.get(), + id, ns: right_attr.namespace, name, value, @@ -179,7 +269,7 @@ impl<'b> VirtualDom { &mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>, - right_template: &'b VNode<'b>, + parent: Option, ) { if std::ptr::eq(left, right) { return; @@ -187,7 +277,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - return self.replace_vcomponent(right_template, right, left); + return self.replace_vcomponent(right, left, parent); } // Make sure the new vcomponent has the right scopeid associated to it @@ -228,11 +318,11 @@ impl<'b> VirtualDom { fn replace_vcomponent( &mut self, - right_template: &'b VNode<'b>, right: &'b VComponent<'b>, left: &'b VComponent<'b>, + parent: Option, ) { - let m = self.create_component_node(right_template, right); + let m = self.create_component_node(parent, right); let pre_edits = self.mutations.edits.len(); @@ -287,11 +377,12 @@ impl<'b> VirtualDom { /// } /// ``` fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let parent = left.parent.take(); match matching_components(left, right) { - None => self.replace(left, [right]), + None => self.replace(left, [right], parent), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r, right)), + .for_each(|(l, r)| self.diff_vcomponent(l, r, parent)), } } @@ -299,11 +390,8 @@ impl<'b> VirtualDom { /// /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. - fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) { - let id = left - .id - .get() - .unwrap_or_else(|| self.next_element(node, &[0])); + fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { + let id = left.id.get().unwrap_or_else(|| self.next_element()); right.id.set(Some(id)); if left.value != right.value { @@ -312,7 +400,12 @@ impl<'b> VirtualDom { } } - fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_empty_fragment( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -325,9 +418,9 @@ impl<'b> VirtualDom { ); if new_is_keyed && old_is_keyed { - self.diff_keyed_children(old, new); + self.diff_keyed_children(old, new, parent); } else { - self.diff_non_keyed_children(old, new); + self.diff_non_keyed_children(old, new, parent); } } @@ -339,7 +432,12 @@ impl<'b> VirtualDom { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_non_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { use std::cmp::Ordering; // Handled these cases in `diff_children` before calling this function. @@ -348,7 +446,9 @@ impl<'b> VirtualDom { match old.len().cmp(&new.len()) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), - Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), + Ordering::Less => { + self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent) + } Ordering::Equal => {} } @@ -373,7 +473,12 @@ impl<'b> VirtualDom { // 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: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_children( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { if cfg!(debug_assertions) { let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { @@ -401,7 +506,7 @@ impl<'b> VirtualDom { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) { + let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) { Some(count) => count, None => return, }; @@ -427,18 +532,18 @@ impl<'b> VirtualDom { if left_offset == 0 { // insert at the beginning of the old list let foothold = &old[old.len() - right_offset]; - self.create_and_insert_before(new_middle, foothold); + self.create_and_insert_before(new_middle, foothold, parent); } else if right_offset == 0 { // insert at the end the old list let foothold = old.last().unwrap(); - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } else { // inserting in the middle let foothold = &old[left_offset - 1]; - self.create_and_insert_after(new_middle, foothold); + self.create_and_insert_after(new_middle, foothold, parent); } } else { - self.diff_keyed_middle(old_middle, new_middle); + self.diff_keyed_middle(old_middle, new_middle, parent); } } @@ -451,6 +556,7 @@ impl<'b> VirtualDom { &mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>], + parent: ElementRef, ) -> Option<(usize, usize)> { let mut left_offset = 0; @@ -466,7 +572,7 @@ impl<'b> VirtualDom { // 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.create_and_insert_after(&new[left_offset..], old.last().unwrap()); + self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent); return None; } @@ -505,7 +611,12 @@ impl<'b> VirtualDom { // // Upon exit from this function, it will be restored to that same self. #[allow(clippy::too_many_lines)] - fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { + fn diff_keyed_middle( + &mut self, + old: &'b [VNode<'b>], + new: &'b [VNode<'b>], + parent: ElementRef, + ) { /* 1. Map the old keys into a numerical ordering based on indices. 2. Create a map of old key to its index @@ -560,9 +671,9 @@ impl<'b> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if old.get(0).is_some() { + if !old.is_empty() { self.remove_nodes(&old[1..]); - self.replace(&old[0], new); + self.replace(&old[0], new, Some(parent)); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements @@ -739,20 +850,38 @@ impl<'b> VirtualDom { .sum() } - fn create_children(&mut self, nodes: impl IntoIterator>) -> usize { + pub(crate) fn create_children( + &mut self, + nodes: impl IntoIterator>, + parent: Option, + ) -> usize { nodes .into_iter() - .fold(0, |acc, child| acc + self.create(child)) + .map(|child| { + self.assign_boundary_ref(parent, child); + self.create(child) + }) + .sum() } - fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_before( + &mut self, + new: &'b [VNode<'b>], + before: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_first_element(before); self.mutations.push(Mutation::InsertBefore { id, m }) } - fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) { - let m = self.create_children(new); + fn create_and_insert_after( + &mut self, + new: &'b [VNode<'b>], + after: &'b VNode<'b>, + parent: ElementRef, + ) { + let m = self.create_children(new, Some(parent)); let id = self.find_last_element(after); self.mutations.push(Mutation::InsertAfter { id, m }) } @@ -762,15 +891,21 @@ impl<'b> VirtualDom { &mut self, l: &'b VPlaceholder, r: impl IntoIterator>, + parent: ElementRef, ) { - let m = self.create_children(r); + let m = self.create_children(r, Some(parent)); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); self.reclaim(id); } - fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator>) { - let m = self.create_children(right); + fn replace( + &mut self, + left: &'b VNode<'b>, + right: impl IntoIterator>, + parent: Option, + ) { + let m = self.create_children(right, parent); let pre_edits = self.mutations.edits.len(); @@ -789,11 +924,12 @@ impl<'b> VirtualDom { }; } - fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { + fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) { // Create the placeholder first, ensuring we get a dedicated ID for the placeholder - let placeholder = self.next_element(&l[0], &[]); + let placeholder = self.next_element(); r.id.set(Some(placeholder)); + r.parent.set(Some(parent)); self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); @@ -831,6 +967,16 @@ impl<'b> VirtualDom { // Clean up the roots, assuming we need to generate mutations for these // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) self.reclaim_roots(node, gen_muts); + + // Clean up the vnode id + self.reclaim_vnode_id(node); + } + + fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) { + // Clean up the vnode id + if let Some(id) = node.stable_id() { + self.element_refs.remove(id.0); + } } fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { @@ -989,6 +1135,13 @@ impl<'b> VirtualDom { } } } + + pub(crate) fn assign_boundary_ref(&mut self, parent: Option, child: &'b VNode<'b>) { + if let Some(parent) = parent { + // assign the parent of the child + child.parent.set(Some(parent)); + } + } } /// Are the templates the same? diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 819d2f99e..26155704b 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,25 +1,68 @@ -use crate::{ScopeId, ScopeState}; +use crate::{ + scope_context::{consume_context, current_scope_id, schedule_update_any}, + Element, IntoDynNode, LazyNodes, Properties, Scope, ScopeId, ScopeState, Template, + TemplateAttribute, TemplateNode, VNode, +}; use std::{ any::{Any, TypeId}, - cell::RefCell, - fmt::Debug, + backtrace::Backtrace, + cell::{Cell, RefCell}, + error::Error, + fmt::{Debug, Display}, + rc::Rc, + sync::Arc, }; -/// A boundary that will capture any errors from child components -pub struct ErrorBoundary { - error: RefCell>, - _id: ScopeId, +/// Provide an error boundary to catch errors from child components +pub fn use_error_boundary(cx: &ScopeState) -> &ErrorBoundary { + cx.use_hook(|| cx.provide_context(ErrorBoundary::new())) } +/// A boundary that will capture any errors from child components +#[derive(Debug, Clone, Default)] +pub struct ErrorBoundary { + inner: Rc, +} + +/// A boundary that will capture any errors from child components +pub struct ErrorBoundaryInner { + error: RefCell>, + _id: ScopeId, + rerun_boundary: Arc, +} + +impl Debug for ErrorBoundaryInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ErrorBoundaryInner") + .field("error", &self.error) + .finish() + } +} + +#[derive(Debug)] /// An instance of an error captured by a descendant component. pub struct CapturedError { /// The error captured by the error boundary pub error: Box, + /// The backtrace of the error + pub backtrace: Backtrace, + /// The scope that threw the error pub scope: ScopeId, } +impl Display for CapturedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}", + self.error, self.scope, self.backtrace + )) + } +} + +impl Error for CapturedError {} + impl CapturedError { /// Downcast the error type into a concrete error type pub fn downcast(&self) -> Option<&T> { @@ -32,17 +75,56 @@ impl CapturedError { } } -impl ErrorBoundary { - pub fn new(id: ScopeId) -> Self { +impl Default for ErrorBoundaryInner { + fn default() -> Self { Self { error: RefCell::new(None), - _id: id, + _id: current_scope_id() + .expect("Cannot create an error boundary outside of a component's scope."), + rerun_boundary: schedule_update_any().unwrap(), + } + } +} + +impl ErrorBoundary { + /// Create a new error boundary + pub fn new() -> Self { + Self::default() + } + + /// Create a new error boundary in the current scope + pub(crate) fn new_in_scope( + scope: ScopeId, + rerun_boundary: Arc, + ) -> Self { + Self { + inner: Rc::new(ErrorBoundaryInner { + error: RefCell::new(None), + _id: scope, + rerun_boundary, + }), } } /// Push an error into this Error Boundary - pub fn insert_error(&self, scope: ScopeId, error: Box) { - self.error.replace(Some(CapturedError { error, scope })); + pub fn insert_error( + &self, + scope: ScopeId, + error: Box, + backtrace: Backtrace, + ) { + println!("{:?} {:?}", error, self.inner._id); + self.inner.error.replace(Some(CapturedError { + error, + scope, + backtrace, + })); + (self.inner.rerun_boundary)(self.inner._id); + } + + /// Take any error that has been captured by this error boundary + pub fn take_error(&self) -> Option { + self.inner.error.take() } } @@ -59,7 +141,7 @@ impl ErrorBoundary { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { -/// let id: i32 = count.parse().throw(cx)?; +/// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } @@ -84,14 +166,14 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw(self, cx: &ScopeState) -> Option; + fn throw(self) -> Option; /// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary. /// @@ -107,45 +189,46 @@ pub trait Throw: Sized { /// ```rust, ignore /// #[component] /// fn App(cx: Scope, count: String) -> Element { - /// let id: i32 = count.parse().throw(cx)?; + /// let id: i32 = count.parse().throw()?; /// /// cx.render(rsx! { /// div { "Count {}" } /// }) /// } /// ``` - fn throw_with( - self, - cx: &ScopeState, - e: impl FnOnce() -> D, - ) -> Option; + fn throw_with(self, e: impl FnOnce() -> D) -> Option { + self.throw().or_else(|| throw_error(e())) + } +} + +fn throw_error(e: impl Debug + 'static) -> Option { + if let Some(cx) = consume_context::() { + match current_scope_id() { + Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()), + None => { + tracing::error!("Cannot throw error outside of a component's scope.") + } + } + } + + None } /// We call clone on any errors that can be owned out of a reference impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { type Out = &'a T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e.to_owned()); - None - } + Err(e) => throw_error(e.to_owned()), } } - fn throw_with( - self, - cx: &ScopeState, - err: impl FnOnce() -> D, - ) -> Option { + fn throw_with(self, err: impl FnOnce() -> D) -> Option { match self { Ok(t) => Some(t), - Err(_e) => { - cx.throw(err()); - None - } + Err(_e) => throw_error(err()), } } } @@ -154,25 +237,15 @@ impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result Throw for Result { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { + fn throw(self) -> Option { match self { Ok(t) => Some(t), - Err(e) => { - cx.throw(e); - None - } + Err(e) => throw_error(e), } } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.ok().or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.ok().or_else(|| throw_error(error())) } } @@ -180,21 +253,236 @@ impl Throw for Result { impl Throw for Option { type Out = T; - fn throw(self, cx: &ScopeState) -> Option { - self.or_else(|| { - cx.throw("None error."); - None - }) + fn throw(self) -> Option { + self.or_else(|| throw_error("Attempted to unwrap a None value.")) } - fn throw_with( - self, - cx: &ScopeState, - error: impl FnOnce() -> D, - ) -> Option { - self.or_else(|| { - cx.throw(error()); - None - }) + fn throw_with(self, error: impl FnOnce() -> D) -> Option { + self.or_else(|| throw_error(error())) + } +} + +pub struct ErrorHandler<'a>(Box LazyNodes<'a, 'a> + 'a>); +impl<'a, F: Fn(CapturedError) -> LazyNodes<'a, 'a> + 'a> From for ErrorHandler<'a> { + fn from(value: F) -> Self { + Self(Box::new(value)) + } +} +fn default_handler<'a>(error: CapturedError) -> LazyNodes<'a, 'a> { + LazyNodes::new(move |__cx: &ScopeState| -> VNode { + static TEMPLATE: Template = Template { + name: "error_handle.rs:42:5:884", + roots: &[TemplateNode::Element { + tag: "pre", + namespace: None, + attrs: &[TemplateAttribute::Static { + name: "color", + namespace: Some("style"), + value: "red", + }], + children: &[TemplateNode::DynamicText { id: 0usize }], + }], + node_paths: &[&[0u8, 0u8]], + attr_paths: &[], + }; + VNode { + parent: Default::default(), + stable_id: Default::default(), + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx + .bump() + .alloc([__cx.text_node(format_args!("{0}", error))]), + dynamic_attrs: __cx.bump().alloc([]), + } + }) +} +pub struct ErrorBoundaryProps<'a> { + children: Element<'a>, + handle_error: ErrorHandler<'a>, +} +impl<'a> ErrorBoundaryProps<'a> { + /** + Create a builder for building `ErrorBoundaryProps`. + On the builder, call `.children(...)`(optional), `.handle_error(...)`(optional) to set the values of the fields. + Finally, call `.build()` to create the instance of `ErrorBoundaryProps`. + */ + #[allow(dead_code)] + pub fn builder() -> ErrorBoundaryPropsBuilder<'a, ((), ())> { + ErrorBoundaryPropsBuilder { + fields: ((), ()), + _phantom: ::core::default::Default::default(), + } + } +} +#[must_use] +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub struct ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> { + fields: TypedBuilderFields, + _phantom: ::core::marker::PhantomData<&'a ()>, +} +impl<'a, TypedBuilderFields> Clone for ErrorBoundaryPropsBuilder<'a, TypedBuilderFields> +where + TypedBuilderFields: Clone, +{ + fn clone(&self) -> Self { + Self { + fields: self.fields.clone(), + _phantom: ::core::default::Default::default(), + } + } +} +impl<'a> Properties<'a> for ErrorBoundaryProps<'a> { + type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>; + const IS_STATIC: bool = false; + fn builder(_: &'a ScopeState) -> Self::Builder { + ErrorBoundaryProps::builder() + } + unsafe fn memoize(&self, _: &Self) -> bool { + false + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub trait ErrorBoundaryPropsBuilder_Optional { + fn into_value T>(self, default: F) -> T; +} +impl ErrorBoundaryPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } +} +impl ErrorBoundaryPropsBuilder_Optional for (T,) { + fn into_value T>(self, _: F) -> T { + self.0 + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((), __handle_error)> { + pub fn children( + self, + children: Element<'a>, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + let children = (children,); + let (_, handle_error) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_children {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __handle_error> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + #[deprecated(note = "Repeated field children")] + pub fn children( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_children, + ) -> ErrorBoundaryPropsBuilder<'a, ((Element<'a>,), __handle_error)> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, ())> { + pub fn handle_error( + self, + handle_error: impl ::core::convert::Into>, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + let handle_error = (handle_error.into(),); + let (children, _) = self.fields; + ErrorBoundaryPropsBuilder { + fields: (children, handle_error), + _phantom: self._phantom, + } + } +} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, non_snake_case)] +pub enum ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error {} +#[doc(hidden)] +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl<'a, __children> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + #[deprecated(note = "Repeated field handle_error")] + pub fn handle_error( + self, + _: ErrorBoundaryPropsBuilder_Error_Repeated_field_handle_error, + ) -> ErrorBoundaryPropsBuilder<'a, (__children, (ErrorHandler<'a>,))> { + self + } +} +#[allow(dead_code, non_camel_case_types, missing_docs)] +impl< + 'a, + __handle_error: ErrorBoundaryPropsBuilder_Optional>, + __children: ErrorBoundaryPropsBuilder_Optional>, + > ErrorBoundaryPropsBuilder<'a, (__children, __handle_error)> +{ + pub fn build(self) -> ErrorBoundaryProps<'a> { + let (children, handle_error) = self.fields; + let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || { + ::core::default::Default::default() + }); + let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || { + ErrorHandler(Box::new(default_handler)) + }); + ErrorBoundaryProps { + children, + handle_error, + } + } +} +/// Create a new error boundary component. +/// +/// ## Details +/// +/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// ErrorBoundary { +/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" } +/// ThrowsError {} +/// } +/// } +/// ``` +/// +/// ## Usage +/// +/// Error boundaries are an easy way to handle errors in your application. +/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them. +/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element { + let error_boundary = use_error_boundary(cx); + match error_boundary.take_error() { + Some(error) => cx.render((cx.props.handle_error.0)(error)), + None => Some({ + let __cx = cx; + static TEMPLATE: Template = Template { + name: "examples/error_handle.rs:81:17:2342", + roots: &[TemplateNode::Dynamic { id: 0usize }], + node_paths: &[&[0u8]], + attr_paths: &[], + }; + VNode { + parent: Cell::new(None), + stable_id: Cell::new(None), + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), + dynamic_nodes: __cx.bump().alloc([{ + let ___nodes = (&cx.props.children).into_dyn_node(__cx); + ___nodes + }]), + dynamic_attrs: __cx.bump().alloc([]), + } + }), } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3b8edb05c..4c8e41a63 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -28,6 +28,27 @@ pub struct Event { } impl Event { + /// Map the event data to a new type + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.map(|data| data.value()); + /// assert_eq!(data.inner(), "hello world"); + /// } + /// } + /// } + /// ``` + pub fn map U>(&self, f: F) -> Event { + Event { + data: Rc::new(f(&self.data)), + propagates: self.propagates.clone(), + } + } + /// Prevent this event from continuing to bubble up the tree to parent elements. /// /// # Example @@ -107,8 +128,6 @@ impl std::fmt::Debug for Event { } } -#[doc(hidden)] - /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// /// This makes it possible to pass `move |evt| {}` style closures into components as property fields. diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index a9e919bfa..4a775a57c 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { let children = cx.props.0.as_ref()?; Some(VNode { key: children.key, - parent: children.parent, + parent: children.parent.clone(), + stable_id: children.stable_id.clone(), template: children.template.clone(), root_ids: children.root_ids.clone(), dynamic_nodes: children.dynamic_nodes, @@ -91,10 +92,10 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> { /// }) /// } /// ``` -impl<'a> Properties for FragmentProps<'a> { +impl<'a> Properties<'_> for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; - fn builder() -> Self::Builder { + fn builder(_cx: &ScopeState) -> Self::Builder { FragmentBuilder(None) } unsafe fn memoize(&self, _other: &Self) -> bool { diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 811dbb734..ae1bd2109 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -18,13 +18,42 @@ use crate::{innerlude::VNode, ScopeState}; /// A concrete type provider for closures that build [`VNode`] structures. /// -/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over +/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching. /// /// /// ```rust, ignore -/// LazyNodes::new(|f| f.element("div", [], [], [] None)) +/// LazyNodes::new(|f| { +/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template { +/// name: "main.rs:5:5:20", // Source location of the template for hot reloading +/// roots: &[ +/// dioxus::core::TemplateNode::Element { +/// tag: dioxus_elements::div::TAG_NAME, +/// namespace: dioxus_elements::div::NAME_SPACE, +/// attrs: &[], +/// children: &[], +/// }, +/// ], +/// node_paths: &[], +/// attr_paths: &[], +/// }; +/// dioxus::core::VNode { +/// parent: None, +/// key: None, +/// template: std::cell::Cell::new(TEMPLATE), +/// root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in( +/// 1usize, +/// f.bump(), +/// ) +/// .into(), +/// dynamic_nodes: f.bump().alloc([]), +/// dynamic_attrs: f.bump().alloc([]), +/// }) +/// } /// ``` +/// +/// Find more information about how to construct [`VNode`] at + pub struct LazyNodes<'a, 'b> { #[cfg(not(miri))] inner: SmallBox VNode<'a> + 'b, S16>, @@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// Call the closure with the given factory to produce real [`VNode`]. /// /// ```rust, ignore - /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); + /// let f = LazyNodes::new(/* Closure for creating VNodes */); /// /// let node = f.call(cac); /// ``` diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5b2f83305..c478ec657 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,4 +1,6 @@ #![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")] #![warn(missing_docs)] mod any_props; @@ -32,7 +34,7 @@ pub(crate) mod innerlude { pub use crate::nodes::RenderReturn; pub use crate::nodes::*; pub use crate::properties::*; - pub use crate::runtime::{in_runtime, override_runtime, Runtime}; + pub use crate::runtime::{Runtime, RuntimeGuard}; pub use crate::scheduler::*; pub use crate::scope_context::*; pub use crate::scopes::*; @@ -74,11 +76,11 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, - CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, - LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, - TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, - VirtualDom, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue, + BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, + Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations, + Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -87,11 +89,12 @@ pub use crate::innerlude::{ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context, - in_runtime, override_runtime, provide_context, provide_context_to_scope, - provide_root_context, push_future, remove_future, schedule_update_any, spawn, - spawn_forever, suspend, throw, AnyValue, Component, Element, Event, EventHandler, Fragment, - IntoAttributeValue, LazyNodes, Properties, Runtime, Scope, ScopeId, ScopeState, Scoped, - TaskId, Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, + provide_context, provide_context_to_scope, provide_root_context, push_future, + remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary, + AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler, + Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute, + Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, + TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 27976c157..25e64cecc 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -26,7 +26,7 @@ pub struct Mutations<'a> { /// Any templates encountered while diffing the DOM. /// /// These must be loaded into a cache before applying the edits - pub templates: Vec>, + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, @@ -91,7 +91,7 @@ pub enum Mutation<'a> { id: ElementId, }, - /// Create an placeholder int he DOM that we will use later. + /// Create a placeholder in the DOM that we will use later. /// /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing CreatePlaceholder { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c83b1059a..6396504a4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,3 +1,4 @@ +use crate::innerlude::{ElementRef, VNodeId}; use crate::{ any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, }; @@ -47,7 +48,10 @@ pub struct VNode<'a> { pub key: Option<&'a str>, /// When rendered, this template will be linked to its parent manually - pub parent: Option, + pub(crate) parent: Cell>, + + /// The bubble id assigned to the child that we need to update and drop when diffing happens + pub(crate) stable_id: Cell>, /// The static nodes and static descriptor of the template pub template: Cell>, @@ -60,7 +64,7 @@ pub struct VNode<'a> { pub dynamic_nodes: &'a [DynamicNode<'a>], /// The dynamic parts of the template - pub dynamic_attrs: &'a [Attribute<'a>], + pub dynamic_attrs: &'a [MountedAttribute<'a>], } impl<'a> VNode<'a> { @@ -68,7 +72,8 @@ impl<'a> VNode<'a> { pub fn empty(cx: &'a ScopeState) -> Element<'a> { Some(VNode { key: None, - parent: None, + parent: Default::default(), + stable_id: Default::default(), root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())), dynamic_nodes: &[], dynamic_attrs: &[], @@ -81,6 +86,30 @@ impl<'a> VNode<'a> { }) } + /// Create a new VNode + pub fn new( + key: Option<&'a str>, + template: Template<'static>, + root_ids: bumpalo::collections::Vec<'a, ElementId>, + dynamic_nodes: &'a [DynamicNode<'a>], + dynamic_attrs: &'a [MountedAttribute<'a>], + ) -> Self { + Self { + key, + parent: Cell::new(None), + stable_id: Cell::new(None), + template: Cell::new(template), + root_ids: RefCell::new(root_ids), + dynamic_nodes, + dynamic_attrs, + } + } + + /// Get the stable id of this node used for bubbling events + pub(crate) fn stable_id(&self) -> Option { + self.stable_id.get() + } + /// Load a dynamic root at the given index /// /// Returns [`None`] if the root is actually a static node (Element/Text) @@ -319,7 +348,7 @@ pub struct VComponent<'a> { /// The function pointer of the component, known at compile time /// - /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key + /// It is possible that components get folded at compile time, so these shouldn't be really used as a key pub(crate) render_fn: *const (), pub(crate) props: RefCell + 'a>>>, @@ -372,6 +401,8 @@ impl<'a> VText<'a> { pub struct VPlaceholder { /// The ID of this node in the real DOM pub(crate) id: Cell>, + /// The parent of this node + pub(crate) parent: Cell>, } impl VPlaceholder { @@ -414,6 +445,51 @@ pub enum TemplateAttribute<'a> { }, } +/// An attribute with information about its position in the DOM and the element it was mounted to +#[derive(Debug)] +pub struct MountedAttribute<'a> { + pub(crate) ty: AttributeType<'a>, + + /// The element in the DOM that this attribute belongs to + pub(crate) mounted_element: Cell, +} + +impl<'a> From> for MountedAttribute<'a> { + fn from(attr: Attribute<'a>) -> Self { + Self { + ty: AttributeType::Single(attr), + mounted_element: Default::default(), + } + } +} + +impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> { + fn from(attr: &'a [Attribute<'a>]) -> Self { + Self { + ty: AttributeType::Many(attr), + mounted_element: Default::default(), + } + } +} + +impl<'a> From<&'a Vec>> for MountedAttribute<'a> { + fn from(attr: &'a Vec>) -> Self { + attr.as_slice().into() + } +} + +impl<'a> MountedAttribute<'a> { + /// Get the type of this attribute + pub fn attribute_type(&self) -> &AttributeType<'a> { + &self.ty + } + + /// Get the element that this attribute is mounted to + pub fn mounted_element(&self) -> ElementId { + self.mounted_element.get() + } +} + /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` #[derive(Debug)] pub struct Attribute<'a> { @@ -430,9 +506,6 @@ pub struct Attribute<'a> { /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated pub volatile: bool, - - /// The element in the DOM that this attribute belongs to - pub(crate) mounted_element: Cell, } impl<'a> Attribute<'a> { @@ -448,13 +521,40 @@ impl<'a> Attribute<'a> { value, namespace, volatile, - mounted_element: Cell::new(ElementId::default()), + } + } +} + +/// The type of an attribute +#[derive(Debug)] +pub enum AttributeType<'a> { + /// A single attribute + Single(Attribute<'a>), + /// Many different attributes sorted by name + Many(&'a [Attribute<'a>]), +} + +impl<'a> AttributeType<'a> { + /// Call the given function on each attribute + pub fn for_each<'b, F>(&'b self, mut f: F) + where + F: FnMut(&'b Attribute<'a>), + { + match self { + Self::Single(attr) => f(attr), + Self::Many(attrs) => attrs.iter().for_each(f), } } - /// Get the element that this attribute is mounted to - pub fn mounted_element(&self) -> ElementId { - self.mounted_element.get() + /// Try to call the given function on each attribute + pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E> + where + F: FnMut(&'b Attribute<'a>) -> Result<(), E>, + { + match self { + Self::Single(attr) => f(attr), + Self::Many(attrs) => attrs.iter().try_for_each(f), + } } } @@ -651,52 +751,52 @@ pub trait IntoDynNode<'a, A = ()> { /// Consume this item along with a scopestate and produce a DynamicNode /// /// You can use the bump alloactor of the scopestate to creat the dynamic node - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a>; } impl<'a> IntoDynNode<'a> for () { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::default() } } impl<'a> IntoDynNode<'a> for VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([self])) } } impl<'a> IntoDynNode<'a> for DynamicNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { self } } impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self { - Some(val) => val.into_vnode(_cx), + Some(val) => val.into_dyn_node(_cx), None => DynamicNode::default(), } } } impl<'a> IntoDynNode<'a> for &Element<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self.as_ref() { - Some(val) => val.clone().into_vnode(_cx), + Some(val) => val.clone().into_dyn_node(_cx), _ => DynamicNode::default(), } } } impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()])) } } impl<'a, 'b> IntoDynNode<'b> for &'a str { - fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> { DynamicNode::Text(VText { value: cx.bump().alloc_str(self), id: Default::default(), @@ -705,24 +805,25 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str { } impl IntoDynNode<'_> for String { - fn into_vnode(self, cx: &ScopeState) -> DynamicNode { + fn into_dyn_node(self, cx: &ScopeState) -> DynamicNode { DynamicNode::Text(VText { - value: cx.bump().alloc(self), + value: cx.bump().alloc_str(&self), id: Default::default(), }) } } impl<'b> IntoDynNode<'b> for Arguments<'_> { - fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { + fn into_dyn_node(self, cx: &'b ScopeState) -> DynamicNode<'b> { cx.text_node(self) } } impl<'a> IntoDynNode<'a> for &'a VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, _cx: &'a ScopeState) -> DynamicNode<'a> { DynamicNode::Fragment(_cx.bump().alloc([VNode { - parent: self.parent, + parent: self.parent.clone(), + stable_id: self.stable_id.clone(), template: self.template.clone(), root_ids: self.root_ids.clone(), key: self.key, @@ -732,24 +833,24 @@ impl<'a> IntoDynNode<'a> for &'a VNode<'a> { } } -pub trait IntoTemplate<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; +pub trait IntoVNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a>; } -impl<'a> IntoTemplate<'a> for VNode<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { +impl<'a> IntoVNode<'a> for VNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> VNode<'a> { self } } -impl<'a> IntoTemplate<'a> for Element<'a> { - fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { +impl<'a> IntoVNode<'a> for Element<'a> { + fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> { match self { - Some(val) => val.into_template(cx), + Some(val) => val.into_vnode(cx), _ => VNode::empty(cx).unwrap(), } } } -impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { - fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { +impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { + fn into_vnode(self, cx: &'a ScopeState) -> VNode<'a> { cx.render(self).unwrap() } } @@ -759,12 +860,12 @@ pub struct FromNodeIterator; impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T where T: Iterator, - I: IntoTemplate<'a>, + I: IntoVNode<'a>, { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { + fn into_dyn_node(self, cx: &'a ScopeState) -> DynamicNode<'a> { let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); - nodes.extend(self.into_iter().map(|node| node.into_template(cx))); + nodes.extend(self.into_iter().map(|node| node.into_vnode(cx))); match nodes.into_bump_slice() { children if children.is_empty() => DynamicNode::default(), @@ -791,6 +892,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str { } } +impl<'a> IntoAttributeValue<'a> for String { + fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Text(cx.alloc_str(&self)) + } +} + impl<'a> IntoAttributeValue<'a> for f64 { fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { AttributeValue::Float(self) @@ -832,3 +939,15 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } } + +/// A trait for anything that has a dynamic list of attributes +pub trait HasAttributes<'a> { + /// Push an attribute onto the list of attributes + fn push_attribute( + self, + name: &'a str, + ns: Option<&'static str>, + attr: impl IntoAttributeValue<'a>, + volatile: bool, + ) -> Self; +} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 3f0d9ef97..b2263e039 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -32,7 +32,7 @@ use crate::innerlude::*; /// data: &'a str /// } /// ``` -pub trait Properties: Sized { +pub trait Properties<'a>: Sized { /// The type of the builder for this component. /// Used to create "in-progress" versions of the props. type Builder; @@ -41,7 +41,7 @@ pub trait Properties: Sized { const IS_STATIC: bool; /// Create a builder for this component. - fn builder() -> Self::Builder; + fn builder(cx: &'a ScopeState) -> Self::Builder; /// Memoization can only happen if the props are valid for the 'static lifetime /// @@ -51,10 +51,10 @@ pub trait Properties: Sized { unsafe fn memoize(&self, other: &Self) -> bool; } -impl Properties for () { +impl Properties<'_> for () { type Builder = EmptyBuilder; const IS_STATIC: bool = true; - fn builder() -> Self::Builder { + fn builder(_cx: &ScopeState) -> Self::Builder { EmptyBuilder {} } unsafe fn memoize(&self, _other: &Self) -> bool { @@ -70,8 +70,11 @@ impl EmptyBuilder { /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder { - T::builder() +pub fn fc_to_builder<'a, T: Properties<'a> + 'a>( + cx: &'a ScopeState, + _: fn(Scope<'a, T>) -> Element<'a>, +) -> T::Builder { + T::builder(cx) } #[cfg(not(miri))] diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 4b9dcdab0..f3735b020 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -7,51 +7,6 @@ thread_local! { static RUNTIMES: RefCell>> = RefCell::new(vec![]); } -/// Run some code within a runtime -pub fn in_runtime(runtime: Rc, f: impl FnOnce() -> R) -> R { - let _guard = RuntimeGuard::new(runtime); - f() -} - -/// Override the current runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. -/// -/// ```rust -/// use dioxus::prelude::*; -/// -/// fn main() { -/// let virtual_dom = VirtualDom::new(app); -/// } -/// -/// fn app(cx: Scope) -> Element { -/// render!{ Component { runtime: Runtime::current().unwrap() } } -/// } -/// -/// // In a dynamic library -/// #[derive(Props)] -/// struct ComponentProps { -/// runtime: std::rc::Rc, -/// } -/// -/// impl PartialEq for ComponentProps { -/// fn eq(&self, _other: &Self) -> bool { -/// true -/// } -/// } -/// -/// fn Component(cx: Scope) -> Element { -/// cx.use_hook(|| override_runtime(cx.props.runtime.clone())); -/// -/// render! { div {} } -/// } -/// ``` -pub fn override_runtime(runtime: Rc) { - RUNTIMES.with(|stack| { - let mut stack = stack.borrow_mut(); - stack.pop(); - stack.push(runtime); - }); -} - /// Pushes a new scope onto the stack pub(crate) fn push_runtime(runtime: Rc) { RUNTIMES.with(|stack| stack.borrow_mut().push(runtime)); @@ -132,6 +87,16 @@ impl Runtime { self.scope_stack.borrow().last().copied() } + /// Call this function with the current scope set to the given scope + /// + /// Useful in a limited number of scenarios, not public. + pub(crate) fn with_scope(&self, id: ScopeId, f: impl FnOnce() -> O) -> O { + self.scope_stack.borrow_mut().push(id); + let o = f(); + self.scope_stack.borrow_mut().pop(); + o + } + /// Get the context for any scope given its ID /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node @@ -143,13 +108,56 @@ impl Runtime { } } -pub(crate) struct RuntimeGuard(Rc); +/// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime. +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn main() { +/// let virtual_dom = VirtualDom::new(app); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// render!{ Component { runtime: Runtime::current().unwrap() } } +/// } +/// +/// // In a dynamic library +/// #[derive(Props)] +/// struct ComponentProps { +/// runtime: std::rc::Rc, +/// } +/// +/// impl PartialEq for ComponentProps { +/// fn eq(&self, _other: &Self) -> bool { +/// true +/// } +/// } +/// +/// fn Component(cx: Scope) -> Element { +/// cx.use_hook(|| RuntimeGuard::new(cx.props.runtime.clone())); +/// +/// render! { div {} } +/// } +/// ``` +pub struct RuntimeGuard(Rc); impl RuntimeGuard { - pub(crate) fn new(runtime: Rc) -> Self { + /// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped + pub fn new(runtime: Rc) -> Self { push_runtime(runtime.clone()); Self(runtime) } + + /// Run a function with a given runtime and scope in context + pub fn with(runtime: Rc, scope: Option, f: impl FnOnce() -> O) -> O { + let guard = Self::new(runtime.clone()); + let o = match scope { + Some(scope) => Runtime::with_scope(&runtime, scope, f), + None => f(), + }; + drop(guard); + o + } } impl Drop for RuntimeGuard { diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fc668b364..d2e9135e6 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -19,7 +19,7 @@ pub struct TaskId(pub usize); /// the task itself is the waker pub(crate) struct LocalTask { pub scope: ScopeId, - pub(super) task: RefCell + 'static>>>, + pub task: RefCell + 'static>>>, pub waker: Waker, } @@ -48,11 +48,15 @@ impl Scheduler { })), }; - entry.insert(task); + let mut cx = std::task::Context::from_waker(&task.waker); - self.sender - .unbounded_send(SchedulerMsg::TaskNotified(task_id)) - .expect("Scheduler should exist"); + if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() { + self.sender + .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .expect("Scheduler should exist"); + } + + entry.insert(task); task_id } @@ -60,8 +64,8 @@ impl Scheduler { /// Drop the future with the given TaskId /// /// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you - pub fn remove(&self, id: TaskId) { - self.tasks.borrow_mut().try_remove(id.0); + pub fn remove(&self, id: TaskId) -> Option { + self.tasks.borrow_mut().try_remove(id.0) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 1ed5b816c..962a55e86 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -35,7 +35,8 @@ impl VirtualDom { hook_idx: Default::default(), borrowed_props: Default::default(), - attributes_to_drop: Default::default(), + attributes_to_drop_before_render: Default::default(), + element_refs_to_drop: Default::default(), })); let context = @@ -54,7 +55,7 @@ impl VirtualDom { let new_nodes = unsafe { let scope = &self.scopes[scope_id.0]; - scope.previous_frame().bump_mut().reset(); + scope.previous_frame().reset(); scope.context().suspended.set(false); diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 59bed3b1f..ffb9e74a1 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -1,5 +1,5 @@ use crate::{ - innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, + innerlude::{Scheduler, SchedulerMsg}, runtime::{with_current_scope, with_runtime}, Element, ScopeId, TaskId, }; @@ -7,7 +7,6 @@ use rustc_hash::FxHashSet; use std::{ any::Any, cell::{Cell, RefCell}, - fmt::Debug, future::Future, rc::Rc, sync::Arc, @@ -230,17 +229,7 @@ impl ScopeContext { /// This is good for tasks that need to be run after the component has been dropped. pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { // The root scope will never be unmounted so we can just add the task at the top of the app - let id = self.tasks.spawn(ScopeId::ROOT, fut); - - // wake up the scheduler if it is sleeping - self.tasks - .sender - .unbounded_send(SchedulerMsg::TaskNotified(id)) - .expect("Scheduler should exist"); - - self.spawned_tasks.borrow_mut().insert(id); - - id + self.tasks.spawn(ScopeId::ROOT, fut) } /// Informs the scheduler that this task is no longer needed and should be removed. @@ -250,19 +239,6 @@ impl ScopeContext { self.tasks.remove(id); } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { self.suspended.set(true); @@ -332,11 +308,6 @@ pub fn suspend() -> Option> { None } -/// Throw an error into the nearest error boundary -pub fn throw(error: impl Debug + 'static) -> Option<()> { - with_current_scope(|cx| cx.throw(error)).flatten() -} - /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(fut: impl Future + 'static) -> Option { with_current_scope(|cx| cx.push_future(fut)) @@ -347,11 +318,16 @@ pub fn spawn(fut: impl Future + 'static) { with_current_scope(|cx| cx.spawn(fut)); } +/// Spawn a future on a component given its [`ScopeId`]. +pub fn spawn_at(fut: impl Future + 'static, scope_id: ScopeId) -> Option { + with_runtime(|rt| rt.get_context(scope_id).unwrap().push_future(fut)) +} + /// Spawn a future that Dioxus won't clean up when this component is unmounted /// /// This is good for tasks that need to be run after the component has been dropped. pub fn spawn_forever(fut: impl Future + 'static) -> Option { - with_current_scope(|cx| cx.spawn_forever(fut)) + spawn_at(fut, ScopeId(0)) } /// Informs the scheduler that this task is no longer needed and should be removed. diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 996529a0d..64a60b112 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -2,13 +2,13 @@ use crate::{ any_props::AnyProps, any_props::VProps, bump_frame::BumpFrame, - innerlude::ErrorBoundary, - innerlude::{DynamicNode, EventHandler, VComponent, VText}, + innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText}, lazynodes::LazyNodes, nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, runtime::Runtime, scope_context::ScopeContext, - AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, + AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute, + Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ @@ -94,7 +94,8 @@ pub struct ScopeState { pub(crate) hook_idx: Cell, pub(crate) borrowed_props: RefCell>>, - pub(crate) attributes_to_drop: RefCell>>, + pub(crate) element_refs_to_drop: RefCell>, + pub(crate) attributes_to_drop_before_render: RefCell>>, pub(crate) props: Option>>, } @@ -348,25 +349,38 @@ impl<'src> ScopeState { pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> { let element = rsx.call(self); - let mut listeners = self.attributes_to_drop.borrow_mut(); + let mut listeners = self.attributes_to_drop_before_render.borrow_mut(); for attr in element.dynamic_attrs { - match attr.value { - AttributeValue::Any(_) | AttributeValue::Listener(_) => { - let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; - listeners.push(unbounded); - } + attr.ty.for_each(|attr| { + match attr.value { + // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped + AttributeValue::Listener(_) => { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + listeners.push(unbounded); + } + // We need to drop any values manually to make sure that their drop implementation is called before the next render + AttributeValue::Any(_) => { + let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) }; + self.previous_frame().add_attribute_to_drop(unbounded); + } - _ => (), - } + _ => (), + } + }) } let mut props = self.borrowed_props.borrow_mut(); + let mut drop_props = self + .previous_frame() + .props_to_drop_before_reset + .borrow_mut(); for node in element.dynamic_nodes { if let DynamicNode::Component(comp) = node { + let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) }; if !comp.static_props { - let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) }; props.push(unbounded); } + drop_props.push(unbounded); } } @@ -395,7 +409,7 @@ impl<'src> ScopeState { /// Convert any item that implements [`IntoDynNode`] into a [`DynamicNode`] using the internal [`Bump`] allocator pub fn make_node<'c, I>(&'src self, into: impl IntoDynNode<'src, I> + 'c) -> DynamicNode { - into.into_vnode(self) + into.into_dyn_node(self) } /// Create a new [`Attribute`] from a name, value, namespace, and volatile bool @@ -408,13 +422,15 @@ impl<'src> ScopeState { value: impl IntoAttributeValue<'src>, namespace: Option<&'static str>, volatile: bool, - ) -> Attribute<'src> { - Attribute { - name, - namespace, - volatile, + ) -> MountedAttribute<'src> { + MountedAttribute { + ty: AttributeType::Single(Attribute { + name, + namespace, + volatile, + value: value.into_value(self.bump()), + }), mounted_element: Default::default(), - value: value.into_value(self.bump()), } } @@ -440,7 +456,7 @@ impl<'src> ScopeState { ) -> DynamicNode<'src> where // The properties must be valid until the next bump frame - P: Properties + 'src, + P: Properties<'src> + 'src, // The current bump allocator frame must outlive the child's borrowed props 'src: 'child, { @@ -455,7 +471,7 @@ impl<'src> ScopeState { render_fn: component as *const (), static_props: P::IS_STATIC, props: RefCell::new(Some(extended)), - scope: Cell::new(None), + scope: Default::default(), }) } @@ -506,19 +522,6 @@ impl<'src> ScopeState { AttributeValue::Any(RefCell::new(Some(boxed))) } - /// Inject an error into the nearest error boundary and quit rendering - /// - /// The error doesn't need to implement Error or any specific traits since the boundary - /// itself will downcast the error into a trait object. - pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { - if let Some(cx) = self.consume_context::>() { - cx.insert_error(self.scope_id(), Box::new(error)); - } - - // Always return none during a throw - None - } - /// Mark this component as suspended and then return None pub fn suspend(&self) -> Option { let cx = self.context(); diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2d83ba63a..28ccbc76a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -11,12 +11,14 @@ use crate::{ nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, + AttributeValue, Element, Event, Scope, VNode, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{ + any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc, sync::Arc, +}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -186,7 +188,10 @@ pub struct VirtualDom { pub(crate) templates: FxHashMap>>, // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template - pub(crate) elements: Slab, + pub(crate) element_refs: Slab>>>, + + // The element ids that are used in the renderer + pub(crate) elements: Slab>, pub(crate) mutations: Mutations<'static>, @@ -263,6 +268,7 @@ impl VirtualDom { dirty_scopes: Default::default(), templates: Default::default(), elements: Default::default(), + element_refs: Default::default(), mutations: Mutations::default(), suspended_scopes: Default::default(), }; @@ -273,10 +279,13 @@ impl VirtualDom { ); // Unlike react, we provide a default error boundary that just renders the error as a string - root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT))); + root.provide_context(Rc::new(ErrorBoundary::new_in_scope( + ScopeId::ROOT, + Arc::new(|_| {}), + ))); // the root element is always given element ID 0 since it's the container for the entire tree - dom.elements.insert(ElementRef::none()); + dom.elements.insert(None); dom } @@ -314,9 +323,9 @@ impl VirtualDom { } } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. + /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.** /// - /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that + /// This method will identify the appropriate element. The data must match up with the listener declared. Note that /// this method does not give any indication as to the success of the listener call. If the listener is not found, /// nothing will happen. /// @@ -353,8 +362,15 @@ impl VirtualDom { | | | <-- no, broke early | <-- no, broke early */ - let mut parent_path = self.elements.get(element.0); - let mut listeners = vec![]; + let parent_path = match self.elements.get(element.0) { + Some(Some(el)) => el, + _ => return, + }; + let mut parent_node = self + .element_refs + .get(parent_path.template.0) + .cloned() + .map(|el| (*parent_path, el)); // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required let uievent = Event { @@ -365,82 +381,93 @@ impl VirtualDom { // If the event bubbles, we traverse through the tree until we find the target element. if bubbles { // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent. - while let Some(el_ref) = parent_path { + while let Some((path, el_ref)) = parent_node { + let mut listeners = vec![]; + // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - if attr.name.trim_start_matches("on") == name - && target_path.is_decendant(&this_path) - { - listeners.push(&attr.value); - - // Break if this is the exact target element. - // This means we won't call two listeners with the same name on the same element. This should be - // documented, or be rejected from the rsx! macro outright - if target_path == this_path { - break; + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + if target_path.is_decendant(&this_path) { + attr.ty.for_each(|attribute| { + if attribute.name.trim_start_matches("on") == name { + if let AttributeValue::Listener(listener) = &attribute.value { + listeners.push(listener); + } } + }); + + // Break if this is the exact target element. + // This means we won't call two listeners with the same name on the same element. This should be + // documented, or be rejected from the rsx! macro outright + if target_path == this_path { + break; } } - - // Now that we've accumulated all the parent attributes for the target element, call them in reverse order - // We check the bubble state between each call to see if the event has been stopped from bubbling - for listener in listeners.drain(..).rev() { - if let AttributeValue::Listener(listener) = listener { - let origin = el_ref.scope; - self.runtime.scope_stack.borrow_mut().push(origin); - self.runtime.rendering.set(false); - if let Some(cb) = listener.borrow_mut().as_deref_mut() { - cb(uievent.clone()); - } - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); - - if !uievent.propagates.get() { - return; - } - } - } - - parent_path = template.parent.and_then(|id| self.elements.get(id.0)); - } else { - break; } + + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order + // We check the bubble state between each call to see if the event has been stopped from bubbling + for listener in listeners.into_iter().rev() { + let origin = path.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + self.runtime.rendering.set(false); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.rendering.set(true); + + if !uievent.propagates.get() { + return; + } + } + + parent_node = template.parent.get().and_then(|element_ref| { + self.element_refs + .get(element_ref.template.0) + .cloned() + .map(|el| (element_ref, el)) + }); } } else { // Otherwise, we just call the listener on the target element - if let Some(el_ref) = parent_path { + if let Some((path, el_ref)) = parent_node { // safety: we maintain references of all vnodes in the element slab - if let Some(template) = el_ref.template { - let template = unsafe { template.as_ref() }; - let node_template = template.template.get(); - let target_path = el_ref.path; + let template = unsafe { el_ref.unwrap().as_ref() }; + let node_template = template.template.get(); + let target_path = path.path; - for (idx, attr) in template.dynamic_attrs.iter().enumerate() { - let this_path = node_template.attr_paths[idx]; + for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + let this_path = node_template.attr_paths[idx]; - // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one - // Only call the listener if this is the exact target element. - if attr.name.trim_start_matches("on") == name && target_path == this_path { - if let AttributeValue::Listener(listener) = &attr.value { - let origin = el_ref.scope; - self.runtime.scope_stack.borrow_mut().push(origin); - self.runtime.rendering.set(false); - if let Some(cb) = listener.borrow_mut().as_deref_mut() { - cb(uievent.clone()); + // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one + // Only call the listener if this is the exact target element. + if target_path == this_path { + let mut should_stop = false; + attr.ty.for_each(|attribute| { + if attribute.name.trim_start_matches("on") == name { + if let AttributeValue::Listener(listener) = &attribute.value { + let origin = path.scope; + self.runtime.scope_stack.borrow_mut().push(origin); + self.runtime.rendering.set(false); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.rendering.set(true); + + should_stop = true; } - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); - - break; } + }); + if should_stop { + return; } } } @@ -563,7 +590,7 @@ impl VirtualDom { // If an error occurs, we should try to render the default error component and context where the error occured RenderReturn::Aborted(placeholder) => { tracing::debug!("Ran into suspended or aborted scope during rebuild"); - let id = self.next_null(); + let id = self.next_element(); placeholder.id.set(Some(id)); self.mutations.push(Mutation::CreatePlaceholder { id }); } @@ -595,15 +622,12 @@ impl VirtualDom { /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content pub async fn wait_for_suspense(&mut self) { loop { - // println!("waiting for suspense {:?}", self.suspended_scopes); if self.suspended_scopes.is_empty() { return; } - // println!("waiting for suspense"); self.wait_for_work().await; - // println!("Rendered immediately"); _ = self.render_immediate(); } } diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs index ce26fba31..58d861169 100644 --- a/packages/core/tests/error_boundary.rs +++ b/packages/core/tests/error_boundary.rs @@ -24,9 +24,9 @@ fn NoneChild(_cx: Scope) -> Element { } fn ThrowChild(cx: Scope) -> Element { - cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?; - let _g: i32 = "123123".parse().throw(cx)?; + let _g: i32 = "123123".parse().throw()?; cx.render(rsx! { div {} diff --git a/packages/core/tests/event_propagation.rs b/packages/core/tests/event_propagation.rs new file mode 100644 index 000000000..fb87bb16a --- /dev/null +++ b/packages/core/tests/event_propagation.rs @@ -0,0 +1,82 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::{rc::Rc, sync::Mutex}; + +static CLICKS: Mutex = Mutex::new(0); + +#[test] +fn events_propagate() { + set_event_converter(Box::new(dioxus_html::SerializedHtmlEventConverter)); + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + // Top-level click is registered + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(1), + true, + ); + assert_eq!(*CLICKS.lock().unwrap(), 1); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Lower click is registered + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(2), + true, + ); + assert_eq!(*CLICKS.lock().unwrap(), 3); + + // break reference.... + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } + + // Stop propagation occurs + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(2), + true, + ); + assert_eq!(*CLICKS.lock().unwrap(), 3); +} + +fn app(cx: Scope) -> Element { + render! { + div { onclick: move |_| { + println!("top clicked"); + *CLICKS.lock().unwrap() += 1; + }, + + vec![ + render! { + problematic_child {} + } + ].into_iter() + } + } +} + +fn problematic_child(cx: Scope) -> Element { + render! { + button { onclick: move |evt| { + println!("bottom clicked"); + let mut clicks = CLICKS.lock().unwrap(); + if *clicks == 3 { + evt.stop_propagation(); + } else { + *clicks += 1; + } + } } + } +} diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 1523c4e22..af5e9d05c 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -1,8 +1,8 @@ #![cfg(not(miri))] use dioxus::prelude::Props; -use dioxus_core::*; -use std::{cell::Cell, collections::HashSet}; +use dioxus_core::{MountedAttribute, *}; +use std::{cfg, collections::HashSet}; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { let range = if depth > 5 { 1 } else { 4 }; match rand::random::() % range { 0 => DynamicNode::Placeholder(Default::default()), - 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { - key: None, - parent: Default::default(), - template: Cell::new(Template { - name: concat!(file!(), ":", line!(), ":", column!(), ":0"), - roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[0]], - attr_paths: &[], - }), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: cx.bump().alloc([cx.component( - create_random_element, - DepthProps { depth, root: false }, - "create_random_element", - )]), - dynamic_attrs: &[], + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| { + VNode::new( + None, + Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }, + bumpalo::collections::Vec::new_in(cx.bump()), + cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + &[], + ) })), 2 => cx.component( create_random_element, @@ -205,7 +206,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { } } -fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { +fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute { let value = match rand::random::() % 7 { 0 => AttributeValue::Text(Box::leak( format!("{}", rand::random::()).into_boxed_str(), @@ -217,7 +218,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { 5 => AttributeValue::None, 6 => { let value = cx.listener(|e: Event| println!("{:?}", e)); - return Attribute::new("ondata", value, None, false); + return Attribute::new("ondata", value, None, false).into(); } _ => unreachable!(), }; @@ -227,6 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { random_ns(), rand::random(), ) + .into() } static mut TEMPLATE_COUNT: usize = 0; @@ -271,13 +273,11 @@ fn create_random_element(cx: Scope) -> Element { ) .into_boxed_str(), )); - // println!("{template:#?}"); - let node = VNode { - key: None, - parent: None, - template: Cell::new(template), - root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: { + let node = VNode::new( + None, + template, + bumpalo::collections::Vec::new_in(cx.bump()), + { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() .map(|ty| match ty { @@ -291,12 +291,12 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( + cx.bump().alloc( (0..template.attr_paths.len()) .map(|_| create_random_dynamic_attr(cx)) .collect::>(), ), - }; + ); Some(node) } _ => None, @@ -306,10 +306,10 @@ fn create_random_element(cx: Scope) -> Element { } // test for panics when creating random nodes and templates -#[cfg(not(miri))] #[test] fn create() { - for _ in 0..1000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); @@ -318,10 +318,10 @@ fn create() { // test for panics when diffing random nodes // This test will change the template every render which is not very realistic, but it helps stress the system -#[cfg(not(miri))] #[test] fn diff() { - for _ in 0..100000 { + let repeat_count = if cfg!(miri) { 100 } else { 1000 }; + for _ in 0..repeat_count { let mut vdom = VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true }); let _ = vdom.rebuild(); diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 191d1da7c..0abe3756e 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { div { key: "12345", class: "asd", class: "{asd}", + class: if true { "{asd}" }, + class: if false { "{asd}" }, onclick: move |_| {}, div { "{var}" } div { @@ -24,6 +26,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { } }) } + #[test] fn dual_stream() { let mut dom = VirtualDom::new(basic_syntax_is_a_template); @@ -36,7 +39,7 @@ fn dual_stream() { LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", - value: (&*bump.alloc("123".into_value(&bump))).into(), + value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(), id: ElementId(1), ns: None, }, diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 08b342631..6db294653 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -4,6 +4,7 @@ //! Tests for the lifecycle of components. use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; +use dioxus_html::SerializedHtmlEventConverter; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -39,6 +40,7 @@ fn manual_diffing() { #[test] fn events_generate() { + set_event_converter(Box::new(SerializedHtmlEventConverter)); fn app(cx: Scope) -> Element { let count = cx.use_hook(|| 0); @@ -56,7 +58,12 @@ fn events_generate() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(1), + true, + ); dom.mark_dirty(ScopeId::ROOT); let edits = dom.render_immediate(); diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs index 33cbd9620..c285db7d5 100644 --- a/packages/core/tests/miri_full_app.rs +++ b/packages/core/tests/miri_full_app.rs @@ -1,15 +1,23 @@ +use crate::dioxus_elements::SerializedMouseData; use dioxus::prelude::*; use dioxus_core::ElementId; +use dioxus_elements::SerializedHtmlEventConverter; use std::rc::Rc; #[test] fn miri_rollover() { + set_event_converter(Box::new(SerializedHtmlEventConverter)); let mut dom = VirtualDom::new(App); _ = dom.rebuild(); for _ in 0..3 { - dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.handle_event( + "click", + Rc::new(PlatformEventData::new(Box::::default())), + ElementId(2), + true, + ); dom.process_events(); _ = dom.render_immediate(); } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 7b2516447..1a7ab3d58 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -11,7 +11,7 @@ fn it_works() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); dom.wait_for_suspense().await; - let out = dioxus_ssr::pre_render(&dom); + let out = dioxus_ssr::render(&dom); assert_eq!(out, "
Waiting for... child
"); @@ -36,7 +36,7 @@ fn suspended_child(cx: Scope) -> Element { cx.spawn(async move { val += 1; }); - return cx.suspend()?; + cx.suspend()?; } render!("child") diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 1d190ccaf..e1ff55c85 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -1,13 +1,33 @@ //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely -use dioxus::prelude::*; -use std::{sync::atomic::AtomicUsize, time::Duration}; - -static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); - #[cfg(not(miri))] #[tokio::test] async fn it_works() { + use dioxus::prelude::*; + use std::{sync::atomic::AtomicUsize, time::Duration}; + + static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); + + fn app(cx: Scope) -> Element { + cx.use_hook(|| { + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(50)).await; + POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); + } + }); + + cx.spawn(async { + for x in 0..10 { + tokio::time::sleep(Duration::from_micros(25)).await; + POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); + } + }); + }); + + cx.render(rsx!(())) + } + let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); @@ -24,23 +44,3 @@ async fn it_works() { 135 ); } - -fn app(cx: Scope) -> Element { - cx.use_hook(|| { - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(50)).await; - POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); - } - }); - - cx.spawn(async { - for x in 0..10 { - tokio::time::sleep(Duration::from_micros(25)).await; - POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); - } - }); - }); - - cx.render(rsx!(())) -} diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index a884928ba..9ecbcef47 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -11,15 +11,24 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true, features = ["serialize", "native-bind"] } -dioxus-interpreter-js = { workspace = true } +dioxus-html = { workspace = true, features = [ + "serialize", + "native-bind", + "mounted", + "eval", +] } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } -wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] } tracing = { workspace = true } +wry = { version = "0.35.0", default-features = false, features = [ + "os-webview", + "protocol", + "file-drop", +] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ "sync", @@ -33,14 +42,23 @@ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" slab = { workspace = true } +rustc-hash = { workspace = true } futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" - +crossbeam-channel = "0.5.8" +tao = { version = "0.24.0", features = ["rwh_05"] } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] -rfd = "0.11.3" +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", optional = true, features = [ + "webp", + "html", +] } +rfd = "0.12" +global-hotkey = "0.4.1" +muda = "0.11.3" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" @@ -56,12 +74,13 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] devtools = ["wry/devtools"] -dox = ["wry/dox"] hot-reload = ["dioxus-hot-reload"] +asset-collect = ["manganis-cli-support"] +gnu = [] [package.metadata.docs.rs] default-features = false -features = [ "dox", "tokio_runtime", "hot-reload" ] +features = ["tokio_runtime", "hot-reload"] [dev-dependencies] dioxus-core-macro = { workspace = true } @@ -70,6 +89,10 @@ dioxus = { workspace = true } exitcode = "1.1.2" scraper = "0.16.0" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } +minify-js = "0.5.6" + # These tests need to be run on the main thread, so they cannot use rust's test harness. [[test]] name = "check_events" diff --git a/packages/desktop/build.rs b/packages/desktop/build.rs new file mode 100644 index 000000000..8d1f4b3e5 --- /dev/null +++ b/packages/desktop/build.rs @@ -0,0 +1,94 @@ +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; + +use std::io::Write; + +const EDITS_PATH: &str = { + #[cfg(any(target_os = "android", target_os = "windows"))] + { + "http://dioxus.index.html/edits" + } + #[cfg(not(any(target_os = "android", target_os = "windows")))] + { + "dioxus://index.html/edits" + } +}; + +fn check_gnu() { + // WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently + if std::env::var("CARGO_CFG_WINDOWS").is_ok() + && std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu" + && !cfg!(feature = "gnu") + { + println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml") + } +} + +fn main() { + check_gnu(); + + let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click + let inputs = document.querySelectorAll("input"); + for (let input of inputs) { + if (!input.getAttribute("data-dioxus-file-listener")) { + // prevent file inputs from opening the file dialog on click + const type = input.getAttribute("type"); + if (type === "file") { + input.setAttribute("data-dioxus-file-listener", true); + input.addEventListener("click", (event) => { + let target = event.target; + let target_id = find_real_id(target); + if (target_id !== null) { + const send = (event_name) => { + const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + window.ipc.postMessage(message); + }; + send("change&input"); + } + event.preventDefault(); + }); + } + } + }"#; + let polling_request = format!( + r#"// Poll for requests + window.interpreter.wait_for_request = (headless) => {{ + fetch(new Request("{EDITS_PATH}")) + .then(response => {{ + response.arrayBuffer() + .then(bytes => {{ + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) {{ + run_from_bytes(bytes); + }} + else {{ + requestAnimationFrame(() => {{ + run_from_bytes(bytes); + }}); + }} + window.interpreter.wait_for_request(headless); + }}); + }}) + }}"# + ); + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload) + .replace("export", "") + + &polling_request; + while let Some(import_start) = interpreter.find("import") { + let import_end = interpreter[import_start..] + .find(|c| c == ';' || c == '\n') + .map(|i| i + import_start) + .unwrap_or_else(|| interpreter.len()); + interpreter.replace_range(import_start..import_end, ""); + } + + let js = format!("{interpreter}\nconst config = new InterpreterConfig(false);"); + + use minify_js::*; + let session = Session::new(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); +} diff --git a/packages/desktop/examples/stress.rs b/packages/desktop/examples/stress.rs new file mode 100644 index 000000000..5f317bc99 --- /dev/null +++ b/packages/desktop/examples/stress.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let state = use_state(cx, || 0); + use_future(cx, (), |_| { + to_owned![state]; + async move { + loop { + state += 1; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + }); + + cx.render(rsx! { + button { + onclick: move |_| { + state.set(0); + }, + "reset" + } + for _ in 0..10000 { + div { + "hello desktop! {state}" + } + } + }) +} + +fn main() { + dioxus_desktop::launch(app); +} diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index ac62cd37b..061c040ed 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -17,7 +17,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); // Stop deadman's switch @@ -52,7 +52,7 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { #[allow(deprecated)] fn app(cx: Scope) -> Element { let desktop_context: DesktopContext = cx.consume_context().unwrap(); - let recieved_events = use_state(cx, || 0); + let received_events = use_state(cx, || 0); // button mock_event( @@ -216,12 +216,12 @@ fn app(cx: Scope) -> Element { r#"new FocusEvent("focusout",{bubbles: true})"#, ); - if **recieved_events == 12 { + if **received_events == 12 { println!("all events recieved"); desktop_context.close(); } - cx.render(rsx! { + render! { div { button { id: "button", @@ -229,127 +229,160 @@ fn app(cx: Scope) -> Element { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); - recieved_events.modify(|x| *x + 1) - }, + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + received_events.modify(|x| *x + 1) + } } div { id: "mouse_move_div", onmousemove: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - recieved_events.modify(|x| *x + 1) - }, + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + received_events.modify(|x| *x + 1) + } } div { id: "mouse_click_div", onclick: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); - recieved_events.modify(|x| *x + 1) - }, - } - div{ - id: "mouse_dblclick_div", - ondblclick: move |event| { - println!("{:?}", event.data); - assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); - recieved_events.modify(|x| *x + 1) + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + received_events.modify(|x| *x + 1) } } - div{ + div { + id: "mouse_dblclick_div", + ondoubleclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!( + event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary), + ); + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + received_events.modify(|x| *x + 1) + } + } + div { id: "mouse_down_div", onmousedown: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); - assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); - recieved_events.modify(|x| *x + 1) + assert!( + event + .data + .held_buttons() + .contains(dioxus_html::input_data::MouseButton::Secondary), + ); + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Secondary), + ); + received_events.modify(|x| *x + 1) } } - div{ + div { id: "mouse_up_div", onmouseup: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert!(event.data.held_buttons().is_empty()); - assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); - recieved_events.modify(|x| *x + 1) + assert_eq!( + event.data.trigger_button(), + Some(dioxus_html::input_data::MouseButton::Primary), + ); + received_events.modify(|x| *x + 1) } } - div{ + div { id: "wheel_div", width: "100px", height: "100px", background_color: "red", onwheel: move |event| { println!("{:?}", event.data); - let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ - panic!("Expected delta to be in pixels") - }; + let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else { + panic!("Expected delta to be in pixels") }; assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(event.data.is_auto_repeating()); - - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(!event.data.is_auto_repeating()); - - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); assert!(event.data.modifiers().is_empty()); assert_eq!(event.data.key().to_string(), "a"); assert_eq!(event.data.code().to_string(), "KeyA"); - assert_eq!(event.data.location, 0); + assert_eq!(event.data.location(), Location::Standard); assert!(!event.data.is_auto_repeating()); - - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } - input{ + input { id: "focus_out_div", onfocusout: move |event| { println!("{:?}", event.data); - recieved_events.modify(|x| *x + 1) + received_events.modify(|x| *x + 1) } } } - }) + } } diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 0907121d4..50d7b8419 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,8 +2,8 @@ use dioxus::prelude::*; use dioxus_desktop::DesktopContext; pub(crate) fn check_app_exits(app: Component) { - use dioxus_desktop::tao::window::WindowBuilder; use dioxus_desktop::Config; + use tao::window::WindowBuilder; // This is a deadman's switch to ensure that the app exits let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); @@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) { dioxus_desktop::launch_cfg( app, - Config::new().with_window(WindowBuilder::new().with_visible(false)), + Config::new().with_window(WindowBuilder::new().with_visible(true)), ); should_panic.store(false, std::sync::atomic::Ordering::SeqCst); diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs new file mode 100644 index 000000000..8d26a48f1 --- /dev/null +++ b/packages/desktop/src/app.rs @@ -0,0 +1,358 @@ +use crate::{ + config::{Config, WindowCloseBehaviour}, + desktop_context::WindowEventHandlers, + element::DesktopElement, + file_upload::FileDialogRequest, + ipc::IpcMessage, + ipc::{EventData, UserWindowEvent}, + query::QueryResult, + shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, + webview::WebviewInstance, +}; +use crossbeam_channel::Receiver; +use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::{ + native_bind::NativeFileEngine, FileEngine, HasFormData, HtmlEvent, MountedData, + PlatformEventData, SerializedHtmlEventConverter, +}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + rc::Rc, + sync::Arc, +}; +use tao::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::WindowId, +}; + +/// The single top-level object that manages all the running windows, assets, shortcuts, etc +pub(crate) struct App

{ + // move the props into a cell so we can pop it out later to create the first window + // iOS panics if we create a window before the event loop is started, so we toss them into a cell + pub(crate) props: Cell>, + pub(crate) cfg: Cell>, + + // Stuff we need mutable access to + pub(crate) root: Component

, + pub(crate) control_flow: ControlFlow, + pub(crate) is_visible_before_start: bool, + pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) webviews: HashMap, + + /// This single blob of state is shared between all the windows so they have access to the runtime state + /// + /// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows + pub(crate) shared: Rc, +} + +/// A bundle of state shared between all the windows, providing a way for us to communicate with running webview. +/// +/// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell +pub struct SharedContext { + pub(crate) event_handlers: WindowEventHandlers, + pub(crate) pending_webviews: RefCell>, + pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) global_hotkey_channel: Receiver, + pub(crate) proxy: EventLoopProxy, + pub(crate) target: EventLoopWindowTarget, +} + +impl App

{ + pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { + let event_loop = EventLoopBuilder::::with_user_event().build(); + + let mut app = Self { + root, + window_behavior: cfg.last_window_close_behaviour, + is_visible_before_start: true, + webviews: HashMap::new(), + control_flow: ControlFlow::Wait, + props: Cell::new(Some(props)), + cfg: Cell::new(Some(cfg)), + shared: Rc::new(SharedContext { + event_handlers: WindowEventHandlers::default(), + pending_webviews: Default::default(), + shortcut_manager: ShortcutRegistry::new(), + global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(), + proxy: event_loop.create_proxy(), + target: event_loop.clone(), + }), + }; + + // Copy over any assets we find + // todo - re-enable this when we have a faster way of copying assets + #[cfg(feature = "collect-assets")] + crate::collect_assets::copy_assets(); + + // Set the event converter + dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter)); + + // Allow hotreloading to work - but only in debug mode + #[cfg(all(feature = "hot-reload", debug_assertions))] + app.connect_hotreload(); + + (event_loop, app) + } + + pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) { + self.control_flow = ControlFlow::Wait; + + self.shared + .event_handlers + .apply_event(window_event, &self.shared.target); + + if let Ok(event) = self.shared.global_hotkey_channel.try_recv() { + self.shared.shortcut_manager.call_handlers(event); + } + } + + #[cfg(all(feature = "hot-reload", debug_assertions))] + pub fn connect_hotreload(&mut self) { + dioxus_hot_reload::connect({ + let proxy = self.shared.proxy.clone(); + move |template| { + let _ = proxy.send_event(UserWindowEvent( + EventData::HotReloadEvent(template), + unsafe { WindowId::dummy() }, + )); + } + }); + } + + pub fn handle_new_window(&mut self) { + for handler in self.shared.pending_webviews.borrow_mut().drain(..) { + let id = handler.desktop_context.window.id(); + self.webviews.insert(id, handler); + _ = self + .shared + .proxy + .send_event(UserWindowEvent(EventData::Poll, id)); + } + } + + pub fn handle_close_requested(&mut self, id: WindowId) { + use WindowCloseBehaviour::*; + + match self.window_behavior { + LastWindowExitsApp => { + self.webviews.remove(&id); + if self.webviews.is_empty() { + self.control_flow = ControlFlow::Exit + } + } + + LastWindowHides => { + let Some(webview) = self.webviews.get(&id) else { + return; + }; + hide_app_window(&webview.desktop_context.webview); + } + + CloseWindow => { + self.webviews.remove(&id); + } + } + } + + pub fn window_destroyed(&mut self, id: WindowId) { + self.webviews.remove(&id); + + if matches!( + self.window_behavior, + WindowCloseBehaviour::LastWindowExitsApp + ) && self.webviews.is_empty() + { + self.control_flow = ControlFlow::Exit + } + } + + pub fn handle_start_cause_init(&mut self) { + let props = self.props.take().unwrap(); + let cfg = self.cfg.take().unwrap(); + + self.is_visible_before_start = cfg.window.window.visible; + + let webview = WebviewInstance::new( + cfg, + VirtualDom::new_with_props(self.root, props), + self.shared.clone(), + ); + + let id = webview.desktop_context.window.id(); + self.webviews.insert(id, webview); + + _ = self + .shared + .proxy + .send_event(UserWindowEvent(EventData::Poll, id)); + } + + pub fn handle_browser_open(&mut self, msg: IpcMessage) { + if let Some(temp) = msg.params().as_object() { + if temp.contains_key("href") { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + tracing::error!("Open Browser error: {:?}", e); + } + } + } + } + + pub fn handle_initialize_msg(&mut self, id: WindowId) { + let view = self.webviews.get_mut(&id).unwrap(); + view.desktop_context.send_edits(view.dom.rebuild()); + view.desktop_context + .window + .set_visible(self.is_visible_before_start); + } + + pub fn handle_close_msg(&mut self, id: WindowId) { + self.webviews.remove(&id); + + if self.webviews.is_empty() { + self.control_flow = ControlFlow::Exit + } + } + + pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) { + let Ok(result) = serde_json::from_value::(msg.params()) else { + return; + }; + + let Some(view) = self.webviews.get(&id) else { + return; + }; + + view.desktop_context.query.send(result); + } + + pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { + let parsed_params = serde_json::from_value(msg.params()) + .map_err(|err| tracing::error!("Error parsing user_event: {:?}", err)); + + let Ok(evt) = parsed_params else { return }; + + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + + let view = self.webviews.get_mut(&id).unwrap(); + let query = view.desktop_context.query.clone(); + + // check for a mounted event placeholder and replace it with a desktop specific element + let as_any = match data { + dioxus_html::EventData::Mounted => { + let element = DesktopElement::new(element, view.desktop_context.clone(), query); + Rc::new(PlatformEventData::new(Box::new(MountedData::new(element)))) + } + _ => data.into_any(), + }; + + view.dom.handle_event(&name, as_any, element, bubbles); + view.desktop_context.send_edits(view.dom.render_immediate()); + } + + #[cfg(all(feature = "hot-reload", debug_assertions))] + pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) { + match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + for webview in self.webviews.values_mut() { + webview.dom.replace_template(template); + webview.poll_vdom(); + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + self.control_flow = ControlFlow::Exit; + } + } + } + + pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { + let Ok(file_dialog) = serde_json::from_value::(msg.params()) else { + return; + }; + struct DesktopFileUploadForm { + files: Arc, + } + + impl HasFormData for DesktopFileUploadForm { + fn files(&self) -> Option> { + Some(self.files.clone()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + } + + let id = ElementId(file_dialog.target); + let event_name = &file_dialog.event; + let event_bubbles = file_dialog.bubbles; + let files = file_dialog.get_file_event(); + + let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm { + files: Arc::new(NativeFileEngine::new(files)), + }))); + + let view = self.webviews.get_mut(&window).unwrap(); + + if event_name == "change&input" { + view.dom + .handle_event("input", data.clone(), id, event_bubbles); + view.dom.handle_event("change", data, id, event_bubbles); + } else { + view.dom.handle_event(event_name, data, id, event_bubbles); + } + + view.desktop_context.send_edits(view.dom.render_immediate()); + } + + /// Poll the virtualdom until it's pending + /// + /// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again + /// + /// All IO is done on the tokio runtime we started earlier + pub fn poll_vdom(&mut self, id: WindowId) { + let Some(view) = self.webviews.get_mut(&id) else { + return; + }; + + view.poll_vdom(); + } +} + +/// Different hide implementations per platform +#[allow(unused)] +pub fn hide_app_window(window: &wry::WebView) { + #[cfg(target_os = "windows")] + { + use tao::platform::windows::WindowExtWindows; + window.set_visible(false); + // window.set_skip_taskbar(true); + } + + #[cfg(target_os = "linux")] + { + use tao::platform::unix::WindowExtUnix; + window.set_visible(false); + } + + #[cfg(target_os = "macos")] + { + // window.set_visible(false); has the wrong behaviour on macOS + // It will hide the window but not show it again when the user switches + // back to the app. `NSApplication::hide:` has the correct behaviour + use objc::runtime::Object; + use objc::{msg_send, sel, sel_impl}; + objc::rc::autoreleasepool(|| unsafe { + let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication]; + let nil = std::ptr::null_mut::(); + let _: () = msg_send![app, hide: nil]; + }); + } +} diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs new file mode 100644 index 000000000..f0f5a03fc --- /dev/null +++ b/packages/desktop/src/assets.rs @@ -0,0 +1,61 @@ +use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId}; +use rustc_hash::FxHashMap; +use std::{cell::RefCell, rc::Rc}; +use wry::{http::Request, RequestAsyncResponder}; + +/// +pub type AssetRequest = Request>; + +pub struct AssetHandler { + f: Box, + scope: ScopeId, +} + +#[derive(Clone)] +pub struct AssetHandlerRegistry { + dom_rt: Rc, + handlers: Rc>>, +} + +impl AssetHandlerRegistry { + pub fn new(dom_rt: Rc) -> Self { + AssetHandlerRegistry { + dom_rt, + handlers: Default::default(), + } + } + + pub fn has_handler(&self, name: &str) -> bool { + self.handlers.borrow().contains_key(name) + } + + pub fn handle_request( + &self, + name: &str, + request: AssetRequest, + responder: RequestAsyncResponder, + ) { + if let Some(handler) = self.handlers.borrow().get(name) { + // make sure the runtime is alive for the duration of the handler + // We should do this for all the things - not just asset handlers + RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || { + (handler.f)(request, responder) + }); + } + } + + pub fn register_handler( + &self, + name: String, + f: Box, + scope: ScopeId, + ) { + self.handlers + .borrow_mut() + .insert(name, AssetHandler { f, scope }); + } + + pub fn remove_handler(&self, name: &str) -> Option { + self.handlers.borrow_mut().remove(name) + } +} diff --git a/packages/desktop/src/collect_assets.rs b/packages/desktop/src/collect_assets.rs new file mode 100644 index 000000000..aa9050a75 --- /dev/null +++ b/packages/desktop/src/collect_assets.rs @@ -0,0 +1,60 @@ +pub fn copy_assets() { + #[cfg(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + ))] + { + // The CLI will copy assets to the current working directory + if std::env::var_os("DIOXUS_ACTIVE").is_some() { + return; + } + use manganis_cli_support::AssetManifest; + use manganis_cli_support::AssetManifestExt; + use manganis_cli_support::Config; + use std::path::PathBuf; + let config = Config::current(); + let asset_location = config.assets_serve_location(); + let asset_location = PathBuf::from(asset_location); + let _ = std::fs::remove_dir_all(&asset_location); + + println!("Finding assets... (Note: if you run a dioxus desktop application with the CLI. This process will be significantly faster.)"); + let manifest = AssetManifest::load(); + let has_assets = manifest + .packages() + .iter() + .any(|package| !package.assets().is_empty()); + + if has_assets { + println!("Copying and optimizing assets..."); + manifest.copy_static_assets_to(&asset_location).unwrap(); + println!("Copied assets to {}", asset_location.display()); + } else { + println!("No assets found"); + } + } + #[cfg(not(all( + debug_assertions, + any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ) + )))] + { + println!( + "Skipping assets in release mode. You compile assets with the dioxus-cli in release mode" + ); + } +} diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/config.rs similarity index 82% rename from packages/desktop/src/cfg.rs rename to packages/desktop/src/config.rs index d14b2e433..d826303e1 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/config.rs @@ -1,16 +1,13 @@ use std::borrow::Cow; use std::path::PathBuf; -use wry::application::window::Icon; +use dioxus_core::prelude::Component; +use tao::window::{Icon, WindowBuilder, WindowId}; use wry::{ - application::window::{Window, WindowBuilder}, http::{Request as HttpRequest, Response as HttpResponse}, - webview::FileDropEvent, - Result as WryResult, + FileDropEvent, }; -// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); - /// The behaviour of the application when the last window is closed. #[derive(Copy, Clone, Eq, PartialEq)] pub enum WindowCloseBehaviour { @@ -36,13 +33,14 @@ pub struct Config { pub(crate) root_name: String, pub(crate) background_color: Option<(u8, u8, u8, u8)>, pub(crate) last_window_close_behaviour: WindowCloseBehaviour, + pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; +type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, - Box>) -> WryResult>> + 'static>, + Box>) -> HttpResponse> + 'static>, ); impl Config { @@ -65,9 +63,32 @@ impl Config { root_name: "main".to_string(), background_color: None, last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp, + enable_default_menu_bar: true, } } + /// Launch a Dioxus app using the given component and config + /// + /// See the [`crate::launch::launch`] function for more details. + pub fn launch(self, root: Component<()>) { + crate::launch::launch_cfg(root, self) + } + + /// Launch a Dioxus app using the given component, config, and props + /// + /// See the [`crate::launch::launch_with_props`] function for more details. + pub fn launch_with_props(self, root: Component

, props: P) { + crate::launch::launch_with_props(root, props, self) + } + + /// Set whether the default menu bar should be enabled. + /// + /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`. + pub fn with_default_menu_bar(mut self, enable: bool) -> Self { + self.enable_default_menu_bar = enable; + self + } + /// set the directory from which assets will be searched in release mode pub fn with_resource_directory(mut self, path: impl Into) -> Self { self.resource_dir = Some(path.into()); @@ -111,7 +132,7 @@ impl Config { /// Set a file drop handler. If this is enabled, html drag events will be disabled. pub fn with_file_drop_handler( mut self, - handler: impl Fn(&Window, FileDropEvent) -> bool + 'static, + handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static, ) -> Self { self.file_drop_handler = Some(Box::new(handler)); self @@ -120,7 +141,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&HttpRequest>) -> WryResult>> + 'static, + F: Fn(HttpRequest>) -> HttpResponse> + 'static, { self.protocols.push((name, Box::new(handler))); self diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 897f8eba8..c97272293 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,41 +1,42 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::rc::Weak; - -use crate::create_new_window; -use crate::events::IpcMessage; -use crate::query::QueryEngine; -use crate::shortcut::ShortcutId; -use crate::shortcut::ShortcutRegistry; -use crate::shortcut::ShortcutRegistryError; -use crate::Config; -use crate::WebviewHandler; -use dioxus_core::ScopeState; -use dioxus_core::VirtualDom; -#[cfg(all(feature = "hot-reload", debug_assertions))] -use dioxus_hot_reload::HotReloadMsg; +use crate::{ + app::SharedContext, + assets::AssetHandlerRegistry, + edits::EditQueue, + ipc::{EventData, UserWindowEvent}, + query::QueryEngine, + shortcut::{HotKey, ShortcutId, ShortcutRegistryError}, + webview::WebviewInstance, + AssetRequest, Config, +}; +use dioxus_core::{ + prelude::{current_scope_id, ScopeId}, + Mutations, VirtualDom, +}; +use dioxus_interpreter_js::binary_protocol::Channel; +use rustc_hash::FxHashMap; use slab::Slab; -use wry::application::accelerator::Accelerator; -use wry::application::event::Event; -use wry::application::event_loop::EventLoopProxy; -use wry::application::event_loop::EventLoopWindowTarget; +use std::{cell::RefCell, fmt::Debug, rc::Rc, rc::Weak, sync::atomic::AtomicU16}; +use tao::{ + event::Event, + event_loop::EventLoopWindowTarget, + window::{Fullscreen as WryFullscreen, Window, WindowId}, +}; +use wry::{RequestAsyncResponder, WebView}; + #[cfg(target_os = "ios")] -use wry::application::platform::ios::WindowExtIOS; -use wry::application::window::Fullscreen as WryFullscreen; -use wry::application::window::Window; -use wry::application::window::WindowId; -use wry::webview::WebView; +use tao::platform::ios::WindowExtIOS; -pub type ProxyType = EventLoopProxy; - -/// Get an imperative handle to the current window -pub fn use_window(cx: &ScopeState) -> &DesktopContext { - cx.use_hook(|| cx.consume_context::()) - .as_ref() - .unwrap() +/// Get an imperative handle to the current window without using a hook +/// +/// ## Panics +/// +/// This function will panic if it is called outside of the context of a Dioxus App. +pub fn window() -> DesktopContext { + dioxus_core::prelude::consume_context().unwrap() } -pub(crate) type WebviewQueue = Rc>>; +/// A handle to the [`DesktopService`] that can be passed around. +pub type DesktopContext = Rc; /// An imperative interface to the current window. /// @@ -51,60 +52,69 @@ pub(crate) type WebviewQueue = Rc>>; /// ``` pub struct DesktopService { /// The wry/tao proxy to the current window - pub webview: Rc, + pub webview: WebView, - /// The proxy to the event loop - pub proxy: ProxyType, + /// The tao window itself + pub window: Window, + + pub(crate) shared: Rc, /// The receiver for queries about the current window pub(super) query: QueryEngine, - - pub(super) pending_windows: WebviewQueue, - - pub(crate) event_loop: EventLoopWindowTarget, - - pub(crate) event_handlers: WindowEventHandlers, - - pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) edit_queue: EditQueue, + pub(crate) templates: RefCell>, + pub(crate) max_template_count: AtomicU16, + pub(crate) channel: RefCell, + pub(crate) asset_handlers: AssetHandlerRegistry, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } -/// A handle to the [`DesktopService`] that can be passed around. -pub type DesktopContext = Rc; - /// A smart pointer to the current window. impl std::ops::Deref for DesktopService { type Target = Window; fn deref(&self) -> &Self::Target { - self.webview.window() + &self.window } } impl DesktopService { pub(crate) fn new( webview: WebView, - proxy: ProxyType, - event_loop: EventLoopWindowTarget, - webviews: WebviewQueue, - event_handlers: WindowEventHandlers, - shortcut_manager: ShortcutRegistry, + window: Window, + shared: Rc, + edit_queue: EditQueue, + asset_handlers: AssetHandlerRegistry, ) -> Self { Self { - webview: Rc::new(webview), - proxy, - event_loop, + window, + webview, + shared, + edit_queue, + asset_handlers, query: Default::default(), - pending_windows: webviews, - event_handlers, - shortcut_manager, + templates: Default::default(), + max_template_count: Default::default(), + channel: Default::default(), #[cfg(target_os = "ios")] views: Default::default(), } } + /// Send a list of mutations to the webview + pub(crate) fn send_edits(&self, edits: Mutations) { + if let Some(bytes) = crate::edits::apply_edits( + edits, + &mut self.channel.borrow_mut(), + &mut self.templates.borrow_mut(), + &self.max_template_count, + ) { + self.edit_queue.add_edits(bytes) + } + } + /// Create a new window using the props and window builder /// /// Returns the webview handle for the new window. @@ -113,35 +123,23 @@ impl DesktopService { /// /// Be careful to not create a cycle of windows, or you might leak memory. pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { - let window = create_new_window( - cfg, - &self.event_loop, - &self.proxy, - dom, - &self.pending_windows, - &self.event_handlers, - self.shortcut_manager.clone(), - ); + let window = WebviewInstance::new(cfg, dom, self.shared.clone()); - let desktop_context = window - .dom - .base_scope() - .consume_context::>() + let cx = window.desktop_context.clone(); + + self.shared + .proxy + .send_event(UserWindowEvent(EventData::NewWindow, cx.id())) .unwrap(); - let id = window.desktop_context.webview.window().id(); - - self.proxy - .send_event(UserWindowEvent(EventData::NewWindow, id)) + self.shared + .proxy + .send_event(UserWindowEvent(EventData::Poll, cx.id())) .unwrap(); - self.proxy - .send_event(UserWindowEvent(EventData::Poll, id)) - .unwrap(); + self.shared.pending_webviews.borrow_mut().push(window); - self.pending_windows.borrow_mut().push(window); - - Rc::downgrade(&desktop_context) + Rc::downgrade(&cx) } /// trigger the drag-window event @@ -153,41 +151,38 @@ impl DesktopService { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let window = self.webview.window(); - - // if the drag_window has any errors, we don't do anything - if window.fullscreen().is_none() { - window.drag_window().unwrap(); + if self.window.fullscreen().is_none() { + _ = self.window.drag_window(); } } /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let window = self.webview.window(); - - window.set_maximized(!window.is_maximized()) + self.window.set_maximized(!self.window.is_maximized()) } - /// close window + /// Close this window pub fn close(&self) { let _ = self + .shared .proxy .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); } - /// close window + /// Close a particular window, given its ID pub fn close_window(&self, id: WindowId) { let _ = self + .shared .proxy .send_event(UserWindowEvent(EventData::CloseWindow, id)); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - if let Some(handle) = self.webview.window().current_monitor() { - self.webview - .window() - .set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle)))); + if let Some(handle) = &self.window.current_monitor() { + self.window.set_fullscreen( + fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))), + ); } } @@ -220,12 +215,12 @@ impl DesktopService { &self, handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, ) -> WryEventHandlerId { - self.event_handlers.add(self.id(), handler) + self.shared.event_handlers.add(self.window.id(), handler) } /// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`] pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) { - self.event_handlers.remove(id) + self.shared.event_handlers.remove(id) } /// Create a global shortcut @@ -233,27 +228,55 @@ impl DesktopService { /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information. pub fn create_shortcut( &self, - accelerator: Accelerator, + hotkey: HotKey, callback: impl FnMut() + 'static, ) -> Result { - self.shortcut_manager - .add_shortcut(accelerator, Box::new(callback)) + self.shared + .shortcut_manager + .add_shortcut(hotkey, Box::new(callback)) } /// Remove a global shortcut pub fn remove_shortcut(&self, id: ShortcutId) { - self.shortcut_manager.remove_shortcut(id) + self.shared.shortcut_manager.remove_shortcut(id) } /// Remove all global shortcuts pub fn remove_all_shortcuts(&self) { - self.shortcut_manager.remove_all() + self.shared.shortcut_manager.remove_all() + } + + /// Provide a callback to handle asset loading yourself. + /// If the ScopeId isn't provided, defaults to a global handler. + /// Note that the handler is namespaced by name, not ScopeId. + /// + /// When the component is dropped, the handler is removed. + /// + /// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook. + pub fn register_asset_handler( + &self, + name: String, + f: Box, + scope: Option, + ) { + self.asset_handlers.register_handler( + name, + f, + scope.unwrap_or(current_scope_id().unwrap_or(ScopeId(0))), + ) + } + + /// Removes an asset handler by its identifier. + /// + /// Returns `None` if the handler did not exist. + pub fn remove_asset_handler(&self, name: &str) -> Option<()> { + self.asset_handlers.remove_handler(name).map(|_| ()) } /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { - let window = self.webview.window(); + let window = &self.window; unsafe { use objc::runtime::Object; @@ -273,7 +296,7 @@ impl DesktopService { /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { - let window = self.webview.window(); + let window = &self.window; unsafe { use objc::runtime::Object; @@ -287,23 +310,6 @@ impl DesktopService { } } -#[derive(Debug, Clone)] -pub struct UserWindowEvent(pub EventData, pub WindowId); - -#[derive(Debug, Clone)] -pub enum EventData { - Poll, - - Ipc(IpcMessage), - - #[cfg(all(feature = "hot-reload", debug_assertions))] - HotReloadEvent(HotReloadMsg), - - NewWindow, - - CloseWindow, -} - #[cfg(target_os = "ios")] fn is_main_thread() -> bool { use objc::runtime::{Class, BOOL, NO}; @@ -369,45 +375,20 @@ impl WryWindowEventHandlerInner { target: &EventLoopWindowTarget, ) { // if this event does not apply to the window this listener cares about, return - match event { - Event::WindowEvent { window_id, .. } - | Event::MenuEvent { - window_id: Some(window_id), - .. - } => { - if *window_id != self.window_id { - return; - } + if let Event::WindowEvent { window_id, .. } = event { + if *window_id != self.window_id { + return; } - _ => (), } (self.handler)(event, target) } } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_wry_event_handler( - cx: &ScopeState, - handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, -) -> &WryEventHandler { - let desktop = use_window(cx); - cx.use_hook(move || { - let desktop = desktop.clone(); - - let id = desktop.create_wry_event_handler(handler); - - WryEventHandler { - handlers: desktop.event_handlers.clone(), - id, - } - }) -} - /// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events. /// /// This will automatically be removed when the component is unmounted. pub struct WryEventHandler { - handlers: WindowEventHandlers, + pub(crate) handlers: WindowEventHandlers, /// The unique identifier of the event handler. pub id: WryEventHandlerId, } diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs new file mode 100644 index 000000000..4562f2ebf --- /dev/null +++ b/packages/desktop/src/edits.rs @@ -0,0 +1,172 @@ +use dioxus_core::{BorrowedAttributeValue, Mutations, Template, TemplateAttribute, TemplateNode}; +use dioxus_html::event_bubbles; +use dioxus_interpreter_js::binary_protocol::Channel; +use rustc_hash::FxHashMap; +use std::{ + sync::atomic::AtomicU16, + sync::Arc, + sync::{atomic::Ordering, Mutex}, +}; + +use wry::RequestAsyncResponder; + +/// This handles communication between the requests that the webview makes and the interpreter. The interpreter +/// constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like +/// server side events. +/// +/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until +/// a new request is made. +#[derive(Default, Clone)] +pub(crate) struct EditQueue { + queue: Arc>>>, + responder: Arc>>, +} + +impl EditQueue { + pub fn handle_request(&self, responder: RequestAsyncResponder) { + let mut queue = self.queue.lock().unwrap(); + if let Some(bytes) = queue.pop() { + responder.respond(wry::http::Response::new(bytes)); + } else { + *self.responder.lock().unwrap() = Some(responder); + } + } + + pub fn add_edits(&self, edits: Vec) { + let mut responder = self.responder.lock().unwrap(); + if let Some(responder) = responder.take() { + responder.respond(wry::http::Response::new(edits)); + } else { + self.queue.lock().unwrap().push(edits); + } + } +} + +pub(crate) fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) -> Option> { + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + + use dioxus_core::Mutation::*; + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + let bytes: Vec<_> = channel.export_memory().collect(); + channel.reset(); + Some(bytes) +} + +pub fn add_template( + template: &Template<'static>, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) { + let current_max_template_count = max_template_count.load(Ordering::Relaxed); + for root in template.roots.iter() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), current_max_template_count); + } + channel.add_templates(current_max_template_count, template.roots.len() as u16); + + max_template_count.fetch_add(1, Ordering::Relaxed); +} + +pub fn create_template_node(channel: &mut Channel, node: &'static TemplateNode<'static>) { + use TemplateNode::*; + match node { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } +} diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index 20ca951bf..4c9dfbbe2 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -3,6 +3,7 @@ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking} use crate::{desktop_context::DesktopContext, query::QueryEngine}; +#[derive(Clone)] /// A mounted element passed to onmounted events pub struct DesktopElement { id: ElementId, @@ -17,8 +18,8 @@ impl DesktopElement { } impl RenderedElementBacking for DesktopElement { - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn get_client_rect( @@ -30,7 +31,7 @@ impl RenderedElementBacking for DesktopElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -54,7 +55,7 @@ impl RenderedElementBacking for DesktopElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -81,7 +82,7 @@ impl RenderedElementBacking for DesktopElement { focus: bool, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.SetFocus({}, {});", + "return window.interpreter.setFocus({}, {});", self.id.0, focus ); diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index e187be450..e9b0f036b 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,21 +1,19 @@ #![allow(clippy::await_holding_refcell_ref)] use async_trait::async_trait; -use dioxus_core::ScopeState; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; use std::{cell::RefCell, rc::Rc}; use crate::{query::Query, DesktopContext}; -/// Provides the DesktopEvalProvider through [`cx.provide_context`]. -pub fn init_eval(cx: &ScopeState) { - let desktop_ctx = cx.consume_context::().unwrap(); - let provider: Rc = Rc::new(DesktopEvalProvider { desktop_ctx }); - cx.provide_context(provider); -} - /// Reprents the desktop-target's provider of evaluators. pub struct DesktopEvalProvider { - desktop_ctx: DesktopContext, + pub(crate) desktop_ctx: DesktopContext, +} + +impl DesktopEvalProvider { + pub fn new(desktop_ctx: DesktopContext) -> Self { + Self { desktop_ctx } + } } impl EvalProvider for DesktopEvalProvider { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 7059a7c85..ac5a3df0d 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,19 +1,148 @@ //! Convert a serialized event to an event trigger -use serde::{Deserialize, Serialize}; +use crate::element::DesktopElement; +use dioxus_html::*; -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct IpcMessage { - method: String, - params: serde_json::Value, -} +pub(crate) struct SerializedHtmlEventConverter; -impl IpcMessage { - pub(crate) fn method(&self) -> &str { - self.method.as_str() +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() } - pub(crate) fn params(self) -> serde_json::Value { - self.params + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() } } diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 7b5f08fc3..98dbec437 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -14,74 +14,77 @@ pub(crate) struct FileDialogRequest { pub bubbles: bool, } -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -)))] -pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec { - vec![] -} +#[allow(unused)] +impl FileDialogRequest { + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + pub(crate) fn get_file_event(&self) -> Vec { + vec![] + } -#[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec { - fn get_file_event_for_folder( - request: &FileDialogRequest, - dialog: rfd::FileDialog, - ) -> Vec { - if request.multiple { - dialog.pick_folders().into_iter().flatten().collect() - } else { - dialog.pick_folder().into_iter().collect() + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) fn get_file_event(&self) -> Vec { + fn get_file_event_for_folder( + request: &FileDialogRequest, + dialog: rfd::FileDialog, + ) -> Vec { + if request.multiple { + dialog.pick_folders().into_iter().flatten().collect() + } else { + dialog.pick_folder().into_iter().collect() + } } - } - fn get_file_event_for_file( - request: &FileDialogRequest, - mut dialog: rfd::FileDialog, - ) -> Vec { - let filters: Vec<_> = request - .accept - .as_deref() - .unwrap_or_default() - .split(',') - .filter_map(|s| Filters::from_str(s).ok()) - .collect(); + fn get_file_event_for_file( + request: &FileDialogRequest, + mut dialog: rfd::FileDialog, + ) -> Vec { + let filters: Vec<_> = request + .accept + .as_deref() + .unwrap_or_default() + .split(',') + .filter_map(|s| Filters::from_str(s).ok()) + .collect(); - let file_extensions: Vec<_> = filters - .iter() - .flat_map(|f| f.as_extensions().into_iter()) - .collect(); + let file_extensions: Vec<_> = filters + .iter() + .flat_map(|f| f.as_extensions().into_iter()) + .collect(); - dialog = dialog.add_filter("name", file_extensions.as_slice()); + dialog = dialog.add_filter("name", file_extensions.as_slice()); - let files: Vec<_> = if request.multiple { - dialog.pick_files().into_iter().flatten().collect() + let files: Vec<_> = if request.multiple { + dialog.pick_files().into_iter().flatten().collect() + } else { + dialog.pick_file().into_iter().collect() + }; + + files + } + + let dialog = rfd::FileDialog::new(); + + if self.directory { + get_file_event_for_folder(self, dialog) } else { - dialog.pick_file().into_iter().collect() - }; - - files - } - - let dialog = rfd::FileDialog::new(); - - if request.directory { - get_file_event_for_folder(request, dialog) - } else { - get_file_event_for_file(request, dialog) + get_file_event_for_file(self, dialog) + } } } diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs new file mode 100644 index 000000000..85d1a170b --- /dev/null +++ b/packages/desktop/src/hooks.rs @@ -0,0 +1,77 @@ +use crate::{ + assets::*, ipc::UserWindowEvent, shortcut::IntoAccelerator, window, DesktopContext, + ShortcutHandle, ShortcutRegistryError, WryEventHandler, +}; +use dioxus_core::ScopeState; +use tao::{event::Event, event_loop::EventLoopWindowTarget}; +use wry::RequestAsyncResponder; + +/// Get an imperative handle to the current window +pub fn use_window(cx: &ScopeState) -> &DesktopContext { + cx.use_hook(|| cx.consume_context::()) + .as_ref() + .unwrap() +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_wry_event_handler( + cx: &ScopeState, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, +) -> &WryEventHandler { + cx.use_hook(move || { + let desktop = window(); + + let id = desktop.create_wry_event_handler(handler); + + WryEventHandler { + handlers: desktop.shared.event_handlers.clone(), + id, + } + }) +} + +/// Provide a callback to handle asset loading yourself. +/// +/// The callback takes a path as requested by the web view, and it should return `Some(response)` +/// if you want to load the asset, and `None` if you want to fallback on the default behavior. +pub fn use_asset_handler( + cx: &ScopeState, + name: &str, + handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static, +) { + cx.use_hook(|| { + crate::window().asset_handlers.register_handler( + name.to_string(), + Box::new(handler), + cx.scope_id(), + ); + + Handler(name.to_string()) + }); + + // todo: can we just put ondrop in core? + struct Handler(String); + impl Drop for Handler { + fn drop(&mut self) { + _ = crate::window().asset_handlers.remove_handler(&self.0); + } + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_global_shortcut( + cx: &ScopeState, + accelerator: impl IntoAccelerator, + handler: impl FnMut() + 'static, +) -> &Result { + cx.use_hook(move || { + let desktop = window(); + + let id = desktop.create_shortcut(accelerator.accelerator(), handler); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 0704fd0f0..45926e3f1 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,12 +1,12 @@ - - Dioxus app - - - - -

- - + + Dioxus app + + + + +
+ + diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs new file mode 100644 index 000000000..c8810c63a --- /dev/null +++ b/packages/desktop/src/ipc.rs @@ -0,0 +1,64 @@ +use serde::{Deserialize, Serialize}; +use tao::window::WindowId; + +/// A pair of data +#[derive(Debug, Clone)] +pub struct UserWindowEvent(pub EventData, pub WindowId); + +/// The data that might eminate from any window/webview +#[derive(Debug, Clone)] +pub enum EventData { + /// Poll the virtualdom + Poll, + + /// Handle an ipc message eminating from the window.postMessage of a given webview + Ipc(IpcMessage), + + /// Handle a hotreload event, basically telling us to update our templates + #[cfg(all(feature = "hot-reload", debug_assertions))] + HotReloadEvent(dioxus_hot_reload::HotReloadMsg), + + /// Create a new window + NewWindow, + + /// Close a given window (could be any window!) + CloseWindow, +} + +/// A message struct that manages the communication between the webview and the eventloop code +/// +/// This needs to be serializable across the JS boundary, so the method names and structs are sensitive. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct IpcMessage { + method: String, + params: serde_json::Value, +} + +/// A set of known messages that we need to respond to +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum IpcMethod<'a> { + FileDialog, + UserEvent, + Query, + BrowserOpen, + Initialize, + Other(&'a str), +} + +impl IpcMessage { + pub(crate) fn method(&self) -> IpcMethod { + match self.method.as_str() { + // todo: this is a misspelling, needs to be fixed + "file_diolog" => IpcMethod::FileDialog, + "user_event" => IpcMethod::UserEvent, + "query" => IpcMethod::Query, + "browser_open" => IpcMethod::BrowserOpen, + "initialize" => IpcMethod::Initialize, + _ => IpcMethod::Other(&self.method), + } + } + + pub(crate) fn params(self) -> serde_json::Value { + self.params + } +} diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs new file mode 100644 index 000000000..e53a5ea77 --- /dev/null +++ b/packages/desktop/src/launch.rs @@ -0,0 +1,131 @@ +use crate::{ + app::App, + ipc::{EventData, IpcMethod, UserWindowEvent}, + Config, +}; +use dioxus_core::*; +use tao::event::{Event, StartCause, WindowEvent}; + +/// Launch the WebView and run the event loop. +/// +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// +/// fn main() { +/// dioxus_desktop::launch(app); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch(root: Component) { + launch_with_props(root, (), Config::default()) +} + +/// Launch the WebView and run the event loop, with configuration. +/// +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_desktop::*; +/// +/// fn main() { +/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App"))); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch_cfg(root: Component, config_builder: Config) { + launch_with_props(root, (), config_builder) +} + +/// Launch the WebView and run the event loop, with configuration and root props. +/// +/// If the [`tokio`] feature is enabled, this will also startup and block a tokio runtime using the unconstrained task. +/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust, no_run +/// use dioxus::prelude::*; +/// use dioxus_desktop::Config; +/// +/// fn main() { +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); +/// } +/// +/// struct AppProps { +/// name: &'static str +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello {cx.props.name}!"} +/// }) +/// } +/// ``` +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { + #[cfg(feature = "tokio")] + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(tokio::task::unconstrained(async move { + launch_with_props_blocking(root, props, cfg); + })); + + #[cfg(not(feature = "tokio"))] + launch_with_props_blocking(root, props, cfg); +} + +/// Launch the WebView and run the event loop, with configuration and root props. +/// +/// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime +/// and is equivalent to calling launch_with_props with the tokio feature disabled. +pub fn launch_with_props_blocking(root: Component

, props: P, cfg: Config) { + let (event_loop, mut app) = App::new(cfg, props, root); + + event_loop.run(move |window_event, _, control_flow| { + app.tick(&window_event); + + match window_event { + Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(), + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => app.handle_close_requested(window_id), + WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), + _ => {} + }, + Event::UserEvent(UserWindowEvent(event, id)) => match event { + EventData::Poll => app.poll_vdom(id), + EventData::NewWindow => app.handle_new_window(), + EventData::CloseWindow => app.handle_close_msg(id), + EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), + EventData::Ipc(msg) => match msg.method() { + IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + IpcMethod::UserEvent => app.handle_user_event_msg(msg, id), + IpcMethod::Query => app.handle_query_msg(msg, id), + IpcMethod::BrowserOpen => app.handle_browser_open(msg), + IpcMethod::Initialize => app.handle_initialize_msg(id), + IpcMethod::Other(_) => {} + }, + }, + _ => {} + } + + *control_flow = app.control_flow; + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index badbe3276..9cd94d128 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,477 +3,49 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] -mod cfg; +mod app; +mod assets; +mod config; mod desktop_context; +mod edits; mod element; mod escape; mod eval; mod events; mod file_upload; +mod hooks; +mod ipc; +mod menubar; mod protocol; mod query; mod shortcut; mod waker; mod webview; +#[cfg(feature = "collect-assets")] +mod collect_assets; + +// mobile shortcut is only supported on mobile platforms #[cfg(any(target_os = "ios", target_os = "android"))] mod mobile_shortcut; -use crate::query::QueryResult; -pub use cfg::{Config, WindowCloseBehaviour}; -pub use desktop_context::DesktopContext; -pub use desktop_context::{ - use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId, -}; -use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; -use dioxus_core::*; -use dioxus_html::MountedData; -use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use element::DesktopElement; -use eval::init_eval; -use futures_util::{pin_mut, FutureExt}; -use shortcut::ShortcutRegistry; -pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; -use std::cell::Cell; -use std::rc::Rc; -use std::task::Waker; -use std::{collections::HashMap, sync::Arc}; -pub use tao::dpi::{LogicalSize, PhysicalSize}; -use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +// The main entrypoint for this crate +pub use launch::*; +mod launch; + +// Reexport tao and wry, might want to re-export other important things +pub use tao; +pub use tao::dpi::{LogicalPosition, LogicalSize}; +pub use tao::event::WindowEvent; pub use tao::window::WindowBuilder; -use tao::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, -}; pub use wry; -pub use wry::application as tao; -use wry::webview::WebView; -use wry::{application::window::WindowId, webview::WebContext}; -/// Launch the WebView and run the event loop. -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. -/// -/// ```rust, ignore -/// use dioxus::prelude::*; -/// -/// fn main() { -/// dioxus_desktop::launch(app); -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello world!"} -/// }) -/// } -/// ``` -pub fn launch(root: Component) { - launch_with_props(root, (), Config::default()) -} - -/// Launch the WebView and run the event loop, with configuration. -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. -/// -/// You can configure the WebView window with a configuration closure -/// -/// ```rust, ignore -/// use dioxus::prelude::*; -/// -/// fn main() { -/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App"))); -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello world!"} -/// }) -/// } -/// ``` -pub fn launch_cfg(root: Component, config_builder: Config) { - launch_with_props(root, (), config_builder) -} - -/// Launch the WebView and run the event loop, with configuration and root props. -/// -/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread. -/// -/// You can configure the WebView window with a configuration closure -/// -/// ```rust, ignore -/// use dioxus::prelude::*; -/// -/// fn main() { -/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); -/// } -/// -/// struct AppProps { -/// name: &'static str -/// } -/// -/// fn app(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello {cx.props.name}!"} -/// }) -/// } -/// ``` -pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let event_loop = EventLoop::::with_user_event(); - - let proxy = event_loop.create_proxy(); - - let window_behaviour = cfg.last_window_close_behaviour; - - // Intialize hot reloading if it is enabled - #[cfg(all(feature = "hot-reload", debug_assertions))] - dioxus_hot_reload::connect({ - let proxy = proxy.clone(); - move |template| { - let _ = proxy.send_event(UserWindowEvent( - EventData::HotReloadEvent(template), - unsafe { WindowId::dummy() }, - )); - } - }); - - // We start the tokio runtime *on this thread* - // Any future we poll later will use this runtime to spawn tasks and for IO - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - // We enter the runtime but we poll futures manually, circumventing the per-task runtime budget - let _guard = rt.enter(); - - // We only have one webview right now, but we'll have more later - // Store them in a hashmap so we can remove them when they're closed - let mut webviews = HashMap::::new(); - - // We use this to allow dynamically adding and removing window event handlers - let event_handlers = WindowEventHandlers::default(); - - let queue = WebviewQueue::default(); - - let shortcut_manager = ShortcutRegistry::new(&event_loop); - - // move the props into a cell so we can pop it out later to create the first window - // iOS panics if we create a window before the event loop is started - let props = Rc::new(Cell::new(Some(props))); - let cfg = Rc::new(Cell::new(Some(cfg))); - - event_loop.run(move |window_event, event_loop, control_flow| { - *control_flow = ControlFlow::Wait; - - event_handlers.apply_event(&window_event, event_loop); - - match window_event { - Event::WindowEvent { - event, window_id, .. - } => match event { - WindowEvent::CloseRequested => match window_behaviour { - cfg::WindowCloseBehaviour::LastWindowExitsApp => { - webviews.remove(&window_id); - - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - cfg::WindowCloseBehaviour::LastWindowHides => { - let Some(webview) = webviews.get(&window_id) else { - return; - }; - hide_app_window(&webview.desktop_context.webview); - } - cfg::WindowCloseBehaviour::CloseWindow => { - webviews.remove(&window_id); - } - }, - WindowEvent::Destroyed { .. } => { - webviews.remove(&window_id); - - if matches!( - window_behaviour, - cfg::WindowCloseBehaviour::LastWindowExitsApp - ) && webviews.is_empty() - { - *control_flow = ControlFlow::Exit - } - } - _ => {} - }, - - Event::NewEvents(StartCause::Init) => { - let props = props.take().unwrap(); - let cfg = cfg.take().unwrap(); - - // Create a dom - let dom = VirtualDom::new_with_props(root, props); - - let handler = create_new_window( - cfg, - event_loop, - &proxy, - dom, - &queue, - &event_handlers, - shortcut_manager.clone(), - ); - - let id = handler.desktop_context.webview.window().id(); - webviews.insert(id, handler); - _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); - } - - Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { - for handler in queue.borrow_mut().drain(..) { - let id = handler.desktop_context.webview.window().id(); - webviews.insert(id, handler); - _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); - } - } - - Event::UserEvent(event) => match event.0 { - #[cfg(all(feature = "hot-reload", debug_assertions))] - EventData::HotReloadEvent(msg) => match msg { - dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { - for webview in webviews.values_mut() { - webview.dom.replace_template(template); - - poll_vdom(webview); - } - } - dioxus_hot_reload::HotReloadMsg::Shutdown => { - *control_flow = ControlFlow::Exit; - } - }, - - EventData::CloseWindow => { - webviews.remove(&event.1); - - if webviews.is_empty() { - *control_flow = ControlFlow::Exit - } - } - - EventData::Poll => { - if let Some(view) = webviews.get_mut(&event.1) { - poll_vdom(view); - } - } - - EventData::Ipc(msg) if msg.method() == "user_event" => { - let params = msg.params(); - - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(_) => return, - }; - - let HtmlEvent { - element, - name, - bubbles, - data, - } = evt; - - let view = webviews.get_mut(&event.1).unwrap(); - - // check for a mounted event placeholder and replace it with a desktop specific element - let as_any = if let dioxus_html::EventData::Mounted = &data { - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query - .clone(); - - let element = - DesktopElement::new(element, view.desktop_context.clone(), query); - - Rc::new(MountedData::new(element)) - } else { - data.into_any() - }; - - view.dom.handle_event(&name, as_any, element, bubbles); - - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); - } - - // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query - EventData::Ipc(msg) if msg.method() == "query" => { - let params = msg.params(); - - if let Ok(result) = serde_json::from_value::(params) { - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query - .clone(); - - query.send(result); - } - } - - EventData::Ipc(msg) if msg.method() == "initialize" => { - let view = webviews.get_mut(&event.1).unwrap(); - send_edits(view.dom.rebuild(), &view.desktop_context.webview); - } - - EventData::Ipc(msg) if msg.method() == "browser_open" => { - if let Some(temp) = msg.params().as_object() { - if temp.contains_key("href") { - let open = webbrowser::open(temp["href"].as_str().unwrap()); - if let Err(e) = open { - tracing::error!("Open Browser error: {:?}", e); - } - } - } - } - - EventData::Ipc(msg) if msg.method() == "file_diolog" => { - if let Ok(file_diolog) = - serde_json::from_value::(msg.params()) - { - let id = ElementId(file_diolog.target); - let event_name = &file_diolog.event; - let event_bubbles = file_diolog.bubbles; - let files = file_upload::get_file_event(&file_diolog); - let data = Rc::new(FormData { - value: Default::default(), - values: Default::default(), - files: Some(Arc::new(NativeFileEngine::new(files))), - }); - - let view = webviews.get_mut(&event.1).unwrap(); - - if event_name == "change&input" { - view.dom - .handle_event("input", data.clone(), id, event_bubbles); - view.dom.handle_event("change", data, id, event_bubbles); - } else { - view.dom.handle_event(event_name, data, id, event_bubbles); - } - - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); - } - } - - _ => {} - }, - Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), - _ => {} - } - }) -} - -fn create_new_window( - mut cfg: Config, - event_loop: &EventLoopWindowTarget, - proxy: &EventLoopProxy, - dom: VirtualDom, - queue: &WebviewQueue, - event_handlers: &WindowEventHandlers, - shortcut_manager: ShortcutRegistry, -) -> WebviewHandler { - let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); - let desktop_context = Rc::from(DesktopService::new( - webview, - proxy.clone(), - event_loop.clone(), - queue.clone(), - event_handlers.clone(), - shortcut_manager, - )); - - let cx = dom.base_scope(); - cx.provide_context(desktop_context.clone()); - - // Init eval - init_eval(cx); - - WebviewHandler { - // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both - waker: waker::tao_waker(proxy, desktop_context.webview.window().id()), - desktop_context, - dom, - _web_context: web_context, - } -} - -struct WebviewHandler { - dom: VirtualDom, - desktop_context: DesktopContext, - waker: Waker, - - // Wry assumes the webcontext is alive for the lifetime of the webview. - // We need to keep the webcontext alive, otherwise the webview will crash - _web_context: WebContext, -} - -/// Poll the virtualdom until it's pending -/// -/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again -/// -/// All IO is done on the tokio runtime we started earlier -fn poll_vdom(view: &mut WebviewHandler) { - let mut cx = std::task::Context::from_waker(&view.waker); - - loop { - { - let fut = view.dom.wait_for_work(); - pin_mut!(fut); - - match fut.poll_unpin(&mut cx) { - std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, - } - } - - send_edits(view.dom.render_immediate(), &view.desktop_context.webview); - } -} - -/// Send a list of mutations to the webview -fn send_edits(edits: Mutations, webview: &WebView) { - let serialized = serde_json::to_string(&edits).unwrap(); - - // todo: use SSE and binary data to send the edits with lower overhead - _ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})")); -} - -/// Different hide implementations per platform -#[allow(unused)] -fn hide_app_window(webview: &WebView) { - #[cfg(target_os = "windows")] - { - use wry::application::platform::windows::WindowExtWindows; - webview.window().set_visible(false); - webview.window().set_skip_taskbar(true); - } - - #[cfg(target_os = "linux")] - { - use wry::application::platform::unix::WindowExtUnix; - webview.window().set_visible(false); - } - - #[cfg(target_os = "macos")] - { - // webview.window().set_visible(false); has the wrong behaviour on macOS - // It will hide the window but not show it again when the user switches - // back to the app. `NSApplication::hide:` has the correct behaviour - use objc::runtime::Object; - use objc::{msg_send, sel, sel_impl}; - objc::rc::autoreleasepool(|| unsafe { - let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication]; - let nil = std::ptr::null_mut::(); - let _: () = msg_send![app, hide: nil]; - }); - } -} +// Public exports +pub use assets::AssetRequest; +pub use config::{Config, WindowCloseBehaviour}; +pub use desktop_context::{ + window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId, +}; +pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; +pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use wry::RequestAsyncResponder; diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs new file mode 100644 index 000000000..71e424240 --- /dev/null +++ b/packages/desktop/src/menubar.rs @@ -0,0 +1,79 @@ +use tao::window::Window; + +#[allow(unused)] +pub fn build_menu(window: &Window, default_menu_bar: bool) { + if default_menu_bar { + #[cfg(not(any(target_os = "ios", target_os = "android")))] + impl_::build_menu_bar(impl_::build_default_menu_bar(), window) + } +} + +#[cfg(not(any(target_os = "ios", target_os = "android")))] +mod impl_ { + use super::*; + use muda::{Menu, PredefinedMenuItem, Submenu}; + + #[allow(unused)] + pub fn build_menu_bar(menu: Menu, window: &Window) { + #[cfg(target_os = "windows")] + use tao::platform::windows::WindowExtWindows; + + #[cfg(target_os = "windows")] + menu.init_for_hwnd(window.hwnd()); + + // #[cfg(target_os = "linux")] + // { + // use tao::platform::unix::WindowExtUnix; + // menu.init_for_gtk_window(window, None); + // menu.init_for_gtk_window(window, Some(&ertical_gtk_box)); + // } + + #[cfg(target_os = "macos")] + menu.init_for_nsapp(); + } + + /// Builds a standard menu bar depending on the users platform. It may be used as a starting point + /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder). + /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste. + /// > The menu bar differs per platform because of constraints introduced + /// > by [`MenuItem`](tao::menu::MenuItem). + pub fn build_default_menu_bar() -> Menu { + let menu = Menu::new(); + + // since it is uncommon on windows to have an "application menu" + // we add a "window" menu to be more consistent across platforms with the standard menu + let window_menu = Submenu::new("Window", true); + window_menu + .append_items(&[ + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::close_window(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]) + .unwrap(); + + let edit_menu = Submenu::new("Window", true); + edit_menu + .append_items(&[ + &PredefinedMenuItem::undo(None), + &PredefinedMenuItem::redo(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::cut(None), + &PredefinedMenuItem::copy(None), + &PredefinedMenuItem::paste(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::select_all(None), + ]) + .unwrap(); + + menu.append_items(&[&window_menu, &edit_menu]).unwrap(); + + menu + } +} diff --git a/packages/desktop/src/mobile_shortcut.rs b/packages/desktop/src/mobile_shortcut.rs index 29149b55b..03566bf36 100644 --- a/packages/desktop/src/mobile_shortcut.rs +++ b/packages/desktop/src/mobile_shortcut.rs @@ -1,29 +1,51 @@ #![allow(unused)] use super::*; -use wry::application::accelerator::Accelerator; -use wry::application::event_loop::EventLoopWindowTarget; +use std::str::FromStr; +use tao::event_loop::EventLoopWindowTarget; -pub struct GlobalShortcut(); -pub struct ShortcutManager(); +use dioxus_html::input_data::keyboard_types::Modifiers; -impl ShortcutManager { - pub fn new(target: &EventLoopWindowTarget) -> Self { - Self() +#[derive(Clone, Debug)] +pub struct Accelerator; + +#[derive(Clone, Copy)] +pub struct HotKey; + +impl HotKey { + pub fn new(mods: Option, key: Code) -> Self { + Self } - pub fn register( - &mut self, - accelerator: Accelerator, - ) -> Result { - Ok(GlobalShortcut()) + pub fn id(&self) -> u32 { + 0 + } +} + +impl FromStr for HotKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(HotKey) + } +} + +pub struct GlobalHotKeyManager(); + +impl GlobalHotKeyManager { + pub fn new() -> Result { + Ok(Self()) } - pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> { + pub fn register(&self, accelerator: HotKey) -> Result { + Ok(HotKey) + } + + pub fn unregister(&self, id: HotKey) -> Result<(), HotkeyError> { Ok(()) } - pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> { + pub fn unregister_all(&self, _: &[HotKey]) -> Result<(), HotkeyError> { Ok(()) } } @@ -33,23 +55,35 @@ use std::{error, fmt}; /// An error whose cause the `ShortcutManager` to fail. #[non_exhaustive] #[derive(Debug)] -pub enum ShortcutManagerError { +pub enum HotkeyError { AcceleratorAlreadyRegistered(Accelerator), AcceleratorNotRegistered(Accelerator), - InvalidAccelerator(String), + HotKeyParseError(String), } -impl error::Error for ShortcutManagerError {} -impl fmt::Display for ShortcutManagerError { +impl error::Error for HotkeyError {} +impl fmt::Display for HotkeyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { - ShortcutManagerError::AcceleratorAlreadyRegistered(e) => { + HotkeyError::AcceleratorAlreadyRegistered(e) => { f.pad(&format!("hotkey already registered: {:?}", e)) } - ShortcutManagerError::AcceleratorNotRegistered(e) => { + HotkeyError::AcceleratorNotRegistered(e) => { f.pad(&format!("hotkey not registered: {:?}", e)) } - ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f), + HotkeyError::HotKeyParseError(e) => e.fmt(f), } } } + +pub struct GlobalHotKeyEvent { + pub id: u32, +} + +impl GlobalHotKeyEvent { + pub fn receiver() -> crossbeam_channel::Receiver { + crossbeam_channel::unbounded().1 + } +} + +pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 53652f82f..06304cdd2 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,151 +1,204 @@ -use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS}; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, -}; +use crate::{assets::*, edits::EditQueue}; +use std::path::{Path, PathBuf}; use wry::{ - http::{status::StatusCode, Request, Response}, - Result, + http::{status::StatusCode, Request, Response, Uri}, + RequestAsyncResponder, Result, }; -fn module_loader(root_name: &str) -> String { - let js = INTERPRETER_JS.replace( - "/*POST_HANDLE_EDITS*/", - r#"// Prevent file inputs from opening the file dialog on click - let inputs = document.querySelectorAll("input"); - for (let input of inputs) { - if (!input.getAttribute("data-dioxus-file-listener")) { - // prevent file inputs from opening the file dialog on click - const type = input.getAttribute("type"); - if (type === "file") { - input.setAttribute("data-dioxus-file-listener", true); - input.addEventListener("click", (event) => { - let target = event.target; - let target_id = find_real_id(target); - if (target_id !== null) { - const send = (event_name) => { - const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); - window.ipc.postMessage(message); - }; - send("change&input"); - } - event.preventDefault(); - }); - } - } - }"#, +static MINIFIED: &str = include_str!("./minified.js"); +static DEFAULT_INDEX: &str = include_str!("./index.html"); + +/// Build the index.html file we use for bootstrapping a new app +/// +/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code +/// +/// This is similar to tauri, except we give more power to your rust code and less power to your frontend code. +/// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually +/// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can +/// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you +/// *do* have native control over elements, but that still won't work with liveview. +pub(super) fn index_request( + request: &Request>, + custom_head: Option, + custom_index: Option, + root_name: &str, + headless: bool, +) -> Option>> { + // If the request is for the root, we'll serve the index.html file. + if request.uri().path() != "/" { + return None; + } + + // Load a custom index file if provided + let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string()); + + // Insert a custom head if provided + // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail + if let Some(head) = custom_head { + index.insert_str(index.find("").expect("Head element to exist"), &head); + } + + // Inject our module loader by looking for a body tag + // A failure mode here, obviously, is if the user provided a custom index without a body tag + // Might want to document this + index.insert_str( + index.find("").expect("Body element to exist"), + &module_loader(root_name, headless), ); + + Response::builder() + .header("Content-Type", "text/html") + .header("Access-Control-Allow-Origin", "*") + .body(index.into()) + .ok() +} + +/// Handle a request from the webview +/// +/// - Tries to stream edits if they're requested. +/// - If that doesn't match, tries a user provided asset handler +/// - If that doesn't match, tries to serve a file from the filesystem +pub(super) fn desktop_handler( + mut request: Request>, + asset_handlers: AssetHandlerRegistry, + edit_queue: &EditQueue, + responder: RequestAsyncResponder, +) { + // If the request is asking for edits (ie binary protocol streaming, do that) + if request.uri().path().trim_matches('/') == "edits" { + return edit_queue.handle_request(responder); + } + + // If the user provided a custom asset handler, then call it and return the response if the request was handled. + // The path is the first part of the URI, so we need to trim the leading slash. + let path = PathBuf::from( + urlencoding::decode(request.uri().path().trim_start_matches('/')) + .expect("expected URL to be UTF-8 encoded") + .as_ref(), + ); + + let Some(name) = path.parent() else { + return tracing::error!("Asset request has no root {path:?}"); + }; + + if let Some(name) = name.to_str() { + if asset_handlers.has_handler(name) { + // Trim the leading path from the URI + // + // I hope this is reliable! + // + // so a request for /assets/logos/logo.png?query=123 will become /logos/logo.png?query=123 + strip_uri_prefix(&mut request, name); + + return asset_handlers.handle_request(name, request, responder); + } + } + + // Else, try to serve a file from the filesystem. + match serve_from_fs(path) { + Ok(res) => responder.respond(res), + Err(e) => tracing::error!("Error serving request from filesystem {}", e), + } +} + +fn serve_from_fs(path: PathBuf) -> Result>> { + // If the path is relative, we'll try to serve it from the assets directory. + let mut asset = get_asset_root_or_default().join(&path); + + // If we can't find it, make it absolute and try again + if !asset.exists() { + asset = PathBuf::from("/").join(path); + } + + if !asset.exists() { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes())?); + } + + Ok(Response::builder() + .header("Content-Type", get_mime_from_path(&asset)?) + .body(std::fs::read(asset)?)?) +} + +fn strip_uri_prefix(request: &mut Request>, name: &str) { + // trim the leading path + if let Some(path) = request.uri().path_and_query() { + let new_path = path + .path() + .trim_start_matches('/') + .strip_prefix(name) + .expect("expected path to have prefix"); + + let new_uri = Uri::builder() + .scheme(request.uri().scheme_str().unwrap_or("http")) + .path_and_query(format!("{}{}", new_path, path.query().unwrap_or(""))) + .authority("index.html") + .build() + .expect("failed to build new URI"); + + *request.uri_mut() = new_uri; + } +} + +/// Construct the inline script that boots up the page and bridges the webview with rust code. +/// +/// The arguments here: +/// - root_name: the root element (by Id) that we stream edits into +/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the +/// interpreter can't connect until the window is ready. +fn module_loader(root_id: &str, headless: bool) -> String { format!( r#" "# ) } -pub(super) fn desktop_handler( - request: &Request>, - custom_head: Option, - custom_index: Option, - root_name: &str, -) -> Result>> { - // If the request is for the root, we'll serve the index.html file. - if request.uri().path() == "/" { - // If a custom index is provided, just defer to that, expecting the user to know what they're doing. - // we'll look for the closing tag and insert our little module loader there. - let body = match custom_index { - Some(custom_index) => custom_index - .replace("", &format!("{}", module_loader(root_name))) - .into_bytes(), - - None => { - // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. - let mut template = include_str!("./index.html").to_string(); - - if let Some(custom_head) = custom_head { - template = template.replace("", &custom_head); - } - - template - .replace("", &module_loader(root_name)) - .into_bytes() - } - }; - - return Response::builder() - .header("Content-Type", "text/html") - .body(Cow::from(body)) - .map_err(From::from); - } else if request.uri().path() == "/common.js" { - return Response::builder() - .header("Content-Type", "text/javascript") - .body(Cow::from(COMMON_JS.as_bytes())) - .map_err(From::from); - } - - // Else, try to serve a file from the filesystem. - let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) - .expect("expected URL to be UTF-8 encoded"); - let path = PathBuf::from(&*decoded); - - // If the path is relative, we'll try to serve it from the assets directory. - let mut asset = get_asset_root() - .unwrap_or_else(|| Path::new(".").to_path_buf()) - .join(&path); - - if !asset.exists() { - asset = PathBuf::from("/").join(path); - } - - if asset.exists() { - return Response::builder() - .header("Content-Type", get_mime_from_path(&asset)?) - .body(Cow::from(std::fs::read(asset)?)) - .map_err(From::from); - } - - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Cow::from(String::from("Not Found").into_bytes())) - .map_err(From::from) +/// Get the assset directory, following tauri/cargo-bundles directory discovery approach +/// +/// Defaults to the current directory if no asset directory is found, which is useful for development when the app +/// isn't bundled. +fn get_asset_root_or_default() -> PathBuf { + get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf()) } +/// Get the asset directory, following tauri/cargo-bundles directory discovery approach +/// +/// Currently supports: +/// - [x] macOS +/// - [ ] Windows +/// - [ ] Linux (rpm) +/// - [ ] Linux (deb) +/// - [ ] iOS +/// - [ ] Android #[allow(unreachable_code)] fn get_asset_root() -> Option { - /* - We're matching exactly how cargo-bundle works. - - - [x] macOS - - [ ] Windows - - [ ] Linux (rpm) - - [ ] Linux (deb) - - [ ] iOS - - [ ] Android - - */ - + // If running under cargo, there's no bundle! + // There might be a smarter/more resilient way of doing this if std::env::var_os("CARGO").is_some() { return None; } - // TODO: support for other platforms #[cfg(target_os = "macos")] { let bundle = core_foundation::bundle::CFBundle::main_bundle(); let bundle_path = bundle.path()?; let resources_path = bundle.resources_path()?; let absolute_resources_root = bundle_path.join(resources_path); - let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; - - return Some(canonical_resources_root); + return dunce::canonicalize(absolute_resources_root).ok(); } None @@ -153,22 +206,14 @@ fn get_asset_root() -> Option { /// Get the mime type from a path-like string fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> { - if trimmed.ends_with(".svg") { + if trimmed.extension().is_some_and(|ext| ext == "svg") { return Ok("image/svg+xml"); } - let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { - Some(f) => { - if f == "text/plain" { - get_mime_by_ext(trimmed) - } else { - f - } - } - None => get_mime_by_ext(trimmed), - }; - - Ok(res) + match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { + Some(f) if f != "text/plain" => Ok(f), + _ => Ok(get_mime_by_ext(trimmed)), + } } /// Get the mime type from a URI using its extension diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index ffc64a232..5173483ed 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -1,15 +1,10 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; +use std::{cell::RefCell, collections::HashMap, str::FromStr}; -use dioxus_core::ScopeState; use dioxus_html::input_data::keyboard_types::Modifiers; use slab::Slab; -use wry::application::{ - accelerator::{Accelerator, AcceleratorId}, - event_loop::EventLoopWindowTarget, - keyboard::{KeyCode, ModifiersState}, -}; +use tao::keyboard::ModifiersState; -use crate::{desktop_context::DesktopContext, use_window}; +use crate::desktop_context::DesktopContext; #[cfg(any( target_os = "windows", @@ -20,22 +15,22 @@ use crate::{desktop_context::DesktopContext, use_window}; target_os = "netbsd", target_os = "openbsd" ))] -use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError}; +pub use global_hotkey::{ + hotkey::{Code, HotKey}, + Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager, +}; #[cfg(any(target_os = "ios", target_os = "android"))] pub use crate::mobile_shortcut::*; -#[derive(Clone)] pub(crate) struct ShortcutRegistry { - manager: Rc>, - shortcuts: ShortcutMap, + manager: GlobalHotKeyManager, + shortcuts: RefCell>, } -type ShortcutMap = Rc>>; - struct Shortcut { #[allow(unused)] - shortcut: GlobalShortcut, + shortcut: HotKey, callbacks: Slab>, } @@ -54,15 +49,15 @@ impl Shortcut { } impl ShortcutRegistry { - pub fn new(target: &EventLoopWindowTarget) -> Self { + pub fn new() -> Self { Self { - manager: Rc::new(RefCell::new(ShortcutManager::new(target))), - shortcuts: Rc::new(RefCell::new(HashMap::new())), + manager: GlobalHotKeyManager::new().unwrap(), + shortcuts: RefCell::new(HashMap::new()), } } - pub(crate) fn call_handlers(&self, id: AcceleratorId) { - if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) { + pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) { + if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) { for (_, callback) in callbacks.iter_mut() { (callback)(); } @@ -71,40 +66,40 @@ impl ShortcutRegistry { pub(crate) fn add_shortcut( &self, - accelerator: Accelerator, + hotkey: HotKey, callback: Box, ) -> Result { - let accelerator_id = accelerator.clone().id(); + let accelerator_id = hotkey.clone().id(); + let mut shortcuts = self.shortcuts.borrow_mut(); - Ok( - if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { - let id = callbacks.insert(callback); - ShortcutId { - id: accelerator_id, - number: id, - } - } else { - match self.manager.borrow_mut().register(accelerator) { - Ok(global_shortcut) => { - let mut slab = Slab::new(); - let id = slab.insert(callback); - let shortcut = Shortcut { - shortcut: global_shortcut, - callbacks: slab, - }; - shortcuts.insert(accelerator_id, shortcut); - ShortcutId { - id: accelerator_id, - number: id, - } - } - Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => { - return Err(ShortcutRegistryError::InvalidShortcut(shortcut)) - } - Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))), - } - }, - ) + + if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { + return Ok(ShortcutId { + id: accelerator_id, + number: callbacks.insert(callback), + }); + }; + + self.manager.register(hotkey).map_err(|e| match e { + HotkeyError::HotKeyParseError(shortcut) => { + ShortcutRegistryError::InvalidShortcut(shortcut) + } + err => ShortcutRegistryError::Other(Box::new(err)), + })?; + + let mut shortcut = Shortcut { + shortcut: hotkey, + callbacks: Slab::new(), + }; + + let id = shortcut.callbacks.insert(callback); + + shortcuts.insert(accelerator_id, shortcut); + + Ok(ShortcutId { + id: accelerator_id, + number: id, + }) } pub(crate) fn remove_shortcut(&self, id: ShortcutId) { @@ -113,16 +108,7 @@ impl ShortcutRegistry { callbacks.remove(id.number); if callbacks.is_empty() { if let Some(_shortcut) = shortcuts.remove(&id.id) { - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut); + let _ = self.manager.unregister(_shortcut.shortcut); } } } @@ -130,8 +116,8 @@ impl ShortcutRegistry { pub(crate) fn remove_all(&self) { let mut shortcuts = self.shortcuts.borrow_mut(); - shortcuts.clear(); - let _ = self.manager.borrow_mut().unregister_all(); + let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect(); + let _ = self.manager.unregister_all(&hotkeys); } } @@ -148,64 +134,45 @@ pub enum ShortcutRegistryError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// An global id for a shortcut. pub struct ShortcutId { - id: AcceleratorId, + id: u32, number: usize, } /// A global shortcut. This will be automatically removed when it is dropped. pub struct ShortcutHandle { - desktop: DesktopContext, + pub(crate) desktop: DesktopContext, /// The id of the shortcut pub shortcut_id: ShortcutId, } pub trait IntoAccelerator { - fn accelerator(&self) -> Accelerator; + fn accelerator(&self) -> HotKey; } impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) { - fn accelerator(&self) -> Accelerator { - Accelerator::new(Some(self.1), self.0.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code()) } } impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) { - fn accelerator(&self) -> Accelerator { - Accelerator::new(Some(self.0), self.1.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code()) } } impl IntoAccelerator for dioxus_html::KeyCode { - fn accelerator(&self) -> Accelerator { - Accelerator::new(None, self.into_key_code()) + fn accelerator(&self) -> HotKey { + HotKey::new(None, self.into_key_code()) } } impl IntoAccelerator for &str { - fn accelerator(&self) -> Accelerator { - Accelerator::from_str(self).unwrap() + fn accelerator(&self) -> HotKey { + HotKey::from_str(self).unwrap() } } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_global_shortcut( - cx: &ScopeState, - accelerator: impl IntoAccelerator, - handler: impl FnMut() + 'static, -) -> &Result { - let desktop = use_window(cx); - cx.use_hook(move || { - let desktop = desktop.clone(); - - let id = desktop.create_shortcut(accelerator.accelerator(), handler); - - Ok(ShortcutHandle { - desktop, - shortcut_id: id?, - }) - }) -} - impl ShortcutHandle { /// Remove the shortcut. pub fn remove(&self) { @@ -220,143 +187,144 @@ impl Drop for ShortcutHandle { } pub trait IntoModifersState { - fn into_modifiers_state(self) -> ModifiersState; + fn into_modifiers_state(self) -> Modifiers; } impl IntoModifersState for ModifiersState { - fn into_modifiers_state(self) -> ModifiersState { - self + fn into_modifiers_state(self) -> Modifiers { + let mut modifiers = Modifiers::default(); + if self.shift_key() { + modifiers |= Modifiers::SHIFT; + } + if self.control_key() { + modifiers |= Modifiers::CONTROL; + } + if self.alt_key() { + modifiers |= Modifiers::ALT; + } + if self.super_key() { + modifiers |= Modifiers::META; + } + + modifiers } } impl IntoModifersState for Modifiers { - fn into_modifiers_state(self) -> ModifiersState { - let mut state = ModifiersState::empty(); - if self.contains(Modifiers::SHIFT) { - state |= ModifiersState::SHIFT - } - if self.contains(Modifiers::CONTROL) { - state |= ModifiersState::CONTROL - } - if self.contains(Modifiers::ALT) { - state |= ModifiersState::ALT - } - if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) { - state |= ModifiersState::SUPER - } - state + fn into_modifiers_state(self) -> Modifiers { + self } } pub trait IntoKeyCode { - fn into_key_code(self) -> KeyCode; + fn into_key_code(self) -> Code; } -impl IntoKeyCode for KeyCode { - fn into_key_code(self) -> KeyCode { +impl IntoKeyCode for Code { + fn into_key_code(self) -> Code { self } } impl IntoKeyCode for dioxus_html::KeyCode { - fn into_key_code(self) -> KeyCode { + fn into_key_code(self) -> Code { match self { - dioxus_html::KeyCode::Backspace => KeyCode::Backspace, - dioxus_html::KeyCode::Tab => KeyCode::Tab, - dioxus_html::KeyCode::Clear => KeyCode::NumpadClear, - dioxus_html::KeyCode::Enter => KeyCode::Enter, - dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft, - dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft, - dioxus_html::KeyCode::Alt => KeyCode::AltLeft, - dioxus_html::KeyCode::Pause => KeyCode::Pause, - dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock, - dioxus_html::KeyCode::Escape => KeyCode::Escape, - dioxus_html::KeyCode::Space => KeyCode::Space, - dioxus_html::KeyCode::PageUp => KeyCode::PageUp, - dioxus_html::KeyCode::PageDown => KeyCode::PageDown, - dioxus_html::KeyCode::End => KeyCode::End, - dioxus_html::KeyCode::Home => KeyCode::Home, - dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft, - dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp, - dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight, - dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown, - dioxus_html::KeyCode::Insert => KeyCode::Insert, - dioxus_html::KeyCode::Delete => KeyCode::Delete, - dioxus_html::KeyCode::Num0 => KeyCode::Numpad0, - dioxus_html::KeyCode::Num1 => KeyCode::Numpad1, - dioxus_html::KeyCode::Num2 => KeyCode::Numpad2, - dioxus_html::KeyCode::Num3 => KeyCode::Numpad3, - dioxus_html::KeyCode::Num4 => KeyCode::Numpad4, - dioxus_html::KeyCode::Num5 => KeyCode::Numpad5, - dioxus_html::KeyCode::Num6 => KeyCode::Numpad6, - dioxus_html::KeyCode::Num7 => KeyCode::Numpad7, - dioxus_html::KeyCode::Num8 => KeyCode::Numpad8, - dioxus_html::KeyCode::Num9 => KeyCode::Numpad9, - dioxus_html::KeyCode::A => KeyCode::KeyA, - dioxus_html::KeyCode::B => KeyCode::KeyB, - dioxus_html::KeyCode::C => KeyCode::KeyC, - dioxus_html::KeyCode::D => KeyCode::KeyD, - dioxus_html::KeyCode::E => KeyCode::KeyE, - dioxus_html::KeyCode::F => KeyCode::KeyF, - dioxus_html::KeyCode::G => KeyCode::KeyG, - dioxus_html::KeyCode::H => KeyCode::KeyH, - dioxus_html::KeyCode::I => KeyCode::KeyI, - dioxus_html::KeyCode::J => KeyCode::KeyJ, - dioxus_html::KeyCode::K => KeyCode::KeyK, - dioxus_html::KeyCode::L => KeyCode::KeyL, - dioxus_html::KeyCode::M => KeyCode::KeyM, - dioxus_html::KeyCode::N => KeyCode::KeyN, - dioxus_html::KeyCode::O => KeyCode::KeyO, - dioxus_html::KeyCode::P => KeyCode::KeyP, - dioxus_html::KeyCode::Q => KeyCode::KeyQ, - dioxus_html::KeyCode::R => KeyCode::KeyR, - dioxus_html::KeyCode::S => KeyCode::KeyS, - dioxus_html::KeyCode::T => KeyCode::KeyT, - dioxus_html::KeyCode::U => KeyCode::KeyU, - dioxus_html::KeyCode::V => KeyCode::KeyV, - dioxus_html::KeyCode::W => KeyCode::KeyW, - dioxus_html::KeyCode::X => KeyCode::KeyX, - dioxus_html::KeyCode::Y => KeyCode::KeyY, - dioxus_html::KeyCode::Z => KeyCode::KeyZ, - dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0, - dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1, - dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2, - dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3, - dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4, - dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5, - dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6, - dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7, - dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8, - dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9, - dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply, - dioxus_html::KeyCode::Add => KeyCode::NumpadAdd, - dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract, - dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal, - dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide, - dioxus_html::KeyCode::F1 => KeyCode::F1, - dioxus_html::KeyCode::F2 => KeyCode::F2, - dioxus_html::KeyCode::F3 => KeyCode::F3, - dioxus_html::KeyCode::F4 => KeyCode::F4, - dioxus_html::KeyCode::F5 => KeyCode::F5, - dioxus_html::KeyCode::F6 => KeyCode::F6, - dioxus_html::KeyCode::F7 => KeyCode::F7, - dioxus_html::KeyCode::F8 => KeyCode::F8, - dioxus_html::KeyCode::F9 => KeyCode::F9, - dioxus_html::KeyCode::F10 => KeyCode::F10, - dioxus_html::KeyCode::F11 => KeyCode::F11, - dioxus_html::KeyCode::F12 => KeyCode::F12, - dioxus_html::KeyCode::NumLock => KeyCode::NumLock, - dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock, - dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon, - dioxus_html::KeyCode::EqualSign => KeyCode::Equal, - dioxus_html::KeyCode::Comma => KeyCode::Comma, - dioxus_html::KeyCode::Period => KeyCode::Period, - dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash, - dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote, - dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft, - dioxus_html::KeyCode::BackSlash => KeyCode::Backslash, - dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight, - dioxus_html::KeyCode::SingleQuote => KeyCode::Quote, + dioxus_html::KeyCode::Backspace => Code::Backspace, + dioxus_html::KeyCode::Tab => Code::Tab, + dioxus_html::KeyCode::Clear => Code::NumpadClear, + dioxus_html::KeyCode::Enter => Code::Enter, + dioxus_html::KeyCode::Shift => Code::ShiftLeft, + dioxus_html::KeyCode::Ctrl => Code::ControlLeft, + dioxus_html::KeyCode::Alt => Code::AltLeft, + dioxus_html::KeyCode::Pause => Code::Pause, + dioxus_html::KeyCode::CapsLock => Code::CapsLock, + dioxus_html::KeyCode::Escape => Code::Escape, + dioxus_html::KeyCode::Space => Code::Space, + dioxus_html::KeyCode::PageUp => Code::PageUp, + dioxus_html::KeyCode::PageDown => Code::PageDown, + dioxus_html::KeyCode::End => Code::End, + dioxus_html::KeyCode::Home => Code::Home, + dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft, + dioxus_html::KeyCode::UpArrow => Code::ArrowUp, + dioxus_html::KeyCode::RightArrow => Code::ArrowRight, + dioxus_html::KeyCode::DownArrow => Code::ArrowDown, + dioxus_html::KeyCode::Insert => Code::Insert, + dioxus_html::KeyCode::Delete => Code::Delete, + dioxus_html::KeyCode::Num0 => Code::Numpad0, + dioxus_html::KeyCode::Num1 => Code::Numpad1, + dioxus_html::KeyCode::Num2 => Code::Numpad2, + dioxus_html::KeyCode::Num3 => Code::Numpad3, + dioxus_html::KeyCode::Num4 => Code::Numpad4, + dioxus_html::KeyCode::Num5 => Code::Numpad5, + dioxus_html::KeyCode::Num6 => Code::Numpad6, + dioxus_html::KeyCode::Num7 => Code::Numpad7, + dioxus_html::KeyCode::Num8 => Code::Numpad8, + dioxus_html::KeyCode::Num9 => Code::Numpad9, + dioxus_html::KeyCode::A => Code::KeyA, + dioxus_html::KeyCode::B => Code::KeyB, + dioxus_html::KeyCode::C => Code::KeyC, + dioxus_html::KeyCode::D => Code::KeyD, + dioxus_html::KeyCode::E => Code::KeyE, + dioxus_html::KeyCode::F => Code::KeyF, + dioxus_html::KeyCode::G => Code::KeyG, + dioxus_html::KeyCode::H => Code::KeyH, + dioxus_html::KeyCode::I => Code::KeyI, + dioxus_html::KeyCode::J => Code::KeyJ, + dioxus_html::KeyCode::K => Code::KeyK, + dioxus_html::KeyCode::L => Code::KeyL, + dioxus_html::KeyCode::M => Code::KeyM, + dioxus_html::KeyCode::N => Code::KeyN, + dioxus_html::KeyCode::O => Code::KeyO, + dioxus_html::KeyCode::P => Code::KeyP, + dioxus_html::KeyCode::Q => Code::KeyQ, + dioxus_html::KeyCode::R => Code::KeyR, + dioxus_html::KeyCode::S => Code::KeyS, + dioxus_html::KeyCode::T => Code::KeyT, + dioxus_html::KeyCode::U => Code::KeyU, + dioxus_html::KeyCode::V => Code::KeyV, + dioxus_html::KeyCode::W => Code::KeyW, + dioxus_html::KeyCode::X => Code::KeyX, + dioxus_html::KeyCode::Y => Code::KeyY, + dioxus_html::KeyCode::Z => Code::KeyZ, + dioxus_html::KeyCode::Numpad0 => Code::Numpad0, + dioxus_html::KeyCode::Numpad1 => Code::Numpad1, + dioxus_html::KeyCode::Numpad2 => Code::Numpad2, + dioxus_html::KeyCode::Numpad3 => Code::Numpad3, + dioxus_html::KeyCode::Numpad4 => Code::Numpad4, + dioxus_html::KeyCode::Numpad5 => Code::Numpad5, + dioxus_html::KeyCode::Numpad6 => Code::Numpad6, + dioxus_html::KeyCode::Numpad7 => Code::Numpad7, + dioxus_html::KeyCode::Numpad8 => Code::Numpad8, + dioxus_html::KeyCode::Numpad9 => Code::Numpad9, + dioxus_html::KeyCode::Multiply => Code::NumpadMultiply, + dioxus_html::KeyCode::Add => Code::NumpadAdd, + dioxus_html::KeyCode::Subtract => Code::NumpadSubtract, + dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal, + dioxus_html::KeyCode::Divide => Code::NumpadDivide, + dioxus_html::KeyCode::F1 => Code::F1, + dioxus_html::KeyCode::F2 => Code::F2, + dioxus_html::KeyCode::F3 => Code::F3, + dioxus_html::KeyCode::F4 => Code::F4, + dioxus_html::KeyCode::F5 => Code::F5, + dioxus_html::KeyCode::F6 => Code::F6, + dioxus_html::KeyCode::F7 => Code::F7, + dioxus_html::KeyCode::F8 => Code::F8, + dioxus_html::KeyCode::F9 => Code::F9, + dioxus_html::KeyCode::F10 => Code::F10, + dioxus_html::KeyCode::F11 => Code::F11, + dioxus_html::KeyCode::F12 => Code::F12, + dioxus_html::KeyCode::NumLock => Code::NumLock, + dioxus_html::KeyCode::ScrollLock => Code::ScrollLock, + dioxus_html::KeyCode::Semicolon => Code::Semicolon, + dioxus_html::KeyCode::EqualSign => Code::Equal, + dioxus_html::KeyCode::Comma => Code::Comma, + dioxus_html::KeyCode::Period => Code::Period, + dioxus_html::KeyCode::ForwardSlash => Code::Slash, + dioxus_html::KeyCode::GraveAccent => Code::Backquote, + dioxus_html::KeyCode::OpenBracket => Code::BracketLeft, + dioxus_html::KeyCode::BackSlash => Code::Backslash, + dioxus_html::KeyCode::CloseBraket => Code::BracketRight, + dioxus_html::KeyCode::SingleQuote => Code::Quote, key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key), } } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 1e34baffc..5c3e915e3 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,14 +1,15 @@ -use crate::desktop_context::{EventData, UserWindowEvent}; +use crate::ipc::{EventData, UserWindowEvent}; use futures_util::task::ArcWake; use std::sync::Arc; -use wry::application::{event_loop::EventLoopProxy, window::WindowId}; +use tao::{event_loop::EventLoopProxy, window::WindowId}; /// Create a waker that will send a poll event to the event loop. /// /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. /// -/// All other IO lives in the Tokio runtime, -pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std::task::Waker { +/// All IO and multithreading lives on other threads. Thanks to tokio's work stealing approach, the main thread can never +/// claim a task while it's blocked by the event loop. +pub fn tao_waker(proxy: EventLoopProxy, id: WindowId) -> std::task::Waker { struct DomHandle { proxy: EventLoopProxy, id: WindowId, @@ -27,8 +28,5 @@ pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std:: } } - futures_util::task::waker(Arc::new(DomHandle { - id, - proxy: proxy.clone(), - })) + futures_util::task::waker(Arc::new(DomHandle { id, proxy })) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index aec59a9a7..9684142af 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,96 +1,186 @@ -use crate::desktop_context::EventData; -use crate::protocol; -use crate::{desktop_context::UserWindowEvent, Config}; -use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; -pub use wry; -pub use wry::application as tao; -use wry::application::window::Window; -use wry::webview::{WebContext, WebView, WebViewBuilder}; +use crate::{ + app::SharedContext, + assets::AssetHandlerRegistry, + edits::EditQueue, + eval::DesktopEvalProvider, + ipc::{EventData, UserWindowEvent}, + protocol::{self}, + waker::tao_waker, + Config, DesktopContext, DesktopService, +}; +use dioxus_core::VirtualDom; +use dioxus_html::prelude::EvalProvider; +use futures_util::{pin_mut, FutureExt}; +use std::{rc::Rc, task::Waker}; +use wry::{RequestAsyncResponder, WebContext, WebViewBuilder}; -pub fn build( - cfg: &mut Config, - event_loop: &EventLoopWindowTarget, - proxy: EventLoopProxy, -) -> (WebView, WebContext) { - let builder = cfg.window.clone(); - let window = builder.build(event_loop).unwrap(); - let file_handler = cfg.file_drop_handler.take(); - let custom_head = cfg.custom_head.clone(); - let index_file = cfg.custom_index.clone(); - let root_name = cfg.root_name.clone(); +pub struct WebviewInstance { + pub dom: VirtualDom, + pub desktop_context: DesktopContext, + pub waker: Waker, - // We assume that if the icon is None in cfg, then the user just didnt set it - if cfg.window.window.window_icon.is_none() { - window.set_window_icon(Some( - tao::window::Icon::from_rgba( - include_bytes!("./assets/default_icon.bin").to_vec(), - 460, - 460, - ) - .expect("image parse failed"), - )); - } + // Wry assumes the webcontext is alive for the lifetime of the webview. + // We need to keep the webcontext alive, otherwise the webview will crash + _web_context: WebContext, +} - let mut web_context = WebContext::new(cfg.data_dir.clone()); +impl WebviewInstance { + pub fn new(mut cfg: Config, dom: VirtualDom, shared: Rc) -> WebviewInstance { + let window = cfg.window.clone().build(&shared.target).unwrap(); - let mut webview = WebViewBuilder::new(window) - .unwrap() - .with_transparent(cfg.window.window.transparent) - .with_url("dioxus://index.html/") - .unwrap() - .with_ipc_handler(move |window: &Window, payload: String| { + // TODO: allow users to specify their own menubars, again :/ + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::menubar::build_menu(&window, cfg.enable_default_menu_bar); + + // We assume that if the icon is None in cfg, then the user just didnt set it + if cfg.window.window.window_icon.is_none() { + window.set_window_icon(Some( + tao::window::Icon::from_rgba( + include_bytes!("./assets/default_icon.bin").to_vec(), + 460, + 460, + ) + .expect("image parse failed"), + )); + } + + let mut web_context = WebContext::new(cfg.data_dir.clone()); + let edit_queue = EditQueue::default(); + let asset_handlers = AssetHandlerRegistry::new(dom.runtime()); + let headless = !cfg.window.window.visible; + + // Rust :( + let window_id = window.id(); + let file_handler = cfg.file_drop_handler.take(); + let custom_head = cfg.custom_head.clone(); + let index_file = cfg.custom_index.clone(); + let root_name = cfg.root_name.clone(); + let asset_handlers_ = asset_handlers.clone(); + let edit_queue_ = edit_queue.clone(); + let proxy_ = shared.proxy.clone(); + + let request_handler = move |request, responder: RequestAsyncResponder| { + // Try to serve the index file first + let index_bytes = protocol::index_request( + &request, + custom_head.clone(), + index_file.clone(), + &root_name, + headless, + ); + + // Otherwise, try to serve an asset, either from the user or the filesystem + match index_bytes { + Some(body) => responder.respond(body), + None => protocol::desktop_handler( + request, + asset_handlers_.clone(), + &edit_queue_, + responder, + ), + } + }; + + let ipc_handler = move |payload: String| { // defer the event to the main thread if let Ok(message) = serde_json::from_str(&payload) { - _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id())); + _ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id)); } - }) - .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name) - }) - .with_web_context(&mut web_context); + }; - if let Some(handler) = file_handler { - webview = webview.with_file_drop_handler(handler) + let mut webview = WebViewBuilder::new(&window) + .with_transparent(cfg.window.window.transparent) + .with_url("dioxus://index.html/") + .unwrap() + .with_ipc_handler(ipc_handler) + .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler) + .with_file_drop_handler(file_drop_handler) + .with_web_context(&mut web_context); + + if let Some(handler) = file_handler { + webview = webview.with_file_drop_handler(handler) + } + + // This was removed from wry, I'm not sure what replaced it + // #[cfg(windows)] + // { + // // Windows has a platform specific settings to disable the browser shortcut keys + // use wry::WebViewBuilderExtWindows; + // webview = webview.with_browser_accelerator_keys(false); + // } + + if let Some(color) = cfg.background_color { + webview = webview.with_background_color(color); + } + + for (name, handler) in cfg.protocols.drain(..) { + webview = webview.with_custom_protocol(name, handler); + } + + const INITIALIZATION_SCRIPT: &str = r#" + if (document.addEventListener) { + document.addEventListener('contextmenu', function(e) { + e.preventDefault(); + }, false); + } else { + document.attachEvent('oncontextmenu', function() { + window.event.returnValue = false; + }); + } + "#; + + if cfg.disable_context_menu { + // in release mode, we don't want to show the dev tool or reload menus + webview = webview.with_initialization_script(INITIALIZATION_SCRIPT) + } else { + // in debug, we are okay with the reload menu showing and dev tool + webview = webview.with_devtools(true); + } + + let desktop_context = Rc::from(DesktopService::new( + webview.build().unwrap(), + window, + shared.clone(), + edit_queue, + asset_handlers, + )); + + // Provide the desktop context to the virtualdom + dom.base_scope().provide_context(desktop_context.clone()); + + // Also set up its eval provider + // It's important that we provide as dyn EvalProvider - using the concrete type has + // a different TypeId. + let provider: Rc = + Rc::new(DesktopEvalProvider::new(desktop_context.clone())); + dom.base_scope().provide_context(provider); + + WebviewInstance { + waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()), + desktop_context, + dom, + _web_context: web_context, + } } - #[cfg(windows)] - { - // Windows has a platform specific settings to disable the browser shortcut keys - use wry::webview::WebViewBuilderExtWindows; - webview = webview.with_browser_accelerator_keys(false); + pub fn poll_vdom(&mut self) { + let mut cx = std::task::Context::from_waker(&self.waker); + + // Continously poll the virtualdom until it's pending + // Wait for work will return Ready when it has edits to be sent to the webview + // It will return Pending when it needs to be polled again - nothing is ready + loop { + { + let fut = self.dom.wait_for_work(); + pin_mut!(fut); + + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => return, + } + } + + self.desktop_context.send_edits(self.dom.render_immediate()); + } } - - if let Some(color) = cfg.background_color { - webview = webview.with_background_color(color); - } - - // These are commented out because wry is currently broken in wry - // let mut web_context = WebContext::new(cfg.data_dir.clone()); - // .with_web_context(&mut web_context); - - for (name, handler) in cfg.protocols.drain(..) { - webview = webview.with_custom_protocol(name, handler) - } - - if cfg.disable_context_menu { - // 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) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, - ) - } else { - // in debug, we are okay with the reload menu showing and dev tool - webview = webview.with_devtools(true); - } - - (webview.build().unwrap(), web_context) } diff --git a/packages/dioxus-tui/Cargo.toml b/packages/dioxus-tui/Cargo.toml index 0d0bb59f4..47ff3f536 100644 --- a/packages/dioxus-tui/Cargo.toml +++ b/packages/dioxus-tui/Cargo.toml @@ -10,7 +10,6 @@ keywords = ["dom", "ui", "gui", "react", "terminal"] license = "MIT OR Apache-2.0" [dependencies] -dioxus = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true } dioxus-native-core = { workspace = true, features = ["dioxus"] } diff --git a/packages/dioxus-tui/examples/all_terminal_events.rs b/packages/dioxus-tui/examples/all_terminal_events.rs index 483e9afdf..004578807 100644 --- a/packages/dioxus-tui/examples/all_terminal_events.rs +++ b/packages/dioxus-tui/examples/all_terminal_events.rs @@ -37,7 +37,7 @@ fn app(cx: Scope) -> Element { // todo: remove let mut trimmed = format!("{event:?}"); trimmed.truncate(200); - rsx!(p { "{trimmed}" }) + rsx!( p { "{trimmed}" } ) }); let log_event = move |event: Event| { @@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element { }; cx.render(rsx! { - div { - width: "100%", - height: "100%", - flex_direction: "column", + div { width: "100%", height: "100%", flex_direction: "column", div { width: "80%", height: "50%", @@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element { onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())), onclick: move |event| log_event(Event::MouseClick(event.inner().clone())), - ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())), + ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())), onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())), onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())), @@ -73,13 +70,8 @@ fn app(cx: Scope) -> Element { onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())), "Hover, click, type or scroll to see the info down below" - }, - div { - width: "80%", - height: "50%", - flex_direction: "column", - events_rendered, - }, - }, + } + div { width: "80%", height: "50%", flex_direction: "column", events_rendered } + } }) } diff --git a/packages/dioxus-tui/examples/colorpicker.rs b/packages/dioxus-tui/examples/colorpicker.rs index 00f8ef7e0..01cc38cda 100644 --- a/packages/dioxus-tui/examples/colorpicker.rs +++ b/packages/dioxus-tui/examples/colorpicker.rs @@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element { let mapping: DioxusElementToNodeId = cx.consume_context().unwrap(); // disable templates so that every node has an id and can be queried cx.render(rsx! { - div{ + div { width: "100%", background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { if let RenderReturn::Ready(node) = cx.root_node() { - if let Some(id) = node.root_ids.borrow().get(0).cloned() { + if let Some(id) = node.root_ids.borrow().first().cloned() { let node = tui_query.get(mapping.get_node_id(id).unwrap()); - let Size{width, height} = node.size().unwrap(); + let Size { width, height } = node.size().unwrap(); let pos = evt.inner().element_coordinates(); - hue.set((pos.x as f32/width as f32)*255.0); - brightness.set((pos.y as f32/height as f32)*100.0); + hue.set((pos.x as f32 / width as f32) * 255.0); + brightness.set((pos.y as f32 / height as f32) * 100.0); } } }, - "hsl({hue}, 70%, {brightness}%)", + "hsl({hue}, 70%, {brightness}%)" } }) } diff --git a/packages/dioxus-tui/examples/hover.rs b/packages/dioxus-tui/examples/hover.rs index 3af2d1616..2c2cda17d 100644 --- a/packages/dioxus-tui/examples/hover.rs +++ b/packages/dioxus-tui/examples/hover.rs @@ -56,7 +56,6 @@ fn app(cx: Scope) -> Element { width: "100%", height: "100%", flex_direction: "column", - div { width: "100%", height: "50%", @@ -71,7 +70,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), onmousedown: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), onmouseup: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]), - onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta().strip_units().y) as i32, 0, 0]), + onwheel: move |w| q1_color.set([q1_color[0] + (10.0 * w.delta().strip_units().y) as i32, 0, 0]), onmouseleave: move |_| q1_color.set([200; 3]), onmousemove: update_data, "click me" @@ -85,7 +84,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q2_color.set([get_brightness(m.inner()); 3]), onmousedown: move |m| q2_color.set([get_brightness(m.inner()); 3]), onmouseup: move |m| q2_color.set([get_brightness(m.inner()); 3]), - onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta().strip_units().y) as i32;3]), + onwheel: move |w| q2_color.set([q2_color[0] + (10.0 * w.delta().strip_units().y) as i32; 3]), onmouseleave: move |_| q2_color.set([200; 3]), onmousemove: update_data, "click me" @@ -105,7 +104,7 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), onmousedown: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), onmouseup: move |m| q3_color.set([0, get_brightness(m.inner()), 0]), - onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta().strip_units().y) as i32, 0]), + onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0 * w.delta().strip_units().y) as i32, 0]), onmouseleave: move |_| q3_color.set([200; 3]), onmousemove: update_data, "click me" @@ -119,16 +118,16 @@ fn app(cx: Scope) -> Element { onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.inner())]), - onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta().strip_units().y) as i32]), + onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0 * w.delta().strip_units().y) as i32]), onmouseleave: move |_| q4_color.set([200; 3]), onmousemove: update_data, "click me" } - }, - div {"Page coordinates: {page_coordinates}"}, - div {"Element coordinates: {element_coordinates}"}, - div {"Buttons: {buttons}"}, - div {"Modifiers: {modifiers}"}, + } + div { "Page coordinates: {page_coordinates}" } + div { "Element coordinates: {element_coordinates}" } + div { "Buttons: {buttons}" } + div { "Modifiers: {modifiers}" } } }) } diff --git a/packages/dioxus-tui/examples/widgets.rs b/packages/dioxus-tui/examples/widgets.rs index b5df35388..2d0a5bf72 100644 --- a/packages/dioxus-tui/examples/widgets.rs +++ b/packages/dioxus-tui/examples/widgets.rs @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { justify_content: "center", input { - oninput: |data| if &data.value == "good"{ + oninput: |data| if &data.value()== "good"{ bg_green.set(true); } else{ bg_green.set(false); @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { checked: "true", } input { - oninput: |data| if &data.value == "hello world"{ + oninput: |data| if &data.value()== "hello world"{ bg_green.set(true); } else{ bg_green.set(false); @@ -41,7 +41,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if (data.value.parse::().unwrap() - 40.0).abs() < 5.0 { + if (data.value().parse::().unwrap() - 40.0).abs() < 5.0 { bg_green.set(true); } else{ bg_green.set(false); @@ -55,7 +55,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if data.value == "10"{ + if data.value()== "10"{ bg_green.set(true); } else{ bg_green.set(false); @@ -68,7 +68,7 @@ fn app(cx: Scope) -> Element { } input { oninput: |data| { - if data.value == "hello world"{ + if data.value()== "hello world"{ bg_green.set(true); } else{ bg_green.set(false); diff --git a/packages/dioxus-tui/src/element.rs b/packages/dioxus-tui/src/element.rs index eeb4d5b86..8fdb60b9f 100644 --- a/packages/dioxus-tui/src/element.rs +++ b/packages/dioxus-tui/src/element.rs @@ -1,7 +1,6 @@ use std::{ any::Any, fmt::{Display, Formatter}, - rc::Rc, }; use dioxus_core::{ElementId, Mutations, VirtualDom}; @@ -30,7 +29,7 @@ pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec { // We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created pub(crate) fn create_mounted_events( vdom: &VirtualDom, - events: &mut Vec<(ElementId, &'static str, Rc, bool)>, + events: &mut Vec<(ElementId, &'static str, Box, bool)>, mount_events: impl Iterator, ) { let query: Query = vdom @@ -42,11 +41,12 @@ pub(crate) fn create_mounted_events( query: query.clone(), id: node_id, }; - events.push((id, "mounted", Rc::new(MountedData::new(element)), false)); + events.push((id, "mounted", Box::new(MountedData::new(element)), false)); } } -struct TuiElement { +#[derive(Clone)] +pub(crate) struct TuiElement { query: Query, id: NodeId, } @@ -82,8 +82,8 @@ impl RenderedElementBacking for TuiElement { }) } - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } } diff --git a/packages/dioxus-tui/src/events.rs b/packages/dioxus-tui/src/events.rs new file mode 100644 index 000000000..3e7b6fd2a --- /dev/null +++ b/packages/dioxus-tui/src/events.rs @@ -0,0 +1,108 @@ +use core::panic; + +use dioxus_html::*; + +use crate::element::TuiElement; + +fn downcast(event: &PlatformEventData) -> plasmo::EventData { + event + .downcast::() + .expect("event should be of type EventData") + .clone() +} + +pub(crate) struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, _: &PlatformEventData) -> AnimationData { + panic!("animation events not supported") + } + + fn convert_clipboard_data(&self, _: &PlatformEventData) -> ClipboardData { + panic!("clipboard events not supported") + } + + fn convert_composition_data(&self, _: &PlatformEventData) -> CompositionData { + panic!("composition events not supported") + } + + fn convert_drag_data(&self, _: &PlatformEventData) -> DragData { + panic!("drag events not supported") + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + if let plasmo::EventData::Focus(event) = downcast(event) { + FocusData::new(event) + } else { + panic!("event should be of type Focus") + } + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + if let plasmo::EventData::Form(event) = downcast(event) { + FormData::new(event) + } else { + panic!("event should be of type Form") + } + } + + fn convert_image_data(&self, _: &PlatformEventData) -> ImageData { + panic!("image events not supported") + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + if let plasmo::EventData::Keyboard(event) = downcast(event) { + KeyboardData::new(event) + } else { + panic!("event should be of type Keyboard") + } + } + + fn convert_media_data(&self, _: &PlatformEventData) -> MediaData { + panic!("media events not supported") + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + if let plasmo::EventData::Mouse(event) = downcast(event) { + MouseData::new(event) + } else { + panic!("event should be of type Mouse") + } + } + + fn convert_pointer_data(&self, _: &PlatformEventData) -> PointerData { + panic!("pointer events not supported") + } + + fn convert_scroll_data(&self, _: &PlatformEventData) -> ScrollData { + panic!("scroll events not supported") + } + + fn convert_selection_data(&self, _: &PlatformEventData) -> SelectionData { + panic!("selection events not supported") + } + + fn convert_toggle_data(&self, _: &PlatformEventData) -> ToggleData { + panic!("toggle events not supported") + } + + fn convert_touch_data(&self, _: &PlatformEventData) -> TouchData { + panic!("touch events not supported") + } + + fn convert_transition_data(&self, _: &PlatformEventData) -> TransitionData { + panic!("transition events not supported") + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + if let plasmo::EventData::Wheel(event) = downcast(event) { + WheelData::new(event) + } else { + panic!("event should be of type Wheel") + } + } +} diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index fd5b63501..fff318c47 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -1,4 +1,9 @@ +#![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")] + mod element; +mod events; use std::{ any::Any, @@ -8,6 +13,7 @@ use std::{ }; use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::PlatformEventData; use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt}; use dioxus_native_core::prelude::*; @@ -24,6 +30,8 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { } pub fn launch_cfg_with_props(app: Component, props: Props, cfg: Config) { + dioxus_html::set_event_converter(Box::new(events::SerializedHtmlEventConverter)); + render(cfg, |rdom, taffy, event_tx| { let dioxus_state = { let mut rdom = rdom.write().unwrap(); @@ -81,7 +89,7 @@ struct DioxusRenderer { vdom: VirtualDom, dioxus_state: Rc>, // Events that are queued up to be sent to the vdom next time the vdom is polled - queued_events: Vec<(ElementId, &'static str, Rc, bool)>, + queued_events: Vec<(ElementId, &'static str, Box, bool)>, #[cfg(all(feature = "hot-reload", debug_assertions))] hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver, } @@ -122,15 +130,19 @@ impl Driver for DioxusRenderer { let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() }; if let Some(id) = id { let inner_value = value.deref().clone(); + let boxed_event = Box::new(inner_value); + let platform_event = PlatformEventData::new(boxed_event); self.vdom - .handle_event(event, inner_value.into_any(), id, bubbles); + .handle_event(event, Rc::new(platform_event), id, bubbles); } } fn poll_async(&mut self) -> std::pin::Pin + '_>> { // Add any queued events for (id, event, value, bubbles) in self.queued_events.drain(..) { - self.vdom.handle_event(event, value, id, bubbles); + let platform_event = PlatformEventData::new(value); + self.vdom + .handle_event(event, Rc::new(platform_event), id, bubbles); } #[cfg(all(feature = "hot-reload", debug_assertions))] diff --git a/packages/dioxus-tui/tests/events.rs b/packages/dioxus-tui/tests/events.rs index 671cfc349..4d2639371 100644 --- a/packages/dioxus-tui/tests/events.rs +++ b/packages/dioxus-tui/tests/events.rs @@ -63,7 +63,7 @@ fn key_down() { onkeydown: move |evt| { assert_eq!(evt.data.code(), Code::KeyA); tui_ctx.quit(); - }, + } } }) } @@ -95,9 +95,11 @@ fn mouse_down() { width: "100%", height: "100%", onmousedown: move |evt| { - assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + assert!( + evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary) + ); tui_ctx.quit(); - }, + } } }) } @@ -136,7 +138,7 @@ fn mouse_up() { height: "100%", onmouseup: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -175,7 +177,7 @@ fn mouse_enter() { height: "50%", onmouseenter: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -214,7 +216,7 @@ fn mouse_exit() { height: "50%", onmouseenter: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -251,9 +253,9 @@ fn mouse_move() { div { width: "100%", height: "100%", - onmousemove: move |_|{ + onmousemove: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -293,7 +295,7 @@ fn wheel() { onwheel: move |evt| { assert!(evt.data.delta().strip_units().y > 0.0); tui_ctx.quit(); - }, + } } }) } @@ -330,9 +332,9 @@ fn click() { div { width: "100%", height: "100%", - onclick: move |_|{ + onclick: move |_| { tui_ctx.quit(); - }, + } } }) } @@ -369,9 +371,9 @@ fn context_menu() { div { width: "100%", height: "100%", - oncontextmenu: move |_|{ + oncontextmenu: move |_| { tui_ctx.quit(); - }, + } } }) } diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 2c284bf1a..170f0321e 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -16,6 +16,7 @@ dioxus-html = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } dioxus-rsx = { workspace = true, optional = true } +manganis = { git = "https://github.com/DioxusLabs/collect-assets" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { workspace = true, optional = true } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index bc2cd76e6..1e64715b0 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -1,3 +1,7 @@ +#![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 use dioxus_core as core; #[cfg(feature = "hooks")] @@ -18,6 +22,9 @@ pub use dioxus_rsx as rsx; pub use dioxus_core_macro as core_macro; pub mod prelude { + pub use manganis; + pub use manganis::mg; + #[cfg(feature = "hooks")] pub use crate::hooks::*; diff --git a/packages/extension/src/lib.rs b/packages/extension/src/lib.rs index abcdaf598..fb9cca8a9 100644 --- a/packages/extension/src/lib.rs +++ b/packages/extension/src/lib.rs @@ -1,17 +1,39 @@ //! This file exports functions into the vscode extension -use dioxus_autofmt::FormattedBlock; +use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType}; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn format_rsx(raw: String) -> String { - let block = dioxus_autofmt::fmt_block(&raw, 0); +pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String { + let block = dioxus_autofmt::fmt_block( + &raw, + 0, + IndentOptions::new( + if use_tabs { + IndentType::Tabs + } else { + IndentType::Spaces + }, + indent_size, + ), + ); block.unwrap() } #[wasm_bindgen] -pub fn format_selection(raw: String) -> String { - let block = dioxus_autofmt::fmt_block(&raw, 0); +pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String { + let block = dioxus_autofmt::fmt_block( + &raw, + 0, + IndentOptions::new( + if use_tabs { + IndentType::Tabs + } else { + IndentType::Spaces + }, + indent_size, + ), + ); block.unwrap() } @@ -35,8 +57,18 @@ impl FormatBlockInstance { } #[wasm_bindgen] -pub fn format_file(contents: String) -> FormatBlockInstance { - let _edits = dioxus_autofmt::fmt_file(&contents); +pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance { + let _edits = dioxus_autofmt::fmt_file( + &contents, + IndentOptions::new( + if use_tabs { + IndentType::Tabs + } else { + IndentType::Spaces + }, + indent_size, + ), + ); let out = dioxus_autofmt::apply_formats(&contents, _edits.clone()); FormatBlockInstance { new: out, _edits } } diff --git a/packages/extension/src/main.ts b/packages/extension/src/main.ts index fc12457ad..76b7261f1 100644 --- a/packages/extension/src/main.ts +++ b/packages/extension/src/main.ts @@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) { if (!editor) return; // Need an editor to apply text edits. const contents = editor.document.getText(); - const formatted = dioxus.format_file(contents); + let tabSize: number; + if (typeof editor.options.tabSize === 'number') { + tabSize = editor.options.tabSize; + } else { + tabSize = 4; + } + const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, tabSize); // Replace the entire text document // Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs index a7ca9155f..bb9f61bf2 100644 --- a/packages/fermi/src/callback.rs +++ b/packages/fermi/src/callback.rs @@ -28,6 +28,7 @@ impl CallbackApi { } } +#[must_use] pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi { todo!() } diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs index 74dd9268b..8dbeb2c4d 100644 --- a/packages/fermi/src/hooks/atom_ref.rs +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -13,6 +13,7 @@ use std::{ /// /// /// +#[must_use] pub fn use_atom_ref<'a, T: 'static>( cx: &'a ScopeState, atom: &'static AtomRef, diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs index 24fd70093..468f5f980 100644 --- a/packages/fermi/src/hooks/atom_root.rs +++ b/packages/fermi/src/hooks/atom_root.rs @@ -7,6 +7,6 @@ use dioxus_core::ScopeState; pub fn use_atom_root(cx: &ScopeState) -> &Rc { cx.use_hook(|| match cx.consume_context::>() { Some(root) => root, - None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"), + None => panic!("No atom root found in context. Did you forget to call use_init_atom_root at the top of your app?"), }) } diff --git a/packages/fermi/src/hooks/read.rs b/packages/fermi/src/hooks/read.rs index 57a544e93..c2538c74d 100644 --- a/packages/fermi/src/hooks/read.rs +++ b/packages/fermi/src/hooks/read.rs @@ -2,10 +2,12 @@ use crate::{use_atom_root, AtomId, AtomRoot, Readable}; use dioxus_core::{ScopeId, ScopeState}; use std::rc::Rc; +#[must_use] pub fn use_read(cx: &ScopeState, f: impl Readable) -> &V { use_read_rc(cx, f).as_ref() } +#[must_use] pub fn use_read_rc(cx: &ScopeState, f: impl Readable) -> &Rc { let root = use_atom_root(cx); diff --git a/packages/fermi/src/hooks/set.rs b/packages/fermi/src/hooks/set.rs index 5e5141227..89fe48b0f 100644 --- a/packages/fermi/src/hooks/set.rs +++ b/packages/fermi/src/hooks/set.rs @@ -2,6 +2,7 @@ use crate::{use_atom_root, Writable}; use dioxus_core::ScopeState; use std::rc::Rc; +#[must_use] pub fn use_set(cx: &ScopeState, f: impl Writable) -> &Rc { let root = use_atom_root(cx); cx.use_hook(|| { diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs index 071b18059..7d473588a 100644 --- a/packages/fermi/src/hooks/state.rs +++ b/packages/fermi/src/hooks/state.rs @@ -30,6 +30,7 @@ use std::{ /// )) /// } /// ``` +#[must_use] pub fn use_atom_state(cx: &ScopeState, f: impl Writable) -> &AtomState { let root = crate::use_atom_root(cx); @@ -85,7 +86,9 @@ impl AtomState { /// ``` #[must_use] pub fn current(&self) -> Rc { - self.value.as_ref().unwrap().clone() + let atoms = self.root.atoms.borrow(); + let slot = atoms.get(&self.id).unwrap(); + slot.value.clone().downcast().unwrap() } /// Get the `setter` function directly without the `AtomState` wrapper. diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index 4943e38a3..dc5ccf010 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -1,4 +1,6 @@ #![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 prelude { pub use crate::*; @@ -20,8 +22,6 @@ mod atoms { pub use atom::*; pub use atomfamily::*; pub use atomref::*; - pub use selector::*; - pub use selectorfamily::*; } pub mod hooks { diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 8ab213e78..0215fcaad 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["ui", "gui", "react", "ssr", "fullstack"] [dependencies] # server functions -server_fn = { version = "0.4.6", default-features = false } +server_fn = { version = "0.5.2", default-features = false } dioxus_server_macro = { workspace = true } # warp @@ -26,7 +26,7 @@ tower = { version = "0.4.13", features = ["util"], optional = true } axum-macros = "0.3.7" # salvo -salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] } +salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] } serde = "1.0.159" # Dioxus + SSR @@ -70,6 +70,10 @@ dioxus-hot-reload = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } +[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +# This is only for debug mode, and it appears mobile does not support some packages this uses +manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] } + [features] default = ["hot-reload", "default-tls"] router = ["dioxus-router"] diff --git a/packages/fullstack/examples/axum-auth/.gitignore b/packages/fullstack/examples/axum-auth/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-auth/.gitignore +++ b/packages/fullstack/examples/axum-auth/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-desktop/.gitignore b/packages/fullstack/examples/axum-desktop/.gitignore index 6047329c6..21fff11dd 100644 --- a/packages/fullstack/examples/axum-desktop/.gitignore +++ b/packages/fullstack/examples/axum-desktop/.gitignore @@ -1,2 +1,4 @@ dist -target \ No newline at end of file +target +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-hello-world/.gitignore b/packages/fullstack/examples/axum-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-hello-world/.gitignore +++ b/packages/fullstack/examples/axum-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index 8f1d3c8c6..e9d3dcad6 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -24,11 +24,10 @@ fn app(cx: Scope) -> Element { let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); + let eval = use_eval(cx); cx.render(rsx! { - div { - "Server state: {state}" - } + div { "Server state: {state}" } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } diff --git a/packages/fullstack/examples/axum-router/.gitignore b/packages/fullstack/examples/axum-router/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/axum-router/.gitignore +++ b/packages/fullstack/examples/axum-router/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/salvo-hello-world/.gitignore b/packages/fullstack/examples/salvo-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/salvo-hello-world/.gitignore +++ b/packages/fullstack/examples/salvo-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/salvo-hello-world/Cargo.toml b/packages/fullstack/examples/salvo-hello-world/Cargo.toml index ca0d85ecf..3d4e74c5b 100644 --- a/packages/fullstack/examples/salvo-hello-world/Cargo.toml +++ b/packages/fullstack/examples/salvo-hello-world/Cargo.toml @@ -12,7 +12,7 @@ dioxus = { workspace = true } dioxus-fullstack = { workspace = true } tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" -salvo = { version = "0.37.9", optional = true } +salvo = { version = "0.63.0", optional = true } execute = "0.2.12" reqwest = "0.11.18" simple_logger = "4.2.0" diff --git a/packages/fullstack/examples/static-hydrated/.gitignore b/packages/fullstack/examples/static-hydrated/.gitignore index c7f237de8..b98a2a695 100644 --- a/packages/fullstack/examples/static-hydrated/.gitignore +++ b/packages/fullstack/examples/static-hydrated/.gitignore @@ -1,3 +1,4 @@ docs target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/examples/warp-hello-world/.gitignore b/packages/fullstack/examples/warp-hello-world/.gitignore index b2a16319f..21fff11dd 100644 --- a/packages/fullstack/examples/warp-hello-world/.gitignore +++ b/packages/fullstack/examples/warp-hello-world/.gitignore @@ -1,3 +1,4 @@ dist target -static \ No newline at end of file +static +.dioxus \ No newline at end of file diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 95d9ee6cd..a14c51c0a 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -254,7 +254,6 @@ where fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { - use crate::layer::Service; move |req: Request| { let mut service = crate::server_fn_service(Default::default(), func); async move { @@ -277,6 +276,9 @@ where fn serve_static_assets(mut self, assets_path: impl Into) -> Self { use tower_http::services::{ServeDir, ServeFile}; + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html @@ -369,15 +371,65 @@ fn apply_request_parts_to_response( } } -/// SSR renderer handler for Axum -pub async fn render_handler( - State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, +/// SSR renderer handler for Axum with added context injection. +/// +/// # Example +/// ```rust,no_run +/// #![allow(non_snake_case)] +/// use std::sync::{Arc, Mutex}; +/// +/// use axum::routing::get; +/// use dioxus::prelude::*; +/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*}; +/// +/// fn app(cx: Scope) -> Element { +/// render! { +/// "hello!" +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() { +/// let cfg = ServeConfigBuilder::new(app, ()) +/// .assets_path("dist") +/// .build(); +/// let ssr_state = SSRState::new(&cfg); +/// +/// // This could be any state you want to be accessible from your server +/// // functions using `[DioxusServerContext::get]`. +/// let state = Arc::new(Mutex::new("state".to_string())); +/// +/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); +/// axum::Server::bind(&addr) +/// .serve( +/// axum::Router::new() +/// // Register server functions, etc. +/// // Note you probably want to use `register_server_fns_with_handler` +/// // to inject the context into server functions running outside +/// // of an SSR render context. +/// .fallback(get(render_handler_with_context).with_state(( +/// move |ctx| ctx.insert(state.clone()).unwrap(), +/// cfg, +/// ssr_state, +/// ))) +/// .into_make_service(), +/// ) +/// .await +/// .unwrap(); +/// } +/// ``` +pub async fn render_handler_with_context< + P: Clone + serde::Serialize + Send + Sync + 'static, + F: FnMut(&mut DioxusServerContext), +>( + State((mut inject_context, cfg, ssr_state)): State<(F, ServeConfig

, SSRState)>, request: Request, ) -> impl IntoResponse { let (parts, _) = request.into_parts(); let url = parts.uri.path_and_query().unwrap().to_string(); let parts: Arc> = Arc::new(RwLock::new(parts.into())); - let server_context = DioxusServerContext::new(parts.clone()); + let mut server_context = DioxusServerContext::new(parts.clone()); + inject_context(&mut server_context); match ssr_state.render(url, &cfg, &server_context).await { Ok(rendered) => { @@ -395,6 +447,14 @@ pub async fn render_handler } } +/// SSR renderer handler for Axum +pub async fn render_handler( + State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, + request: Request, +) -> impl IntoResponse { + render_handler_with_context(State((|_: &mut _| (), cfg, ssr_state)), request).await +} + fn report_err(e: E) -> Response { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) diff --git a/packages/fullstack/src/adapters/mod.rs b/packages/fullstack/src/adapters/mod.rs index 98d190919..d368b9c73 100644 --- a/packages/fullstack/src/adapters/mod.rs +++ b/packages/fullstack/src/adapters/mod.rs @@ -89,26 +89,26 @@ impl Service for ServerFnHandler { let parts = Arc::new(RwLock::new(parts)); // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime - let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); let pool = get_local_pool(); - pool.spawn_pinned({ - let function = function.clone(); - let mut server_context = server_context.clone(); - server_context.parts = parts; - move || async move { - let data = match function.encoding() { - Encoding::Url | Encoding::Cbor => &body, - Encoding::GetJSON | Encoding::GetCBOR => &query, - }; - let server_function_future = function.call((), data); - let server_function_future = - ProvideServerContext::new(server_function_future, server_context.clone()); - let resp = server_function_future.await; - - resp_tx.send(resp).unwrap(); - } - }); - let result = resp_rx.await.unwrap(); + let result = pool + .spawn_pinned({ + let function = function.clone(); + let mut server_context = server_context.clone(); + server_context.parts = parts; + move || async move { + let data = match function.encoding() { + Encoding::Url | Encoding::Cbor => &body, + Encoding::GetJSON | Encoding::GetCBOR => &query, + }; + let server_function_future = function.call((), data); + let server_function_future = ProvideServerContext::new( + server_function_future, + server_context.clone(), + ); + server_function_future.await + } + }) + .await?; let mut res = http::Response::builder(); // Set the headers from the server context diff --git a/packages/fullstack/src/adapters/salvo_adapter.rs b/packages/fullstack/src/adapters/salvo_adapter.rs index 674722665..84894bbda 100644 --- a/packages/fullstack/src/adapters/salvo_adapter.rs +++ b/packages/fullstack/src/adapters/salvo_adapter.rs @@ -241,6 +241,9 @@ impl DioxusRouterExt for Router { } fn serve_static_assets(mut self, assets_path: impl Into) -> Self { + // Copy over any assets we find + crate::collect_assets::copy_assets(); + let assets_path = assets_path.into(); // Serve all files in dist folder except index.html diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index 316d70e56..682063084 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -187,6 +187,9 @@ pub fn serve_dioxus_application Element { -/// let state1 = use_state(cx, || from_server(|| { +/// let state1 = server_cached(cx, || from_server(|| { /// 1234 /// })); +/// +/// todo!() /// } /// ``` pub fn server_cached(server_fn: impl Fn() -> O) -> O { diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs index e4d89480e..f8eaef963 100644 --- a/packages/fullstack/src/hooks/server_future.rs +++ b/packages/fullstack/src/hooks/server_future.rs @@ -22,6 +22,7 @@ use std::sync::Arc; /// will be allowed to continue /// /// - dependencies: a tuple of references to values that are PartialEq + Clone +#[must_use = "Consider using `cx.spawn` to run a future without reading its value"] pub fn use_server_future( cx: &ScopeState, dependencies: D, diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index c982a1c90..bd145f363 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -121,8 +121,15 @@ impl BoxedService; } @@ -17,7 +19,9 @@ where } } +/// A service is a function that takes a request and returns an async response pub trait Service { + /// Run the service and produce a future that resolves to a response fn run( &mut self, req: http::Request, @@ -55,6 +59,7 @@ where } } +/// A boxed service is a type-erased service that can be used without knowing the underlying type pub struct BoxedService(pub Box); impl tower::Service> for BoxedService { diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index f2d3e5533..8b166757f 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -14,6 +14,7 @@ pub mod router; mod adapters; #[cfg(feature = "ssr")] pub use adapters::*; +mod collect_assets; mod hooks; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; @@ -40,6 +41,8 @@ pub mod prelude { #[cfg(not(feature = "ssr"))] pub use crate::html_storage::deserialize::get_root_props_from_document; pub use crate::launch::LaunchBuilder; + #[cfg(feature = "ssr")] + pub use crate::layer::{Layer, Service}; #[cfg(all(feature = "ssr", feature = "router"))] pub use crate::render::pre_cache_static_routes_with_props; #[cfg(feature = "ssr")] @@ -64,3 +67,10 @@ pub mod prelude { pub use hooks::{server_cached::server_cached, server_future::use_server_future}; } + +// Warn users about overlapping features +#[cfg(all(feature = "ssr", feature = "web"))] +compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other."); + +#[cfg(all(feature = "ssr", feature = "desktop"))] +compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other."); diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index cda41f82c..886a5bd99 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -45,6 +45,8 @@ impl SsrRendererPool { .expect("couldn't spawn runtime") .block_on(async move { let mut vdom = VirtualDom::new_with_props(component, props); + // Make sure the evaluator is initialized + dioxus_ssr::eval::init_eval(vdom.base_scope()); let mut to = WriteBuffer { buffer: Vec::new() }; // before polling the future, we need to set the context let prev_context = diff --git a/packages/fullstack/src/router.rs b/packages/fullstack/src/router.rs index b556aa000..b8d93f0e0 100644 --- a/packages/fullstack/src/router.rs +++ b/packages/fullstack/src/router.rs @@ -53,7 +53,7 @@ fn default_external_navigation_handler() -> fn(Scope) -> Element { dioxus_router::prelude::FailureExternalNavigation } -/// The configeration for the router +/// The configuration for the router #[derive(Props, serde::Serialize, serde::Deserialize)] pub struct FullstackRouterConfig where diff --git a/packages/fullstack/src/server_fn.rs b/packages/fullstack/src/server_fn.rs index 539a0d70a..fd0639372 100644 --- a/packages/fullstack/src/server_fn.rs +++ b/packages/fullstack/src/server_fn.rs @@ -125,14 +125,6 @@ impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry { } } - fn register( - url: &'static str, - server_function: ServerFunction, - encoding: server_fn::Encoding, - ) -> Result<(), Self::Error> { - Self::register_explicit("", url, server_function, encoding) - } - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. fn get(url: &str) -> Option> { REGISTERED_SERVER_FUNCTIONS diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index 6c5a5a841..64d36bd13 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -1,9 +1,12 @@ [package] name = "generational-box" authors = ["Evan Almloff"] -version = "0.0.0" +version = "0.4.3" edition = "2018" - +description = "A box backed by a generational runtime" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +keywords = ["generational", "box", "memory", "allocator"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -15,3 +18,5 @@ rand = "0.8.5" [features] default = ["check_generation"] check_generation = [] +debug_borrows = [] +debug_ownership = [] diff --git a/packages/generational-box/README.md b/packages/generational-box/README.md index 5ab7926ba..95aab8402 100644 --- a/packages/generational-box/README.md +++ b/packages/generational-box/README.md @@ -11,6 +11,8 @@ Three main types manage state in Generational Box: Example: ```rust +use generational_box::Store; + // Create a store for this thread let store = Store::default(); @@ -31,4 +33,4 @@ let store = Store::default(); ## How it works -Internally \ No newline at end of file +Internally, `generational-box` creates an arena of generational RefCell's that are recyled when the owner is dropped. You can think of the cells as something like `&'static RefCell>` with a generational check to make recyling a cell easier to debug. Then GenerationalBox's are `Copy` because the `&'static` pointer is `Copy` diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 60bd41419..57a02a5d7 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -2,9 +2,12 @@ #![warn(missing_docs)] use std::{ + any::Any, cell::{Cell, Ref, RefCell, RefMut}, - fmt::Debug, + error::Error, + fmt::{Debug, Display}, marker::PhantomData, + ops::{Deref, DerefMut}, rc::Rc, }; @@ -29,12 +32,12 @@ fn reused() { let first_ptr; { let owner = store.owner(); - first_ptr = owner.insert(1).raw.data.as_ptr(); + first_ptr = owner.insert(1).raw.0.data.as_ptr(); drop(owner); } { let owner = store.owner(); - let second_ptr = owner.insert(1234).raw.data.as_ptr(); + let second_ptr = owner.insert(1234).raw.0.data.as_ptr(); assert_eq!(first_ptr, second_ptr); drop(owner); } @@ -53,7 +56,10 @@ fn leaking_is_ok() { // don't drop the owner std::mem::forget(owner); } - assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string())); + assert_eq!( + key.try_read().as_deref().unwrap(), + &"hello world".to_string() + ); } #[test] @@ -68,7 +74,7 @@ fn drops() { key = owner.insert(data); // drop the owner } - assert!(key.try_read().is_none()); + assert!(key.try_read().is_err()); } #[test] @@ -129,7 +135,7 @@ fn fuzz() { println!("{:?}", path); for key in valid_keys.iter() { let value = key.read(); - println!("{:?}", value); + println!("{:?}", &*value); assert!(value.starts_with("hello world")); } #[cfg(any(debug_assertions, feature = "check_generation"))] @@ -153,6 +159,8 @@ pub struct GenerationalBox { raw: MemoryLocation, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: u32, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: &'static std::panic::Location<'static>, _marker: PhantomData, } @@ -161,7 +169,7 @@ impl Debug for GenerationalBox { #[cfg(any(debug_assertions, feature = "check_generation"))] f.write_fmt(format_args!( "{:?}@{:?}", - self.raw.data.as_ptr(), + self.raw.0.data.as_ptr(), self.generation ))?; #[cfg(not(any(debug_assertions, feature = "check_generation")))] @@ -175,7 +183,7 @@ impl GenerationalBox { fn validate(&self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] { - self.raw.generation.get() == self.generation + self.raw.0.generation.get() == self.generation } #[cfg(not(any(debug_assertions, feature = "check_generation")))] { @@ -184,43 +192,51 @@ impl GenerationalBox { } /// Try to read the value. Returns None if the value is no longer valid. - pub fn try_read(&self) -> Option> { - self.validate() - .then(|| { - Ref::filter_map(self.raw.data.borrow(), |any| { - any.as_ref()?.downcast_ref::() - }) - .ok() - }) - .flatten() + #[track_caller] + pub fn try_read(&self) -> Result, BorrowError> { + if !self.validate() { + return Err(BorrowError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: self.created_at, + })); + } + self.raw.try_borrow( + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.created_at, + ) } /// Read the value. Panics if the value is no longer valid. - pub fn read(&self) -> Ref<'_, T> { + #[track_caller] + pub fn read(&self) -> GenerationalRef { self.try_read().unwrap() } /// Try to write the value. Returns None if the value is no longer valid. - pub fn try_write(&self) -> Option> { - self.validate() - .then(|| { - RefMut::filter_map(self.raw.data.borrow_mut(), |any| { - any.as_mut()?.downcast_mut::() - }) - .ok() - }) - .flatten() + #[track_caller] + pub fn try_write(&self) -> Result, BorrowMutError> { + if !self.validate() { + return Err(BorrowMutError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: self.created_at, + })); + } + self.raw.try_borrow_mut( + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.created_at, + ) } /// Write the value. Panics if the value is no longer valid. - pub fn write(&self) -> RefMut<'_, T> { + #[track_caller] + pub fn write(&self) -> GenerationalRefMut { self.try_write().unwrap() } /// Set the value. Panics if the value is no longer valid. pub fn set(&self, value: T) { self.validate().then(|| { - *self.raw.data.borrow_mut() = Some(Box::new(value)); + *self.raw.0.data.borrow_mut() = Some(Box::new(value)); }); } @@ -228,7 +244,8 @@ impl GenerationalBox { pub fn ptr_eq(&self, other: &Self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] { - self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation + self.raw.0.data.as_ptr() == other.raw.0.data.as_ptr() + && self.generation == other.generation } #[cfg(not(any(debug_assertions, feature = "check_generation")))] { @@ -246,26 +263,37 @@ impl Clone for GenerationalBox { } #[derive(Clone, Copy)] -struct MemoryLocation { - data: &'static RefCell>>, +struct MemoryLocation(&'static MemoryLocationInner); + +struct MemoryLocationInner { + data: RefCell>>, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: &'static Cell, + generation: Cell, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: RefCell>>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: Cell>>, } impl MemoryLocation { #[allow(unused)] fn drop(&self) { - let old = self.data.borrow_mut().take(); + let old = self.0.data.borrow_mut().take(); #[cfg(any(debug_assertions, feature = "check_generation"))] if old.is_some() { drop(old); - let new_generation = self.generation.get() + 1; - self.generation.set(new_generation); + let new_generation = self.0.generation.get() + 1; + self.0.generation.set(new_generation); } } - fn replace(&mut self, value: T) -> GenerationalBox { - let mut inner_mut = self.data.borrow_mut(); + fn replace_with_caller( + &mut self, + value: T, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + caller: &'static std::panic::Location<'static>, + ) -> GenerationalBox { + let mut inner_mut = self.0.data.borrow_mut(); let raw = Box::new(value); let old = inner_mut.replace(raw); @@ -273,10 +301,315 @@ impl MemoryLocation { GenerationalBox { raw: *self, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: self.generation.get(), + generation: self.0.generation.get(), + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: caller, _marker: PhantomData, } } + + #[track_caller] + fn try_borrow( + &self, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: &'static std::panic::Location<'static>, + ) -> Result, BorrowError> { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.0 + .borrowed_at + .borrow_mut() + .push(std::panic::Location::caller()); + match self.0.data.try_borrow() { + Ok(borrow) => match Ref::filter_map(borrow, |any| any.as_ref()?.downcast_ref::()) { + Ok(reference) => Ok(GenerationalRef { + inner: reference, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: std::panic::Location::caller(), + borrowed_from: self.0, + }, + }), + Err(_) => Err(BorrowError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at, + })), + }, + Err(_) => Err(BorrowError::AlreadyBorrowedMut(AlreadyBorrowedMutError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: self.0.borrowed_mut_at.get().unwrap(), + })), + } + } + + #[track_caller] + fn try_borrow_mut( + &self, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: &'static std::panic::Location<'static>, + ) -> Result, BorrowMutError> { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + { + self.0 + .borrowed_mut_at + .set(Some(std::panic::Location::caller())); + } + match self.0.data.try_borrow_mut() { + Ok(borrow_mut) => { + match RefMut::filter_map(borrow_mut, |any| any.as_mut()?.downcast_mut::()) { + Ok(reference) => Ok(GenerationalRefMut { + inner: reference, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefMutBorrowInfo { + borrowed_from: self.0, + }, + }), + Err(_) => Err(BorrowMutError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at, + })), + } + } + Err(_) => Err(BorrowMutError::AlreadyBorrowed(AlreadyBorrowedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: self.0.borrowed_at.borrow().clone(), + })), + } + } +} + +#[derive(Debug, Clone)] +/// An error that can occur when trying to borrow a value. +pub enum BorrowError { + /// The value was dropped. + Dropped(ValueDroppedError), + /// The value was already borrowed mutably. + AlreadyBorrowedMut(AlreadyBorrowedMutError), +} + +impl Display for BorrowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BorrowError::Dropped(error) => Display::fmt(error, f), + BorrowError::AlreadyBorrowedMut(error) => Display::fmt(error, f), + } + } +} + +impl Error for BorrowError {} + +#[derive(Debug, Clone)] +/// An error that can occur when trying to borrow a value mutably. +pub enum BorrowMutError { + /// The value was dropped. + Dropped(ValueDroppedError), + /// The value was already borrowed. + AlreadyBorrowed(AlreadyBorrowedError), + /// The value was already borrowed mutably. + AlreadyBorrowedMut(AlreadyBorrowedMutError), +} + +impl Display for BorrowMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BorrowMutError::Dropped(error) => Display::fmt(error, f), + BorrowMutError::AlreadyBorrowedMut(error) => Display::fmt(error, f), + BorrowMutError::AlreadyBorrowed(error) => Display::fmt(error, f), + } + } +} + +impl Error for BorrowMutError {} + +/// An error that can occur when trying to use a value that has been dropped. +#[derive(Debug, Copy, Clone)] +pub struct ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: &'static std::panic::Location<'static>, +} + +impl Display for ValueDroppedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow because the value was dropped.")?; + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + f.write_fmt(format_args!("created_at: {}", self.created_at))?; + Ok(()) + } +} + +impl std::error::Error for ValueDroppedError {} + +/// An error that can occur when trying to borrow a value that has already been borrowed mutably. +#[derive(Debug, Copy, Clone)] +pub struct AlreadyBorrowedMutError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: &'static std::panic::Location<'static>, +} + +impl Display for AlreadyBorrowedMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow because the value was already borrowed mutably.")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + f.write_fmt(format_args!("borrowed_mut_at: {}", self.borrowed_mut_at))?; + Ok(()) + } +} + +impl std::error::Error for AlreadyBorrowedMutError {} + +/// An error that can occur when trying to borrow a value mutably that has already been borrowed immutably. +#[derive(Debug, Clone)] +pub struct AlreadyBorrowedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: Vec<&'static std::panic::Location<'static>>, +} + +impl Display for AlreadyBorrowedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow mutably because the value was already borrowed immutably.")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + f.write_str("borrowed_at:")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + for location in self.borrowed_at.iter() { + f.write_fmt(format_args!("\t{}", location))?; + } + Ok(()) + } +} + +impl std::error::Error for AlreadyBorrowedError {} + +/// A reference to a value in a generational box. +pub struct GenerationalRef { + inner: Ref<'static, T>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo, +} + +impl GenerationalRef { + /// Map one ref type to another. + pub fn map(orig: GenerationalRef, f: F) -> GenerationalRef + where + F: FnOnce(&T) -> &U, + { + GenerationalRef { + inner: Ref::map(orig.inner, f), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: orig.borrow.borrowed_at, + borrowed_from: orig.borrow.borrowed_from, + }, + } + } + + /// Filter one ref type to another. + pub fn filter_map(orig: GenerationalRef, f: F) -> Option> + where + F: FnOnce(&T) -> Option<&U>, + { + let Self { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + } = orig; + Ref::filter_map(inner, f).ok().map(|inner| GenerationalRef { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: borrow.borrowed_at, + borrowed_from: borrow.borrowed_from, + }, + }) + } +} + +impl Deref for GenerationalRef { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +struct GenerationalRefBorrowInfo { + borrowed_at: &'static std::panic::Location<'static>, + borrowed_from: &'static MemoryLocationInner, +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +impl Drop for GenerationalRefBorrowInfo { + fn drop(&mut self) { + self.borrowed_from + .borrowed_at + .borrow_mut() + .retain(|location| std::ptr::eq(*location, self.borrowed_at as *const _)); + } +} + +/// A mutable reference to a value in a generational box. +pub struct GenerationalRefMut { + inner: RefMut<'static, T>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefMutBorrowInfo, +} + +impl GenerationalRefMut { + /// Map one ref type to another. + pub fn map(orig: GenerationalRefMut, f: F) -> GenerationalRefMut + where + F: FnOnce(&mut T) -> &mut U, + { + GenerationalRefMut { + inner: RefMut::map(orig.inner, f), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: orig.borrow, + } + } + + /// Filter one ref type to another. + pub fn filter_map(orig: GenerationalRefMut, f: F) -> Option> + where + F: FnOnce(&mut T) -> Option<&mut U>, + { + let Self { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + } = orig; + RefMut::filter_map(inner, f) + .ok() + .map(|inner| GenerationalRefMut { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + }) + } +} + +impl Deref for GenerationalRefMut { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl DerefMut for GenerationalRefMut { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.deref_mut() + } +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +struct GenerationalRefMutBorrowInfo { + borrowed_from: &'static MemoryLocationInner, +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +impl Drop for GenerationalRefMutBorrowInfo { + fn drop(&mut self) { + self.borrowed_from.borrowed_mut_at.take(); + } } /// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread. @@ -305,12 +638,16 @@ impl Store { if let Some(location) = self.recycled.borrow_mut().pop() { location } else { - let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None)); - MemoryLocation { - data, + let data: &'static MemoryLocationInner = self.bump.alloc(MemoryLocationInner { + data: RefCell::new(None), #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: self.bump.alloc(Cell::new(0)), - } + generation: Cell::new(0), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: Default::default(), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: Default::default(), + }); + MemoryLocation(data) } } @@ -331,9 +668,31 @@ pub struct Owner { impl Owner { /// Insert a value into the store. The value will be dropped when the owner is dropped. + #[track_caller] pub fn insert(&self, value: T) -> GenerationalBox { let mut location = self.store.claim(); - let key = location.replace(value); + let key = location.replace_with_caller( + value, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + std::panic::Location::caller(), + ); + self.owned.borrow_mut().push(location); + key + } + + /// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped. + pub fn insert_with_caller( + &self, + value: T, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + caller: &'static std::panic::Location<'static>, + ) -> GenerationalBox { + let mut location = self.store.claim(); + let key = location.replace_with_caller( + value, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + caller, + ); self.owned.borrow_mut().push(location); key } @@ -341,12 +700,16 @@ impl Owner { /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value. pub fn invalid(&self) -> GenerationalBox { let location = self.store.claim(); - GenerationalBox { + let key = GenerationalBox { raw: location, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: location.generation.get(), + generation: location.0.generation.get(), + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + created_at: std::panic::Location::caller(), _marker: PhantomData, - } + }; + self.owned.borrow_mut().push(location); + key } } diff --git a/packages/hooks/src/computed.rs b/packages/hooks/src/computed.rs index 3369d6832..dbea75d73 100644 --- a/packages/hooks/src/computed.rs +++ b/packages/hooks/src/computed.rs @@ -37,6 +37,7 @@ use std::{ /// } /// } /// ``` +#[must_use] pub fn use_tracked_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked { cx.use_hook(|| { let init = init(); @@ -160,6 +161,7 @@ impl Drop for Tracker { } } +#[must_use = "Consider using the `use_effect` hook to rerun an effect whenever the tracked state changes if you don't need the result of the computation"] pub fn use_selector( cx: &ScopeState, tracked: &Tracked, diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 6d7ab6f53..8178d4d3d 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,7 +1,10 @@ +#![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")] #![cfg_attr(feature = "nightly-features", feature(debug_refcell))] #[macro_export] -/// A helper macro for using hooks and properties in async environements. +/// A helper macro for using hooks and properties in async environments. /// /// # Usage /// @@ -54,35 +57,40 @@ macro_rules! to_owned { pub mod computed; -mod use_on_unmount; -pub use use_on_unmount::*; +mod use_on_destroy; +pub use use_on_destroy::*; -mod usecontext; -pub use usecontext::*; +mod use_const; +pub use use_const::*; -mod usestate; -pub use usestate::{use_state, UseState}; +mod use_context; +pub use use_context::*; -mod useref; -pub use useref::*; +mod use_state; +pub use use_state::{use_state, UseState}; + +mod use_ref; +pub use use_ref::*; mod use_shared_state; pub use use_shared_state::*; -mod usecoroutine; -pub use usecoroutine::*; +mod use_coroutine; +pub use use_coroutine::*; -mod usefuture; -pub use usefuture::*; +mod use_future; +pub use use_future::*; -mod useeffect; -pub use useeffect::*; +mod use_effect; +pub use use_effect::*; -mod usecallback; -pub use usecallback::*; +mod use_callback; +pub use use_callback::*; -mod usememo; -pub use usememo::*; +mod use_memo; +pub use use_memo::*; -mod userootcontext; -pub use userootcontext::*; +mod use_on_create; +pub use use_on_create::*; +mod use_root_context; +pub use use_root_context::*; diff --git a/packages/hooks/src/usecallback.rs b/packages/hooks/src/use_callback.rs similarity index 99% rename from packages/hooks/src/usecallback.rs rename to packages/hooks/src/use_callback.rs index 3f223b581..52e592f6b 100644 --- a/packages/hooks/src/usecallback.rs +++ b/packages/hooks/src/use_callback.rs @@ -24,6 +24,7 @@ macro_rules! use_callback { ) }; } + pub fn use_callback(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_ where R: FnMut(T) -> F + 'static, diff --git a/packages/hooks/src/usecollection.rs b/packages/hooks/src/use_collection.rs similarity index 100% rename from packages/hooks/src/usecollection.rs rename to packages/hooks/src/use_collection.rs diff --git a/packages/hooks/src/use_const.rs b/packages/hooks/src/use_const.rs new file mode 100644 index 000000000..47ddb1ed2 --- /dev/null +++ b/packages/hooks/src/use_const.rs @@ -0,0 +1,76 @@ +use std::rc::Rc; + +use dioxus_core::prelude::*; + +/// Store constant state between component renders. +/// +/// UseConst allows you to store state that is initialized once and then remains constant across renders. +/// You can only get an immutable reference after initalization. +/// This can be useful for values that don't need to update reactively, thus can be memoized easily +/// +/// ```rust, ignore +/// struct ComplexData(i32); +/// +/// fn Component(cx: Scope) -> Element { +/// let id = use_const(cx, || ComplexData(100)); +/// +/// cx.render(rsx! { +/// div { "{id.0}" } +/// }) +/// } +/// ``` +#[must_use] +pub fn use_const( + cx: &ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> &UseConst { + cx.use_hook(|| UseConst { + value: Rc::new(initial_state_fn()), + }) +} + +#[derive(Clone)] +pub struct UseConst { + value: Rc, +} + +impl PartialEq for UseConst { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.value, &other.value) + } +} + +impl core::fmt::Display for UseConst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +impl UseConst { + pub fn get_rc(&self) -> &Rc { + &self.value + } +} + +impl std::ops::Deref for UseConst { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.value.as_ref() + } +} + +#[test] +fn use_const_makes_sense() { + #[allow(unused)] + + fn app(cx: Scope) -> Element { + let const_val = use_const(cx, || vec![0, 1, 2, 3]); + + assert!(const_val[0] == 0); + + // const_val.remove(0); // Cannot Compile, cannot get mutable reference now + + None + } +} diff --git a/packages/hooks/src/usecontext.rs b/packages/hooks/src/use_context.rs similarity index 98% rename from packages/hooks/src/usecontext.rs rename to packages/hooks/src/use_context.rs index fdf363252..0d94492f6 100644 --- a/packages/hooks/src/usecontext.rs +++ b/packages/hooks/src/use_context.rs @@ -3,6 +3,7 @@ use dioxus_core::ScopeState; /// Consume some context in the tree, providing a sharable handle to the value /// /// Does not regenerate the value if the value is changed at the parent. +#[must_use] pub fn use_context(cx: &ScopeState) -> Option<&T> { cx.use_hook(|| cx.consume_context::()).as_ref() } diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/use_coroutine.rs similarity index 99% rename from packages/hooks/src/usecoroutine.rs rename to packages/hooks/src/use_coroutine.rs index e5563d793..f2577b90c 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/use_coroutine.rs @@ -79,6 +79,7 @@ where /// Get a handle to a coroutine higher in the tree /// /// See the docs for [`use_coroutine`] for more details. +#[must_use] pub fn use_coroutine_handle(cx: &ScopeState) -> Option<&Coroutine> { cx.use_hook(|| cx.consume_context::>()) .as_ref() diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs new file mode 100644 index 000000000..72744e8ae --- /dev/null +++ b/packages/hooks/src/use_effect.rs @@ -0,0 +1,174 @@ +use dioxus_core::{ScopeState, TaskId}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + future::Future, + rc::Rc, +}; + +use crate::UseFutureDep; + +/// A hook that provides a future that executes after the hooks have been applied. +/// +/// Whenever the hooks dependencies change, the future will be re-evaluated. +/// If a future is pending when the dependencies change, the previous future +/// will be allowed to continue. +/// +/// **Note:** If your dependency list is always empty, use [`use_on_create`](crate::use_on_create). +/// +/// ## Arguments +/// +/// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`. +/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future. That future may return nothing or a closure that will be executed when the dependencies change to clean up the effect. +/// +/// ## Examples +/// +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// #[component] +/// fn Profile(cx: Scope, id: usize) -> Element { +/// let name = use_state(cx, || None); +/// +/// // Only fetch the user data when the id changes. +/// use_effect(cx, (id,), |(id,)| { +/// to_owned![name]; +/// async move { +/// let user = fetch_user(id).await; +/// name.set(user.name); +/// } +/// }); +/// +/// // Only fetch the user data when the id changes. +/// use_effect(cx, (id,), |(id,)| { +/// to_owned![name]; +/// async move { +/// let user = fetch_user(id).await; +/// name.set(user.name); +/// move || println!("Cleaning up from {}", id) +/// } +/// }); +/// +/// let name = name.get().clone().unwrap_or("Loading...".to_string()); +/// +/// render!( +/// p { "{name}" } +/// ) +/// } +/// +/// #[component] +/// fn App(cx: Scope) -> Element { +/// render!(Profile { id: 0 }) +/// } +/// ``` +pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> R) +where + D: UseFutureDep, + R: UseEffectReturn, +{ + struct UseEffect { + needs_regen: bool, + task: Cell>, + dependencies: Vec>, + cleanup: UseEffectCleanup, + } + + impl Drop for UseEffect { + fn drop(&mut self) { + if let Some(cleanup) = self.cleanup.borrow_mut().take() { + cleanup(); + } + } + } + + let state = cx.use_hook(move || UseEffect { + needs_regen: true, + task: Cell::new(None), + dependencies: Vec::new(), + cleanup: Rc::new(RefCell::new(None)), + }); + + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen { + // Call the cleanup function if it exists + if let Some(cleanup) = state.cleanup.borrow_mut().take() { + cleanup(); + } + + // We don't need regen anymore + state.needs_regen = false; + + // Create the new future + let return_value = future(dependencies.out()); + + let task = return_value.apply(state.cleanup.clone(), cx); + state.task.set(Some(task)); + } +} + +type UseEffectCleanup = Rc>>>; + +/// Something that can be returned from a `use_effect` hook. +pub trait UseEffectReturn { + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId; +} + +impl UseEffectReturn<()> for T +where + T: Future + 'static, +{ + fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> TaskId { + cx.push_future(self) + } +} + +#[doc(hidden)] +pub struct CleanupFutureMarker; +impl UseEffectReturn for T +where + T: Future + 'static, + F: FnOnce() + 'static, +{ + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId { + cx.push_future(async move { + let cleanup = self.await; + *oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box); + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(unused)] + #[test] + fn test_use_future() { + use dioxus_core::prelude::*; + + struct MyProps { + a: String, + b: i32, + c: i32, + d: i32, + e: i32, + } + + fn app(cx: Scope) -> Element { + // should only ever run once + use_effect(cx, (), |_| async move { + // + }); + + // runs when a is changed + use_effect(cx, (&cx.props.a,), |(a,)| async move { + // + }); + + // runs when a or b is changed + use_effect(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { + // + }); + + todo!() + } + } +} diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/use_future.rs similarity index 97% rename from packages/hooks/src/usefuture.rs rename to packages/hooks/src/use_future.rs index 67bcdc74e..b9aa20c31 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/use_future.rs @@ -31,13 +31,14 @@ where let state = cx.use_hook(move || UseFuture { update: cx.schedule_update(), - needs_regen: Cell::new(true), + needs_regen: Rc::new(Cell::new(true)), state: val.clone(), task: Default::default(), - dependencies: Vec::new(), }); - if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + let state_dependencies = cx.use_hook(Vec::new); + + if dependencies.clone().apply(state_dependencies) || state.needs_regen.get() { // kill the old one, if it exists if let Some(task) = state.task.take() { cx.remove_future(task); @@ -69,11 +70,11 @@ pub enum FutureState<'a, T> { Regenerating(&'a T), // the old value } +#[derive(Clone)] pub struct UseFuture { update: Arc, - needs_regen: Cell, + needs_regen: Rc>, task: Rc>>, - dependencies: Vec>, state: UseState>, } diff --git a/packages/hooks/src/usememo.rs b/packages/hooks/src/use_memo.rs similarity index 93% rename from packages/hooks/src/usememo.rs rename to packages/hooks/src/use_memo.rs index 3181d57b8..be61b8d53 100644 --- a/packages/hooks/src/usememo.rs +++ b/packages/hooks/src/use_memo.rs @@ -28,6 +28,7 @@ use crate::UseFutureDep; /// render!(Calculator { number: 0 }) /// } /// ``` +#[must_use = "Consider using `use_effect` to run rerun a callback when dependencies change"] pub fn use_memo(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T where T: 'static, diff --git a/packages/hooks/src/usemodel.rs b/packages/hooks/src/use_model.rs similarity index 100% rename from packages/hooks/src/usemodel.rs rename to packages/hooks/src/use_model.rs diff --git a/packages/hooks/src/use_on_create.rs b/packages/hooks/src/use_on_create.rs new file mode 100644 index 000000000..28f7bcd6f --- /dev/null +++ b/packages/hooks/src/use_on_create.rs @@ -0,0 +1,27 @@ +use dioxus_core::ScopeState; +use std::cell::Cell; +use std::future::Future; + +/// A hook that runs a future when the component is mounted. +/// +/// This is just [`use_effect`](crate::use_effect), but with no dependencies. +/// If you have no dependencies, it's recommended to use this, not just because it's more readable, +/// but also because it's a tiny bit more efficient. +pub fn use_on_create(cx: &ScopeState, future: impl FnOnce() -> F) +where + T: 'static, + F: Future + 'static, +{ + let needs_regen = cx.use_hook(|| Cell::new(true)); + + if needs_regen.get() { + // We don't need regen anymore + needs_regen.set(false); + + let fut = future(); + + cx.push_future(async move { + fut.await; + }); + } +} diff --git a/packages/hooks/src/use_on_unmount.rs b/packages/hooks/src/use_on_destroy.rs similarity index 76% rename from packages/hooks/src/use_on_unmount.rs rename to packages/hooks/src/use_on_destroy.rs index 1309ca122..8b09b6a20 100644 --- a/packages/hooks/src/use_on_unmount.rs +++ b/packages/hooks/src/use_on_destroy.rs @@ -1,9 +1,20 @@ -/// Creates a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect) +#[deprecated( + note = "Use `use_on_destroy` instead, which has the same functionality. \ +This is deprecated because of the introduction of `use_on_create` which is better mirrored by `use_on_destroy`. \ +The reason why `use_on_create` is not `use_on_mount` is because of potential confusion with `dioxus::events::onmounted`." +)] +pub fn use_on_unmount(cx: &dioxus_core::ScopeState, destroy: D) { + use_on_destroy(cx, destroy); +} + +/// Creates a callback that will be run before the component is removed. +/// This can be used to clean up side effects from the component +/// (created with [`use_effect`](crate::use_effect)). /// /// Example: /// ```rust /// use dioxus::prelude::*; - +/// /// fn app(cx: Scope) -> Element { /// let state = use_state(cx, || true); /// render! { @@ -25,7 +36,7 @@ /// } /// } /// } - +/// /// fn child_component(cx: Scope) -> Element { /// let original_scroll_position = use_state(cx, || 0.0); /// use_effect(cx, (), move |_| { @@ -38,8 +49,8 @@ /// original_scroll_position.set(window.scroll_y().unwrap()); /// } /// }); - -/// use_on_unmount(cx, { +/// +/// use_on_destroy(cx, { /// to_owned![original_scroll_position]; /// /// restore scroll to the top of the page /// move || { @@ -47,7 +58,7 @@ /// window.scroll_with_x_and_y(*original_scroll_position.current(), 0.0); /// } /// }); - +/// /// render!{ /// div { /// id: "my_element", @@ -56,7 +67,7 @@ /// } /// } /// ``` -pub fn use_on_unmount(cx: &dioxus_core::ScopeState, destroy: D) { +pub fn use_on_destroy(cx: &dioxus_core::ScopeState, destroy: D) { cx.use_hook(|| LifeCycle { ondestroy: Some(destroy), }); diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/use_ref.rs similarity index 99% rename from packages/hooks/src/useref.rs rename to packages/hooks/src/use_ref.rs index cf1b0306f..d0b95074e 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/use_ref.rs @@ -110,6 +110,7 @@ use std::{ /// } /// }) /// ``` +#[must_use] pub fn use_ref(cx: &ScopeState, initialize_refcell: impl FnOnce() -> T) -> &UseRef { let hook = cx.use_hook(|| UseRef { update: cx.schedule_update(), diff --git a/packages/hooks/src/userootcontext.rs b/packages/hooks/src/use_root_context.rs similarity index 100% rename from packages/hooks/src/userootcontext.rs rename to packages/hooks/src/use_root_context.rs diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 18ce1e5f1..0c0f43508 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -26,6 +26,7 @@ macro_rules! debug_location { } pub mod error { + #[cfg(debug_assertions)] fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String { locations .iter() @@ -158,6 +159,7 @@ impl ProvidedStateInner { /// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider. /// /// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified. +#[must_use] pub fn use_shared_state(cx: &ScopeState) -> Option<&UseSharedState> { let state_owner: &mut Option> = &mut *cx.use_hook(move || { let scope_id = cx.scope_id(); diff --git a/packages/cli/examples/README.md b/packages/hooks/src/use_signal.rs similarity index 100% rename from packages/cli/examples/README.md rename to packages/hooks/src/use_signal.rs diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/use_state.rs similarity index 92% rename from packages/hooks/src/usestate.rs rename to packages/hooks/src/use_state.rs index 9e3fb5eed..254493510 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/use_state.rs @@ -30,6 +30,7 @@ use std::{ /// )) /// } /// ``` +#[must_use] pub fn use_state( cx: &ScopeState, initial_state_fn: impl FnOnce() -> T, @@ -336,6 +337,50 @@ impl PartialEq> for UseState { } } +impl PartialOrd for UseState { + fn ge(&self, other: &T) -> bool { + *self.current_val >= *other + } + + fn gt(&self, other: &T) -> bool { + *self.current_val > *other + } + + fn le(&self, other: &T) -> bool { + *self.current_val <= *other + } + + fn lt(&self, other: &T) -> bool { + *self.current_val < *other + } + + fn partial_cmp(&self, other: &T) -> Option { + (*self.current_val).partial_cmp(other) + } +} + +impl PartialOrd> for UseState { + fn ge(&self, other: &UseState) -> bool { + self.current_val >= other.current_val + } + + fn gt(&self, other: &UseState) -> bool { + self.current_val > other.current_val + } + + fn le(&self, other: &UseState) -> bool { + self.current_val <= other.current_val + } + + fn lt(&self, other: &UseState) -> bool { + self.current_val < other.current_val + } + + fn partial_cmp(&self, other: &UseState) -> Option { + self.current_val.partial_cmp(&other.current_val) + } +} + impl Debug for UseState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.current_val) diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs deleted file mode 100644 index 76a3f5245..000000000 --- a/packages/hooks/src/useeffect.rs +++ /dev/null @@ -1,110 +0,0 @@ -use dioxus_core::{ScopeState, TaskId}; -use std::{any::Any, cell::Cell, future::Future}; - -use crate::UseFutureDep; - -/// A hook that provides a future that executes after the hooks have been applied -/// -/// Whenever the hooks dependencies change, the future will be re-evaluated. -/// If a future is pending when the dependencies change, the previous future -/// will be allowed to continue -/// -/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone` -/// -/// ## Examples -/// -/// ```rust, no_run -/// # use dioxus::prelude::*; -/// #[component] -/// fn Profile(cx: Scope, id: usize) -> Element { -/// let name = use_state(cx, || None); -/// -/// // Only fetch the user data when the id changes. -/// use_effect(cx, (id,), |(id,)| { -/// to_owned![name]; -/// async move { -/// let user = fetch_user(id).await; -/// name.set(user.name); -/// } -/// }); -/// -/// let name = name.get().clone().unwrap_or("Loading...".to_string()); -/// -/// render!( -/// p { "{name}" } -/// ) -/// } -/// -/// #[component] -/// fn App(cx: Scope) -> Element { -/// render!(Profile { id: 0 }) -/// } -/// ``` -pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) -where - T: 'static, - F: Future + 'static, - D: UseFutureDep, -{ - struct UseEffect { - needs_regen: bool, - task: Cell>, - dependencies: Vec>, - } - - let state = cx.use_hook(move || UseEffect { - needs_regen: true, - task: Cell::new(None), - dependencies: Vec::new(), - }); - - if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen { - // We don't need regen anymore - state.needs_regen = false; - - // Create the new future - let fut = future(dependencies.out()); - - state.task.set(Some(cx.push_future(async move { - fut.await; - }))); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(unused)] - #[test] - fn test_use_future() { - use dioxus_core::prelude::*; - - struct MyProps { - a: String, - b: i32, - c: i32, - d: i32, - e: i32, - } - - fn app(cx: Scope) -> Element { - // should only ever run once - use_effect(cx, (), |_| async move { - // - }); - - // runs when a is changed - use_effect(cx, (&cx.props.a,), |(a,)| async move { - // - }); - - // runs when a or b is changed - use_effect(cx, (&cx.props.a, &cx.props.b), |(a, b)| async move { - // - }); - - todo!() - } - } -} diff --git a/packages/hooks/src/usesignal.rs b/packages/hooks/src/usesignal.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml index b724ad685..70057114a 100644 --- a/packages/hot-reload/Cargo.toml +++ b/packages/hot-reload/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "hot-reloading"] [dependencies] dioxus-rsx = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-html = { workspace = true } +dioxus-html = { workspace = true, optional = true } interprocess-docfix = { version = "1.2.2" } notify = { version = "5.0.0", optional = true } @@ -24,5 +24,6 @@ once_cell = { version = "1.17.0", optional = true } ignore = { version = "0.4.19", optional = true } [features] -default = [] -file_watcher = ["ignore", "chrono", "notify", "execute", "once_cell", "ignore", "dioxus-html/hot-reload-context"] +default = ["dioxus-html"] +custom_file_watcher = ["ignore", "chrono", "notify", "execute", "once_cell", "ignore"] +file_watcher = ["custom_file_watcher", "dioxus-html/hot-reload-context"] diff --git a/packages/hot-reload/README.md b/packages/hot-reload/README.md index d7418d4b1..d2428e9e9 100644 --- a/packages/hot-reload/README.md +++ b/packages/hot-reload/README.md @@ -68,7 +68,7 @@ rsx! { ## Usage -> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/cli) project. +> This crate implements hot reloading for native compilation targets not WASM. For hot relaoding with the web renderer, see the [dioxus-cli](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli) project. Add this to the top of your main function on any renderer that supports hot reloading to start the hot reloading server: diff --git a/packages/hot-reload/src/file_watcher.rs b/packages/hot-reload/src/file_watcher.rs index 48d97dfa7..b3be7de11 100644 --- a/packages/hot-reload/src/file_watcher.rs +++ b/packages/hot-reload/src/file_watcher.rs @@ -13,11 +13,12 @@ use dioxus_rsx::{ }; use interprocess_docfix::local_socket::{LocalSocketListener, LocalSocketStream}; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; - -pub use dioxus_html::HtmlCtx; use serde::{Deserialize, Serialize}; -pub struct Config { +#[cfg(feature = "file_watcher")] +use dioxus_html::HtmlCtx; + +pub struct Config { root_path: &'static str, listening_paths: &'static [&'static str], excluded_paths: &'static [&'static str], @@ -39,6 +40,7 @@ impl Default for Config { } } +#[cfg(feature = "file_watcher")] impl Config { pub const fn new() -> Self { Self { @@ -120,7 +122,7 @@ pub fn init(cfg: Config) { } = cfg; if let Ok(crate_dir) = PathBuf::from_str(root_path) { - // try to find the gitingore file + // try to find the gitignore file let gitignore_file_path = crate_dir.join(".gitignore"); let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); @@ -150,21 +152,20 @@ pub fn init(cfg: Config) { } let file_map = Arc::new(Mutex::new(file_map)); - #[cfg(target_os = "macos")] + let target_dir = crate_dir.join("target"); + let hot_reload_socket_path = target_dir.join("dioxusin"); + + #[cfg(unix)] { // On unix, if you force quit the application, it can leave the file socket open // This will cause the local socket listener to fail to open // We check if the file socket is already open from an old session and then delete it - let paths = ["./dioxusin", "./@dioxusin"]; - for path in paths { - let path = PathBuf::from(path); - if path.exists() { - let _ = std::fs::remove_file(path); - } + if hot_reload_socket_path.exists() { + let _ = std::fs::remove_file(hot_reload_socket_path); } } - match LocalSocketListener::bind("@dioxusin") { + match LocalSocketListener::bind(hot_reload_socket_path) { Ok(local_socket_stream) => { let aborted = Arc::new(Mutex::new(false)); diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs index f7906b141..46efb8341 100644 --- a/packages/hot-reload/src/lib.rs +++ b/packages/hot-reload/src/lib.rs @@ -1,4 +1,7 @@ -use std::io::{BufRead, BufReader}; +use std::{ + io::{BufRead, BufReader}, + path::PathBuf, +}; use dioxus_core::Template; #[cfg(feature = "file_watcher")] @@ -6,9 +9,9 @@ pub use dioxus_html::HtmlCtx; use interprocess_docfix::local_socket::LocalSocketStream; use serde::{Deserialize, Serialize}; -#[cfg(feature = "file_watcher")] +#[cfg(feature = "custom_file_watcher")] mod file_watcher; -#[cfg(feature = "file_watcher")] +#[cfg(feature = "custom_file_watcher")] pub use file_watcher::*; /// A message the hot reloading server sends to the client @@ -24,7 +27,8 @@ pub enum HotReloadMsg { /// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) { std::thread::spawn(move || { - if let Ok(socket) = LocalSocketStream::connect("@dioxusin") { + let path = PathBuf::from("./").join("target").join("dioxusin"); + if let Ok(socket) = LocalSocketStream::connect(path) { let mut buf_reader = BufReader::new(socket); loop { let mut buf = String::new(); diff --git a/packages/html-internal-macro/Cargo.toml b/packages/html-internal-macro/Cargo.toml new file mode 100644 index 000000000..ce8ad54b1 --- /dev/null +++ b/packages/html-internal-macro/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dioxus-html-internal-macro" +version = { workspace = true } +edition = "2021" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "liveview"] +license = "MIT OR Apache-2.0" +description = "HTML function macros for Dioxus" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0.66" +syn = { version = "2", features = ["full"] } +quote = "^1.0.26" +convert_case = "^0.6.0" + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/progress.rs" + +[dev-dependencies] +trybuild = { version = "1.0.82", features = ["diff"] } \ No newline at end of file diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs new file mode 100644 index 000000000..a404a9b16 --- /dev/null +++ b/packages/html-internal-macro/src/lib.rs @@ -0,0 +1,76 @@ +use proc_macro::TokenStream; + +use convert_case::{Case, Casing}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::__private::TokenStream2; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{braced, parse_macro_input, Ident, Token}; + +#[proc_macro] +pub fn impl_extension_attributes(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ImplExtensionAttributes); + input.to_token_stream().into() +} + +struct ImplExtensionAttributes { + is_element: bool, + name: Ident, + attrs: Punctuated, +} + +impl Parse for ImplExtensionAttributes { + fn parse(input: ParseStream) -> syn::Result { + let content; + + let element: Ident = input.parse()?; + let name = input.parse()?; + braced!(content in input); + let attrs = content.parse_terminated(Ident::parse, Token![,])?; + + Ok(ImplExtensionAttributes { + is_element: element == "ELEMENT", + name, + attrs, + }) + } +} + +impl ToTokens for ImplExtensionAttributes { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let name_string = name.to_string(); + let camel_name = name_string + .strip_prefix("r#") + .unwrap_or(&name_string) + .to_case(Case::UpperCamel); + let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span()); + let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span()); + + if !self.is_element { + tokens.append_all(quote! { + struct #impl_name; + impl #name for #impl_name {} + }); + } + + let impls = self.attrs.iter().map(|ident| { + let d = if self.is_element { + quote! { #name::#ident } + } else { + quote! { <#impl_name as #name>::#ident } + }; + quote! { + fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self { + let d = #d; + self.push_attribute(d.0, d.1, value, d.2) + } + } + }); + tokens.append_all(quote! { + pub trait #extension_name<'a>: HasAttributes<'a> + Sized { + #(#impls)* + } + }); + } +} diff --git a/packages/html-internal-macro/tests/01-simple.rs b/packages/html-internal-macro/tests/01-simple.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/html-internal-macro/tests/01-simple.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/html-internal-macro/tests/progress.rs b/packages/html-internal-macro/tests/progress.rs new file mode 100644 index 000000000..563805397 --- /dev/null +++ b/packages/html-internal-macro/tests/progress.rs @@ -0,0 +1,5 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/01-simple.rs"); +} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index db1551508..b327f8e96 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -12,16 +12,17 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-core = { workspace = true } dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true } +dioxus-html-internal-macro = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } wasm-bindgen = { workspace = true, optional = true } euclid = "0.22.7" -enumset = "1.0.11" +enumset = "1.1.2" keyboard-types = "0.7" async-trait = "0.1.58" serde-value = "0.7.0" tokio = { workspace = true, features = ["fs", "io-util"], optional = true } -rfd = { version = "0.11.3", optional = true } +rfd = { version = "0.12", optional = true } async-channel = "1.8.0" serde_json = { version = "1", optional = true } @@ -29,31 +30,29 @@ serde_json = { version = "1", optional = true } optional = true version = "0.3.56" features = [ - "TouchEvent", - "MouseEvent", - "InputEvent", - "ClipboardEvent", - "DragEvent", - "KeyboardEvent", - "TouchEvent", - "WheelEvent", - "AnimationEvent", - "TransitionEvent", - "PointerEvent", - "FocusEvent", - "CompositionEvent", - "ClipboardEvent", + "Touch", + "TouchList", + "TouchEvent", + "MouseEvent", + "DragEvent", + "InputEvent", + "ClipboardEvent", + "KeyboardEvent", + "WheelEvent", + "AnimationEvent", + "TransitionEvent", + "PointerEvent", + "FocusEvent", + "CompositionEvent", ] [dev-dependencies] serde_json = "1" [features] -default = ["serialize", "mounted"] +default = ["serialize", "mounted", "eval"] serialize = [ - "serde", "serde_repr", - "serde_json", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize", @@ -66,6 +65,11 @@ mounted = [ "web-sys/ScrollBehavior", "web-sys/HtmlElement", ] +eval = [ + "serde", + "serde_json" +] wasm-bind = ["web-sys", "wasm-bindgen"] native-bind = ["tokio"] hot-reload-context = ["dioxus-rsx"] +html-to-rsx = [] diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 7800369a9..37e6bb682 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,9 +1,14 @@ #![allow(non_upper_case_globals)] + +use dioxus_core::prelude::IntoAttributeValue; +use dioxus_core::HasAttributes; +use dioxus_html_internal_macro::impl_extension_attributes; +#[cfg(feature = "hot-reload-context")] +use dioxus_rsx::HotReloadingContext; + #[cfg(feature = "hot-reload-context")] use crate::{map_global_attributes, map_svg_attributes}; use crate::{GlobalAttributes, SvgAttributes}; -#[cfg(feature = "hot-reload-context")] -use dioxus_rsx::HotReloadingContext; pub type AttributeDiscription = (&'static str, Option<&'static str>, bool); @@ -74,7 +79,26 @@ macro_rules! impl_attribute_match { $attr:ident $fil:ident: $vil:ident (in $ns:literal), ) => { if $attr == stringify!($fil) { - return Some((stringify!(fil), Some(ns))); + return Some((stringify!(fil), Some($ns))); + } + }; +} + +#[cfg(feature = "html-to-rsx")] +macro_rules! impl_html_to_rsx_attribute_match { + ( + $attr:ident $fil:ident $name:literal + ) => { + if $attr == $name { + return Some(stringify!($fil)); + } + }; + + ( + $attr:ident $fil:ident $_:tt + ) => { + if $attr == stringify!($fil) { + return Some(stringify!($fil)); } }; } @@ -180,14 +204,26 @@ macro_rules! impl_element_match { }; ( - $el:ident $name:ident $namespace:tt { + $el:ident $name:ident $namespace:literal { $( $fil:ident: $vil:ident $extra:tt, )* } ) => { if $el == stringify!($name) { - return Some((stringify!($name), Some(stringify!($namespace)))); + return Some((stringify!($name), Some($namespace))); + } + }; + + ( + $el:ident $name:ident [$_:literal, $namespace:tt] { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return Some((stringify!($name), Some($namespace))); } }; } @@ -207,6 +243,8 @@ macro_rules! impl_element_match_attributes { $attr $fil: $vil ($extra), ); )* + + return impl_map_global_attributes!($el $attr $name None); } }; @@ -223,10 +261,41 @@ macro_rules! impl_element_match_attributes { $attr $fil: $vil ($extra), ); )* + + return impl_map_global_attributes!($el $attr $name $namespace); } } } +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_map_global_attributes { + ( + $el:ident $attr:ident $element:ident None + ) => { + map_global_attributes($attr) + }; + + ( + $el:ident $attr:ident $element:ident $namespace:literal + ) => { + if $namespace == "http://www.w3.org/2000/svg" { + map_svg_attributes($attr) + } else { + map_global_attributes($attr) + } + }; + + ( + $el:ident $attr:ident $element:ident [$name:literal, $namespace:tt] + ) => { + if $namespace == "http://www.w3.org/2000/svg" { + map_svg_attributes($attr) + } else { + map_global_attributes($attr) + } + }; +} + macro_rules! builder_constructors { ( $( @@ -254,7 +323,7 @@ macro_rules! builder_constructors { } ); )* - map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute)) + None } fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> { @@ -271,6 +340,38 @@ macro_rules! builder_constructors { } } + #[cfg(feature = "html-to-rsx")] + pub fn map_html_attribute_to_rsx(html: &str) -> Option<&'static str> { + $( + $( + impl_html_to_rsx_attribute_match!( + html $fil $extra + ); + )* + )* + + if let Some(name) = crate::map_html_global_attributes_to_rsx(html) { + return Some(name); + } + + if let Some(name) = crate::map_html_svg_attributes_to_rsx(html) { + return Some(name); + } + + None + } + + #[cfg(feature = "html-to-rsx")] + pub fn map_html_element_to_rsx(html: &str) -> Option<&'static str> { + $( + if html == stringify!($name) { + return Some(stringify!($name)); + } + )* + + None + } + $( impl_element!( $(#[$attr])* @@ -282,6 +383,13 @@ macro_rules! builder_constructors { } ); )* + + pub(crate) mod extensions { + use super::*; + $( + impl_extension_attributes![ELEMENT $name { $($fil,)* }]; + )* + } }; } @@ -782,6 +890,7 @@ builder_constructors! { decoding: ImageDecoding DEFAULT, height: usize DEFAULT, ismap: Bool DEFAULT, + loading: String DEFAULT, src: Uri DEFAULT, srcset: String DEFAULT, // FIXME this is much more complicated usemap: String DEFAULT, // FIXME should be a fragment starting with '#' @@ -952,9 +1061,8 @@ builder_constructors! { src: Uri DEFAULT, text: String DEFAULT, - // r#async: Bool, - // r#type: String, // TODO could be an enum - r#type: String "type", + r#async: Bool "async", + r#type: String "type", // TODO could be an enum r#script: String "script", }; @@ -1107,6 +1215,7 @@ builder_constructors! { formnovalidate: Bool DEFAULT, formtarget: Target DEFAULT, height: isize DEFAULT, + initial_checked: Bool DEFAULT, list: Id DEFAULT, max: String DEFAULT, maxlength: usize DEFAULT, @@ -1203,6 +1312,7 @@ builder_constructors! { value: String DEFAULT, selected: Bool volatile, + initial_selected: Bool DEFAULT, }; /// Build a diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index c90f5125b..394713af6 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -33,6 +33,7 @@ type EvalCreator = Rc Result>; /// parts is practically asking for a hacker to find an XSS vulnerability in /// it. **This applies especially to web targets, where the JavaScript context /// has access to most, if not all of your application data.** +#[must_use] pub fn use_eval(cx: &ScopeState) -> &EvalCreator { &*cx.use_hook(|| { let eval_provider = cx @@ -47,6 +48,15 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator { }) } +pub fn eval(script: &str) -> Result { + let eval_provider = dioxus_core::prelude::consume_context::>() + .expect("evaluator not provided"); + + eval_provider + .new_evaluator(script.to_string()) + .map(UseEval::new) +} + /// A wrapper around the target platform's evaluator. #[derive(Clone)] pub struct UseEval { diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index c59149493..2e5cabf75 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1,26 +1,223 @@ +use std::any::Any; +use std::sync::RwLock; + macro_rules! impl_event { ( $data:ty; $( $( #[$attr:meta] )* - $name:ident + $name:ident $(: $js_name:literal)? )* ) => { $( $( #[$attr] )* #[inline] - pub fn $name<'a, E: crate::EventReturn, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> { + pub fn $name<'a, E: crate::EventReturn, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> { ::dioxus_core::Attribute::new( - stringify!($name), - _cx.listener(move |e: ::dioxus_core::Event<$data>| { - _f(e).spawn(_cx); + impl_event!(@name $name $($js_name)?), + _cx.listener(move |e: ::dioxus_core::Event| { + _f(e.map(|e|e.into())).spawn(_cx); }), None, false, - ) + ).into() } )* }; + + (@name $name:ident $js_name:literal) => { + $js_name + }; + (@name $name:ident) => { + stringify!($name) + }; +} + +static EVENT_CONVERTER: RwLock>> = RwLock::new(None); + +#[inline] +pub fn set_event_converter(converter: Box) { + *EVENT_CONVERTER.write().unwrap() = Some(converter); +} + +#[inline] +pub(crate) fn with_event_converter(f: F) -> R +where + F: FnOnce(&dyn HtmlEventConverter) -> R, +{ + let converter = EVENT_CONVERTER.read().unwrap(); + f(converter.as_ref().unwrap().as_ref()) +} + +/// A platform specific event. +pub struct PlatformEventData { + event: Box, +} + +impl PlatformEventData { + pub fn new(event: Box) -> Self { + Self { event } + } + + pub fn downcast(&self) -> Option<&T> { + self.event.downcast_ref::() + } + + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.event.downcast_mut::() + } + + pub fn into_inner(self) -> Option { + self.event.downcast::().ok().map(|e| *e) + } +} + +/// A converter between a platform specific event and a general event. All code in a renderer that has a large binary size should be placed in this trait. Each of these functions should be snipped in high levels of optimization. +pub trait HtmlEventConverter: Send + Sync { + /// Convert a general event to an animation data event + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData; + /// Convert a general event to a clipboard data event + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData; + /// Convert a general event to a composition data event + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData; + /// Convert a general event to a drag data event + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData; + /// Convert a general event to a focus data event + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData; + /// Convert a general event to a form data event + fn convert_form_data(&self, event: &PlatformEventData) -> FormData; + /// Convert a general event to an image data event + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData; + /// Convert a general event to a keyboard data event + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData; + /// Convert a general event to a media data event + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData; + /// Convert a general event to a mounted data event + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData; + /// Convert a general event to a mouse data event + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData; + /// Convert a general event to a pointer data event + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData; + /// Convert a general event to a scroll data event + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData; + /// Convert a general event to a selection data event + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData; + /// Convert a general event to a toggle data event + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData; + /// Convert a general event to a touch data event + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData; + /// Convert a general event to a transition data event + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData; + /// Convert a general event to a wheel data event + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData; +} + +impl From<&PlatformEventData> for AnimationData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_animation_data(val)) + } +} + +impl From<&PlatformEventData> for ClipboardData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_clipboard_data(val)) + } +} + +impl From<&PlatformEventData> for CompositionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_composition_data(val)) + } +} + +impl From<&PlatformEventData> for DragData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_drag_data(val)) + } +} + +impl From<&PlatformEventData> for FocusData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_focus_data(val)) + } +} + +impl From<&PlatformEventData> for FormData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_form_data(val)) + } +} + +impl From<&PlatformEventData> for ImageData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_image_data(val)) + } +} + +impl From<&PlatformEventData> for KeyboardData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_keyboard_data(val)) + } +} + +impl From<&PlatformEventData> for MediaData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_media_data(val)) + } +} + +impl From<&PlatformEventData> for MountedData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_mounted_data(val)) + } +} + +impl From<&PlatformEventData> for MouseData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_mouse_data(val)) + } +} + +impl From<&PlatformEventData> for PointerData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_pointer_data(val)) + } +} + +impl From<&PlatformEventData> for ScrollData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_scroll_data(val)) + } +} + +impl From<&PlatformEventData> for SelectionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_selection_data(val)) + } +} + +impl From<&PlatformEventData> for ToggleData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_toggle_data(val)) + } +} + +impl From<&PlatformEventData> for TouchData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_touch_data(val)) + } +} + +impl From<&PlatformEventData> for TransitionData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_transition_data(val)) + } +} + +impl From<&PlatformEventData> for WheelData { + fn from(val: &PlatformEventData) -> Self { + with_event_converter(|c| c.convert_wheel_data(val)) + } } mod animation; @@ -151,8 +348,6 @@ pub fn event_bubbles(evt: &str) -> bool { } } -use std::future::Future; - #[doc(hidden)] pub trait EventReturn

: Sized { fn spawn(self, _cx: &dioxus_core::ScopeState) {} @@ -164,7 +359,7 @@ pub struct AsyncMarker; impl EventReturn for T where - T: Future + 'static, + T: std::future::Future + 'static, { #[inline] fn spawn(self, cx: &dioxus_core::ScopeState) { diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs index 4495191fc..26db06f9d 100644 --- a/packages/html/src/events/animation.rs +++ b/packages/html/src/events/animation.rs @@ -2,12 +2,132 @@ use dioxus_core::Event; pub type AnimationEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] pub struct AnimationData { - pub animation_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, + inner: Box, +} + +impl AnimationData { + /// Create a new AnimationData + pub fn new(inner: impl HasAnimationData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The name of the animation + pub fn animation_name(&self) -> String { + self.inner.animation_name() + } + + /// The name of the pseudo-element the animation runs on + pub fn pseudo_element(&self) -> String { + self.inner.pseudo_element() + } + + /// The amount of time the animation has been running + pub fn elapsed_time(&self) -> f32 { + self.inner.elapsed_time() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_ref().as_any().downcast_ref::() + } +} + +impl From for AnimationData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for AnimationData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnimationData") + .field("animation_name", &self.animation_name()) + .field("pseudo_element", &self.pseudo_element()) + .field("elapsed_time", &self.elapsed_time()) + .finish() + } +} + +impl PartialEq for AnimationData { + fn eq(&self, other: &Self) -> bool { + self.animation_name() == other.animation_name() + && self.pseudo_element() == other.pseudo_element() + && self.elapsed_time() == other.elapsed_time() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of AnimationData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedAnimationData { + animation_name: String, + pseudo_element: String, + elapsed_time: f32, +} + +#[cfg(feature = "serialize")] +impl From<&AnimationData> for SerializedAnimationData { + fn from(data: &AnimationData) -> Self { + Self { + animation_name: data.animation_name(), + pseudo_element: data.pseudo_element(), + elapsed_time: data.elapsed_time(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasAnimationData for SerializedAnimationData { + fn animation_name(&self) -> String { + self.animation_name.clone() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element.clone() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for AnimationData { + fn serialize(&self, serializer: S) -> Result { + SerializedAnimationData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for AnimationData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedAnimationData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for an animation event +pub trait HasAnimationData: std::any::Any { + /// The name of the animation + fn animation_name(&self) -> String; + + /// The name of the pseudo-element the animation runs on + fn pseudo_element(&self) -> String; + + /// The amount of time the animation has been running + fn elapsed_time(&self) -> f32; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs index 54390c6b0..c0180c728 100644 --- a/packages/html/src/events/clipboard.rs +++ b/packages/html/src/events/clipboard.rs @@ -1,10 +1,82 @@ use dioxus_core::Event; pub type ClipboardEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] + pub struct ClipboardData { - // DOMDataTransfer clipboardData + inner: Box, +} + +impl From for ClipboardData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ClipboardData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClipboardData").finish() + } +} + +impl PartialEq for ClipboardData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl ClipboardData { + /// Create a new ClipboardData + pub fn new(inner: impl HasClipboardData) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_ref().as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ClipboardData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedClipboardData {} + +#[cfg(feature = "serialize")] +impl From<&ClipboardData> for SerializedClipboardData { + fn from(_: &ClipboardData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasClipboardData for SerializedClipboardData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ClipboardData { + fn serialize(&self, serializer: S) -> Result { + SerializedClipboardData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ClipboardData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedClipboardData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasClipboardData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event![ diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs index 480868f24..4d161436e 100644 --- a/packages/html/src/events/composition.rs +++ b/packages/html/src/events/composition.rs @@ -1,10 +1,98 @@ use dioxus_core::Event; pub type CompositionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] + pub struct CompositionData { - pub data: String, + inner: Box, +} + +impl std::fmt::Debug for CompositionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CompositionData") + .field("data", &self.data()) + .finish() + } +} + +impl From for CompositionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl PartialEq for CompositionData { + fn eq(&self, other: &Self) -> bool { + self.data() == other.data() + } +} + +impl CompositionData { + /// Create a new CompositionData + pub fn new(inner: impl HasCompositionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The characters generated by the input method that raised the event + pub fn data(&self) -> String { + self.inner.data() + } + + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of CompositionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedCompositionData { + data: String, +} + +#[cfg(feature = "serialize")] +impl From<&CompositionData> for SerializedCompositionData { + fn from(data: &CompositionData) -> Self { + Self { data: data.data() } + } +} + +#[cfg(feature = "serialize")] +impl HasCompositionData for SerializedCompositionData { + fn data(&self) -> String { + self.data.clone() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for CompositionData { + fn serialize(&self, serializer: S) -> Result { + SerializedCompositionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for CompositionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedCompositionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for a composition event +pub trait HasCompositionData: std::any::Any { + /// The characters generated by the input method that raised the event + fn data(&self) -> String; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 323bcc87a..a985e5ecd 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,5 +1,12 @@ +use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::input_data::{MouseButton, MouseButtonSet}; +use crate::prelude::*; use crate::FileEngine; -use crate::MouseData; + +use dioxus_core::Event; +use keyboard_types::Modifiers; + +use crate::HasMouseData; use dioxus_core::Event; use std::fmt::Debug; @@ -10,38 +17,185 @@ pub type DragEvent = Event; /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] pub struct DragData { - /// Inherit mouse data - pub mouse: MouseData, - - #[cfg_attr( - feature = "serialize", - serde( - default, - skip_serializing, - deserialize_with = "crate::events::deserialize_file_engine" - ) - )] - pub files: Option>, + inner: Box, } -impl Debug for DragData { +impl From for DragData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for DragData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DragData") - .field("mouse", &self.mouse) + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) .finish() } } impl PartialEq for DragData { fn eq(&self, other: &Self) -> bool { - self.mouse == other.mouse + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() } } -impl Eq for DragData {} +impl DragData { + /// Create a new DragData + pub fn new(inner: impl HasDragData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event data to a specific type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } + + /// Get the files of the form event + pub fn files(&self) -> Option> { + self.inner.files() + } +} + +impl InteractionLocation for DragData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +impl InteractionElementOffset for DragData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.inner.coordinates() + } +} + +impl ModifiersInteraction for DragData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +impl PointerInteraction for DragData { + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() + } + + // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here + fn trigger_button(&self) -> Option { + self.inner.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of DragData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedDragData { + mouse: crate::point_interaction::SerializedPointInteraction, +} + +#[cfg(feature = "serialize")] +impl From<&DragData> for SerializedDragData { + fn from(data: &DragData) -> Self { + Self { + mouse: crate::point_interaction::SerializedPointInteraction::from(data), + } + } +} + +#[cfg(feature = "serialize")] +impl HasDragData for SerializedDragData {} + +#[cfg(feature = "serialize")] +impl HasMouseData for SerializedDragData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedDragData { + fn client_coordinates(&self) -> ClientPoint { + self.mouse.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.mouse.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.mouse.screen_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedDragData { + fn element_coordinates(&self) -> ElementPoint { + self.mouse.element_coordinates() + } + + fn coordinates(&self) -> Coordinates { + self.mouse.coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedDragData { + fn modifiers(&self) -> Modifiers { + self.mouse.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedDragData { + fn held_buttons(&self) -> MouseButtonSet { + self.mouse.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.mouse.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for DragData { + fn serialize(&self, serializer: S) -> Result { + SerializedDragData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for DragData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedDragData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for a drag event +pub trait HasDragData: HasMouseData {} impl_event! { DragData; diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs index 2092c972e..f121feb7b 100644 --- a/packages/html/src/events/focus.rs +++ b/packages/html/src/events/focus.rs @@ -2,9 +2,82 @@ use dioxus_core::Event; pub type FocusEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} +pub struct FocusData { + inner: Box, +} + +impl From for FocusData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for FocusData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FocusData").finish() + } +} + +impl PartialEq for FocusData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl FocusData { + /// Create a new FocusData + pub fn new(inner: impl HasFocusData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event data to a specific type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of FocusData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub struct SerializedFocusData {} + +#[cfg(feature = "serialize")] +impl From<&FocusData> for SerializedFocusData { + fn from(_: &FocusData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasFocusData for SerializedFocusData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for FocusData { + fn serialize(&self, serializer: S) -> Result { + SerializedFocusData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for FocusData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedFocusData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasFocusData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ FocusData; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 6b25f2d78..8f83a3a66 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -4,100 +4,207 @@ use dioxus_core::Event; pub type FormEvent = Event; +/// A form value that may either be a list of values or a single value +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + // this will serialize Text(String) -> String and VecText(Vec) to Vec + serde(untagged) +)] +#[derive(Debug, Clone, PartialEq)] +pub enum FormValue { + Text(String), + VecText(Vec), +} + /* DOMEvent: Send + SyncTarget relatedTarget */ -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] pub struct FormData { - pub value: String, - - pub values: HashMap>, - - #[cfg_attr( - feature = "serialize", - serde( - default, - skip_serializing, - deserialize_with = "deserialize_file_engine" - ) - )] - pub files: Option>, + inner: Box, } -#[cfg(feature = "serialize")] -#[derive(serde::Serialize, serde::Deserialize)] -struct SerializedFileEngine { - files: HashMap>, -} - -#[cfg(feature = "serialize")] -#[async_trait::async_trait(?Send)] -impl FileEngine for SerializedFileEngine { - fn files(&self) -> Vec { - self.files.keys().cloned().collect() +impl From for FormData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } } - - async fn read_file(&self, file: &str) -> Option> { - self.files.get(file).cloned() - } - - async fn read_file_to_string(&self, file: &str) -> Option { - self.read_file(file) - .await - .map(|bytes| String::from_utf8_lossy(&bytes).to_string()) - } - - async fn get_native_file(&self, file: &str) -> Option> { - self.read_file(file) - .await - .map(|val| Box::new(val) as Box) - } -} - -#[cfg(feature = "serialize")] -pub(crate) fn deserialize_file_engine<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize; - - let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { - return Ok(None); - }; - - let file_engine = std::sync::Arc::new(file_engine); - Ok(Some(file_engine)) } impl PartialEq for FormData { fn eq(&self, other: &Self) -> bool { - self.value == other.value && self.values == other.values + self.value() == other.value() && self.values() == other.values() } } impl Debug for FormData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FormEvent") - .field("value", &self.value) - .field("values", &self.values) + .field("value", &self.value()) + .field("values", &self.values()) .finish() } } -#[async_trait::async_trait(?Send)] -pub trait FileEngine { - // get a list of file names - fn files(&self) -> Vec; +impl FormData { + /// Create a new form event + pub fn new(event: impl HasFormData + 'static) -> Self { + Self { + inner: Box::new(event), + } + } - // read a file to bytes - async fn read_file(&self, file: &str) -> Option>; + /// Get the value of the form event + pub fn value(&self) -> String { + self.inner.value() + } - // read a file to string - async fn read_file_to_string(&self, file: &str) -> Option; + /// Get the values of the form event + pub fn values(&self) -> HashMap { + self.inner.values() + } - // returns a file in platform's native representation - async fn get_native_file(&self, file: &str) -> Option>; + /// Get the files of the form event + pub fn files(&self) -> Option> { + self.inner.files() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +/// An object that has all the data for a form event +pub trait HasFormData: HasFileData + std::any::Any { + fn value(&self) -> String { + Default::default() + } + + fn values(&self) -> HashMap { + Default::default() + } + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} + +impl FormData { + #[cfg(feature = "serialize")] + /// Parse the values into a struct with one field per value + pub fn parsed_values(&self) -> Result + where + T: serde::de::DeserializeOwned, + { + use serde::Serialize; + + fn convert_hashmap_to_json(hashmap: &HashMap) -> serde_json::Result + where + K: Serialize + std::hash::Hash + Eq, + V: Serialize, + { + serde_json::to_string(hashmap) + } + + let parsed_json = + convert_hashmap_to_json(&self.values()).expect("Failed to parse values to JSON"); + + serde_json::from_str(&parsed_json) + } +} + +#[cfg(feature = "serialize")] +/// A serialized form data object +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedFormData { + value: String, + values: HashMap, + files: Option, +} + +#[cfg(feature = "serialize")] +impl SerializedFormData { + /// Create a new serialized form data object + pub fn new( + value: String, + values: HashMap, + files: Option, + ) -> Self { + Self { + value, + values, + files, + } + } + + /// Create a new serialized form data object from a traditional form data object + pub async fn async_from(data: &FormData) -> Self { + Self { + value: data.value(), + values: data.values(), + files: match data.files() { + Some(files) => { + let mut resolved_files = HashMap::new(); + + for file in files.files() { + let bytes = files.read_file(&file).await; + resolved_files.insert(file, bytes.unwrap_or_default()); + } + + Some(SerializedFileEngine { + files: resolved_files, + }) + } + None => None, + }, + } + } + + fn from_lossy(data: &FormData) -> Self { + Self { + value: data.value(), + values: data.values(), + files: None, + } + } +} + +#[cfg(feature = "serialize")] +impl HasFormData for SerializedFormData { + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap { + self.values.clone() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl HasFileData for SerializedFormData { + fn files(&self) -> Option> { + self.files + .as_ref() + .map(|files| std::sync::Arc::new(files.clone()) as _) + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for FormData { + fn serialize(&self, serializer: S) -> Result { + SerializedFormData::from_lossy(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for FormData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedFormData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } } impl_event! { diff --git a/packages/html/src/events/image.rs b/packages/html/src/events/image.rs index d5858a7f1..3c75edbdd 100644 --- a/packages/html/src/events/image.rs +++ b/packages/html/src/events/image.rs @@ -1,11 +1,97 @@ use dioxus_core::Event; pub type ImageEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageData { - #[cfg_attr(feature = "serialize", serde(default))] - pub load_error: bool, + inner: Box, +} + +impl From for ImageData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ImageData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ImageData") + .field("load_error", &self.load_error()) + .finish() + } +} + +impl PartialEq for ImageData { + fn eq(&self, other: &Self) -> bool { + self.load_error() == other.load_error() + } +} + +impl ImageData { + /// Create a new ImageData + pub fn new(e: impl HasImageData) -> Self { + Self { inner: Box::new(e) } + } + + /// If the renderer encountered an error while loading the image + pub fn load_error(&self) -> bool { + self.inner.load_error() + } + + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ImageData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedImageData { + load_error: bool, +} + +#[cfg(feature = "serialize")] +impl From<&ImageData> for SerializedImageData { + fn from(data: &ImageData) -> Self { + Self { + load_error: data.load_error(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasImageData for SerializedImageData { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn load_error(&self) -> bool { + self.load_error + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ImageData { + fn serialize(&self, serializer: S) -> Result { + SerializedImageData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ImageData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedImageData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +/// A trait for any object that has the data for an image event +pub trait HasImageData: std::any::Any { + /// If the renderer encountered an error while loading the image + fn load_error(&self) -> bool; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! [ diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs index d00dcf8d2..feb0ab313 100644 --- a/packages/html/src/events/keyboard.rs +++ b/packages/html/src/events/keyboard.rs @@ -1,9 +1,8 @@ -use crate::input_data::{decode_key_location, encode_key_location}; use dioxus_core::Event; use keyboard_types::{Code, Key, Location, Modifiers}; -use std::convert::TryInto; -use std::fmt::{Debug, Formatter}; -use std::str::FromStr; +use std::fmt::Debug; + +use crate::prelude::ModifiersInteraction; #[cfg(feature = "serialize")] fn resilient_deserialize_code<'de, D>(deserializer: D) -> Result @@ -16,57 +15,199 @@ where } pub type KeyboardEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Eq)] pub struct KeyboardData { - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use key() instead." - )] - pub char_code: u32, + inner: Box, +} - /// Identify which "key" was entered. - #[deprecated(since = "0.3.0", note = "use key() instead")] - pub key: String, +impl From for KeyboardData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} - /// Get the key code as an enum Variant. - #[deprecated( - since = "0.3.0", - note = "This may not work in all environments. Use code() instead." - )] - pub key_code: KeyCode, +impl std::fmt::Debug for KeyboardData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyboardData") + .field("key", &self.key()) + .field("code", &self.code()) + .field("modifiers", &self.modifiers()) + .field("location", &self.location()) + .field("is_auto_repeating", &self.is_auto_repeating()) + .finish() + } +} - /// the physical key on the keyboard - #[cfg_attr( - feature = "serialize", - serde(deserialize_with = "resilient_deserialize_code") - )] +impl PartialEq for KeyboardData { + fn eq(&self, other: &Self) -> bool { + self.key() == other.key() + && self.code() == other.code() + && self.modifiers() == other.modifiers() + && self.location() == other.location() + && self.is_auto_repeating() == other.is_auto_repeating() + } +} + +impl KeyboardData { + /// Create a new KeyboardData + pub fn new(inner: impl HasKeyboardData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. + pub fn key(&self) -> Key { + self.inner.key() + } + + /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. + pub fn code(&self) -> Code { + self.inner.code() + } + + /// The location of the key on the keyboard or other input device. + pub fn location(&self) -> Location { + self.inner.location() + } + + /// `true` iff the key is being held down such that it is automatically repeating. + pub fn is_auto_repeating(&self) -> bool { + self.inner.is_auto_repeating() + } + + /// Downcast this KeyboardData to a concrete type. + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl ModifiersInteraction for KeyboardData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of KeyboardData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedKeyboardData { + char_code: u32, + key: String, + key_code: KeyCode, + #[serde(deserialize_with = "resilient_deserialize_code")] code: Code, + alt_key: bool, + ctrl_key: bool, + meta_key: bool, + shift_key: bool, + location: usize, + repeat: bool, + which: usize, +} - /// Indicate if the `alt` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, +#[cfg(feature = "serialize")] +impl SerializedKeyboardData { + /// Create a new SerializedKeyboardData + pub fn new( + key: Key, + code: Code, + location: Location, + is_auto_repeating: bool, + modifiers: Modifiers, + ) -> Self { + Self { + char_code: key.legacy_charcode(), + key: key.to_string(), + key_code: KeyCode::from_raw_code( + std::convert::TryInto::try_into(key.legacy_keycode()) + .expect("could not convert keycode to u8"), + ), + code, + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + location: crate::input_data::encode_key_location(location), + repeat: is_auto_repeating, + which: std::convert::TryInto::try_into(key.legacy_charcode()) + .expect("could not convert charcode to usize"), + } + } +} - /// Indicate if the `ctrl` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, +#[cfg(feature = "serialize")] +impl From<&KeyboardData> for SerializedKeyboardData { + fn from(data: &KeyboardData) -> Self { + Self::new( + data.key(), + data.code(), + data.location(), + data.is_auto_repeating(), + data.modifiers(), + ) + } +} - /// Indicate if the `meta` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, +#[cfg(feature = "serialize")] +impl HasKeyboardData for SerializedKeyboardData { + fn key(&self) -> Key { + std::str::FromStr::from_str(&self.key).unwrap_or(Key::Unidentified) + } - /// Indicate if the `shift` modifier key was pressed during this keyboard event - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, + fn code(&self) -> Code { + self.code + } - #[deprecated(since = "0.3.0", note = "use location() instead")] - pub location: usize, + fn location(&self) -> Location { + crate::input_data::decode_key_location(self.location) + } - #[deprecated(since = "0.3.0", note = "use is_auto_repeating() instead")] - pub repeat: bool, + fn is_auto_repeating(&self) -> bool { + self.repeat + } - #[deprecated(since = "0.3.0", note = "use code() or key() instead")] - pub which: usize, + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedKeyboardData { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for KeyboardData { + fn serialize(&self, serializer: S) -> Result { + SerializedKeyboardData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for KeyboardData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedKeyboardData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } } impl_event! { @@ -82,94 +223,21 @@ impl_event! { onkeyup } -impl KeyboardData { - pub fn new( - key: Key, - code: Code, - location: Location, - is_auto_repeating: bool, - modifiers: Modifiers, - ) -> Self { - #[allow(deprecated)] - KeyboardData { - char_code: key.legacy_charcode(), - key: key.to_string(), - key_code: KeyCode::from_raw_code( - key.legacy_keycode() - .try_into() - .expect("could not convert keycode to u8"), - ), - code, - alt_key: modifiers.contains(Modifiers::ALT), - ctrl_key: modifiers.contains(Modifiers::CONTROL), - meta_key: modifiers.contains(Modifiers::META), - shift_key: modifiers.contains(Modifiers::SHIFT), - location: encode_key_location(location), - repeat: is_auto_repeating, - which: key - .legacy_charcode() - .try_into() - .expect("could not convert charcode to usize"), - } - } - +pub trait HasKeyboardData: ModifiersInteraction + std::any::Any { /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout. - pub fn key(&self) -> Key { - #[allow(deprecated)] - FromStr::from_str(&self.key).unwrap_or(Key::Unidentified) - } + fn key(&self) -> Key; /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys. - pub fn code(&self) -> Code { - self.code - } - - /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); - } - } - - modifiers - } + fn code(&self) -> Code; /// The location of the key on the keyboard or other input device. - pub fn location(&self) -> Location { - #[allow(deprecated)] - decode_key_location(self.location) - } + fn location(&self) -> Location; /// `true` iff the key is being held down such that it is automatically repeating. - pub fn is_auto_repeating(&self) -> bool { - #[allow(deprecated)] - self.repeat - } -} + fn is_auto_repeating(&self) -> bool; -impl Debug for KeyboardData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KeyboardData") - .field("key", &self.key()) - .field("code", &self.code()) - .field("modifiers", &self.modifiers()) - .field("location", &self.location()) - .field("is_auto_repeating", &self.is_auto_repeating()) - .finish() - } + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } #[cfg(feature = "serialize")] @@ -178,6 +246,8 @@ impl<'de> serde::Deserialize<'de> for KeyCode { where D: serde::Deserializer<'de>, { + use std::convert::TryInto; + // We could be deserializing a unicode character, so we need to use u64 even if the output only takes u8 let value = u64::deserialize(deserializer)?; diff --git a/packages/html/src/events/media.rs b/packages/html/src/events/media.rs index d2ba71c08..7f3180cf7 100644 --- a/packages/html/src/events/media.rs +++ b/packages/html/src/events/media.rs @@ -1,9 +1,82 @@ use dioxus_core::Event; pub type MediaEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MediaData {} +pub struct MediaData { + inner: Box, +} + +impl From for MediaData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for MediaData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MediaData").finish() + } +} + +impl PartialEq for MediaData { + fn eq(&self, _: &Self) -> bool { + true + } +} + +impl MediaData { + /// Create a new MediaData + pub fn new(inner: impl HasMediaData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of MediaData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedMediaData {} + +#[cfg(feature = "serialize")] +impl From<&MediaData> for SerializedMediaData { + fn from(_: &MediaData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasMediaData for SerializedMediaData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for MediaData { + fn serialize(&self, serializer: S) -> Result { + SerializedMediaData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for MediaData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedMediaData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasMediaData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ MediaData; @@ -29,7 +102,7 @@ impl_event! [ ///ended onended - // todo: this conflicts with image events + // todo: this conflicts with Media events // neither have data, so it's okay // ///error // onerror diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index bc561bf1b..e3908ca8a 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -3,22 +3,18 @@ use euclid::Rect; use std::{ - any::Any, fmt::{Display, Formatter}, future::Future, pin::Pin, - rc::Rc, }; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. // we can not use async_trait here because it does not create a trait that is object safe -pub trait RenderedElementBacking { - /// Get the renderer specific element for the given id - fn get_raw_element(&self) -> MountedResult<&dyn Any> { - Err(MountedError::NotSupported) - } +pub trait RenderedElementBacking: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) #[allow(clippy::type_complexity)] @@ -40,7 +36,11 @@ pub trait RenderedElementBacking { } } -impl RenderedElementBacking for () {} +impl RenderedElementBacking for () { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} /// The way that scrolling should be performed #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -57,22 +57,23 @@ pub enum ScrollBehavior { /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. pub struct MountedData { - inner: Rc, + inner: Box, +} + +impl From for MountedData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } } impl MountedData { /// Create a new MountedData pub fn new(registry: impl RenderedElementBacking + 'static) -> Self { Self { - inner: Rc::new(registry), + inner: Box::new(registry), } } - /// Get the renderer specific element for the given id - pub fn get_raw_element(&self) -> MountedResult<&dyn Any> { - self.inner.get_raw_element() - } - /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) pub async fn get_client_rect(&self) -> MountedResult> { self.inner.get_client_rect().await @@ -90,6 +91,11 @@ impl MountedData { pub fn set_focus(&self, focus: bool) -> Pin>>> { self.inner.set_focus(focus) } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } } use dioxus_core::Event; diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index 55d081900..f6b3d14a9 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -1,91 +1,47 @@ use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use crate::input_data::{ - decode_mouse_button_set, encode_mouse_button_set, MouseButton, MouseButtonSet, -}; +use crate::input_data::{MouseButton, MouseButtonSet}; +use crate::prelude::*; use dioxus_core::Event; use keyboard_types::Modifiers; -use std::fmt::{Debug, Formatter}; pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Default, PartialEq, Eq)] /// Data associated with a mouse event -/// -/// Do not use the deprecated fields; they may change or become private in the future. pub struct MouseData { - /// True if the alt key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub alt_key: bool, + inner: Box, +} - /// The button number that was pressed (if applicable) when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use trigger_button() instead")] - pub button: i16, +impl From for MouseData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} - /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. - /// - /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). - /// - /// - 1: Primary button (usually the left button) - /// - 2: Secondary button (usually the right button) - /// - 4: Auxiliary button (usually the mouse wheel button or middle button) - /// - 8: 4th button (typically the "Browser Back" button) - /// - 16 : 5th button (typically the "Browser Forward" button) - #[deprecated(since = "0.3.0", note = "use held_buttons() instead")] - pub buttons: u16, +impl std::fmt::Debug for MouseData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MouseData") + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} - /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_x: i32, +impl PartialEq for MouseData { + fn eq(&self, other: &E) -> bool { + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} - /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). - /// - /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. - #[deprecated(since = "0.3.0", note = "use client_coordinates() instead")] - pub client_y: i32, - - /// True if the control key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub ctrl_key: bool, - - /// True if the meta key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub meta_key: bool, - - /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_x: i32, - - /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. - #[deprecated(since = "0.3.0", note = "use element_coordinates() instead")] - pub offset_y: i32, - - /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. - /// - /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_x: i32, - - /// The Y (vertical) coordinate in pixels of the event relative to the whole document. - /// - /// See `page_x`. - #[deprecated(since = "0.3.0", note = "use page_coordinates() instead")] - pub page_y: i32, - - /// The X coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_x: i32, - - /// The Y coordinate of the mouse pointer in global (screen) coordinates. - #[deprecated(since = "0.3.0", note = "use screen_coordinates() instead")] - pub screen_y: i32, - - /// True if the shift key was down when the mouse event was fired. - #[deprecated(since = "0.3.0", note = "use modifiers() instead")] - pub shift_key: bool, +/// A trait for any object that has the data for a mouse event +pub trait HasMouseData: PointerInteraction { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! { @@ -119,12 +75,11 @@ impl_event! { /// oncontextmenu oncontextmenu - /// ondoubleclick - ondoubleclick - - /// ondoubleclick + #[deprecated(since = "0.5.0", note = "use ondoubleclick instead")] ondblclick + ondoubleclick: "ondblclick" + /// onmousedown onmousedown @@ -150,131 +105,172 @@ impl_event! { } impl MouseData { - /// Construct MouseData with the specified properties - /// - /// Note: the current implementation truncates coordinates. In the future, when we change the internal representation, it may also support a fractional part. - pub fn new( - coordinates: Coordinates, - trigger_button: Option, - held_buttons: MouseButtonSet, - modifiers: Modifiers, - ) -> Self { - let alt_key = modifiers.contains(Modifiers::ALT); - let ctrl_key = modifiers.contains(Modifiers::CONTROL); - let meta_key = modifiers.contains(Modifiers::META); - let shift_key = modifiers.contains(Modifiers::SHIFT); - - let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); - let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); - let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); - let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); - - #[allow(deprecated)] + /// Create a new instance of MouseData + pub fn new(inner: impl HasMouseData + 'static) -> Self { Self { - alt_key, - ctrl_key, - meta_key, - shift_key, - - button: trigger_button.map_or(0, |b| b.into_web_code()), - buttons: encode_mouse_button_set(held_buttons), - - client_x, - client_y, - offset_x, - offset_y, - page_x, - page_y, - screen_x, - screen_y, + inner: Box::new(inner), } } - /// The event's coordinates relative to the application's viewport (as opposed to the coordinate within the page). - /// - /// For example, clicking in the top left corner of the viewport will always result in a mouse event with client coordinates (0., 0.), regardless of whether the page is scrolled horizontally. - pub fn client_coordinates(&self) -> ClientPoint { - #[allow(deprecated)] - ClientPoint::new(self.client_x.into(), self.client_y.into()) + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl InteractionLocation for MouseData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() } - /// The event's coordinates relative to the padding edge of the target element - /// - /// For example, clicking in the top left corner of an element will result in element coordinates (0., 0.) - pub fn element_coordinates(&self) -> ElementPoint { - #[allow(deprecated)] - ElementPoint::new(self.offset_x.into(), self.offset_y.into()) + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() } - /// The event's coordinates relative to the entire document. This includes any portion of the document not currently visible. - /// - /// For example, if the page is scrolled 200 pixels to the right and 300 pixels down, clicking in the top left corner of the viewport would result in page coordinates (200., 300.) - pub fn page_coordinates(&self) -> PagePoint { - #[allow(deprecated)] - PagePoint::new(self.page_x.into(), self.page_y.into()) + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +impl InteractionElementOffset for MouseData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() } - /// The event's coordinates relative to the entire screen. This takes into account the window's offset. - pub fn screen_coordinates(&self) -> ScreenPoint { - #[allow(deprecated)] - ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) - } - - pub fn coordinates(&self) -> Coordinates { - Coordinates::new( - self.screen_coordinates(), - self.client_coordinates(), - self.element_coordinates(), - self.page_coordinates(), - ) + fn coordinates(&self) -> Coordinates { + self.inner.coordinates() } +} +impl ModifiersInteraction for MouseData { /// The set of modifier keys which were pressed when the event occurred - pub fn modifiers(&self) -> Modifiers { - let mut modifiers = Modifiers::empty(); - - #[allow(deprecated)] - { - if self.alt_key { - modifiers.insert(Modifiers::ALT); - } - if self.ctrl_key { - modifiers.insert(Modifiers::CONTROL); - } - if self.meta_key { - modifiers.insert(Modifiers::META); - } - if self.shift_key { - modifiers.insert(Modifiers::SHIFT); - } - } - - modifiers + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() } +} +impl PointerInteraction for MouseData { /// The set of mouse buttons which were held when the event occurred. - pub fn held_buttons(&self) -> MouseButtonSet { - #[allow(deprecated)] - decode_mouse_button_set(self.buttons) + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() } /// The mouse button that triggered the event /// // todo the following is kind of bad; should we just return None when the trigger_button is unreliable (and frankly irrelevant)? i guess we would need the event_type here /// This is only guaranteed to indicate which button was pressed during events caused by pressing or releasing a button. As such, it is not reliable for events such as mouseenter, mouseleave, mouseover, mouseout, or mousemove. For example, a value of MouseButton::Primary may also indicate that no button was pressed. - pub fn trigger_button(&self) -> Option { - #[allow(deprecated)] - Some(MouseButton::from_web_code(self.button)) + fn trigger_button(&self) -> Option { + self.inner.trigger_button() } } -impl Debug for MouseData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MouseData") - .field("coordinates", &self.coordinates()) - .field("modifiers", &self.modifiers()) - .field("held_buttons", &self.held_buttons()) - .field("trigger_button", &self.trigger_button()) - .finish() +impl PartialEq for MouseData { + fn eq(&self, other: &Self) -> bool { + self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of [`MouseData`] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub struct SerializedMouseData { + /// Common data for all pointer/mouse events + #[serde(flatten)] + point_data: crate::point_interaction::SerializedPointInteraction, +} + +#[cfg(feature = "serialize")] +impl SerializedMouseData { + /// Create a new instance of SerializedMouseData + pub fn new( + trigger_button: Option, + held_buttons: MouseButtonSet, + coordinates: Coordinates, + modifiers: Modifiers, + ) -> Self { + Self { + point_data: crate::point_interaction::SerializedPointInteraction::new( + trigger_button, + held_buttons, + coordinates, + modifiers, + ), + } + } +} + +#[cfg(feature = "serialize")] +impl From<&MouseData> for SerializedMouseData { + fn from(e: &MouseData) -> Self { + Self { + point_data: crate::point_interaction::SerializedPointInteraction::from(e), + } + } +} + +#[cfg(feature = "serialize")] +impl HasMouseData for SerializedMouseData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedMouseData { + fn client_coordinates(&self) -> ClientPoint { + self.point_data.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.point_data.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.point_data.screen_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedMouseData { + fn element_coordinates(&self) -> ElementPoint { + self.point_data.element_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedMouseData { + fn modifiers(&self) -> Modifiers { + self.point_data.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedMouseData { + fn held_buttons(&self) -> MouseButtonSet { + self.point_data.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.point_data.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for MouseData { + fn serialize(&self, serializer: S) -> Result { + SerializedMouseData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for MouseData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedMouseData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) } } diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs index 4134ac852..9556502d3 100644 --- a/packages/html/src/events/pointer.rs +++ b/packages/html/src/events/pointer.rs @@ -1,33 +1,107 @@ use dioxus_core::Event; +use keyboard_types::Modifiers; +use crate::{geometry::*, input_data::*, prelude::*}; + +/// A synthetic event that wraps a web-style [`PointerEvent`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) pub type PointerEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] + pub struct PointerData { - // Mouse only - pub alt_key: bool, - pub button: i16, - pub buttons: u16, - pub client_x: i32, - pub client_y: i32, - pub ctrl_key: bool, - pub meta_key: bool, - pub page_x: i32, - pub page_y: i32, - pub screen_x: i32, - pub screen_y: i32, - pub shift_key: bool, - pub pointer_id: i32, - pub width: i32, - pub height: i32, - pub pressure: f32, - pub tangential_pressure: f32, - pub tilt_x: i32, - pub tilt_y: i32, - pub twist: i32, - pub pointer_type: String, - pub is_primary: bool, - // pub get_modifier_state: bool, + inner: Box, +} + +impl PointerData { + /// Create a new PointerData + pub fn new(data: impl HasPointerData + 'static) -> Self { + Self::from(data) + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl From for PointerData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for PointerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PointerData") + .field("pointer_id", &self.pointer_id()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("pressure", &self.pressure()) + .field("tangential_pressure", &self.tangential_pressure()) + .field("tilt_x", &self.tilt_x()) + .field("tilt_y", &self.tilt_y()) + .field("twist", &self.twist()) + .field("pointer_type", &self.pointer_type()) + .field("is_primary", &self.is_primary()) + .field("coordinates", &self.coordinates()) + .field("modifiers", &self.modifiers()) + .field("held_buttons", &self.held_buttons()) + .field("trigger_button", &self.trigger_button()) + .finish() + } +} + +impl PartialEq for PointerData { + fn eq(&self, other: &Self) -> bool { + self.pointer_id() == other.pointer_id() + && self.width() == other.width() + && self.height() == other.height() + && self.pressure() == other.pressure() + && self.tangential_pressure() == other.tangential_pressure() + && self.tilt_x() == other.tilt_x() + && self.tilt_y() == other.tilt_y() + && self.twist() == other.twist() + && self.pointer_type() == other.pointer_type() + && self.is_primary() == other.is_primary() + && self.coordinates() == other.coordinates() + && self.modifiers() == other.modifiers() + && self.held_buttons() == other.held_buttons() + && self.trigger_button() == other.trigger_button() + } +} + +/// A trait for any object that has the data for a pointer event +pub trait HasPointerData: PointerInteraction { + /// Gets the unique identifier of the pointer causing the event. + fn pointer_id(&self) -> i32; + + /// Gets the width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + fn width(&self) -> i32; + + /// Gets the height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + fn height(&self) -> i32; + + /// Gets the normalized pressure of the pointer input in the range of 0 to 1, + fn pressure(&self) -> f32; + + /// Gets the normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + fn tangential_pressure(&self) -> f32; + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + fn tilt_x(&self) -> i32; + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + fn tilt_y(&self) -> i32; + + /// Gets the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + fn twist(&self) -> i32; + + /// Gets the device type that caused the event (mouse, pen, touch, etc.). + fn pointer_type(&self) -> String; + + /// Gets if the pointer represents the primary pointer of this pointer type. + fn is_primary(&self) -> bool; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event![ @@ -62,3 +136,253 @@ impl_event![ /// pointerout onpointerout ]; + +impl PointerData { + /// Gets the unique identifier of the pointer causing the event. + pub fn pointer_id(&self) -> i32 { + self.inner.pointer_id() + } + + /// Gets the width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + pub fn width(&self) -> i32 { + self.inner.width() + } + + /// Gets the height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + pub fn height(&self) -> i32 { + self.inner.height() + } + + /// Gets the normalized pressure of the pointer input in the range of 0 to 1, + pub fn pressure(&self) -> f32 { + self.inner.pressure() + } + + /// Gets the normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + pub fn tangential_pressure(&self) -> f32 { + self.inner.tangential_pressure() + } + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + pub fn tilt_x(&self) -> i32 { + self.inner.tilt_x() + } + + /// Gets the plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + pub fn tilt_y(&self) -> i32 { + self.inner.tilt_y() + } + + /// Gets the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + pub fn twist(&self) -> i32 { + self.inner.twist() + } + + /// Gets the device type that caused the event (mouse, pen, touch, etc.). + pub fn pointer_type(&self) -> String { + self.inner.pointer_type() + } + + /// Gets if the pointer represents the primary pointer of this pointer type. + pub fn is_primary(&self) -> bool { + self.inner.is_primary() + } +} + +impl InteractionLocation for PointerData { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } +} + +impl InteractionElementOffset for PointerData { + fn element_coordinates(&self) -> ElementPoint { + self.inner.element_coordinates() + } +} + +impl ModifiersInteraction for PointerData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +impl PointerInteraction for PointerData { + fn held_buttons(&self) -> MouseButtonSet { + self.inner.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.inner.trigger_button() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of PointerData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedPointerData { + /// Common data for all pointer/mouse events + #[serde(flatten)] + point_data: crate::point_interaction::SerializedPointInteraction, + + /// The unique identifier of the pointer causing the event. + pointer_id: i32, + + /// The width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. + width: i32, + + /// The height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. + height: i32, + + /// The normalized pressure of the pointer input in the range of 0 to 1, + pressure: f32, + + /// The normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, + tangential_pressure: f32, + + /// The plane angle (in degrees, in the range of -90 to 90) between the Y-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the Y axis. + tilt_x: i32, + + /// The plane angle (in degrees, in the range of -90 to 90) between the X-Z plane and the plane containing both the transducer (e.g. pen stylus) axis and the X axis. + tilt_y: i32, + + /// The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359.The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. + twist: i32, + + /// Indicates the device type that caused the event (mouse, pen, touch, etc.). + pointer_type: String, + + /// Indicates if the pointer represents the primary pointer of this pointer type. + is_primary: bool, +} + +#[cfg(feature = "serialize")] +impl HasPointerData for SerializedPointerData { + fn pointer_id(&self) -> i32 { + self.pointer_id + } + + fn width(&self) -> i32 { + self.width + } + + fn height(&self) -> i32 { + self.height + } + + fn pressure(&self) -> f32 { + self.pressure + } + + fn tangential_pressure(&self) -> f32 { + self.tangential_pressure + } + + fn tilt_x(&self) -> i32 { + self.tilt_x + } + + fn tilt_y(&self) -> i32 { + self.tilt_y + } + + fn twist(&self) -> i32 { + self.twist + } + + fn pointer_type(&self) -> String { + self.pointer_type.clone() + } + + fn is_primary(&self) -> bool { + self.is_primary + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedPointerData { + fn client_coordinates(&self) -> ClientPoint { + self.point_data.client_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.point_data.screen_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.point_data.page_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedPointerData { + fn element_coordinates(&self) -> ElementPoint { + self.point_data.element_coordinates() + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedPointerData { + fn modifiers(&self) -> Modifiers { + self.point_data.modifiers() + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedPointerData { + fn held_buttons(&self) -> MouseButtonSet { + self.point_data.held_buttons() + } + + fn trigger_button(&self) -> Option { + self.point_data.trigger_button() + } +} + +#[cfg(feature = "serialize")] +impl From<&PointerData> for SerializedPointerData { + fn from(data: &PointerData) -> Self { + Self { + point_data: data.into(), + pointer_id: data.pointer_id(), + width: data.width(), + height: data.height(), + pressure: data.pressure(), + tangential_pressure: data.tangential_pressure(), + tilt_x: data.tilt_x(), + tilt_y: data.tilt_y(), + twist: data.twist(), + pointer_type: data.pointer_type().to_string(), + is_primary: data.is_primary(), + } + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for PointerData { + fn serialize(&self, serializer: S) -> Result { + SerializedPointerData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for PointerData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedPointerData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} diff --git a/packages/html/src/events/scroll.rs b/packages/html/src/events/scroll.rs index 798218b02..3693cc144 100644 --- a/packages/html/src/events/scroll.rs +++ b/packages/html/src/events/scroll.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type ScrollEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ScrollData {} + +pub struct ScrollData { + inner: Box, +} + +impl From for ScrollData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl ScrollData { + /// Create a new ScrollData + pub fn new(inner: impl HasScrollData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl std::fmt::Debug for ScrollData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScrollData").finish() + } +} + +impl PartialEq for ScrollData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ScrollData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedScrollData {} + +#[cfg(feature = "serialize")] +impl From<&ScrollData> for SerializedScrollData { + fn from(_: &ScrollData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasScrollData for SerializedScrollData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ScrollData { + fn serialize(&self, serializer: S) -> Result { + SerializedScrollData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ScrollData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedScrollData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasScrollData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! { ScrollData; diff --git a/packages/html/src/events/selection.rs b/packages/html/src/events/selection.rs index 249353e63..30c3d9bf0 100644 --- a/packages/html/src/events/selection.rs +++ b/packages/html/src/events/selection.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type SelectionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SelectionData {} + +pub struct SelectionData { + inner: Box, +} + +impl SelectionData { + /// Create a new SelectionData + pub fn new(inner: impl HasSelectionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl From for SelectionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for SelectionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SelectionData").finish() + } +} + +impl PartialEq for SelectionData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of SelectionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedSelectionData {} + +#[cfg(feature = "serialize")] +impl From<&SelectionData> for SerializedSelectionData { + fn from(_: &SelectionData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasSelectionData for SerializedSelectionData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for SelectionData { + fn serialize(&self, serializer: S) -> Result { + SerializedSelectionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for SelectionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedSelectionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasSelectionData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! [ SelectionData; diff --git a/packages/html/src/events/toggle.rs b/packages/html/src/events/toggle.rs index 4f225a8e6..e929a6a64 100644 --- a/packages/html/src/events/toggle.rs +++ b/packages/html/src/events/toggle.rs @@ -1,9 +1,83 @@ use dioxus_core::Event; pub type ToggleEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ToggleData {} + +pub struct ToggleData { + inner: Box, +} + +impl From for ToggleData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for ToggleData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ToggleData").finish() + } +} + +impl PartialEq for ToggleData { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl ToggleData { + /// Create a new ToggleData + pub fn new(inner: impl HasToggleData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of ToggleData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedToggleData {} + +#[cfg(feature = "serialize")] +impl From<&ToggleData> for SerializedToggleData { + fn from(_: &ToggleData) -> Self { + Self {} + } +} + +#[cfg(feature = "serialize")] +impl HasToggleData for SerializedToggleData { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for ToggleData { + fn serialize(&self, serializer: S) -> Result { + SerializedToggleData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for ToggleData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedToggleData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasToggleData: std::any::Any { + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} impl_event! { ToggleData; diff --git a/packages/html/src/events/touch.rs b/packages/html/src/events/touch.rs index 1e795b6da..b30ace17c 100644 --- a/packages/html/src/events/touch.rs +++ b/packages/html/src/events/touch.rs @@ -1,17 +1,353 @@ use dioxus_core::Event; +use keyboard_types::Modifiers; + +use crate::geometry::*; +use crate::prelude::{InteractionLocation, ModifiersInteraction}; pub type TouchEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct TouchData { - pub alt_key: bool, - pub ctrl_key: bool, - pub meta_key: bool, - pub shift_key: bool, - // get_modifier_state: bool, - // changedTouches: DOMTouchList, - // targetTouches: DOMTouchList, - // touches: DOMTouchList, + inner: Box, +} + +impl From for TouchData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for TouchData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TouchData") + .field("modifiers", &self.modifiers()) + .field("touches", &self.touches()) + .field("touches_changed", &self.touches_changed()) + .field("target_touches", &self.target_touches()) + .finish() + } +} + +impl PartialEq for TouchData { + fn eq(&self, other: &Self) -> bool { + self.modifiers() == other.modifiers() + } +} + +impl TouchData { + /// Create a new TouchData + pub fn new(inner: impl HasTouchData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Get the pointers that are currently down + pub fn touches(&self) -> Vec { + self.inner.touches() + } + + /// Get the touches that have changed since the last event + pub fn touches_changed(&self) -> Vec { + self.inner.touches_changed() + } + + /// Get the touches that started and stayed on the element that triggered this event + pub fn target_touches(&self) -> Vec { + self.inner.target_touches() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl ModifiersInteraction for TouchData { + fn modifiers(&self) -> Modifiers { + self.inner.modifiers() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of TouchData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedTouchData { + alt_key: bool, + ctrl_key: bool, + meta_key: bool, + shift_key: bool, + touches: Vec, + changed_touches: Vec, + target_touches: Vec, +} + +#[cfg(feature = "serialize")] +impl From<&TouchData> for SerializedTouchData { + fn from(data: &TouchData) -> Self { + let modifiers = data.modifiers(); + Self { + alt_key: modifiers.contains(Modifiers::ALT), + ctrl_key: modifiers.contains(Modifiers::CONTROL), + meta_key: modifiers.contains(Modifiers::META), + shift_key: modifiers.contains(Modifiers::SHIFT), + touches: data.touches().iter().map(|t| t.into()).collect(), + changed_touches: data.touches_changed().iter().map(|t| t.into()).collect(), + target_touches: data.target_touches().iter().map(|t| t.into()).collect(), + } + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedTouchData { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::default(); + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + modifiers + } +} + +#[cfg(feature = "serialize")] +impl HasTouchData for SerializedTouchData { + fn touches(&self) -> Vec { + self.touches.clone().into_iter().map(Into::into).collect() + } + + fn touches_changed(&self) -> Vec { + self.changed_touches + .clone() + .into_iter() + .map(Into::into) + .collect() + } + + fn target_touches(&self) -> Vec { + self.target_touches + .clone() + .into_iter() + .map(Into::into) + .collect() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for TouchData { + fn serialize(&self, serializer: S) -> Result { + SerializedTouchData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for TouchData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedTouchData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasTouchData: ModifiersInteraction + std::any::Any { + /// Get the touches that are currently down + fn touches(&self) -> Vec; + + /// Get the touches that have changed since the last event + fn touches_changed(&self) -> Vec; + + /// Get the touches that started and stayed on the element that triggered this event + fn target_touches(&self) -> Vec; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} + +pub struct TouchPoint { + inner: Box, +} + +impl From for TouchPoint { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl TouchPoint { + /// Create a new TouchPoint + pub fn new(inner: impl HasTouchPointData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32 { + self.inner.identifier() + } + + /// the pressure of the touch + fn force(&self) -> f64 { + self.inner.force() + } + + /// the radius of the touch + fn radius(&self) -> ScreenPoint { + self.inner.radius() + } + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64 { + self.inner.rotation() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +impl std::fmt::Debug for TouchPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TouchPoint") + .field("client_coordinates", &self.client_coordinates()) + .field("page_coordinates", &self.page_coordinates()) + .field("screen_coordinates", &self.screen_coordinates()) + .field("identifier", &self.identifier()) + .field("force", &self.force()) + .field("radius", &self.radius()) + .field("rotation", &self.rotation()) + .finish() + } +} + +impl InteractionLocation for TouchPoint { + fn client_coordinates(&self) -> ClientPoint { + self.inner.client_coordinates() + } + + fn page_coordinates(&self) -> PagePoint { + self.inner.page_coordinates() + } + + fn screen_coordinates(&self) -> ScreenPoint { + self.inner.screen_coordinates() + } +} + +/// A trait for touch point data +pub trait HasTouchPointData: InteractionLocation + std::any::Any { + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32; + + /// the pressure of the touch + fn force(&self) -> f64; + + /// the radius of the touch + fn radius(&self) -> ScreenPoint; + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64; + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; +} + +#[cfg(feature = "serialize")] +/// A serialized version of TouchPoint +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +struct SerializedTouchPoint { + identifier: i32, + client_x: f64, + client_y: f64, + page_x: f64, + page_y: f64, + screen_x: f64, + screen_y: f64, + force: f64, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, +} + +#[cfg(feature = "serialize")] +impl From<&TouchPoint> for SerializedTouchPoint { + fn from(point: &TouchPoint) -> Self { + let client_coordinates = point.client_coordinates(); + + let page_coordinates = point.page_coordinates(); + let screen_coordinates = point.screen_coordinates(); + Self { + identifier: point.identifier(), + client_x: client_coordinates.x, + client_y: client_coordinates.y, + page_x: page_coordinates.x, + page_y: page_coordinates.y, + screen_x: screen_coordinates.x, + screen_y: screen_coordinates.y, + force: point.force(), + radius_x: point.radius().x, + radius_y: point.radius().y, + rotation_angle: point.rotation(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasTouchPointData for SerializedTouchPoint { + /// A unique identifier for this touch point that will be the same for the duration of the touch + fn identifier(&self) -> i32 { + self.identifier + } + + /// the pressure of the touch + fn force(&self) -> f64 { + self.force + } + + /// the radius of the touch + fn radius(&self) -> ScreenPoint { + ScreenPoint::new(self.radius_x, self.radius_y) + } + + /// the rotation of the touch in degrees between 0 and 90 + fn rotation(&self) -> f64 { + self.rotation_angle + } + + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedTouchPoint { + /// Gets the coordinates of the event relative to the browser viewport. + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x, self.client_y) + } + + /// Gets the coordinates of the event relative to the screen. + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x, self.screen_y) + } + + /// Gets the coordinates of the event relative to the page. + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x, self.page_y) + } } impl_event! { diff --git a/packages/html/src/events/transition.rs b/packages/html/src/events/transition.rs index ecc3e1636..4e680d650 100644 --- a/packages/html/src/events/transition.rs +++ b/packages/html/src/events/transition.rs @@ -1,12 +1,111 @@ use dioxus_core::Event; pub type TransitionEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] + pub struct TransitionData { - pub property_name: String, - pub pseudo_element: String, - pub elapsed_time: f32, + inner: Box, +} + +impl From for TransitionData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for TransitionData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransitionData") + .field("property_name", &self.inner.property_name()) + .field("pseudo_element", &self.inner.pseudo_element()) + .field("elapsed_time", &self.inner.elapsed_time()) + .finish() + } +} + +impl PartialEq for TransitionData { + fn eq(&self, other: &Self) -> bool { + self.inner.property_name() == other.inner.property_name() + && self.inner.pseudo_element() == other.inner.pseudo_element() + && self.inner.elapsed_time() == other.inner.elapsed_time() + } +} + +impl TransitionData { + /// Create a new TransitionData + pub fn new(inner: impl HasTransitionData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of TransitionData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedTransitionData { + property_name: String, + pseudo_element: String, + elapsed_time: f32, +} + +#[cfg(feature = "serialize")] +impl From<&TransitionData> for SerializedTransitionData { + fn from(data: &TransitionData) -> Self { + Self { + property_name: data.inner.property_name(), + pseudo_element: data.inner.pseudo_element(), + elapsed_time: data.inner.elapsed_time(), + } + } +} + +#[cfg(feature = "serialize")] +impl HasTransitionData for SerializedTransitionData { + fn property_name(&self) -> String { + self.property_name.clone() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element.clone() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for TransitionData { + fn serialize(&self, serializer: S) -> Result { + SerializedTransitionData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for TransitionData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedTransitionData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + +pub trait HasTransitionData: std::any::Any { + fn property_name(&self) -> String; + fn pseudo_element(&self) -> String; + fn elapsed_time(&self) -> f32; + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } impl_event! { diff --git a/packages/html/src/events/wheel.rs b/packages/html/src/events/wheel.rs index 758fa7936..2661eee2b 100644 --- a/packages/html/src/events/wheel.rs +++ b/packages/html/src/events/wheel.rs @@ -1,23 +1,118 @@ use dioxus_core::Event; -use euclid::UnknownUnit; -use std::fmt::{Debug, Formatter}; +use std::fmt::Formatter; -use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta}; +use crate::geometry::WheelDelta; pub type WheelEvent = Event; -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Default)] + pub struct WheelData { - #[deprecated(since = "0.3.0", note = "use delta() instead")] + inner: Box, +} + +impl From for WheelData { + fn from(e: E) -> Self { + Self { inner: Box::new(e) } + } +} + +impl std::fmt::Debug for WheelData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WheelData") + .field("delta", &self.delta()) + .finish() + } +} + +impl PartialEq for WheelData { + fn eq(&self, other: &Self) -> bool { + self.inner.delta() == other.inner.delta() + } +} + +impl WheelData { + /// Create a new WheelData + pub fn new(inner: impl HasWheelData + 'static) -> Self { + Self { + inner: Box::new(inner), + } + } + + /// The amount of wheel movement + #[allow(deprecated)] + pub fn delta(&self) -> WheelDelta { + self.inner.delta() + } + + /// Downcast this event to a concrete event type + pub fn downcast(&self) -> Option<&T> { + self.inner.as_any().downcast_ref::() + } +} + +#[cfg(feature = "serialize")] +/// A serialized version of WheelData +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] +pub struct SerializedWheelData { pub delta_mode: u32, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_x: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_y: f64, - #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_z: f64, } +#[cfg(feature = "serialize")] +impl SerializedWheelData { + /// Create a new SerializedWheelData + pub fn new(delta: WheelDelta) -> Self { + let delta_mode = match delta { + WheelDelta::Pixels(_) => 0, + WheelDelta::Lines(_) => 1, + WheelDelta::Pages(_) => 2, + }; + let delta_raw = delta.strip_units(); + Self { + delta_mode, + delta_x: delta_raw.x, + delta_y: delta_raw.y, + delta_z: delta_raw.z, + } + } +} + +#[cfg(feature = "serialize")] +impl From<&WheelData> for SerializedWheelData { + fn from(data: &WheelData) -> Self { + Self::new(data.delta()) + } +} + +#[cfg(feature = "serialize")] +impl HasWheelData for SerializedWheelData { + fn delta(&self) -> WheelDelta { + WheelDelta::from_web_attributes(self.delta_mode, self.delta_x, self.delta_y, self.delta_z) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for WheelData { + fn serialize(&self, serializer: S) -> Result { + SerializedWheelData::from(self).serialize(serializer) + } +} + +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for WheelData { + fn deserialize>(deserializer: D) -> Result { + let data = SerializedWheelData::deserialize(deserializer)?; + Ok(Self { + inner: Box::new(data), + }) + } +} + impl_event![ WheelData; @@ -25,54 +120,10 @@ impl_event![ onwheel ]; -impl WheelData { - /// Construct a new WheelData with the specified wheel movement delta - pub fn new(delta: WheelDelta) -> Self { - let (delta_mode, vector) = match delta { - WheelDelta::Pixels(v) => (0, v.cast_unit::()), - WheelDelta::Lines(v) => (1, v.cast_unit::()), - WheelDelta::Pages(v) => (2, v.cast_unit::()), - }; - - #[allow(deprecated)] - WheelData { - delta_mode, - delta_x: vector.x, - delta_y: vector.y, - delta_z: vector.z, - } - } - - /// Construct from the attributes of the web wheel event - pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self { - #[allow(deprecated)] - Self { - delta_mode, - delta_x, - delta_y, - delta_z, - } - } - +pub trait HasWheelData: std::any::Any { /// The amount of wheel movement - #[allow(deprecated)] - pub fn delta(&self) -> WheelDelta { - let x = self.delta_x; - let y = self.delta_y; - let z = self.delta_z; - match self.delta_mode { - 0 => WheelDelta::Pixels(PixelsVector::new(x, y, z)), - 1 => WheelDelta::Lines(LinesVector::new(x, y, z)), - 2 => WheelDelta::Pages(PagesVector::new(x, y, z)), - _ => panic!("Invalid delta mode, {:?}", self.delta_mode), - } - } -} + fn delta(&self) -> WheelDelta; -impl Debug for WheelData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WheelData") - .field("delta", &self.delta()) - .finish() - } + /// return self as Any + fn as_any(&self) -> &dyn std::any::Any; } diff --git a/packages/html/src/geometry.rs b/packages/html/src/geometry.rs index 990ca5b5e..4a0b16f4a 100644 --- a/packages/html/src/geometry.rs +++ b/packages/html/src/geometry.rs @@ -47,7 +47,7 @@ pub type PagesVector = Vector3D; /// A vector representing the amount the mouse wheel was moved /// /// This may be expressed in Pixels, Lines or Pages -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum WheelDelta { /// Movement in Pixels @@ -59,6 +59,16 @@ pub enum WheelDelta { } impl WheelDelta { + /// Construct from the attributes of the web wheel event + pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self { + match delta_mode { + 0 => WheelDelta::Pixels(PixelsVector::new(delta_x, delta_y, delta_z)), + 1 => WheelDelta::Lines(LinesVector::new(delta_x, delta_y, delta_z)), + 2 => WheelDelta::Pages(PagesVector::new(delta_x, delta_y, delta_z)), + _ => panic!("Invalid delta mode, {:?}", delta_mode), + } + } + /// Convenience function for constructing a WheelDelta with pixel units pub fn pixels(x: f64, y: f64, z: f64) -> Self { WheelDelta::Pixels(PixelsVector::new(x, y, z)) @@ -96,7 +106,7 @@ impl WheelDelta { } /// Coordinates of a point in the app's interface -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Coordinates { screen: ScreenPoint, client: ClientPoint, diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 3792397ad..b9f06a754 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,5 +1,9 @@ #![allow(non_upper_case_globals)] +use dioxus_core::prelude::IntoAttributeValue; +use dioxus_core::HasAttributes; +use dioxus_html_internal_macro::impl_extension_attributes; + use crate::AttributeDiscription; #[cfg(feature = "hot-reload-context")] @@ -33,12 +37,44 @@ macro_rules! trait_method_mapping { }; } +#[cfg(feature = "html-to-rsx")] +macro_rules! html_to_rsx_attribute_mapping { + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident; + ) => { + if $matching == stringify!($name) { + return Some(stringify!($name)); + } + }; + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident: $lit:literal; + ) => { + if $matching == stringify!($lit) { + return Some(stringify!($name)); + } + }; + ( + $matching:ident; + $(#[$attr:meta])* + $name:ident: $lit:literal, $ns:literal; + ) => { + if $matching == stringify!($lit) { + return Some(stringify!($name)); + } + }; +} + macro_rules! trait_methods { ( @base $(#[$trait_attr:meta])* $trait:ident; $fn:ident; + $fn_html_to_rsx:ident; $( $(#[$attr:meta])* $name:ident $(: $($arg:literal),*)*; @@ -62,6 +98,20 @@ macro_rules! trait_methods { )* None } + + #[cfg(feature = "html-to-rsx")] + #[doc = "Converts an HTML attribute to an RSX attribute"] + pub(crate) fn $fn_html_to_rsx(html: &str) -> Option<&'static str> { + $( + html_to_rsx_attribute_mapping! { + html; + $name$(: $($arg),*)*; + } + )* + None + } + + impl_extension_attributes![GLOBAL $trait { $($name,)* }]; }; // Rename the incoming ident and apply a custom namespace @@ -79,6 +129,7 @@ trait_methods! { GlobalAttributes; map_global_attributes; + map_html_global_attributes_to_rsx; /// Prevent the default action for this element. /// @@ -269,6 +320,9 @@ trait_methods! { /// azimuth: "azimuth", "style"; + /// + backdrop_filter: "backdrop-filter", "style"; + /// backface_visibility: "backface-visibility", "style"; @@ -1590,6 +1644,7 @@ trait_methods! { @base SvgAttributes; map_svg_attributes; + map_html_svg_attributes_to_rsx; /// Prevent the default action for this element. /// diff --git a/packages/html/src/input_data.rs b/packages/html/src/input_data.rs index 28d182fdc..d354c78a9 100644 --- a/packages/html/src/input_data.rs +++ b/packages/html/src/input_data.rs @@ -8,9 +8,10 @@ use keyboard_types::Location; /// A mouse button type (such as Primary/Secondary) // note: EnumSetType also derives Copy and Clone for some reason #[allow(clippy::unused_unit)] -#[derive(EnumSetType, Debug)] +#[derive(EnumSetType, Debug, Default)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum MouseButton { + #[default] /// Primary button (typically the left button) Primary, /// Secondary button (typically the right button) diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 2fc1baeac..379b5f581 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -1,3 +1,6 @@ +#![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")] #![allow(non_snake_case)] //! # Dioxus Namespace for HTML @@ -16,12 +19,15 @@ mod elements; #[cfg(feature = "hot-reload-context")] pub use elements::HtmlCtx; +#[cfg(feature = "html-to-rsx")] +pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx}; pub mod events; pub mod geometry; mod global_attributes; pub mod input_data; #[cfg(feature = "native-bind")] pub mod native_bind; +pub mod point_interaction; mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; @@ -37,9 +43,20 @@ pub use events::*; pub use global_attributes::*; pub use render_template::*; -mod eval; +#[cfg(feature = "eval")] +pub mod eval; + +pub mod extensions { + pub use crate::elements::extensions::*; + pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension}; +} pub mod prelude { + pub use crate::elements::extensions::*; + #[cfg(feature = "eval")] pub use crate::eval::*; pub use crate::events::*; + pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension}; + pub use crate::point_interaction::*; + pub use keyboard_types::{self, Code, Key, Location, Modifiers}; } diff --git a/packages/html/src/point_interaction.rs b/packages/html/src/point_interaction.rs new file mode 100644 index 000000000..9aa4e0f31 --- /dev/null +++ b/packages/html/src/point_interaction.rs @@ -0,0 +1,214 @@ +use keyboard_types::Modifiers; + +use crate::{ + geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}, + input_data::{MouseButton, MouseButtonSet}, +}; + +/// A interaction that contains data about the location of the event. +pub trait InteractionLocation { + /// Gets the coordinates of the event relative to the browser viewport. + fn client_coordinates(&self) -> ClientPoint; + + /// Gets the coordinates of the event relative to the screen. + fn screen_coordinates(&self) -> ScreenPoint; + + /// Gets the coordinates of the event relative to the page. + fn page_coordinates(&self) -> PagePoint; +} + +/// A interaction that contains data about the location of the event. +pub trait InteractionElementOffset: InteractionLocation { + /// Gets the coordinates of the event. + fn coordinates(&self) -> Coordinates { + Coordinates::new( + self.screen_coordinates(), + self.client_coordinates(), + self.element_coordinates(), + self.page_coordinates(), + ) + } + + /// Gets the coordinates of the event relative to the target element. + fn element_coordinates(&self) -> ElementPoint; +} + +/// A interaction that contains data about the pointer button(s) that triggered the event. +pub trait PointerInteraction: InteractionElementOffset + ModifiersInteraction { + /// Gets the button that triggered the event. + fn trigger_button(&self) -> Option; + + /// Gets the buttons that are currently held down. + fn held_buttons(&self) -> MouseButtonSet; +} + +/// A interaction that contains data about the current state of the keyboard modifiers. +pub trait ModifiersInteraction { + /// Gets the modifiers of the pointer event. + fn modifiers(&self) -> Modifiers; +} + +#[cfg(feature = "serialize")] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)] +pub(crate) struct SerializedPointInteraction { + pub alt_key: bool, + + /// The button number that was pressed (if applicable) when the mouse event was fired. + pub button: i16, + + /// Indicates which buttons are pressed on the mouse (or other input device) when a mouse event is triggered. + /// + /// Each button that can be pressed is represented by a given number (see below). If more than one button is pressed, the button values are added together to produce a new number. For example, if the secondary (2) and auxiliary (4) buttons are pressed simultaneously, the value is 6 (i.e., 2 + 4). + /// + /// - 1: Primary button (usually the left button) + /// - 2: Secondary button (usually the right button) + /// - 4: Auxiliary button (usually the mouse wheel button or middle button) + /// - 8: 4th button (typically the "Browser Back" button) + /// - 16 : 5th button (typically the "Browser Forward" button) + pub buttons: u16, + + /// The horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the left edge of the viewport will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally. + pub client_x: i32, + + /// The vertical coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page). + /// + /// For example, clicking on the top edge of the viewport will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically. + pub client_y: i32, + + /// True if the control key was down when the mouse event was fired. + pub ctrl_key: bool, + + /// True if the meta key was down when the mouse event was fired. + pub meta_key: bool, + + /// The offset in the X coordinate of the mouse pointer between that event and the padding edge of the target node. + pub offset_x: i32, + + /// The offset in the Y coordinate of the mouse pointer between that event and the padding edge of the target node. + pub offset_y: i32, + + /// The X (horizontal) coordinate (in pixels) of the mouse, relative to the left edge of the entire document. This includes any portion of the document not currently visible. + /// + /// Being based on the edge of the document as it is, this property takes into account any horizontal scrolling of the page. For example, if the page is scrolled such that 200 pixels of the left side of the document are scrolled out of view, and the mouse is clicked 100 pixels inward from the left edge of the view, the value returned by pageX will be 300. + pub page_x: i32, + + /// The Y (vertical) coordinate in pixels of the event relative to the whole document. + /// + /// See `page_x`. + pub page_y: i32, + + /// The X coordinate of the mouse pointer in global (screen) coordinates. + pub screen_x: i32, + + /// The Y coordinate of the mouse pointer in global (screen) coordinates. + pub screen_y: i32, + + /// True if the shift key was down when the mouse event was fired. + pub shift_key: bool, +} + +#[cfg(feature = "serialize")] +impl SerializedPointInteraction { + pub fn new( + trigger_button: Option, + held_buttons: MouseButtonSet, + coordinates: Coordinates, + modifiers: Modifiers, + ) -> Self { + let alt_key = modifiers.contains(Modifiers::ALT); + let ctrl_key = modifiers.contains(Modifiers::CONTROL); + let meta_key = modifiers.contains(Modifiers::META); + let shift_key = modifiers.contains(Modifiers::SHIFT); + + let [client_x, client_y]: [i32; 2] = coordinates.client().cast().into(); + let [offset_x, offset_y]: [i32; 2] = coordinates.element().cast().into(); + let [page_x, page_y]: [i32; 2] = coordinates.page().cast().into(); + let [screen_x, screen_y]: [i32; 2] = coordinates.screen().cast().into(); + Self { + button: trigger_button + .map_or(MouseButton::default(), |b| b) + .into_web_code(), + buttons: crate::input_data::encode_mouse_button_set(held_buttons), + meta_key, + ctrl_key, + shift_key, + alt_key, + client_x, + client_y, + screen_x, + screen_y, + offset_x, + offset_y, + page_x, + page_y, + } + } +} + +#[cfg(feature = "serialize")] +impl From<&E> for SerializedPointInteraction { + fn from(data: &E) -> Self { + let trigger_button = data.trigger_button(); + let held_buttons = data.held_buttons(); + let coordinates = data.coordinates(); + let modifiers = data.modifiers(); + Self::new(trigger_button, held_buttons, coordinates, modifiers) + } +} + +#[cfg(feature = "serialize")] +impl PointerInteraction for SerializedPointInteraction { + fn held_buttons(&self) -> MouseButtonSet { + crate::input_data::decode_mouse_button_set(self.buttons) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button)) + } +} + +#[cfg(feature = "serialize")] +impl ModifiersInteraction for SerializedPointInteraction { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key { + modifiers.insert(Modifiers::META); + } + if self.shift_key { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +#[cfg(feature = "serialize")] +impl InteractionLocation for SerializedPointInteraction { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x.into(), self.client_y.into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x.into(), self.screen_y.into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x.into(), self.page_y.into()) + } +} + +#[cfg(feature = "serialize")] +impl InteractionElementOffset for SerializedPointInteraction { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x.into(), self.offset_y.into()) + } +} diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index b0fa0d16f..17a8a5f2d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -4,7 +4,8 @@ use crate::events::*; use dioxus_core::ElementId; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Debug, Clone, PartialEq)] +#[cfg(feature = "serialize")] +#[derive(Serialize, Debug, PartialEq)] pub struct HtmlEvent { pub element: ElementId, pub name: String, @@ -12,6 +13,7 @@ pub struct HtmlEvent { pub data: EventData, } +#[cfg(feature = "serialize")] impl<'de> Deserialize<'de> for HtmlEvent { fn deserialize(deserializer: D) -> Result where @@ -41,6 +43,7 @@ impl<'de> Deserialize<'de> for HtmlEvent { } } +#[cfg(feature = "serialize")] fn fun_name( name: &str, data: serde_value::Value, @@ -130,57 +133,90 @@ fn fun_name( Ok(data) } +#[cfg(feature = "serialize")] impl HtmlEvent { pub fn bubbles(&self) -> bool { event_bubbles(&self.name) } } -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] #[serde(untagged)] #[non_exhaustive] pub enum EventData { - Mouse(MouseData), - Clipboard(ClipboardData), - Composition(CompositionData), - Keyboard(KeyboardData), - Focus(FocusData), - Form(FormData), - Drag(DragData), - Pointer(PointerData), - Selection(SelectionData), - Touch(TouchData), - Scroll(ScrollData), - Wheel(WheelData), - Media(MediaData), - Animation(AnimationData), - Transition(TransitionData), - Toggle(ToggleData), - Image(ImageData), + Mouse(SerializedMouseData), + Clipboard(SerializedClipboardData), + Composition(SerializedCompositionData), + Keyboard(SerializedKeyboardData), + Focus(SerializedFocusData), + Form(SerializedFormData), + Drag(SerializedDragData), + Pointer(SerializedPointerData), + Selection(SerializedSelectionData), + Touch(SerializedTouchData), + Scroll(SerializedScrollData), + Wheel(SerializedWheelData), + Media(SerializedMediaData), + Animation(SerializedAnimationData), + Transition(SerializedTransitionData), + Toggle(SerializedToggleData), + Image(SerializedImageData), Mounted, } impl EventData { pub fn into_any(self) -> Rc { match self { - EventData::Mouse(data) => Rc::new(data) as Rc, - EventData::Clipboard(data) => Rc::new(data) as Rc, - EventData::Composition(data) => Rc::new(data) as Rc, - EventData::Keyboard(data) => Rc::new(data) as Rc, - EventData::Focus(data) => Rc::new(data) as Rc, - EventData::Form(data) => Rc::new(data) as Rc, - EventData::Drag(data) => Rc::new(data) as Rc, - EventData::Pointer(data) => Rc::new(data) as Rc, - EventData::Selection(data) => Rc::new(data) as Rc, - EventData::Touch(data) => Rc::new(data) as Rc, - EventData::Scroll(data) => Rc::new(data) as Rc, - EventData::Wheel(data) => Rc::new(data) as Rc, - EventData::Media(data) => Rc::new(data) as Rc, - EventData::Animation(data) => Rc::new(data) as Rc, - EventData::Transition(data) => Rc::new(data) as Rc, - EventData::Toggle(data) => Rc::new(data) as Rc, - EventData::Image(data) => Rc::new(data) as Rc, - EventData::Mounted => Rc::new(MountedData::new(())) as Rc, + EventData::Mouse(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Clipboard(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Composition(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Keyboard(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Focus(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Form(data) => Rc::new(PlatformEventData::new(Box::new(data))) as Rc, + EventData::Drag(data) => Rc::new(PlatformEventData::new(Box::new(data))) as Rc, + EventData::Pointer(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Selection(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Touch(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Scroll(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Wheel(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Media(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Animation(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Transition(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Toggle(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Image(data) => { + Rc::new(PlatformEventData::new(Box::new(data))) as Rc + } + EventData::Mounted => { + Rc::new(PlatformEventData::new(Box::new(MountedData::new(())))) as Rc + } } } } @@ -189,7 +225,7 @@ impl EventData { fn test_back_and_forth() { let data = HtmlEvent { element: ElementId(0), - data: EventData::Mouse(MouseData::default()), + data: EventData::Mouse(SerializedMouseData::default()), name: "click".to_string(), bubbles: true, }; @@ -224,3 +260,148 @@ fn test_back_and_forth() { assert_eq!(data, p); } + +/// A trait for converting from a serialized event to a concrete event type. +pub struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, _: &PlatformEventData) -> MountedData { + MountedData::from(()) + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index b2e89bf91..b1e6c5341 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -1,17 +1,17 @@ +use crate::events::HasKeyboardData; use crate::events::{ AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData, TransitionData, WheelData, }; -use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::{DragData, MountedData}; +use crate::prelude::*; use keyboard_types::{Code, Key, Modifiers}; -use std::convert::TryInto; use std::str::FromStr; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ - AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, - TransitionEvent, WheelEvent, + AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, Touch, + TouchEvent, TransitionEvent, WheelEvent, }; macro_rules! uncheck_convert { @@ -20,7 +20,7 @@ macro_rules! uncheck_convert { #[inline] fn from(e: Event) -> Self { let e: $t = e.unchecked_into(); - Self::from(&e) + Self::from(e) } } @@ -28,7 +28,7 @@ macro_rules! uncheck_convert { #[inline] fn from(e: &Event) -> Self { let e: &$t = e.unchecked_ref(); - Self::from(e) + Self::from(e.clone()) } } }; @@ -38,160 +38,368 @@ macro_rules! uncheck_convert { } uncheck_convert![ - CompositionEvent => CompositionData, - KeyboardEvent => KeyboardData, - MouseEvent => MouseData, - MouseEvent => DragData, - TouchEvent => TouchData, - PointerEvent => PointerData, - WheelEvent => WheelData, - AnimationEvent => AnimationData, - TransitionEvent => TransitionData, + web_sys::CompositionEvent => CompositionData, + web_sys::KeyboardEvent => KeyboardData, + web_sys::MouseEvent => MouseData, + web_sys::TouchEvent => TouchData, + web_sys::PointerEvent => PointerData, + web_sys::WheelEvent => WheelData, + web_sys::AnimationEvent => AnimationData, + web_sys::TransitionEvent => TransitionData, + web_sys::MouseEvent => DragData, + web_sys::FocusEvent => FocusData, ]; -impl From<&CompositionEvent> for CompositionData { - fn from(e: &CompositionEvent) -> Self { - Self { - data: e.data().unwrap_or_default(), - } +impl HasCompositionData for CompositionEvent { + fn data(&self) -> std::string::String { + self.data().unwrap_or_default() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&KeyboardEvent> for KeyboardData { - fn from(e: &KeyboardEvent) -> Self { +impl HasKeyboardData for KeyboardEvent { + fn key(&self) -> Key { + Key::from_str(self.key().as_str()).unwrap_or(Key::Unidentified) + } + + fn code(&self) -> Code { + Code::from_str(self.code().as_str()).unwrap_or(Code::Unidentified) + } + + fn location(&self) -> keyboard_types::Location { + decode_key_location(self.location() as usize) + } + + fn is_auto_repeating(&self) -> bool { + self.repeat() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl ModifiersInteraction for KeyboardEvent { + fn modifiers(&self) -> Modifiers { let mut modifiers = Modifiers::empty(); - if e.alt_key() { + if self.alt_key() { modifiers.insert(Modifiers::ALT); } - if e.ctrl_key() { + if self.ctrl_key() { modifiers.insert(Modifiers::CONTROL); } - if e.meta_key() { + if self.meta_key() { modifiers.insert(Modifiers::META); } - if e.shift_key() { + if self.shift_key() { modifiers.insert(Modifiers::SHIFT); } - Self::new( - Key::from_str(&e.key()).expect("could not parse key"), - Code::from_str(&e.code()).expect("could not parse code"), - decode_key_location( - e.location() - .try_into() - .expect("could not convert location to u32"), - ), - e.repeat(), - modifiers, - ) + modifiers } } -impl From<&MouseEvent> for MouseData { - fn from(e: &MouseEvent) -> Self { +impl HasDragData for MouseEvent {} + +impl InteractionLocation for MouseEvent { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } +} + +impl InteractionElementOffset for MouseEvent { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x().into(), self.offset_y().into()) + } +} + +impl ModifiersInteraction for MouseEvent { + fn modifiers(&self) -> Modifiers { let mut modifiers = Modifiers::empty(); - if e.alt_key() { + if self.alt_key() { modifiers.insert(Modifiers::ALT); } - if e.ctrl_key() { + if self.ctrl_key() { modifiers.insert(Modifiers::CONTROL); } - if e.meta_key() { + if self.meta_key() { modifiers.insert(Modifiers::META); } - if e.shift_key() { + if self.shift_key() { modifiers.insert(Modifiers::SHIFT); } - MouseData::new( - Coordinates::new( - ScreenPoint::new(e.screen_x().into(), e.screen_y().into()), - ClientPoint::new(e.client_x().into(), e.client_y().into()), - ElementPoint::new(e.offset_x().into(), e.offset_y().into()), - PagePoint::new(e.page_x().into(), e.page_y().into()), - ), - Some(MouseButton::from_web_code(e.button())), - decode_mouse_button_set(e.buttons()), - modifiers, + modifiers + } +} + +impl PointerInteraction for MouseEvent { + fn held_buttons(&self) -> crate::input_data::MouseButtonSet { + decode_mouse_button_set(self.buttons()) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button())) + } +} + +impl HasMouseData for MouseEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl ModifiersInteraction for TouchEvent { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key() { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key() { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key() { + modifiers.insert(Modifiers::META); + } + if self.shift_key() { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +impl crate::events::HasTouchData for TouchEvent { + fn touches(&self) -> Vec { + let touches = TouchEvent::touches(self); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn touches_changed(&self) -> Vec { + let touches = self.changed_touches(); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn target_touches(&self) -> Vec { + let touches = self.target_touches(); + let mut result = Vec::with_capacity(touches.length() as usize); + for i in 0..touches.length() { + let touch = touches.get(i).unwrap(); + result.push(TouchPoint::new(touch)); + } + result + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasTouchPointData for Touch { + fn identifier(&self) -> i32 { + self.identifier() + } + + fn radius(&self) -> ScreenPoint { + ScreenPoint::new(self.radius_x().into(), self.radius_y().into()) + } + + fn rotation(&self) -> f64 { + self.rotation_angle() as f64 + } + + fn force(&self) -> f64 { + self.force() as f64 + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl InteractionLocation for Touch { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } +} + +impl HasPointerData for PointerEvent { + fn pointer_id(&self) -> i32 { + self.pointer_id() + } + + fn width(&self) -> i32 { + self.width() + } + + fn height(&self) -> i32 { + self.height() + } + + fn pressure(&self) -> f32 { + self.pressure() + } + + fn tangential_pressure(&self) -> f32 { + self.tangential_pressure() + } + + fn tilt_x(&self) -> i32 { + self.tilt_x() + } + + fn tilt_y(&self) -> i32 { + self.tilt_y() + } + + fn twist(&self) -> i32 { + self.twist() + } + + fn pointer_type(&self) -> String { + self.pointer_type() + } + + fn is_primary(&self) -> bool { + self.is_primary() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl InteractionLocation for PointerEvent { + fn client_coordinates(&self) -> ClientPoint { + ClientPoint::new(self.client_x().into(), self.client_y().into()) + } + + fn screen_coordinates(&self) -> ScreenPoint { + ScreenPoint::new(self.screen_x().into(), self.screen_y().into()) + } + + fn page_coordinates(&self) -> PagePoint { + PagePoint::new(self.page_x().into(), self.page_y().into()) + } +} + +impl InteractionElementOffset for PointerEvent { + fn element_coordinates(&self) -> ElementPoint { + ElementPoint::new(self.offset_x().into(), self.offset_y().into()) + } +} + +impl ModifiersInteraction for PointerEvent { + fn modifiers(&self) -> Modifiers { + let mut modifiers = Modifiers::empty(); + + if self.alt_key() { + modifiers.insert(Modifiers::ALT); + } + if self.ctrl_key() { + modifiers.insert(Modifiers::CONTROL); + } + if self.meta_key() { + modifiers.insert(Modifiers::META); + } + if self.shift_key() { + modifiers.insert(Modifiers::SHIFT); + } + + modifiers + } +} + +impl PointerInteraction for PointerEvent { + fn held_buttons(&self) -> crate::input_data::MouseButtonSet { + decode_mouse_button_set(self.buttons()) + } + + fn trigger_button(&self) -> Option { + Some(MouseButton::from_web_code(self.button())) + } +} + +impl HasWheelData for WheelEvent { + fn delta(&self) -> crate::geometry::WheelDelta { + crate::geometry::WheelDelta::from_web_attributes( + self.delta_mode(), + self.delta_x(), + self.delta_y(), + self.delta_z(), ) } -} -impl From<&MouseEvent> for DragData { - fn from(value: &MouseEvent) -> Self { - Self { - mouse: MouseData::from(value), - files: None, - } + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&TouchEvent> for TouchData { - fn from(e: &TouchEvent) -> Self { - Self { - alt_key: e.alt_key(), - ctrl_key: e.ctrl_key(), - meta_key: e.meta_key(), - shift_key: e.shift_key(), - } +impl HasAnimationData for AnimationEvent { + fn animation_name(&self) -> String { + self.animation_name() + } + + fn pseudo_element(&self) -> String { + self.pseudo_element() + } + + fn elapsed_time(&self) -> f32 { + self.elapsed_time() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } -impl From<&PointerEvent> for PointerData { - fn from(e: &PointerEvent) -> Self { - Self { - alt_key: e.alt_key(), - button: e.button(), - buttons: e.buttons(), - client_x: e.client_x(), - client_y: e.client_y(), - ctrl_key: e.ctrl_key(), - meta_key: e.meta_key(), - page_x: e.page_x(), - page_y: e.page_y(), - screen_x: e.screen_x(), - screen_y: e.screen_y(), - shift_key: e.shift_key(), - pointer_id: e.pointer_id(), - width: e.width(), - height: e.height(), - pressure: e.pressure(), - tangential_pressure: e.tangential_pressure(), - tilt_x: e.tilt_x(), - tilt_y: e.tilt_y(), - twist: e.twist(), - pointer_type: e.pointer_type(), - is_primary: e.is_primary(), - // get_modifier_state: evt.get_modifier_state(), - } +impl HasTransitionData for TransitionEvent { + fn elapsed_time(&self) -> f32 { + self.elapsed_time() } -} -impl From<&WheelEvent> for WheelData { - fn from(e: &WheelEvent) -> Self { - WheelData::from_web_attributes(e.delta_mode(), e.delta_x(), e.delta_y(), e.delta_z()) + fn property_name(&self) -> String { + self.property_name() } -} -impl From<&AnimationEvent> for AnimationData { - fn from(e: &AnimationEvent) -> Self { - Self { - elapsed_time: e.elapsed_time(), - animation_name: e.animation_name(), - pseudo_element: e.pseudo_element(), - } + fn pseudo_element(&self) -> String { + self.pseudo_element() } -} -impl From<&TransitionEvent> for TransitionData { - fn from(e: &TransitionEvent) -> Self { - Self { - elapsed_time: e.elapsed_time(), - property_name: e.property_name(), - pseudo_element: e.pseudo_element(), - } + fn as_any(&self) -> &dyn std::any::Any { + self } } @@ -217,8 +425,8 @@ impl crate::RenderedElementBacking for web_sys::Element { Box::pin(async { result }) } - fn get_raw_element(&self) -> crate::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn scroll_to( @@ -262,3 +470,45 @@ impl std::fmt::Display for FocusError { } impl std::error::Error for FocusError {} + +impl HasScrollData for Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasClipboardData for Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl From<&Event> for ClipboardData { + fn from(e: &Event) -> Self { + ClipboardData::new(e.clone()) + } +} + +impl HasFocusData for web_sys::FocusEvent { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasToggleData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasSelectionData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl HasMediaData for web_sys::Event { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7298cd4bd..31e31a37f 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,13 +14,14 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { workspace = true, optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } -sledgehammer_bindgen = { version = "0.2.1", optional = true } +sledgehammer_bindgen = { version = "0.3.1", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] serialize = ["serde"] -web = ["wasm-bindgen", "js-sys", "web-sys"] -sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] +sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"] +web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"] +binary-protocol = ["sledgehammer", "wasm-bindgen"] minimal_bindings = [] diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs deleted file mode 100644 index 4fd9016f1..000000000 --- a/packages/interpreter/src/bindings.rs +++ /dev/null @@ -1,83 +0,0 @@ -#![allow(clippy::unused_unit, non_upper_case_globals)] - -use js_sys::Function; -use wasm_bindgen::prelude::*; -use web_sys::Element; - -#[wasm_bindgen(module = "/src/interpreter.js")] -extern "C" { - pub type InterpreterConfig; - #[wasm_bindgen(constructor)] - pub fn new(intercept_link_redirects: bool) -> InterpreterConfig; - - pub type Interpreter; - - #[wasm_bindgen(constructor)] - pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter; - - #[wasm_bindgen(method)] - pub fn SaveTemplate(this: &Interpreter, template: JsValue); - - #[wasm_bindgen(method)] - pub fn MountToRoot(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); - - #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32); - - #[wasm_bindgen(method)] - pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32); - - #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32); - - #[wasm_bindgen(method)] - pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32); - - #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, id: u32, n: u32); - - #[wasm_bindgen(method)] - pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool); - - #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, id: u32, text: JsValue); - - #[wasm_bindgen(method)] - pub fn NewEventListener( - this: &Interpreter, - name: &str, - id: u32, - bubbles: bool, - handler: &Function, - ); - - #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32); - - #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn PushRoot(this: &Interpreter, id: u32); - - #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, id: u32, m: u32); -} diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js index 1583da10e..0c64085f9 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.js @@ -1,3 +1,49 @@ +export function setAttributeInner(node, field, value, ns) { + const name = field; + if (ns === "style") { + // ????? why do we need to do this + if (node.style === undefined) { + node.style = {}; + } + node.style[name] = value; + } else if (!!ns) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value !== node.value) { + node.value = value; + } + break; + case "initial_value": + node.defaultValue = value; + break; + case "checked": + node.checked = truthy(value); + break; + case "initial_checked": + node.defaultChecked = truthy(value); + break; + case "selected": + node.selected = truthy(value); + break; + case "initial_selected": + node.defaultSelected = truthy(value); + 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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } +} + const bool_attrs = { allowfullscreen: true, allowpaymentrequest: true, @@ -28,46 +74,6 @@ const bool_attrs = { webkitdirectory: true, }; -export function setAttributeInner(node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - 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 "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - 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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } -} - function truthy(val) { return val === "true" || val === true; } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 441bb5634..c8384a8b7 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,390 +1,9 @@ -import { setAttributeInner } from "./common.js"; - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener( - event_name, - this.global[event_name].callback - ); - delete this.global[event_name]; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } - - removeAllNonBubbling(element) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } -} - class InterpreterConfig { constructor(intercept_link_redirects) { this.intercept_link_redirects = intercept_link_redirects; } } -class Interpreter { - constructor(root, config) { - this.config = config; - this.root = root; - this.listeners = new ListenerMap(root); - this.nodes = [root]; - this.stack = [root]; - this.handlers = {}; - this.templates = {}; - this.lastNodeWasText = false; - } - top() { - return this.stack[this.stack.length - 1]; - } - pop() { - return this.stack.pop(); - } - MountToRoot() { - this.AppendChildren(this.stack.length - 1); - } - SetNode(id, node) { - this.nodes[id] = node; - } - PushRoot(root) { - const node = this.nodes[root]; - this.stack.push(node); - } - PopRoot() { - this.stack.pop(); - } - AppendChildren(many) { - // let root = this.nodes[id]; - 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); - if (is_element_node(root.nodeType)) { - this.listeners.removeAllNonBubbling(root); - } - 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) { - if (is_element_node(node)) { - this.listeners.removeAllNonBubbling(node); - } - node.remove(); - } - } - CreateTextNode(text, root) { - const node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - } - CreatePlaceholder(root) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - NewEventListener(event_name, root, bubbles, handler) { - const element = this.nodes[root]; - element.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, element, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - const element = this.nodes[root]; - element.removeAttribute(`data-dioxus-id`); - this.listeners.remove(element, event_name, bubbles); - } - SetText(root, text) { - this.nodes[root].textContent = text; - } - SetAttribute(id, field, value, ns) { - if (value === null) { - this.RemoveAttribute(id, field, ns); - } else { - const node = this.nodes[id]; - setAttributeInner(node, field, value, ns); - } - } - RemoveAttribute(root, field, ns) { - const node = this.nodes[root]; - if (!ns) { - switch (field) { - case "value": - node.value = ""; - break; - case "checked": - node.checked = false; - break; - case "selected": - node.selected = false; - break; - case "dangerous_inner_html": - node.innerHTML = ""; - break; - default: - node.removeAttribute(field); - break; - } - } else if (ns == "style") { - node.style.removeProperty(name); - } else { - node.removeAttributeNS(ns, field); - } - } - - GetClientRect(id) { - const node = this.nodes[id]; - if (!node) { - return; - } - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - - ScrollTo(id, behavior) { - const node = this.nodes[id]; - if (!node) { - return false; - } - node.scrollIntoView({ - behavior: behavior, - }); - return true; - } - - /// Set the focus on the element - SetFocus(id, focus) { - const node = this.nodes[id]; - if (!node) { - return false; - } - if (focus) { - node.focus(); - } else { - node.blur(); - } - return true; - } - - handleEdits(edits) { - for (let template of edits.templates) { - this.SaveTemplate(template); - } - - for (let edit of edits.edits) { - this.handleEdit(edit); - } - - /*POST_HANDLE_EDITS*/ - } - - SaveTemplate(template) { - let roots = []; - for (let root of template.roots) { - roots.push(this.MakeTemplateNode(root)); - } - this.templates[template.name] = roots; - } - - MakeTemplateNode(node) { - switch (node.type) { - case "Text": - return document.createTextNode(node.text); - case "Dynamic": - let dyn = document.createElement("pre"); - dyn.hidden = true; - return dyn; - case "DynamicText": - return document.createTextNode("placeholder"); - case "Element": - let el; - - if (node.namespace != null) { - el = document.createElementNS(node.namespace, node.tag); - } else { - el = document.createElement(node.tag); - } - - for (let attr of node.attrs) { - if (attr.type == "Static") { - setAttributeInner(el, attr.name, attr.value, attr.namespace); - } - } - - for (let child of node.children) { - el.appendChild(this.MakeTemplateNode(child)); - } - - return el; - } - } - AssignId(path, id) { - this.nodes[id] = this.LoadChild(path); - } - LoadChild(path) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < path.length; i++) { - node = node.childNodes[path[i]]; - } - - return node; - } - HydrateText(path, value, id) { - let node = this.LoadChild(path); - - if (node.nodeType == Node.TEXT_NODE) { - node.textContent = value; - } else { - // replace with a textnode - let text = document.createTextNode(value); - node.replaceWith(text); - node = text; - } - - this.nodes[id] = node; - } - ReplacePlaceholder(path, m) { - let els = this.stack.splice(this.stack.length - m); - let node = this.LoadChild(path); - node.replaceWith(...els); - } - LoadTemplate(name, index, id) { - let node = this.templates[name][index].cloneNode(true); - this.nodes[id] = node; - this.stack.push(node); - } - handleEdit(edit) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit.m); - break; - case "AssignId": - this.AssignId(edit.path, edit.id); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.id); - break; - case "CreateTextNode": - this.CreateTextNode(edit.value, edit.id); - break; - case "HydrateText": - this.HydrateText(edit.path, edit.value, edit.id); - break; - case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index, edit.id); - break; - case "PushRoot": - this.PushRoot(edit.id); - break; - case "ReplaceWith": - this.ReplaceWith(edit.id, edit.m); - break; - case "ReplacePlaceholder": - this.ReplacePlaceholder(edit.path, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.id, edit.m); - break; - case "InsertBefore": - this.InsertBefore(edit.id, edit.m); - break; - case "Remove": - this.Remove(edit.id); - break; - case "SetText": - this.SetText(edit.id, edit.value); - break; - case "SetAttribute": - this.SetAttribute(edit.id, edit.name, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.id, edit.name, edit.ns); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.id, edit.name); - break; - case "NewEventListener": - let bubbles = event_bubbles(edit.name); - - // if this is a mounted listener, we send the event immediately - if (edit.name === "mounted") { - window.ipc.postMessage( - serializeIpcMessage("user_event", { - name: edit.name, - element: edit.id, - data: null, - bubbles, - }) - ); - } else { - this.NewEventListener(edit.name, edit.id, bubbles, (event) => { - handler(event, edit.name, bubbles, this.config); - }); - } - break; - } - } -} - // this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation async function handler(event, name, bubbles, config) { @@ -416,7 +35,7 @@ async function handler(event, name, bubbles, config) { const href = a_element.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) + window.interpreter.serializeIpcMessage("browser_open", { href }) ); } } @@ -444,7 +63,44 @@ async function handler(event, name, bubbles, config) { let contents = await serialize_event(event); - /*POST_EVENT_SERIALIZATION*/ + // TODO: this should be liveview only + if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + } if ( target.tagName === "FORM" && @@ -453,8 +109,18 @@ async function handler(event, name, bubbles, config) { const formData = new FormData(target); for (let name of formData.keys()) { - let value = formData.getAll(name); - contents.values[name] = value; + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } } } @@ -476,7 +142,7 @@ async function handler(event, name, bubbles, config) { return; } window.ipc.postMessage( - serializeIpcMessage("user_event", { + window.interpreter.serializeIpcMessage("user_event", { name: name, element: parseInt(realId), data: contents, @@ -506,6 +172,130 @@ function find_real_id(target) { return realId; } +class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = null; + } + + create(event_name, element, bubbles, handler) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.root.addEventListener(event_name, handler); + } else { + this.global[event_name].active++; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, handler); + } + } + + remove(element, event_name, bubbles) { + if (bubbles) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.global[event_name].callback); + } + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} +function LoadChild(array) { + // iterate through each number and get that child + node = stack[stack.length - 1]; + + for (let i = 0; i < array.length; i++) { + end = array[i]; + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; +} +const listeners = new ListenerMap(); +let nodes = []; +let stack = []; +let root; +const templates = {}; +let node, els, end, k; + +function AppendChildren(id, many) { + root = nodes[id]; + els = stack.splice(stack.length - many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } +} + +window.interpreter = {} + +window.interpreter.initialize = function (root) { + nodes = [root]; + stack = [root]; + listeners.root = root; +} + +window.interpreter.getClientRect = function (id) { + const node = nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [rect.x, rect.y], + size: [rect.width, rect.height], + }; +} + +window.interpreter.scrollTo = function (id, behavior) { + const node = nodes[id]; + if (!node) { + return false; + } + node.scrollIntoView({ + behavior: behavior, + }); + return true; +} + +/// Set the focus on the element +window.interpreter.setFocus = function (id, focus) { + const node = nodes[id]; + if (!node) { + return false; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return true; +} + function get_mouse_data(event) { const { altKey, @@ -787,21 +577,7 @@ async function serialize_event(event) { } } } -async function serializeFileList(fileList) { - const file_contents = {}; - - for (let i = 0; i < fileList.length; i++) { - const file = fileList[i]; - - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - return { - files: file_contents, - }; -} -function serializeIpcMessage(method, params = {}) { +window.interpreter.serializeIpcMessage = function (method, params = {}) { return JSON.stringify({ method, params }); } diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 47093c29c..ccad95abd 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,3 +1,7 @@ +#![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 static INTERPRETER_JS: &str = include_str!("./interpreter.js"); pub static COMMON_JS: &str = include_str!("./common.js"); @@ -6,14 +10,8 @@ mod sledgehammer_bindings; #[cfg(feature = "sledgehammer")] pub use sledgehammer_bindings::*; -#[cfg(feature = "web")] -mod bindings; - -#[cfg(feature = "web")] -pub use bindings::Interpreter; - // Common bindings for minimal usage. -#[cfg(feature = "minimal_bindings")] +#[cfg(all(feature = "minimal_bindings", feature = "web"))] pub mod minimal_bindings { use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; #[wasm_bindgen(module = "/src/common.js")] diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 7d5f44f7d..23831f3e6 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -1,9 +1,17 @@ +#[cfg(feature = "web")] use js_sys::Function; +#[cfg(feature = "web")] use sledgehammer_bindgen::bindgen; +#[cfg(feature = "web")] use web_sys::Node; -#[bindgen] +#[cfg(feature = "web")] +pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + +#[cfg(feature = "web")] +#[bindgen(module)] mod js { + const JS_FILE: &str = "./src/common.js"; const JS: &str = r#" class ListenerMap { constructor(root) { @@ -57,45 +65,6 @@ mod js { delete this.local[id]; } } - function SetAttributeInner(node, field, value, ns) { - const name = field; - if (ns === "style") { - // ????? why do we need to do this - if (node.style === undefined) { - node.style = {}; - } - node.style[name] = value; - } else if (ns !== null && ns !== undefined && ns !== "") { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "initial_value": - node.defaultValue = value; - break; - case "checked": - node.checked = truthy(value); - break; - case "selected": - node.selected = truthy(value); - 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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } function LoadChild(ptr, len) { // iterate through each number and get that child node = stack[stack.length - 1]; @@ -117,13 +86,48 @@ mod js { export function save_template(nodes, tmpl_id) { templates[tmpl_id] = nodes; } - export function set_node(id, node) { - nodes[id] = node; + export function hydrate(ids) { + console.log("hydrating", ids); + const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); + for (let i = 0; i < hydrateNodes.length; i++) { + const hydrateNode = hydrateNodes[i]; + const hydration = hydrateNode.getAttribute('data-node-hydration'); + const split = hydration.split(','); + const id = ids[parseInt(split[0])]; + nodes[id] = hydrateNode; + console.log("hydrating node", hydrateNode, id); + if (split.length > 1) { + hydrateNode.listening = split.length - 1; + hydrateNode.setAttribute('data-dioxus-id', id); + for (let j = 1; j < split.length; j++) { + const listener = split[j]; + const split2 = listener.split(':'); + const event_name = split2[0]; + const bubbles = split2[1] === '1'; + console.log("hydrating listener", event_name, bubbles); + listeners.create(event_name, hydrateNode, bubbles); + } + } + } + const treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_COMMENT, + ); + let currentNode = treeWalker.nextNode(); + while (currentNode) { + const id = currentNode.textContent; + const split = id.split('node-id'); + if (split.length > 1) { + console.log("hydrating text", currentNode.nextSibling, id); + nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; + } + currentNode = treeWalker.nextNode(); + } } export function get_node(id) { return nodes[id]; } - export function initilize(root, handler) { + export function initialize(root, handler) { listeners.handler = handler; nodes = [root]; stack = [root]; @@ -136,52 +140,20 @@ mod js { root.appendChild(els[k]); } } - 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, - webkitdirectory: true, - }; - function truthy(val) { - return val === "true" || val === true; - } "#; extern "C" { #[wasm_bindgen] - pub fn save_template(nodes: Vec, tmpl_id: u32); + pub fn save_template(nodes: Vec, tmpl_id: u16); #[wasm_bindgen] - pub fn set_node(id: u32, node: Node); + pub fn hydrate(ids: Vec); #[wasm_bindgen] pub fn get_node(id: u32) -> Node; #[wasm_bindgen] - pub fn initilize(root: Node, handler: &Function); + pub fn initialize(root: Node, handler: &Function); } fn mount_to_root() { @@ -190,19 +162,19 @@ mod js { fn push_root(root: u32) { "{stack.push(nodes[$root$]);}" } - fn append_children(id: u32, many: u32) { + fn append_children(id: u32, many: u16) { "{AppendChildren($id$, $many$);}" } fn pop_root() { "{stack.pop();}" } - fn replace_with(id: u32, n: u32) { + fn replace_with(id: u32, n: u16) { "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" } - fn insert_after(id: u32, n: u32) { + fn insert_after(id: u32, n: u16) { "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" } - fn insert_before(id: u32, n: u32) { + fn insert_before(id: u32, n: u16) { "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" } fn remove(id: u32) { @@ -227,7 +199,7 @@ mod js { "{nodes[$id$].textContent = $text$;}" } fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { - "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" } fn remove_attribute(id: u32, field: &str, ns: &str) { r#"{ @@ -273,10 +245,167 @@ mod js { nodes[$id$] = node; }"# } - fn replace_placeholder(ptr: u32, len: u8, n: u32) { + fn replace_placeholder(ptr: u32, len: u8, n: u16) { "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" } - fn load_template(tmpl_id: u32, index: u32, id: u32) { + fn load_template(tmpl_id: u16, index: u16, id: u32) { "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" } } + +#[cfg(feature = "binary-protocol")] +pub mod binary_protocol { + use sledgehammer_bindgen::bindgen; + pub const SLEDGEHAMMER_JS: &str = GENERATED_JS; + + #[bindgen] + mod protocol_js { + const JS_FILE: &str = "./src/interpreter.js"; + const JS_FILE: &str = "./src/common.js"; + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u16) { + "{AppendChildren($id$, $many$);}" + } + fn append_children_to_top(many: u16) { + "{ + root = stack[stack.length-many-1]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + }" + } + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u16) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u16) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u16) { + "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + } + fn remove(id: u32) { + "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + } + fn create_raw_text(text: &str) { + "{stack.push(document.createTextNode($text$));}" + } + fn create_text_node(text: &str, id: u32) { + "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + } + fn create_element(element: &'static str) { + "{stack.push(document.createElement($element$))}" + } + fn create_element_ns(element: &'static str, ns: &'static str) { + "{stack.push(document.createElementNS($ns$, $element$))}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn add_placeholder() { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}" + } + fn new_event_listener(event: &str, id: u32, bubbles: u8) { + r#" + bubbles = bubbles == 1; + node = nodes[id]; + if(node.listening){ + node.listening += 1; + } else { + node.listening = 1; + } + node.setAttribute('data-dioxus-id', `\${id}`); + const event_name = $event$; + + // if this is a mounted listener, we send the event immediately + if (event_name === "mounted") { + window.ipc.postMessage( + window.interpreter.serializeIpcMessage("user_event", { + name: event_name, + element: id, + data: null, + bubbles, + }) + ); + } else { + listeners.create(event_name, node, bubbles, (event) => { + handler(event, event_name, bubbles, config); + }); + }"# + } + fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { + "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + } + fn set_text(id: u32, text: &str) { + "{nodes[$id$].textContent = $text$;}" + } + fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { + "{node = nodes[$id$]; setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn set_top_attribute(field: &str, value: &str, ns: &str) { + "{setAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{ + node = nodes[$id$]; + if (!ns) { + switch (field) { + case "value": + node.value = ""; + break; + case "checked": + node.checked = false; + break; + case "selected": + node.selected = false; + break; + case "dangerous_inner_html": + node.innerHTML = ""; + break; + default: + node.removeAttribute(field); + break; + } + } else if (ns == "style") { + node.style.removeProperty(name); + } else { + node.removeAttributeNS(ns, field); + } + }"# + } + fn assign_id(array: &[u8], id: u32) { + "{nodes[$id$] = LoadChild($array$);}" + } + fn hydrate_text(array: &[u8], value: &str, id: u32) { + r#"{ + node = LoadChild($array$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + nodes[$id$] = node; + }"# + } + fn replace_placeholder(array: &[u8], n: u16) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u16, index: u16, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } + fn add_templates(tmpl_id: u16, len: u16) { + "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}" + } + } +} diff --git a/packages/liveview/.gitignore b/packages/liveview/.gitignore new file mode 100644 index 000000000..1aec3a9f9 --- /dev/null +++ b/packages/liveview/.gitignore @@ -0,0 +1 @@ +/src/minified.js \ No newline at end of file diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index cbd7b75f5..2ed7894c7 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -22,9 +22,10 @@ tokio-stream = { version = "0.1.11", features = ["net"] } tokio-util = { version = "0.7.4", features = ["rt"] } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" -dioxus-html = { workspace = true, features = ["serialize"] } +dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] } +rustc-hash = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } -dioxus-interpreter-js = { workspace = true } +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-hot-reload = { workspace = true, optional = true } # warp @@ -34,10 +35,14 @@ warp = { version = "0.3.3", optional = true } axum = { version = "0.6.1", optional = true, features = ["ws"] } # salvo -salvo = { version = "0.44.1", optional = true, features = ["ws"] } +salvo = { version = "0.63.0", optional = true, features = ["websocket"] } once_cell = "1.17.1" async-trait = "0.1.71" +# rocket +rocket = { version = "0.5.0", optional = true } +rocket_ws = { version = "0.1.0", optional = true } + # actix is ... complicated? # actix-files = { version = "0.6.2", optional = true } # actix-web = { version = "4.2.1", optional = true } @@ -49,18 +54,29 @@ tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } warp = "0.3.3" axum = { version = "0.6.1", features = ["ws"] } -salvo = { version = "0.44.1", features = ["affix", "ws"] } +salvo = { version = "0.63.0", features = ["affix", "websocket"] } +rocket = "0.5.0" +rocket_ws = "0.1.0" tower = "0.4.13" +[build-dependencies] +dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } +minify-js = "0.5.6" + [features] default = ["hot-reload"] # actix = ["actix-files", "actix-web", "actix-ws"] hot-reload = ["dioxus-hot-reload"] +rocket = ["dep:rocket", "dep:rocket_ws"] [[example]] name = "axum" required-features = ["axum"] +[[example]] +name = "axum_stress" +required-features = ["axum"] + [[example]] name = "salvo" required-features = ["salvo"] @@ -68,3 +84,7 @@ required-features = ["salvo"] [[example]] name = "warp" required-features = ["warp"] + +[[example]] +name = "rocket" +required-features = ["rocket"] diff --git a/packages/liveview/README.md b/packages/liveview/README.md index cbae3c53a..13af3c36f 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -28,6 +28,7 @@ The current backend frameworks supported include: - Axum - Warp - Salvo +- Rocket Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework. diff --git a/packages/liveview/build.rs b/packages/liveview/build.rs new file mode 100644 index 000000000..d4dbeb09a --- /dev/null +++ b/packages/liveview/build.rs @@ -0,0 +1,64 @@ +use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS; +use minify_js::*; +use std::io::Write; + +fn main() { + let serialize_file_uploads = r#"if ( + target.tagName === "INPUT" && + (event.type === "change" || event.type === "input") + ) { + const type = target.getAttribute("type"); + if (type === "file") { + async function read_files() { + const files = target.files; + const file_contents = {}; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + file_contents[file.name] = Array.from( + new Uint8Array(await file.arrayBuffer()) + ); + } + let file_engine = { + files: file_contents, + }; + contents.files = file_engine; + + if (realId === null) { + return; + } + const message = window.interpreter.serializeIpcMessage("user_event", { + name: name, + element: parseInt(realId), + data: contents, + bubbles, + }); + window.ipc.postMessage(message); + } + read_files(); + return; + } + }"#; + let mut interpreter = SLEDGEHAMMER_JS + .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads) + .replace("export", ""); + while let Some(import_start) = interpreter.find("import") { + let import_end = interpreter[import_start..] + .find(|c| c == ';' || c == '\n') + .map(|i| i + import_start) + .unwrap_or_else(|| interpreter.len()); + interpreter.replace_range(import_start..import_end, ""); + } + + let main_js = std::fs::read_to_string("src/main.js").unwrap(); + + let js = format!("{interpreter}\n{main_js}"); + + let session = Session::new(); + let mut out = Vec::new(); + minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap(); + let minified = String::from_utf8(out).unwrap(); + let mut file = std::fs::File::create("src/minified.js").unwrap(); + file.write_all(minified.as_bytes()).unwrap(); +} diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 2e7f10ee7..6584d5e9b 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -19,32 +19,43 @@ async fn main() { let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); let view = dioxus_liveview::LiveViewPool::new(); + let index_page_with_glue = |glue: &str| { + Html(format!( + r#" + + + Dioxus LiveView with axum +

+ {glue} + + "#, + )) + }; - let app = Router::new() - .route( - "/", - get(move || async move { - Html(format!( - r#" - - - Dioxus LiveView with axum -
- {glue} - - "#, - glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) - )) - }), - ) - .route( - "/ws", - get(move |ws: WebSocketUpgrade| async move { - ws.on_upgrade(move |socket| async move { - _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; - }) - }), - ); + let app = + Router::new() + .route( + "/", + get(move || async move { + index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!( + "ws://{addr}/ws" + ))) + }), + ) + .route( + "/as-path", + get(move || async move { + index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws")) + }), + ) + .route( + "/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; + }) + }), + ); println!("Listening on http://{addr}"); diff --git a/packages/liveview/examples/axum_stress.rs b/packages/liveview/examples/axum_stress.rs new file mode 100644 index 000000000..46a4ceb10 --- /dev/null +++ b/packages/liveview/examples/axum_stress.rs @@ -0,0 +1,65 @@ +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let state = use_state(cx, || 0); + use_future(cx, (), |_| { + to_owned![state]; + async move { + loop { + state += 1; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } + }); + + cx.render(rsx! { + for _ in 0..10000 { + div { + "hello axum! {state}" + } + } + }) +} + +#[tokio::main] +async fn main() { + pretty_env_logger::init(); + + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = dioxus_liveview::LiveViewPool::new(); + + let app = Router::new() + .route( + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with axum +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + .route( + "/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; + }) + }), + ); + + println!("Listening on http://{addr}"); + + axum::Server::bind(&addr.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/packages/liveview/examples/rocket.rs b/packages/liveview/examples/rocket.rs new file mode 100644 index 000000000..8256db7f8 --- /dev/null +++ b/packages/liveview/examples/rocket.rs @@ -0,0 +1,76 @@ +#[macro_use] +extern crate rocket; + +use dioxus::prelude::*; +use dioxus_liveview::LiveViewPool; +use rocket::response::content::RawHtml; +use rocket::{Config, Rocket, State}; +use rocket_ws::{Channel, WebSocket}; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello Rocket! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} + +fn index_page_with_glue(glue: &str) -> RawHtml { + RawHtml(format!( + r#" + + + Dioxus LiveView with Rocket +
+ {glue} + + "#, + glue = glue + )) +} + +#[get("/")] +async fn index(config: &Config) -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!( + "ws://{addr}:{port}/ws", + addr = config.address, + port = config.port, + ))) +} + +#[get("/as-path")] +async fn as_path() -> RawHtml { + index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws")) +} + +#[get("/ws")] +fn ws(ws: WebSocket, pool: &State) -> Channel<'static> { + let pool = pool.inner().to_owned(); + + ws.channel(move |stream| { + Box::pin(async move { + let _ = pool + .launch(dioxus_liveview::rocket_socket(stream), app) + .await; + Ok(()) + }) + }) +} + +#[tokio::main] +async fn main() { + let view = dioxus_liveview::LiveViewPool::new(); + + Rocket::build() + .manage(view) + .mount("/", routes![index, as_path, ws]) + .ignite() + .await + .expect("Failed to ignite rocket") + .launch() + .await + .expect("Failed to launch rocket"); +} diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index dda37acc1..8c6f5ce31 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -20,5 +20,5 @@ fn transform_rx(message: Result) -> Result, LiveVi } async fn transform_tx(message: Vec) -> Result { - Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) + Ok(Message::Binary(message)) } diff --git a/packages/liveview/src/adapters/rocket_adapter.rs b/packages/liveview/src/adapters/rocket_adapter.rs new file mode 100644 index 000000000..8a9840bf2 --- /dev/null +++ b/packages/liveview/src/adapters/rocket_adapter.rs @@ -0,0 +1,25 @@ +use crate::{LiveViewError, LiveViewSocket}; +use rocket::futures::{SinkExt, StreamExt}; +use rocket_ws::{result::Error, stream::DuplexStream, Message}; + +/// Convert a rocket websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the rocket web framework +pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket { + stream + .map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result, LiveViewError> { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map(|s| s.into_bytes()) + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: Vec) -> Result { + Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) +} diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs index ae0eb4efe..d1a66efa2 100644 --- a/packages/liveview/src/element.rs +++ b/packages/liveview/src/element.rs @@ -4,6 +4,7 @@ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking} use crate::query::QueryEngine; /// A mounted element passed to onmounted events +#[derive(Clone)] pub struct LiveviewElement { id: ElementId, query: QueryEngine, @@ -16,8 +17,8 @@ impl LiveviewElement { } impl RenderedElementBacking for LiveviewElement { - fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { - Ok(self) + fn as_any(&self) -> &dyn std::any::Any { + self } fn get_client_rect( @@ -29,7 +30,7 @@ impl RenderedElementBacking for LiveviewElement { >, >, > { - let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let script = format!("return window.interpreter.getClientRect({});", self.id.0); let fut = self .query @@ -53,7 +54,7 @@ impl RenderedElementBacking for LiveviewElement { behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.ScrollTo({}, {});", + "return window.interpreter.scrollTo({}, {});", self.id.0, serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") ); @@ -77,7 +78,7 @@ impl RenderedElementBacking for LiveviewElement { focus: bool, ) -> std::pin::Pin>>> { let script = format!( - "return window.interpreter.SetFocus({}, {});", + "return window.interpreter.setFocus({}, {});", self.id.0, focus ); diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs new file mode 100644 index 000000000..b764bf7cc --- /dev/null +++ b/packages/liveview/src/events.rs @@ -0,0 +1,149 @@ +//! Convert a serialized event to an event trigger + +use dioxus_html::*; + +use crate::element::LiveviewElement; + +pub(crate) struct SerializedHtmlEventConverter; + +impl HtmlEventConverter for SerializedHtmlEventConverter { + fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_drag_data(&self, event: &PlatformEventData) -> DragData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_form_data(&self, event: &PlatformEventData) -> FormData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_image_data(&self, event: &PlatformEventData) -> ImageData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_media_data(&self, event: &PlatformEventData) -> MediaData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData { + event.downcast::().cloned().unwrap().into() + } + + fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } + + fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData { + event + .downcast::() + .cloned() + .unwrap() + .into() + } +} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 0acc5cfc9..a8bc83622 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -1,3 +1,7 @@ +#![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 adapters { #[cfg(feature = "warp")] pub mod warp_adapter; @@ -14,6 +18,11 @@ pub mod adapters { #[cfg(feature = "salvo")] pub use salvo_adapter::*; + + #[cfg(feature = "rocket")] + pub mod rocket_adapter; + #[cfg(feature = "rocket")] + pub use rocket_adapter::*; } pub use adapters::*; @@ -24,6 +33,7 @@ mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; mod eval; +mod events; pub trait WebsocketTx: SinkExt {} impl WebsocketTx for T where T: SinkExt {} @@ -37,62 +47,53 @@ pub enum LiveViewError { SendingFailed, } -use once_cell::sync::Lazy; - -static INTERPRETER_JS: Lazy = Lazy::new(|| { - let interpreter = dioxus_interpreter_js::INTERPRETER_JS; - let serialize_file_uploads = r#"if ( - target.tagName === "INPUT" && - (event.type === "change" || event.type === "input") - ) { - const type = target.getAttribute("type"); - if (type === "file") { - async function read_files() { - const file_engine=await serializeFileList( target.files); - contents.files = file_engine; - - if (realId === null) { - return; - } - const message = serializeIpcMessage("user_event", { - name: name, - element: parseInt(realId), - data: contents, - bubbles, - }); - window.ipc.postMessage(message); - } - read_files(); - return; - } - }"#; - - let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads); - interpreter.replace("import { setAttributeInner } from \"./common.js\";", "") -}); - -static COMMON_JS: Lazy = Lazy::new(|| { - let common = dioxus_interpreter_js::COMMON_JS; - common.replace("export", "") -}); - -static MAIN_JS: &str = include_str!("./main.js"); +static MINIFIED: &str = include_str!("./minified.js"); /// This script that gets injected into your app connects this page to the websocket endpoint /// /// Once the endpoint is connected, it will send the initial state of the app, and then start -/// processing user events and returning edits to the liveview instance -pub fn interpreter_glue(url: &str) -> String { - let js = &*INTERPRETER_JS; - let common = &*COMMON_JS; +/// processing user events and returning edits to the liveview instance. +/// +/// You can pass a relative path prefixed with "/", or enter a full URL including the protocol +/// (`ws:` or `wss:`) as an argument. +/// +/// If you enter a relative path, the web client automatically prefixes the host address in +/// `window.location` when creating a web socket to LiveView. +/// +/// ``` +/// // Creates websocket connection to same host as current page +/// interpreter_glue("/api/liveview"); +/// +/// // Creates websocket connection to specified url +/// interpreter_glue("ws://localhost:8080/api/liveview"); +/// ``` +pub fn interpreter_glue(url_or_path: &str) -> String { + // If the url starts with a `/`, generate glue which reuses current host + let get_ws_url = if url_or_path.starts_with('/') { + r#" + let loc = window.location; + let new_url = ""; + if (loc.protocol === "https:") {{ + new_url = "wss:"; + }} else {{ + new_url = "ws:"; + }} + new_url += "//" + loc.host + path; + return new_url; + "# + } else { + "return path;" + }; + format!( r#" "# ) diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index e98d02a9c..22267dbab 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -1,3 +1,5 @@ +const config = new InterpreterConfig(false); + function main() { let root = window.document.getElementById("main"); if (root != null) { @@ -7,10 +9,9 @@ function main() { class IPC { constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root, new InterpreterConfig(false)); - - let ws = new WebSocket(WS_ADDR); + window.interpreter.initialize(root); + const ws = new WebSocket(WS_ADDR); + ws.binaryType = "arraybuffer"; function ping() { ws.send("__ping__"); @@ -19,7 +20,7 @@ class IPC { ws.onopen = () => { // we ping every 30 seconds to keep the websocket alive setInterval(ping, 30000); - ws.send(serializeIpcMessage("initialize")); + ws.send(window.interpreter.serializeIpcMessage("initialize")); }; ws.onerror = (err) => { @@ -27,17 +28,29 @@ class IPC { }; ws.onmessage = (message) => { - // Ignore pongs - if (message.data != "__pong__") { - const event = JSON.parse(message.data); - switch (event.type) { - case "edits": - let edits = event.data; - window.interpreter.handleEdits(edits); - break; - case "query": - Function("Eval", `"use strict";${event.data};`)(); - break; + const u8view = new Uint8Array(message.data); + const binaryFrame = u8view[0] == 1; + const messageData = message.data.slice(1) + // The first byte tells the shim if this is a binary of text frame + if (binaryFrame) { + // binary frame + run_from_bytes(messageData); + } + else { + // text frame + + let decoder = new TextDecoder("utf-8"); + + // Using decode method to get string output + let str = decoder.decode(messageData); + // Ignore pongs + if (str != "__pong__") { + const event = JSON.parse(str); + switch (event.type) { + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } } }; @@ -49,3 +62,5 @@ class IPC { this.ws.send(msg); } } + +main(); \ No newline at end of file diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 2fd06cec6..6fac0abfe 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,12 +1,15 @@ use crate::{ element::LiveviewElement, eval::init_eval, + events::SerializedHtmlEventConverter, query::{QueryEngine, QueryResult}, LiveViewError, }; -use dioxus_core::{prelude::*, Mutations}; -use dioxus_html::{EventData, HtmlEvent, MountedData}; +use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations}; +use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData, PlatformEventData}; +use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, SinkExt, StreamExt}; +use rustc_hash::FxHashMap; use serde::Serialize; use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; @@ -24,6 +27,9 @@ impl Default for LiveViewPool { impl LiveViewPool { pub fn new() -> Self { + // Set the event converter + dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter)); + LiveViewPool { pool: LocalPoolHandle::new(16), } @@ -107,7 +113,7 @@ impl LiveViewSocket for S where /// /// This function makes it easy to integrate Dioxus LiveView with any socket-based framework. /// -/// As long as your framework can provide a Sink and Stream of Strings, you can use this function. +/// As long as your framework can provide a Sink and Stream of Bytes, you can use this function. /// /// You might need to transform the error types of the web backend into the LiveView error type. pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> { @@ -120,20 +126,31 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li rx }; + let mut templates: FxHashMap = Default::default(); + let mut max_template_count = 0; + // Create the a proxy for query engine let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); let query_engine = QueryEngine::new(query_tx); vdom.base_scope().provide_context(query_engine.clone()); init_eval(vdom.base_scope()); - // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); - // pin the futures so we can use select! pin_mut!(ws); - // send the initial render to the client - ws.send(edits.into_bytes()).await?; + let mut edit_channel = Channel::default(); + if let Some(edits) = { + let mutations = vdom.rebuild(); + apply_edits( + mutations, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + // send the initial render to the client + ws.send(edits).await?; + } // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry @@ -160,7 +177,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li match evt.as_ref().map(|o| o.as_deref()) { // respond with a pong every ping to keep the websocket alive Some(Ok(b"__ping__")) => { - ws.send(b"__pong__".to_vec()).await?; + ws.send(text_frame("__pong__")).await?; } Some(Ok(evt)) => { if let Ok(message) = serde_json::from_str::(&String::from_utf8_lossy(evt)) { @@ -171,12 +188,11 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li let element = LiveviewElement::new(evt.element, query_engine.clone()); vdom.handle_event( &evt.name, - Rc::new(MountedData::new(element)), + Rc::new(PlatformEventData::new(Box::new(MountedData::new(element)))), evt.element, evt.bubbles, ); - } - else{ + } else { vdom.handle_event( &evt.name, evt.data.into_any(), @@ -199,7 +215,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // handle any new queries Some(query) = query_rx.recv() => { - ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?; + ws.send(text_frame(&serde_json::to_string(&ClientUpdate::Query(query)).unwrap())).await?; } Some(msg) = hot_reload_wait => { @@ -221,20 +237,156 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send( - serde_json::to_string(&ClientUpdate::Edits(edits)) - .unwrap() - .into_bytes(), - ) - .await?; + if let Some(edits) = { + apply_edits( + edits, + &mut edit_channel, + &mut templates, + &mut max_template_count, + ) + } { + ws.send(edits).await?; + } } } +fn text_frame(text: &str) -> Vec { + let mut bytes = vec![0]; + bytes.extend(text.as_bytes()); + bytes +} + +fn add_template( + template: &Template<'static>, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) { + for root in template.roots.iter() { + create_template_node(channel, root); + templates.insert(template.name.to_owned(), *max_template_count); + } + channel.add_templates(*max_template_count, template.roots.len() as u16); + + *max_template_count += 1 +} + +fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + // Push the current node onto the stack + match namespace { + Some(ns) => channel.create_element_ns(tag, ns), + None => channel.create_element(tag), + } + // Set attributes on the current node + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + channel.set_top_attribute(name, value, namespace.unwrap_or_default()) + } + } + // Add each child to the stack + for child in *children { + create_template_node(channel, child); + } + // Add all children to the parent + channel.append_children_to_top(children.len() as u16); + } + Text { text } => channel.create_raw_text(text), + DynamicText { .. } => channel.create_raw_text("p"), + Dynamic { .. } => channel.add_placeholder(), + } +} + +fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &mut u16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + for edit in mutations.edits { + match edit { + AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), + AssignId { path, id } => channel.assign_id(path, id.0 as u32), + CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32), + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = templates.get(name) { + channel.load_template(*tmpl_id, index as u16, id.0 as u32) + } + } + ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16), + ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16), + InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16), + InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16), + SetAttribute { + name, + value, + id, + ns, + } => match value { + BorrowedAttributeValue::Text(txt) => { + channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default()) + } + BorrowedAttributeValue::Float(f) => { + channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Int(n) => { + channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default()) + } + BorrowedAttributeValue::Bool(b) => channel.set_attribute( + id.0 as u32, + name, + if b { "true" } else { "false" }, + ns.unwrap_or_default(), + ), + BorrowedAttributeValue::None => { + channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default()) + } + _ => unreachable!(), + }, + SetText { value, id } => channel.set_text(id.0 as u32, value), + NewEventListener { name, id, .. } => { + channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + RemoveEventListener { name, id } => { + channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + } + Remove { id } => channel.remove(id.0 as u32), + PushRoot { id } => channel.push_root(id.0 as u32), + } + } + + // Add an extra one at the beginning to tell the shim this is a binary frame + let mut bytes = vec![1]; + bytes.extend(channel.export_memory()); + channel.reset(); + Some(bytes) +} + #[derive(Serialize)] #[serde(tag = "type", content = "data")] -enum ClientUpdate<'a> { - #[serde(rename = "edits")] - Edits(Mutations<'a>), +enum ClientUpdate { #[serde(rename = "query")] Query(String), } diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs index a638625c1..c99dd5151 100644 --- a/packages/liveview/src/query.rs +++ b/packages/liveview/src/query.rs @@ -169,10 +169,7 @@ pub(crate) struct Query { impl Query { /// Resolve the query pub async fn resolve(mut self) -> Result { - match self.receiver.recv().await { - Some(result) => V::deserialize(result).map_err(QueryError::Deserialize), - None => Err(QueryError::Recv(RecvError::Closed)), - } + V::deserialize(self.result().await?).map_err(QueryError::Deserialize) } /// Send a message to the query diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 36940cff7..5cc2c6ae9 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -1,3 +1,5 @@ #![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 use dioxus_desktop::*; diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index d122838bd..fdaa12cf5 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + extern crate proc_macro; use std::collections::HashSet; diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index 6f3f5aa04..db75fcfd8 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -16,7 +16,6 @@ keyboard-types = "0.7" smallvec = "1.6" rustc-hash = { workspace = true } anymap = "1.0.0-beta.2" -slab = { workspace = true } parking_lot = { version = "0.12.1", features = ["send_guard"] } dashmap = "5.4.0" @@ -24,7 +23,6 @@ dashmap = "5.4.0" taffy = { version = "0.3.12", optional = true } lightningcss = { version = "1.0.0-alpha.39", optional = true } -rayon = "1.6.1" shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false } [dev-dependencies] diff --git a/packages/native-core/src/dioxus.rs b/packages/native-core/src/dioxus.rs index 3e9ff9ee0..c245de4fc 100644 --- a/packages/native-core/src/dioxus.rs +++ b/packages/native-core/src/dioxus.rs @@ -57,7 +57,12 @@ impl DioxusState { node.insert(ElementIdComponent(element_id)); if self.node_id_mapping.len() <= element_id.0 { self.node_id_mapping.resize(element_id.0 + 1, None); + } else if let Some(mut node) = + self.node_id_mapping[element_id.0].and_then(|id| node.real_dom_mut().get_mut(id)) + { + node.remove(); } + self.node_id_mapping[element_id.0] = Some(node_id); } diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index c5ad38341..dd36990a7 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -1,4 +1,6 @@ #![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")] #![warn(missing_docs)] use std::any::Any; diff --git a/packages/native-core/tests/fuzzing.rs b/packages/native-core/tests/fuzzing.rs index 4f0a34b70..4abcc2351 100644 --- a/packages/native-core/tests/fuzzing.rs +++ b/packages/native-core/tests/fuzzing.rs @@ -3,7 +3,6 @@ use dioxus_core::*; use dioxus_native_core::prelude::*; use dioxus_native_core_macro::partial_derive_state; use shipyard::Component; -use std::cell::Cell; fn random_ns() -> Option<&'static str> { let namespace = rand::random::() % 2; @@ -178,22 +177,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { let range = if depth > 3 { 1 } else { 3 }; match rand::random::() % range { 0 => DynamicNode::Placeholder(Default::default()), - 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| VNode { - key: None, - parent: Default::default(), - template: Cell::new(Template { - name: concat!(file!(), ":", line!(), ":", column!(), ":0"), - roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[0]], - attr_paths: &[], - }), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(), - dynamic_nodes: cx.bump().alloc([cx.component( - create_random_element, - DepthProps { depth, root: false }, - "create_random_element", - )]), - dynamic_attrs: &[], + 1 => cx.make_node((0..(rand::random::() % 5)).map(|_| { + VNode::new( + None, + Template { + name: concat!(file!(), ":", line!(), ":", column!(), ":0"), + roots: &[TemplateNode::Dynamic { id: 0 }], + node_paths: &[&[0]], + attr_paths: &[], + }, + dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()), + cx.bump().alloc([cx.component( + create_random_element, + DepthProps { depth, root: false }, + "create_random_element", + )]), + &[], + ) })), 2 => cx.component( create_random_element, @@ -204,6 +204,20 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode { } } +fn create_random_dynamic_mounted_attr(cx: &ScopeState) -> MountedAttribute { + match rand::random::() % 2 { + 0 => MountedAttribute::from( + &*cx.bump().alloc( + (0..(rand::random::() % 3) as usize) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + ), + 1 => MountedAttribute::from(create_random_dynamic_attr(cx)), + _ => unreachable!(), + } +} + fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { let value = match rand::random::() % 6 { 0 => AttributeValue::Text(Box::leak( @@ -214,7 +228,6 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { 3 => AttributeValue::Bool(rand::random()), 4 => cx.any_value(rand::random::()), 5 => AttributeValue::None, - // Listener(RefCell>>), _ => unreachable!(), }; Attribute::new( @@ -253,13 +266,11 @@ fn create_random_element(cx: Scope) -> Element { .into_boxed_str(), )); println!("{template:#?}"); - let node = VNode { - key: None, - parent: None, - template: Cell::new(template), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()) - .into(), - dynamic_nodes: { + let node = VNode::new( + None, + template, + dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()), + { let dynamic_nodes: Vec<_> = dynamic_node_types .iter() .map(|ty| match ty { @@ -273,12 +284,14 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - dynamic_attrs: cx.bump().alloc( - (0..template.attr_paths.len()) - .map(|_| create_random_dynamic_attr(cx)) - .collect::>(), - ), - }; + cx.bump() + .alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_mounted_attr(cx)) + .collect::>(), + ) + .as_slice(), + ); Some(node) } _ => None, diff --git a/packages/rink/Cargo.toml b/packages/rink/Cargo.toml index ba3d549f8..28440a932 100644 --- a/packages/rink/Cargo.toml +++ b/packages/rink/Cargo.toml @@ -10,11 +10,11 @@ keywords = ["dom", "ui", "gui", "react", "terminal"] license = "MIT OR Apache-2.0" [dependencies] -dioxus-html = { workspace = true } +dioxus-html = { workspace = true, features = ["serialize", "mounted"] } dioxus-native-core = { workspace = true, features = ["layout-attributes"] } dioxus-native-core-macro = { workspace = true } -tui = "0.17.0" +ratatui = "0.24.0" crossterm = "0.26.1" anyhow = "1.0.42" tokio = { workspace = true, features = ["full"] } diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs index e1f71fe42..42dc3723d 100644 --- a/packages/rink/examples/widgets.rs +++ b/packages/rink/examples/widgets.rs @@ -1,3 +1,4 @@ +use dioxus_html::HasFormData; use dioxus_native_core::{ prelude::*, real_dom::{NodeImmutable, NodeTypeMut}, @@ -79,7 +80,7 @@ impl Driver for Counter { if event_type == "input" { // when the button is clicked, increment the counter if let EventData::Form(input_event) = &*event { - if let Ok(value) = input_event.value.parse::() { + if let Ok(value) = input_event.value().parse::() { self.count = value; } } diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index f8c4500d2..012c0c1eb 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -2,6 +2,10 @@ use crossterm::event::{ Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, ModifierKeyCode, MouseButton, MouseEventKind, }; +use dioxus_html::{ + HasFormData, HasKeyboardData, HasWheelData, SerializedFocusData, SerializedKeyboardData, + SerializedMouseData, SerializedWheelData, +}; use dioxus_native_core::prelude::*; use dioxus_native_core::real_dom::NodeImmutable; use rustc_hash::{FxHashMap, FxHashSet}; @@ -11,9 +15,11 @@ use dioxus_html::geometry::{ ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint, WheelDelta, }; use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers}; -use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons; -use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; -use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData}; +use dioxus_html::input_data::{ + MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons, +}; +use dioxus_html::FormValue; +use dioxus_html::{event_bubbles, prelude::*}; use std::any::Any; use std::collections::HashMap; use std::{ @@ -38,10 +44,10 @@ pub struct Event { #[derive(Debug, Clone, PartialEq)] pub enum EventData { - Mouse(MouseData), - Keyboard(KeyboardData), - Focus(FocusData), - Wheel(WheelData), + Mouse(SerializedMouseData), + Keyboard(SerializedKeyboardData), + Focus(SerializedFocusData), + Wheel(SerializedWheelData), Form(FormData), } @@ -52,27 +58,31 @@ impl EventData { EventData::Keyboard(k) => Rc::new(k), EventData::Focus(f) => Rc::new(f), EventData::Wheel(w) => Rc::new(w), - EventData::Form(f) => Rc::new(f.into_html()), + EventData::Form(f) => Rc::new(f), } } } #[derive(Clone, Debug, PartialEq)] pub struct FormData { - pub value: String, + pub(crate) value: String, - pub values: HashMap>, + pub values: HashMap, - pub files: Option, + pub(crate) files: Option, } -impl FormData { - fn into_html(self) -> dioxus_html::FormData { - dioxus_html::FormData { - value: self.value, - values: self.values, - files: None, - } +impl HasFormData for FormData { + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap { + self.values.clone() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } @@ -89,9 +99,9 @@ type EventCore = (&'static str, EventData); const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); pub struct InnerInputState { - mouse: Option, - wheel: Option, - last_key_pressed: Option<(KeyboardData, Instant)>, + mouse: Option, + wheel: Option, + last_key_pressed: Option<(SerializedKeyboardData, Instant)>, pub(crate) focus_state: FocusState, // subscribers: Vec>, } @@ -114,7 +124,7 @@ impl InnerInputState { EventData::Mouse(ref mut m) => { let mut held_buttons = match &self.mouse { Some(previous_data) => previous_data.held_buttons(), - None => MouseButtonSet::empty(), + None => DioxusMouseButtons::empty(), }; match evt.0 { @@ -133,10 +143,16 @@ impl InnerInputState { _ => {} } - let new_mouse_data = MouseData::new( - m.coordinates(), + let coordinates = m.coordinates(); + let new_mouse_data = SerializedMouseData::new( m.trigger_button(), held_buttons, + Coordinates::new( + m.screen_coordinates(), + m.client_coordinates(), + coordinates.element(), + m.page_coordinates(), + ), m.modifiers(), ); @@ -155,7 +171,13 @@ impl InnerInputState { .is_some(); if is_repeating { - *k = KeyboardData::new(k.key(), k.code(), k.location(), true, k.modifiers()); + *k = SerializedKeyboardData::new( + k.key(), + k.code(), + k.location(), + is_repeating, + k.modifiers(), + ); } self.last_key_pressed = Some((k.clone(), Instant::now())); @@ -199,13 +221,13 @@ impl InnerInputState { resolved_events.push(Event { name: "focus", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focus"), }); resolved_events.push(Event { name: "focusin", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focusin"), }); } @@ -213,7 +235,7 @@ impl InnerInputState { resolved_events.push(Event { name: "focusout", id, - data: EventData::Focus(FocusData {}), + data: EventData::Focus(SerializedFocusData::default()), bubbles: event_bubbles("focusout"), }); } @@ -226,7 +248,7 @@ impl InnerInputState { fn resolve_mouse_events( &mut self, - previous_mouse: Option, + previous_mouse: Option, resolved_events: &mut Vec, layout: &Taffy, dom: &mut RealDom, @@ -273,7 +295,10 @@ impl InnerInputState { } } - fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData { + fn prepare_mouse_data( + mouse_data: &SerializedMouseData, + layout: &Layout, + ) -> SerializedMouseData { let Point { x, y } = layout.location; let node_origin = ClientPoint::new( layout_to_screen_space(x).into(), @@ -284,17 +309,15 @@ impl InnerInputState { .to_point() .cast_unit(); - let coordinates = Coordinates::new( - mouse_data.screen_coordinates(), - mouse_data.client_coordinates(), - new_client_coordinates, - mouse_data.page_coordinates(), - ); - - MouseData::new( - coordinates, + SerializedMouseData::new( mouse_data.trigger_button(), mouse_data.held_buttons(), + Coordinates::new( + mouse_data.screen_coordinates(), + mouse_data.client_coordinates(), + new_client_coordinates, + mouse_data.page_coordinates(), + ), mouse_data.modifiers(), ) } @@ -305,7 +328,7 @@ impl InnerInputState { // a mouse button is pressed if a button was not down and is now down let previous_buttons = previous_mouse - .map_or(MouseButtonSet::empty(), |previous_data| { + .map_or(DioxusMouseButtons::empty(), |previous_data| { previous_data.held_buttons() }); let was_pressed = !(mouse_data.held_buttons() - previous_buttons).is_empty(); @@ -689,17 +712,6 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent - // The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively. - // todo? - // But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway. - let coordinates = Coordinates::new( - ScreenPoint::new(x, y), - ClientPoint::new(x, y), - // offset x/y are set when the origin of the event is assigned to an element - ElementPoint::new(0., 0.), - PagePoint::new(x, y), - ); - let mut modifiers = Modifiers::empty(); if shift { modifiers.insert(Modifiers::SHIFT); @@ -715,17 +727,26 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { } // held mouse buttons get set later by maintaining state, as crossterm does not provide them - EventData::Mouse(MouseData::new( - coordinates, + EventData::Mouse(SerializedMouseData::new( button, DioxusMouseButtons::empty(), + Coordinates::new( + // The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively. + // todo? + // But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway. + ScreenPoint::new(x, y), + ClientPoint::new(x, y), + // offset x/y are set when the origin of the event is assigned to an element + ElementPoint::new(0., 0.), + PagePoint::new(x, y), + ), modifiers, )) }; let get_wheel_data = |up| { let y = if up { -1.0 } else { 1.0 }; - EventData::Wheel(WheelData::new(WheelDelta::lines(0., y, 0.))) + EventData::Wheel(SerializedWheelData::new(WheelDelta::lines(0., y, 0.))) }; match m.kind { @@ -750,7 +771,7 @@ fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { let code = guess_code_from_crossterm_key_code(event.code)?; let modifiers = modifiers_from_crossterm_modifiers(event.modifiers); - Some(EventData::Keyboard(KeyboardData::new( + Some(EventData::Keyboard(SerializedKeyboardData::new( key, code, Location::Standard, diff --git a/packages/rink/src/layout.rs b/packages/rink/src/layout.rs index dbdadac3f..72c8f68b2 100644 --- a/packages/rink/src/layout.rs +++ b/packages/rink/src/layout.rs @@ -92,10 +92,10 @@ impl State for TaffyLayout { attribute, value, .. } in attributes { - if let Some(text) = value.as_text() { + if value.as_custom().is_none() { apply_layout_attributes_cfg( &attribute.name, - text, + &value.to_string(), &mut style, &LayoutConfigeration { border_widths: BorderWidths { diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 81ab45a85..4a7261b55 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + use crate::focus::Focus; use anyhow::Result; use crossterm::{ @@ -13,6 +17,7 @@ use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt}; use futures_channel::mpsc::unbounded; use layout::TaffyLayout; use prevent_default::PreventDefault; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::{io, time::Duration}; use std::{ pin::Pin, @@ -22,7 +27,6 @@ use std::{rc::Rc, sync::RwLock}; use style_attributes::StyleModifier; pub use taffy::{geometry::Point, prelude::*}; use tokio::select; -use tui::{backend::CrosstermBackend, Terminal}; use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject}; mod config; @@ -176,7 +180,7 @@ pub fn render( if !to_rerender.is_empty() || updated { updated = false; - fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { + fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { let width = screen_to_layout_space(dims.width); let height = screen_to_layout_space(dims.height); let root_node = rdom @@ -218,7 +222,7 @@ pub fn render( } else { let rdom = rdom.read().unwrap(); resize( - tui::layout::Rect { + ratatui::layout::Rect { x: 0, y: 0, width: 1000, diff --git a/packages/rink/src/render.rs b/packages/rink/src/render.rs index 19d642720..4185f3a62 100644 --- a/packages/rink/src/render.rs +++ b/packages/rink/src/render.rs @@ -1,11 +1,10 @@ use dioxus_native_core::{prelude::*, tree::TreeRef}; -use std::io::Stdout; +use ratatui::{layout::Rect, style::Color}; use taffy::{ geometry::Point, prelude::{Dimension, Layout, Size}, Taffy, }; -use tui::{backend::CrosstermBackend, layout::Rect, style::Color}; use crate::{ focus::Focused, @@ -20,7 +19,7 @@ use crate::{ const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; pub(crate) fn render_vnode( - frame: &mut tui::Frame>, + frame: &mut ratatui::Frame, layout: &Taffy, node: NodeRef, cfg: Config, @@ -96,7 +95,7 @@ pub(crate) fn render_vnode( impl RinkWidget for NodeRef<'_> { fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { - use tui::symbols::line::*; + use ratatui::symbols::line::*; enum Direction { Left, diff --git a/packages/rink/src/style.rs b/packages/rink/src/style.rs index 857322079..905cc4b34 100644 --- a/packages/rink/src/style.rs +++ b/packages/rink/src/style.rs @@ -1,6 +1,6 @@ use std::{num::ParseFloatError, str::FromStr}; -use tui::style::{Color, Modifier, Style}; +use ratatui::style::{Color, Modifier, Style}; use crate::RenderingMode; @@ -442,6 +442,7 @@ impl RinkStyle { impl From for Style { fn from(val: RinkStyle) -> Self { Style { + underline_color: None, fg: val.fg.map(|c| c.color), bg: val.bg.map(|c| c.color), add_modifier: val.add_modifier, diff --git a/packages/rink/src/style_attributes.rs b/packages/rink/src/style_attributes.rs index 0c87e3f83..d1e8c6bee 100644 --- a/packages/rink/src/style_attributes.rs +++ b/packages/rink/src/style_attributes.rs @@ -187,8 +187,8 @@ pub enum BorderStyle { } impl BorderStyle { - pub fn symbol_set(&self) -> Option { - use tui::symbols::line::*; + pub fn symbol_set(&self) -> Option { + use ratatui::symbols::line::*; const DASHED: Set = Set { horizontal: "╌", vertical: "╎", @@ -570,7 +570,7 @@ fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifier) { } fn apply_font(name: &str, value: &str, style: &mut StyleModifier) { - use tui::style::Modifier; + use ratatui::style::Modifier; match name { "font" => (), "font-family" => (), @@ -593,7 +593,7 @@ fn apply_font(name: &str, value: &str, style: &mut StyleModifier) { } fn apply_text(name: &str, value: &str, style: &mut StyleModifier) { - use tui::style::Modifier; + use ratatui::style::Modifier; match name { "text-align" => todo!(), diff --git a/packages/rink/src/widget.rs b/packages/rink/src/widget.rs index ab9044998..91af5b4f6 100644 --- a/packages/rink/src/widget.rs +++ b/packages/rink/src/widget.rs @@ -1,4 +1,4 @@ -use tui::{ +use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Modifier}, diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index 523b2392a..25f0e07ef 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -11,7 +11,7 @@ use dioxus_native_core::{ }; use shipyard::UniqueView; -use crate::FormData; +use crate::hooks::FormData; use super::{RinkWidget, WidgetContext}; diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index fab809ed2..a8a8253a0 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -11,7 +11,7 @@ use dioxus_native_core::{ }; use shipyard::UniqueView; -use crate::FormData; +use crate::hooks::FormData; use super::{RinkWidget, WidgetContext}; diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs index 371c8b41d..c2a71ceac 100644 --- a/packages/rink/src/widgets/number.rs +++ b/packages/rink/src/widgets/number.rs @@ -1,4 +1,4 @@ -use dioxus_html::input_data::keyboard_types::Key; +use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData}; use dioxus_native_core::{ custom_element::CustomElement, real_dom::{NodeImmutable, RealDom}, diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index 7080b6c12..bb0639610 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_html::{ + input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData, + SerializedMouseData, +}; use dioxus_native_core::{ custom_element::CustomElement, node::{OwnedAttributeDiscription, OwnedAttributeValue}, @@ -12,7 +15,8 @@ use dioxus_native_core::{ use shipyard::UniqueView; use super::{RinkWidget, WidgetContext}; -use crate::{query::get_layout, Event, EventData, FormData, Query}; +use crate::hooks::FormData; +use crate::{query::get_layout, Event, EventData, Query}; #[derive(Debug)] pub(crate) struct Slider { @@ -209,7 +213,7 @@ impl Slider { } } - fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) { let key = data.key(); let step = self.step(); @@ -231,7 +235,7 @@ impl Slider { self.write_value(rdom, id); } - fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) { if !data.held_buttons().is_empty() { let id = root.id(); let rdom = root.real_dom_mut(); diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index 6521073d0..770dd9d33 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -1,7 +1,10 @@ use std::{collections::HashMap, io::stdout}; use crossterm::{cursor::MoveTo, execute}; -use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_html::{ + input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData, + SerializedMouseData, +}; use dioxus_native_core::{ custom_element::CustomElement, node::OwnedAttributeDiscription, @@ -14,7 +17,8 @@ use dioxus_native_core::{ use shipyard::UniqueView; use taffy::geometry::Point; -use crate::{query::get_layout, Event, EventData, FormData, Query}; +use crate::hooks::FormData; +use crate::{query::get_layout, Event, EventData, Query}; use super::{RinkWidget, WidgetContext}; @@ -184,7 +188,7 @@ impl TextLike { } } - fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) { let key = data.key(); let modifiers = data.modifiers(); let code = data.code(); @@ -228,7 +232,7 @@ impl TextLike { } } - fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) { if self.dragging { let id = root.id(); let offset = data.element_coordinates(); @@ -245,7 +249,7 @@ impl TextLike { } } - fn handle_mousedown(&mut self, mut root: NodeMut, data: &MouseData) { + fn handle_mousedown(&mut self, mut root: NodeMut, data: &SerializedMouseData) { let offset = data.element_coordinates(); let mut new = Pos::new(offset.x as usize, offset.y as usize); diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index b1ff7b0fe..024570ae1 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -1,3 +1,7 @@ +#![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")] + extern crate proc_macro; use layout::Layout; @@ -32,7 +36,7 @@ mod segment; /// 1. Static Segments: "/static" /// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants) /// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants) -/// 4. Query Segments: "/?:query" (where query has a type that is FromQuery in all child Variants) +/// 4. Query Segments: "/?:..query" (where query has a type that is FromQuery in all child Variants) or "/?:query&:other_query" (where query and other_query has a type that is FromQueryArgument in all child Variants) /// /// Routes are matched: /// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route") diff --git a/packages/router-macro/src/query.rs b/packages/router-macro/src/query.rs index c97d3be29..40af2b39a 100644 --- a/packages/router-macro/src/query.rs +++ b/packages/router-macro/src/query.rs @@ -4,12 +4,62 @@ use syn::{Ident, Type}; use proc_macro2::TokenStream as TokenStream2; #[derive(Debug)] -pub struct QuerySegment { +pub enum QuerySegment { + Single(FullQuerySegment), + Segments(Vec), +} + +impl QuerySegment { + pub fn contains_ident(&self, ident: &Ident) -> bool { + match self { + QuerySegment::Single(segment) => segment.ident == *ident, + QuerySegment::Segments(segments) => { + segments.iter().any(|segment| segment.ident == *ident) + } + } + } + + pub fn parse(&self) -> TokenStream2 { + match self { + QuerySegment::Single(segment) => segment.parse(), + QuerySegment::Segments(segments) => { + let mut tokens = TokenStream2::new(); + tokens.extend(quote! { let split_query: std::collections::HashMap<&str, &str> = query.split('&').filter_map(|s| s.split_once('=')).collect(); }); + for segment in segments { + tokens.extend(segment.parse()); + } + tokens + } + } + } + + pub fn write(&self) -> TokenStream2 { + match self { + QuerySegment::Single(segment) => segment.write(), + QuerySegment::Segments(segments) => { + let mut tokens = TokenStream2::new(); + tokens.extend(quote! { write!(f, "?")?; }); + let mut segments_iter = segments.iter(); + if let Some(first_segment) = segments_iter.next() { + tokens.extend(first_segment.write()); + } + for segment in segments_iter { + tokens.extend(quote! { write!(f, "&")?; }); + tokens.extend(segment.write()); + } + tokens + } + } + } +} + +#[derive(Debug)] +pub struct FullQuerySegment { pub ident: Ident, pub ty: Type, } -impl QuerySegment { +impl FullQuerySegment { pub fn parse(&self) -> TokenStream2 { let ident = &self.ident; let ty = &self.ty; @@ -25,3 +75,29 @@ impl QuerySegment { } } } + +#[derive(Debug)] +pub struct QueryArgument { + pub ident: Ident, + pub ty: Type, +} + +impl QueryArgument { + pub fn parse(&self) -> TokenStream2 { + let ident = &self.ident; + let ty = &self.ty; + quote! { + let #ident = match split_query.get(stringify!(#ident)) { + Some(query_argument) => <#ty as dioxus_router::routable::FromQueryArgument>::from_query_argument(query_argument).unwrap_or_default(), + None => <#ty as Default>::default(), + }; + } + } + + pub fn write(&self) -> TokenStream2 { + let ident = &self.ident; + quote! { + write!(f, "{}={}", stringify!(#ident), #ident)?; + } + } +} diff --git a/packages/router-macro/src/redirect.rs b/packages/router-macro/src/redirect.rs index c1efb8b4d..2933bbc8e 100644 --- a/packages/router-macro/src/redirect.rs +++ b/packages/router-macro/src/redirect.rs @@ -75,6 +75,7 @@ impl Redirect { let (segments, query) = parse_route_segments( path.span(), + #[allow(clippy::map_identity)] closure_arguments.iter().map(|(name, ty)| (name, ty)), &path.value(), )?; diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 26665e575..4724a4d7b 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -282,7 +282,7 @@ impl Route { } } if let Some(query) = &self.query { - if &query.ident == name { + if query.contains_ident(name) { from_route = true } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 7e4e40662..21ebe5b60 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -3,7 +3,7 @@ use syn::{Ident, Type}; use proc_macro2::{Span, TokenStream as TokenStream2}; -use crate::query::QuerySegment; +use crate::query::{FullQuerySegment, QueryArgument, QuerySegment}; #[derive(Debug, Clone)] pub enum RouteSegment { @@ -201,7 +201,7 @@ pub fn parse_route_segments<'a>( // check if the route has a query string let parsed_query = match query { Some(query) => { - if let Some(query) = query.strip_prefix(':') { + if let Some(query) = query.strip_prefix(":..") { let query_ident = Ident::new(query, Span::call_site()); let field = fields.find(|(name, _)| *name == &query_ident); @@ -214,12 +214,44 @@ pub fn parse_route_segments<'a>( )); }; - Some(QuerySegment { + Some(QuerySegment::Single(FullQuerySegment { ident: query_ident, ty, - }) + })) } else { - None + let mut query_arguments = Vec::new(); + for segment in query.split('&') { + if segment.is_empty() { + return Err(syn::Error::new( + route_span, + "Query segments should be non-empty", + )); + } + if let Some(query_argument) = segment.strip_prefix(':') { + let query_ident = Ident::new(query_argument, Span::call_site()); + let field = fields.find(|(name, _)| *name == &query_ident); + + let ty = if let Some((_, ty)) = field { + ty.clone() + } else { + return Err(syn::Error::new( + route_span, + format!("Could not find a field with the name '{}'", query_ident), + )); + }; + + query_arguments.push(QueryArgument { + ident: query_ident, + ty, + }); + } else { + return Err(syn::Error::new( + route_span, + "Query segments should be a : followed by the name of the query argument", + )); + } + } + Some(QuerySegment::Segments(query_arguments)) } } None => None, diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 80923d539..da9d3fe85 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } futures-util = { workspace = true } urlencoding = "2.1.3" serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1.0.91", optional = true } url = "2.3.1" wasm-bindgen = { workspace = true, optional = true } web-sys = { version = "0.3.60", optional = true, features = [ @@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [ ] } js-sys = { version = "0.3.63", optional = true } gloo-utils = { version = "0.1.6", optional = true } +dioxus-liveview = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true } tokio = { workspace = true, features = ["full"], optional = true } [features] default = ["web"] ssr = ["dioxus-ssr", "tokio"] +liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"] wasm_test = [] serde = ["dep:serde", "gloo-utils/serde"] web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"] [dev-dependencies] +axum = { version = "0.6.1", features = ["ws"] } dioxus = { path = "../dioxus" } +dioxus-liveview = { workspace = true, features = ["axum"] } dioxus-ssr = { path = "../ssr" } criterion = { version = "0.5", features = ["async_tokio", "html_reports"] } diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 3634f7df3..6c33c275b 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -2,6 +2,47 @@ use dioxus::prelude::*; use dioxus_router::prelude::*; use std::str::FromStr; +#[cfg(feature = "liveview")] +#[tokio::main] +async fn main() { + use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; + + let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + let view = dioxus_liveview::LiveViewPool::new(); + let app = Router::new() + .fallback(get(move || async move { + Html(format!( + r#" + + + +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws")) + )) + })) + .route( + "/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + _ = view + .launch(dioxus_liveview::axum_socket(socket), Root) + .await; + }) + }), + ); + + println!("Listening on http://{listen_address}"); + + axum::Server::bind(&listen_address.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "liveview"))] fn main() { #[cfg(not(target_arch = "wasm32"))] dioxus_desktop::launch(Root); @@ -10,21 +51,24 @@ fn main() { dioxus_web::launch(root); } +#[cfg(feature = "liveview")] #[component] fn Root(cx: Scope) -> Element { - render! { - Router:: {} - } + let history = LiveviewHistory::new(cx); + render! { Router:: { config: || RouterConfig::default().history(history) } } +} + +#[cfg(not(feature = "liveview"))] +#[component] +fn Root(cx: Scope) -> Element { + render! { Router:: {} } } #[component] fn UserFrame(cx: Scope, user_id: usize) -> Element { render! { - pre { - "UserFrame{{\n\tuser_id:{user_id}\n}}" - } - div { - background_color: "rgba(0,0,0,50%)", + pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" } + div { background_color: "rgba(0,0,0,50%)", "children:" Outlet:: {} } @@ -38,12 +82,19 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}" } Link { - to: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." }, + to: Route::Route1 { + user_id: *user_id, + dynamic: *dynamic, + query: String::new(), + extra: extra.clone() + ".", + }, "Route1 with extra+\".\"" } p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -52,13 +103,13 @@ fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: Strin #[component] fn Route2(cx: Scope, user_id: usize) -> Element { render! { - pre { - "Route2{{\n\tuser_id:{user_id}\n}}" - } + pre { "Route2{{\n\tuser_id:{user_id}\n}}" } (0..*user_id).map(|i| rsx!{ p { "{i}" } }), p { "Footer" } Link { - to: Route::Route3 { dynamic: String::new() }, + to: Route::Route3 { + dynamic: String::new(), + }, "Home" } } @@ -66,7 +117,6 @@ fn Route2(cx: Scope, user_id: usize) -> Element { #[component] fn Route3(cx: Scope, dynamic: String) -> Element { - let navigator = use_navigator(cx); let current_route = use_route(cx)?; let current_route_str = use_ref(cx, String::new); let parsed = Route::from_str(¤t_route_str.read()); @@ -76,20 +126,35 @@ fn Route3(cx: Scope, dynamic: String) -> Element { .flat_map(|seg| seg.flatten().into_iter()) .collect::>(); + let navigator = use_navigator(cx); + render! { input { oninput: move |evt| { - *current_route_str.write() = evt.value.clone(); + *current_route_str.write() = evt.value(); }, value: "{current_route_str.read()}" } "dynamic: {dynamic}" - Link { - to: Route::Route2 { user_id: 8888 }, - "hello world link" + Link { to: Route::Route2 { user_id: 8888 }, "hello world link" } + button { + disabled: !navigator.can_go_back(), + onclick: move |_| { + navigator.go_back(); + }, + "go back" } button { - onclick: move |_| { navigator.push("https://www.google.com"); }, + disabled: !navigator.can_go_forward(), + onclick: move |_| { + navigator.go_forward(); + }, + "go forward" + } + button { + onclick: move |_| { + navigator.push("https://www.google.com"); + }, "google link" } p { "Site Map" } diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs index eb62e9285..ae11ad276 100644 --- a/packages/router/src/components/history_buttons.rs +++ b/packages/router/src/components/history_buttons.rs @@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { } }; - let disabled = !router.can_go_back(); + let disabled = !router.can_go_forward(); render! { button { diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 787f9cdc9..639d99ae7 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -11,6 +11,8 @@ use crate::navigation::NavigationTarget; use crate::prelude::Routable; use crate::utils::use_router_internal::use_router_internal; +use url::Url; + /// Something that can be converted into a [`NavigationTarget`]. #[derive(Clone)] pub enum IntoRoutable { @@ -53,6 +55,18 @@ impl From<&str> for IntoRoutable { } } +impl From for IntoRoutable { + fn from(url: Url) -> Self { + IntoRoutable::FromStr(url.to_string()) + } +} + +impl From<&Url> for IntoRoutable { + fn from(url: &Url) -> Self { + IntoRoutable::FromStr(url.to_string()) + } +} + /// The properties for a [`Link`]. #[derive(Props)] pub struct LinkProps<'a> { diff --git a/packages/router/src/contexts/navigator.rs b/packages/router/src/contexts/navigator.rs index 961122c05..8c8bdd109 100644 --- a/packages/router/src/contexts/navigator.rs +++ b/packages/router/src/contexts/navigator.rs @@ -1,5 +1,19 @@ use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext}; +/// Acquire the navigator without subscribing to updates. +/// +/// Can be called anywhere in the application provided a Router has been initialized. +/// +/// ## Panics +/// +/// Panics if there is no router present. +pub fn navigator() -> Navigator { + Navigator( + dioxus::core::prelude::consume_context::() + .expect("A router must be present to use navigator"), + ) +} + /// A view into the navigation state of a router. #[derive(Clone)] pub struct Navigator(pub(crate) RouterContext); diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index b15956991..079640e61 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -232,7 +232,7 @@ impl RouterContext { IntoRoutable::FromStr(url) => { let parsed_route: NavigationTarget> = match self.route_from_str(&url) { Ok(route) => NavigationTarget::Internal(route), - Err(err) => NavigationTarget::External(err), + Err(_) => NavigationTarget::External(url), }; parsed_route } diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs new file mode 100644 index 000000000..9ffb09c34 --- /dev/null +++ b/packages/router/src/history/liveview.rs @@ -0,0 +1,441 @@ +use super::HistoryProvider; +use crate::routable::Routable; +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; +use std::sync::{Mutex, RwLock}; +use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc}; + +/// A [`HistoryProvider`] that evaluates history through JS. +pub struct LiveviewHistory +where + ::Err: std::fmt::Display, +{ + action_tx: tokio::sync::mpsc::UnboundedSender>, + timeline: Arc>>, + updater_callback: Arc>>, +} + +struct Timeline +where + ::Err: std::fmt::Display, +{ + current_index: usize, + routes: BTreeMap, +} + +#[derive(Serialize, Deserialize, Debug)] +struct State { + index: usize, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Session +where + ::Err: std::fmt::Display, +{ + #[serde(with = "routes")] + routes: BTreeMap, + last_visited: usize, +} + +#[derive(Serialize, Deserialize)] +struct SessionStorage { + liveview: Option, +} + +enum Action { + GoBack, + GoForward, + Push(R), + Replace(R), + External(String), +} + +impl Timeline +where + ::Err: std::fmt::Display, +{ + fn new(initial_path: R) -> Self { + Self { + current_index: 0, + routes: BTreeMap::from([(0, initial_path)]), + } + } + + fn init( + &mut self, + route: R, + state: Option, + session: Option>, + depth: usize, + ) -> State { + if let Some(session) = session { + self.routes = session.routes; + if state.is_none() { + // top of stack + let last_visited = session.last_visited; + self.routes.retain(|&lhs, _| lhs <= last_visited); + } + }; + let state = match state { + Some(state) => { + self.current_index = state.index; + state + } + None => { + let index = depth - 1; + self.current_index = index; + State { index } + } + }; + self.routes.insert(state.index, route); + state + } + + fn update(&mut self, route: R, state: Option) -> State { + if let Some(state) = state { + self.current_index = state.index; + self.routes.insert(self.current_index, route); + state + } else { + self.push(route) + } + } + + fn push(&mut self, route: R) -> State { + // top of stack + let index = self.current_index + 1; + self.current_index = index; + self.routes.insert(index, route); + self.routes.retain(|&rhs, _| index >= rhs); + State { + index: self.current_index, + } + } + + fn replace(&mut self, route: R) -> State { + self.routes.insert(self.current_index, route); + State { + index: self.current_index, + } + } + + fn current_route(&self) -> &R { + &self.routes[&self.current_index] + } + + fn session(&self) -> Session { + Session { + routes: self.routes.clone(), + last_visited: self.current_index, + } + } +} + +impl LiveviewHistory +where + ::Err: std::fmt::Display, +{ + /// Create a [`LiveviewHistory`] in the given scope. + /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with. + /// + /// # Panics + /// + /// Panics if not in a Liveview context. + pub fn new(cx: &ScopeState) -> Self { + Self::new_with_initial_path( + cx, + "/".parse().unwrap_or_else(|err| { + panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err) + }), + ) + } + + /// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`. + /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with. + /// + /// # Panics + /// + /// Panics if not in a Liveview context. + pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self { + let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::>(); + let action_rx = Arc::new(Mutex::new(action_rx)); + let timeline = Arc::new(Mutex::new(Timeline::new(initial_path))); + let updater_callback: Arc>> = + Arc::new(RwLock::new(Arc::new(|| {}))); + + let eval_provider = cx + .consume_context::>() + .expect("evaluator not provided"); + + let create_eval = Rc::new(move |script: &str| { + eval_provider + .new_evaluator(script.to_string()) + .map(UseEval::new) + }) as Rc Result>; + + // Listen to server actions + cx.push_future({ + let timeline = timeline.clone(); + let action_rx = action_rx.clone(); + let create_eval = create_eval.clone(); + async move { + let mut action_rx = action_rx.lock().expect("unpoisoned mutex"); + loop { + let eval = action_rx.recv().await.expect("sender to exist"); + let _ = match eval { + Action::GoBack => create_eval( + r#" + // this triggers a PopState event + history.back(); + "#, + ), + Action::GoForward => create_eval( + r#" + // this triggers a PopState event + history.forward(); + "#, + ), + Action::Push(route) => { + let mut timeline = timeline.lock().expect("unpoisoned mutex"); + let state = timeline.push(route.clone()); + let state = serde_json::to_string(&state).expect("serializable state"); + let session = serde_json::to_string(&timeline.session()) + .expect("serializable session"); + create_eval(&format!( + r#" + // this does not trigger a PopState event + history.pushState({state}, "", "{route}"); + sessionStorage.setItem("liveview", '{session}'); + "# + )) + } + Action::Replace(route) => { + let mut timeline = timeline.lock().expect("unpoisoned mutex"); + let state = timeline.replace(route.clone()); + let state = serde_json::to_string(&state).expect("serializable state"); + let session = serde_json::to_string(&timeline.session()) + .expect("serializable session"); + create_eval(&format!( + r#" + // this does not trigger a PopState event + history.replaceState({state}, "", "{route}"); + sessionStorage.setItem("liveview", '{session}'); + "# + )) + } + Action::External(url) => create_eval(&format!( + r#" + location.href = "{url}"; + "# + )), + }; + } + } + }); + + // Listen to browser actions + cx.push_future({ + let updater = updater_callback.clone(); + let timeline = timeline.clone(); + let create_eval = create_eval.clone(); + async move { + let popstate_eval = { + let init_eval = create_eval( + r#" + return [ + document.location.pathname + "?" + document.location.search + "\#" + document.location.hash, + history.state, + JSON.parse(sessionStorage.getItem("liveview")), + history.length, + ]; + "#, + ).expect("failed to load state").await.expect("serializable state"); + let (route, state, session, depth) = serde_json::from_value::<( + String, + Option, + Option>, + usize, + )>(init_eval).expect("serializable state"); + let Ok(route) = R::from_str(&route.to_string()) else { + return; + }; + let mut timeline = timeline.lock().expect("unpoisoned mutex"); + let state = timeline.init(route.clone(), state, session, depth); + let state = serde_json::to_string(&state).expect("serializable state"); + let session = serde_json::to_string(&timeline.session()) + .expect("serializable session"); + + // Call the updater callback + (updater.read().unwrap())(); + + create_eval(&format!(r#" + // this does not trigger a PopState event + history.replaceState({state}, "", "{route}"); + sessionStorage.setItem("liveview", '{session}'); + + window.addEventListener("popstate", (event) => {{ + dioxus.send([ + document.location.pathname + "?" + document.location.search + "\#" + document.location.hash, + event.state, + ]); + }}); + "#)).expect("failed to initialize popstate") + }; + + loop { + let event = match popstate_eval.recv().await { + Ok(event) => event, + Err(_) => continue, + }; + let (route, state) = serde_json::from_value::<(String, Option)>(event).expect("serializable state"); + let Ok(route) = R::from_str(&route.to_string()) else { + return; + }; + let mut timeline = timeline.lock().expect("unpoisoned mutex"); + let state = timeline.update(route.clone(), state); + let state = serde_json::to_string(&state).expect("serializable state"); + let session = serde_json::to_string(&timeline.session()) + .expect("serializable session"); + + let _ = create_eval(&format!( + r#" + // this does not trigger a PopState event + history.replaceState({state}, "", "{route}"); + sessionStorage.setItem("liveview", '{session}'); + "#)); + + // Call the updater callback + (updater.read().unwrap())(); + } + } + }); + + Self { + action_tx, + timeline, + updater_callback, + } + } +} + +impl HistoryProvider for LiveviewHistory +where + ::Err: std::fmt::Display, +{ + fn go_back(&mut self) { + let _ = self.action_tx.send(Action::GoBack); + } + + fn go_forward(&mut self) { + let _ = self.action_tx.send(Action::GoForward); + } + + fn push(&mut self, route: R) { + let _ = self.action_tx.send(Action::Push(route)); + } + + fn replace(&mut self, route: R) { + let _ = self.action_tx.send(Action::Replace(route)); + } + + fn external(&mut self, url: String) -> bool { + let _ = self.action_tx.send(Action::External(url)); + true + } + + fn current_route(&self) -> R { + let timeline = self.timeline.lock().expect("unpoisoned mutex"); + timeline.current_route().clone() + } + + fn can_go_back(&self) -> bool { + let timeline = self.timeline.lock().expect("unpoisoned mutex"); + // Check if the one before is contiguous (i.e., not an external page) + let visited_indices: Vec = timeline.routes.keys().cloned().collect(); + visited_indices + .iter() + .position(|&rhs| timeline.current_index == rhs) + .map_or(false, |index| { + index > 0 && visited_indices[index - 1] == timeline.current_index - 1 + }) + } + + fn can_go_forward(&self) -> bool { + let timeline = self.timeline.lock().expect("unpoisoned mutex"); + // Check if the one after is contiguous (i.e., not an external page) + let visited_indices: Vec = timeline.routes.keys().cloned().collect(); + visited_indices + .iter() + .rposition(|&rhs| timeline.current_index == rhs) + .map_or(false, |index| { + index < visited_indices.len() - 1 + && visited_indices[index + 1] == timeline.current_index + 1 + }) + } + + fn updater(&mut self, callback: Arc) { + let mut updater_callback = self.updater_callback.write().unwrap(); + *updater_callback = callback; + } +} + +mod routes { + use crate::prelude::Routable; + use core::str::FromStr; + use serde::de::{MapAccess, Visitor}; + use serde::{ser::SerializeMap, Deserializer, Serializer}; + use std::collections::BTreeMap; + + pub fn serialize(routes: &BTreeMap, serializer: S) -> Result + where + S: Serializer, + R: Routable, + { + let mut map = serializer.serialize_map(Some(routes.len()))?; + for (index, route) in routes.iter() { + map.serialize_entry(&index.to_string(), &route.to_string())?; + } + map.end() + } + + pub fn deserialize<'de, D, R>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + R: Routable, + ::Err: std::fmt::Display, + { + struct BTreeMapVisitor { + marker: std::marker::PhantomData, + } + + impl<'de, R> Visitor<'de> for BTreeMapVisitor + where + R: Routable, + ::Err: std::fmt::Display, + { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map with indices and routable values") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut routes = BTreeMap::new(); + while let Some((index, route)) = map.next_entry::()? { + let index = index.parse::().map_err(serde::de::Error::custom)?; + let route = R::from_str(&route).map_err(serde::de::Error::custom)?; + routes.insert(index, route); + } + Ok(routes) + } + } + + deserializer.deserialize_map(BTreeMapVisitor { + marker: std::marker::PhantomData, + }) + } +} diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs index e9668b979..cf7183366 100644 --- a/packages/router/src/history/mod.rs +++ b/packages/router/src/history/mod.rs @@ -22,6 +22,11 @@ pub use web::*; #[cfg(feature = "web")] pub(crate) mod web_history; +#[cfg(feature = "liveview")] +mod liveview; +#[cfg(feature = "liveview")] +pub use liveview::*; + // #[cfg(feature = "web")] // mod web_hash; // #[cfg(feature = "web")] diff --git a/packages/router/src/hooks/use_navigator.rs b/packages/router/src/hooks/use_navigator.rs index c1c21ee63..a05cd036f 100644 --- a/packages/router/src/hooks/use_navigator.rs +++ b/packages/router/src/hooks/use_navigator.rs @@ -48,6 +48,7 @@ use crate::prelude::{Navigator, RouterContext}; /// # let mut vdom = VirtualDom::new(App); /// # let _ = vdom.rebuild(); /// ``` +#[must_use] pub fn use_navigator(cx: &ScopeState) -> &Navigator { &*cx.use_hook(|| { let router = cx diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 42cca27fb..51d78cfca 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -46,6 +46,7 @@ use crate::utils::use_router_internal::use_router_internal; /// # let _ = vdom.rebuild(); /// # assert_eq!(dioxus_ssr::render(&vdom), "

App

Current Path

/

") /// ``` +#[must_use] pub fn use_route(cx: &ScopeState) -> Option { match use_router_internal(cx) { Some(r) => Some(r.current()), diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index 0891ebf97..2cb628160 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -2,10 +2,16 @@ use dioxus::prelude::ScopeState; use crate::{prelude::RouterContext, utils::use_router_internal::use_router_internal}; -#[deprecated = "prefer the use_navigator or use_route functions"] +#[deprecated = "prefer the `router()` function or `use_route` functions"] +#[must_use] /// A hook that provides access to information about the router. pub fn use_router(cx: &ScopeState) -> &RouterContext { use_router_internal(cx) .as_ref() .expect("use_route must have access to a router") } + +/// Aquire the router without subscribing to updates. +pub fn router() -> RouterContext { + dioxus::core::prelude::consume_context().unwrap() +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index a92394513..a1432d5b9 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -1,4 +1,6 @@ #![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")] // cannot use forbid, because props derive macro generates #[allow(missing_docs)] #![deny(missing_docs)] #![allow(non_snake_case)] @@ -51,6 +53,8 @@ pub mod hooks { pub use use_navigator::*; } +pub use hooks::router; + /// A collection of useful items most applications might need. pub mod prelude { pub use crate::components::*; diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index b1bb6d915..92277fd71 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -24,7 +24,7 @@ impl Display for RouteParseError { } } -/// Something that can be created from a query string. +/// Something that can be created from an entire query string. /// /// This trait needs to be implemented if you want to turn a query string into a struct. /// @@ -40,6 +40,41 @@ impl From<&'a str>> FromQuery for T { } } +/// Something that can be created from a query argument. +/// +/// This trait must be implemented for every type used within a query string in the router macro. +pub trait FromQueryArgument: Default { + /// The error that can occur when parsing a query argument. + type Err; + + /// Create an instance of `Self` from a query string. + fn from_query_argument(argument: &str) -> Result; +} + +impl FromQueryArgument for T +where + ::Err: Display, +{ + type Err = ::Err; + + fn from_query_argument(argument: &str) -> Result { + let result = match urlencoding::decode(argument) { + Ok(argument) => T::from_str(&argument), + Err(err) => { + tracing::error!("Failed to decode url encoding: {}", err); + T::from_str(argument) + } + }; + match result { + Ok(result) => Ok(result), + Err(err) => { + tracing::error!("Failed to parse query argument: {}", err); + Err(err) + } + } + } +} + /// Something that can be created from a route segment. pub trait FromRouteSegment: Sized { /// The error that can occur when parsing a route segment. diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml index ab5d4e882..9cd53b02f 100644 --- a/packages/rsx-rosetta/Cargo.toml +++ b/packages/rsx-rosetta/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"] [dependencies] dioxus-autofmt = { workspace = true } dioxus-rsx = { workspace = true } +dioxus-html = { workspace = true, features = ["html-to-rsx"]} html_parser.workspace = true proc-macro2 = "1.0.49" quote = "1.0.23" diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 2ceb5f038..fe68f78ad 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,6 +1,12 @@ +#![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")] + use convert_case::{Case, Casing}; +use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx}; use dioxus_rsx::{ - BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput, + AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, + ElementName, IfmtInput, }; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; @@ -20,49 +26,76 @@ pub fn rsx_node_from_html(node: &Node) -> Option { match node { Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), Node::Element(el) => { - let el_name = el.name.to_case(Case::Snake); - let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site())); + let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) { + ElementName::Ident(Ident::new(name, Span::call_site())) + } else { + // if we don't recognize it and it has a dash, we assume it's a web component + if el.name.contains('-') { + ElementName::Custom(LitStr::new(&el.name, Span::call_site())) + } else { + // otherwise, it might be an element that isn't supported yet + ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site())) + } + }; let mut attributes: Vec<_> = el .attributes .iter() .map(|(name, value)| { - let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") { - Ident::new_raw(name.as_str(), Span::call_site()) + let value = ifmt_from_text(value.as_deref().unwrap_or("false")); + let attr = if let Some(name) = map_html_attribute_to_rsx(name) { + let ident = if let Some(name) = name.strip_prefix("r#") { + Ident::new_raw(name, Span::call_site()) + } else { + Ident::new(name, Span::call_site()) + }; + ElementAttr { + value: dioxus_rsx::ElementAttrValue::AttrLiteral(value), + name: dioxus_rsx::ElementAttrName::BuiltIn(ident), + } } else { - let new_name = name.to_case(Case::Snake); - Ident::new(new_name.as_str(), Span::call_site()) + // If we don't recognize the attribute, we assume it's a custom attribute + ElementAttr { + value: dioxus_rsx::ElementAttrValue::AttrLiteral(value), + name: dioxus_rsx::ElementAttrName::Custom(LitStr::new( + name, + Span::call_site(), + )), + } }; - ElementAttrNamed { + AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - value: ifmt_from_text(value.as_deref().unwrap_or("false")), - name: ident, - }, - } + attr, + }) }) .collect(); let class = el.classes.join(" "); if !class.is_empty() { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("class", Span::call_site()), - value: ifmt_from_text(&class), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "class", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)), }, - }); + })); } if let Some(id) = &el.id { - attributes.push(ElementAttrNamed { + attributes.push(AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name: Ident::new("id", Span::call_site()), - value: ifmt_from_text(id), + attr: ElementAttr { + name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new( + "id", + Span::call_site(), + )), + value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, - }); + })); } let children = el.children.iter().filter_map(rsx_node_from_html).collect(); @@ -71,6 +104,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { name: el_name, children, attributes, + merged_attributes: Default::default(), key: None, brace: Default::default(), })) diff --git a/packages/rsx-rosetta/tests/h-tags.rs b/packages/rsx-rosetta/tests/h-tags.rs new file mode 100644 index 000000000..eb179c57f --- /dev/null +++ b/packages/rsx-rosetta/tests/h-tags.rs @@ -0,0 +1,33 @@ +use html_parser::Dom; + +#[test] +fn h_tags_translate() { + let html = r#" +
+

hello world!

+

hello world!

+

hello world!

+

hello world!

+
hello world!
+
hello world!
+
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { + h1 { "hello world!" } + h2 { "hello world!" } + h3 { "hello world!" } + h4 { "hello world!" } + h5 { "hello world!" } + h6 { "hello world!" } + }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx-rosetta/tests/raw.rs b/packages/rsx-rosetta/tests/raw.rs new file mode 100644 index 000000000..a3ed1eef8 --- /dev/null +++ b/packages/rsx-rosetta/tests/raw.rs @@ -0,0 +1,21 @@ +use html_parser::Dom; + +#[test] +fn raw_attribute() { + let html = r#" +
+
hello world!
+
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { div { "unrecognizedattribute": "asd", "hello world!" } }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx-rosetta/tests/simple.rs b/packages/rsx-rosetta/tests/simple.rs index b8fd095e1..3f3f8230a 100644 --- a/packages/rsx-rosetta/tests/simple.rs +++ b/packages/rsx-rosetta/tests/simple.rs @@ -9,8 +9,6 @@ fn simple_elements() {
hello world!
hello world!
hello world!
-
hello world!
- hello world! "# .trim(); @@ -28,8 +26,6 @@ fn simple_elements() { div { id: "asd", "hello world!" } div { r#for: "asd", "hello world!" } div { r#async: "asd", "hello world!" } - div { large_thing: "asd", "hello world!" } - ai_is_awesome { "hello world!" } }"#; pretty_assertions::assert_eq!(&out, &expected); } diff --git a/packages/rsx-rosetta/tests/web-component.rs b/packages/rsx-rosetta/tests/web-component.rs new file mode 100644 index 000000000..c7309e1b7 --- /dev/null +++ b/packages/rsx-rosetta/tests/web-component.rs @@ -0,0 +1,21 @@ +use html_parser::Dom; + +#[test] +fn web_components_translate() { + let html = r#" +
+ +
+ "# + .trim(); + + let dom = Dom::parse(html).unwrap(); + + let body = rsx_rosetta::rsx_from_html(&dom); + + let out = dioxus_autofmt::write_block_out(body).unwrap(); + + let expected = r#" + div { my-component {} }"#; + pretty_assertions::assert_eq!(&out, &expected); +} diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index bc866acb7..20893e7f4 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -20,7 +20,10 @@ quote = { version = "1.0" } serde = { version = "1.0", features = ["derive"], optional = true } internment = { version = "0.7.0", optional = true } krates = { version = "0.12.6", optional = true } +tracing.workspace = true [features] +default = ["html"] hot_reload = ["krates", "internment"] serde = ["dep:serde"] +html = [] diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs new file mode 100644 index 000000000..4c0db5a33 --- /dev/null +++ b/packages/rsx/src/attribute.rs @@ -0,0 +1,354 @@ +use std::fmt::{Display, Formatter}; + +use super::*; + +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum AttributeType { + Named(ElementAttrNamed), + Spread(Expr), +} + +impl AttributeType { + pub fn start(&self) -> Span { + match self { + AttributeType::Named(n) => n.attr.start(), + AttributeType::Spread(e) => e.span(), + } + } + + pub(crate) fn try_combine(&self, other: &Self) -> Option { + match (self, other) { + (Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named), + _ => None, + } + } +} + +impl ToTokens for AttributeType { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + AttributeType::Named(n) => tokens.append_all(quote! { #n }), + AttributeType::Spread(e) => tokens.append_all(quote! { (&#e).into() }), + } + } +} + +#[derive(Clone, Debug)] +pub struct ElementAttrNamed { + pub el_name: ElementName, + pub attr: ElementAttr, +} + +impl Hash for ElementAttrNamed { + fn hash(&self, state: &mut H) { + self.attr.name.hash(state); + } +} + +impl PartialEq for ElementAttrNamed { + fn eq(&self, other: &Self) -> bool { + self.attr == other.attr + } +} + +impl Eq for ElementAttrNamed {} + +impl ElementAttrNamed { + pub(crate) fn try_combine(&self, other: &Self) -> Option { + if self.el_name == other.el_name && self.attr.name == other.attr.name { + if let Some(separator) = self.attr.name.multi_attribute_separator() { + return Some(ElementAttrNamed { + el_name: self.el_name.clone(), + attr: ElementAttr { + name: self.attr.name.clone(), + value: self.attr.value.combine(separator, &other.attr.value), + }, + }); + } + } + None + } +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + let ns = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.1 } + } + _ => quote! { None }, + }; + let volitile = |name: &ElementAttrName| match (el_name, name) { + (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => { + quote! { dioxus_elements::#i::#name.2 } + } + _ => quote! { false }, + }; + let attribute = |name: &ElementAttrName| match name { + ElementAttrName::BuiltIn(name) => match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.0 }, + ElementName::Custom(_) => { + let as_string = name.to_string(); + quote!(#as_string) + } + }, + ElementAttrName::Custom(s) => quote! { #s }, + }; + + let attribute = { + match &attr.value { + ElementAttrValue::AttrLiteral(_) + | ElementAttrValue::AttrExpr(_) + | ElementAttrValue::AttrOptionalExpr { .. } => { + let name = &self.attr.name; + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); + let value = &self.attr.value; + let value = quote! { #value }; + quote! { + __cx.attr( + #attribute, + #value, + #ns, + #volitile + ) + } + } + ElementAttrValue::EventTokens(tokens) => match &self.attr.name { + ElementAttrName::BuiltIn(name) => { + quote! { + dioxus_elements::events::#name(__cx, #tokens) + } + } + ElementAttrName::Custom(_) => todo!(), + }, + } + }; + + tokens.append_all(attribute); + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct ElementAttr { + pub name: ElementAttrName, + pub value: ElementAttrValue, +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrValue { + /// attribute: "value" + AttrLiteral(IfmtInput), + /// attribute: if bool { "value" } + AttrOptionalExpr { + condition: Expr, + value: Box, + }, + /// attribute: true + AttrExpr(Expr), + /// onclick: move |_| {} + EventTokens(Expr), +} + +impl Parse for ElementAttrValue { + fn parse(input: ParseStream) -> syn::Result { + Ok(if input.peek(Token![if]) { + let if_expr = input.parse::()?; + if is_if_chain_terminated(&if_expr) { + ElementAttrValue::AttrExpr(Expr::If(if_expr)) + } else { + ElementAttrValue::AttrOptionalExpr { + condition: *if_expr.cond, + value: { + let stmts = if_expr.then_branch.stmts; + Box::new(syn::parse2(quote! { + #(#stmts)* + })?) + }, + } + } + } else if input.peek(LitStr) { + let value = input.parse()?; + ElementAttrValue::AttrLiteral(value) + } else { + let value = input.parse::()?; + ElementAttrValue::AttrExpr(value) + }) + } +} + +impl ToTokens for ElementAttrValue { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrValue::AttrLiteral(lit) => tokens.append_all(quote! { #lit }), + ElementAttrValue::AttrOptionalExpr { condition, value } => { + tokens.append_all(quote! { if #condition { Some(#value) } else { None } }) + } + ElementAttrValue::AttrExpr(expr) => tokens.append_all(quote! { #expr }), + ElementAttrValue::EventTokens(expr) => tokens.append_all(quote! { #expr }), + } + } +} + +impl ElementAttrValue { + fn to_str_expr(&self) -> Option { + match self { + ElementAttrValue::AttrLiteral(lit) => Some(quote!(#lit.to_string())), + ElementAttrValue::AttrOptionalExpr { value, .. } => value.to_str_expr(), + ElementAttrValue::AttrExpr(expr) => Some(quote!(#expr.to_string())), + _ => None, + } + } + + fn combine(&self, separator: &str, other: &Self) -> Self { + match (self, other) { + (Self::AttrLiteral(lit1), Self::AttrLiteral(lit2)) => { + let fmt = lit1.clone().join(lit2.clone(), separator); + Self::AttrLiteral(fmt) + } + (Self::AttrLiteral(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = expr1.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrLiteral(expr2)) => { + let mut ifmt = expr2.clone(); + ifmt.push_str(separator); + ifmt.push_expr(expr1.clone()); + Self::AttrLiteral(ifmt) + } + (Self::AttrExpr(expr1), Self::AttrExpr(expr2)) => { + let mut ifmt = IfmtInput::default(); + ifmt.push_expr(expr1.clone()); + ifmt.push_str(separator); + ifmt.push_expr(expr2.clone()); + Self::AttrLiteral(ifmt) + } + ( + Self::AttrOptionalExpr { + condition: condition1, + value: value1, + }, + Self::AttrOptionalExpr { + condition: condition2, + value: value2, + }, + ) => { + let first_as_string = value1.to_str_expr(); + let second_as_string = value2.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = String::new(); + if #condition1 { + __combined.push_str(&#first_as_string); + } + if #condition2 { + if __combined.len() > 0 { + __combined.push_str(&#separator); + } + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } + (Self::AttrOptionalExpr { condition, value }, other) => { + let first_as_string = value.to_str_expr(); + let second_as_string = other.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #second_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#first_as_string); + } + __combined + } + }) + } + (other, Self::AttrOptionalExpr { condition, value }) => { + let first_as_string = other.to_str_expr(); + let second_as_string = value.to_str_expr(); + Self::AttrExpr(parse_quote! { + { + let mut __combined = #first_as_string; + if #condition { + __combined.push_str(&#separator); + __combined.push_str(&#second_as_string); + } + __combined + } + }) + } + _ => todo!(), + } + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementAttrName { + BuiltIn(Ident), + Custom(LitStr), +} + +impl ElementAttrName { + fn multi_attribute_separator(&self) -> Option<&'static str> { + match self { + ElementAttrName::BuiltIn(i) => match i.to_string().as_str() { + "class" => Some(" "), + "style" => Some(";"), + _ => None, + }, + ElementAttrName::Custom(_) => None, + } + } + + pub fn start(&self) -> Span { + match self { + ElementAttrName::BuiltIn(i) => i.span(), + ElementAttrName::Custom(s) => s.span(), + } + } +} + +impl ToTokens for ElementAttrName { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementAttrName::BuiltIn(i) => tokens.append_all(quote! { #i }), + ElementAttrName::Custom(s) => tokens.append_all(quote! { #s }), + } + } +} + +impl Display for ElementAttrName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ElementAttrName::BuiltIn(i) => write!(f, "{}", i), + ElementAttrName::Custom(s) => write!(f, "{}", s.value()), + } + } +} + +impl ElementAttr { + pub fn start(&self) -> Span { + self.name.start() + } + + pub fn is_expr(&self) -> bool { + matches!( + self, + ElementAttr { + value: ElementAttrValue::AttrExpr(_) | ElementAttrValue::EventTokens(_), + .. + } + ) + } +} diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 6487e5ab7..bccef2638 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -151,8 +151,8 @@ impl ToTokens for Component { } None => { let mut toks = match prop_gen_args { - Some(gen_args) => quote! { fc_to_builder(#name #gen_args) }, - None => quote! { fc_to_builder(#name) }, + Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) }, + None => quote! { fc_to_builder(__cx, #name) }, }; for field in &self.fields { match field.name.to_string().as_str() { diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 8f45d91ff..e62fc6d6e 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -8,7 +8,7 @@ use syn::{ parse::{Parse, ParseBuffer, ParseStream}, punctuated::Punctuated, spanned::Spanned, - Error, Expr, Ident, LitStr, Result, Token, + Expr, Ident, LitStr, Result, Token, }; // ======================================= @@ -18,7 +18,8 @@ use syn::{ pub struct Element { pub name: ElementName, pub key: Option, - pub attributes: Vec, + pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, } @@ -31,10 +32,9 @@ impl Parse for Element { let content: ParseBuffer; let brace = syn::braced!(content in stream); - let mut attributes: Vec = vec![]; + let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; - let mut _el_ref = None; // parse fields with commas // break when we don't get this pattern anymore @@ -42,6 +42,22 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { + if content.peek(Token![..]) { + content.parse::()?; + let expr = content.parse::()?; + let span = expr.span(); + attributes.push(attribute::AttributeType::Spread(expr)); + + if content.is_empty() { + break; + } + + if content.parse::().is_err() { + missing_trailing_comma!(span); + } + continue; + } + // Parse the raw literal fields if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; @@ -49,19 +65,14 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) { - let value = content.parse()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } + let value = content.parse::()?; + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::Custom(name), + value, + }, + })); if content.is_empty() { break; @@ -84,41 +95,27 @@ impl Parse for Element { let span = content.span(); if name_str.starts_with("on") { - attributes.push(ElementAttrNamed { + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value: ElementAttrValue::EventTokens(content.parse()?), }, - }); + })); } else { match name_str.as_str() { "key" => { key = Some(content.parse()?); } - "classes" => todo!("custom class list not supported yet"), - // "namespace" => todo!("custom namespace not supported yet"), - "node_ref" => { - _el_ref = Some(content.parse::()?); - } _ => { - if content.peek(LitStr) { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name, - value: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, - }, - }); - } + let value = content.parse::()?; + attributes.push(attribute::AttributeType::Named(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr { + name: ElementAttrName::BuiltIn(name), + value, + }, + })); } } } @@ -127,7 +124,6 @@ impl Parse for Element { break; } - // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { missing_trailing_comma!(span); } @@ -137,6 +133,37 @@ impl Parse for Element { break; } + // Deduplicate any attributes that can be combined + // For example, if there are two `class` attributes, combine them into one + let mut merged_attributes: Vec = Vec::new(); + for attr in &attributes { + if let Some(old_attr_index) = merged_attributes.iter().position(|a| { + matches!((a, attr), ( + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(old_name), + .. + }, + .. + }), + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + name: ElementAttrName::BuiltIn(new_name), + .. + }, + .. + }), + ) if old_name == new_name) + }) { + let old_attr = &mut merged_attributes[old_attr_index]; + if let Some(combined) = old_attr.try_combine(attr) { + *old_attr = combined; + } + } else { + merged_attributes.push(attr.clone()); + } + } + while !content.is_empty() { if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { attr_after_element!(content.span()); @@ -158,6 +185,7 @@ impl Parse for Element { key, name: el_name, attributes, + merged_attributes, children, brace, }) @@ -174,15 +202,31 @@ impl ToTokens for Element { None => quote! { None }, }; - let listeners = self - .attributes - .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + let listeners = self.merged_attributes.iter().filter(|f| { + matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); - let attr = self - .attributes - .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + let attr = self.merged_attributes.iter().filter(|f| { + !matches!( + f, + AttributeType::Named(ElementAttrNamed { + attr: ElementAttr { + value: ElementAttrValue::EventTokens { .. }, + .. + }, + .. + }) + ) + }); tokens.append_all(quote! { __cx.element( @@ -264,136 +308,3 @@ impl ToTokens for ElementName { } } } - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub enum ElementAttr { - /// `attribute: "value"` - AttrText { name: Ident, value: IfmtInput }, - - /// `attribute: true` - AttrExpression { name: Ident, value: Expr }, - - /// `"attribute": "value"` - CustomAttrText { name: LitStr, value: IfmtInput }, - - /// `"attribute": true` - CustomAttrExpression { name: LitStr, value: Expr }, - - // /// onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, - /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, -} - -impl ElementAttr { - pub fn start(&self) -> Span { - match self { - ElementAttr::AttrText { name, .. } => name.span(), - ElementAttr::AttrExpression { name, .. } => name.span(), - ElementAttr::CustomAttrText { name, .. } => name.span(), - ElementAttr::CustomAttrExpression { name, .. } => name.span(), - ElementAttr::EventTokens { name, .. } => name.span(), - } - } - - pub fn is_expr(&self) -> bool { - matches!( - self, - ElementAttr::AttrExpression { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } - ) - } -} - -#[derive(PartialEq, Eq, Clone, Debug, Hash)] -pub struct ElementAttrNamed { - pub el_name: ElementName, - pub attr: ElementAttr, -} - -impl ToTokens for ElementAttrNamed { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ElementAttrNamed { el_name, attr } = self; - - let ns = |name| match el_name { - ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, - ElementName::Custom(_) => quote! { None }, - }; - let volitile = |name| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.2 }, - ElementName::Custom(_) => quote! { false }, - }; - let attribute = |name: &Ident| match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { - let as_string = name.to_string(); - quote!(#as_string) - } - }; - - let attribute = match attr { - ElementAttr::AttrText { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::AttrExpression { name, value } => { - let ns = ns(name); - let volitile = volitile(name); - let attribute = attribute(name); - quote! { - __cx.attr( - #attribute, - #value, - #ns, - #volitile - ) - } - } - ElementAttr::CustomAttrText { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { - __cx.attr( - #name, - #value, - None, - false - ) - } - } - ElementAttr::EventTokens { name, tokens } => { - quote! { - dioxus_elements::events::#name(__cx, #tokens) - } - } - }; - - tokens.append_all(attribute); - } -} - -// ::dioxus::core::Attribute { -// name: stringify!(#name), -// namespace: None, -// volatile: false, -// mounted_node: Default::default(), -// value: ::dioxus::core::AttributeValue::Text(#value), -// } diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs index f89b28500..6cc34e1fb 100644 --- a/packages/rsx/src/errors.rs +++ b/packages/rsx/src/errors.rs @@ -1,12 +1,12 @@ macro_rules! missing_trailing_comma { ($span:expr) => { - return Err(Error::new($span, "missing trailing comma")); + return Err(syn::Error::new($span, "missing trailing comma")); }; } macro_rules! attr_after_element { ($span:expr) => { - return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); + return Err(syn::Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); }; } diff --git a/packages/rsx/src/hot_reload/hot_reload_diff.rs b/packages/rsx/src/hot_reload/hot_reload_diff.rs index 22a01875d..7e19d3aad 100644 --- a/packages/rsx/src/hot_reload/hot_reload_diff.rs +++ b/packages/rsx/src/hot_reload/hot_reload_diff.rs @@ -1,4 +1,5 @@ use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{File, Macro}; pub enum DiffResult { @@ -10,13 +11,30 @@ pub enum DiffResult { pub fn find_rsx(new: &File, old: &File) -> DiffResult { let mut rsx_calls = Vec::new(); if new.items.len() != old.items.len() { + tracing::trace!( + "found not hot reload-able change {:#?} != {:#?}", + new.items + .iter() + .map(|i| i.to_token_stream().to_string()) + .collect::>(), + old.items + .iter() + .map(|i| i.to_token_stream().to_string()) + .collect::>() + ); return DiffResult::CodeChanged; } for (new, old) in new.items.iter().zip(old.items.iter()) { if find_rsx_item(new, old, &mut rsx_calls) { + tracing::trace!( + "found not hot reload-able change {:#?} != {:#?}", + new.to_token_stream().to_string(), + old.to_token_stream().to_string() + ); return DiffResult::CodeChanged; } } + tracing::trace!("found hot reload-able changes {:#?}", rsx_calls); DiffResult::RsxChanged(rsx_calls) } @@ -94,6 +112,9 @@ fn find_rsx_item( (syn::ImplItem::Macro(new_item), syn::ImplItem::Macro(old_item)) => { old_item != new_item } + (syn::ImplItem::Verbatim(stream), syn::ImplItem::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } { return true; @@ -186,10 +207,12 @@ fn find_rsx_trait( } } (syn::TraitItem::Fn(new_item), syn::TraitItem::Fn(old_item)) => { - if let (Some(new_block), Some(old_block)) = (&new_item.default, &old_item.default) { - find_rsx_block(new_block, old_block, rsx_calls) - } else { - true + match (&new_item.default, &old_item.default) { + (Some(new_block), Some(old_block)) => { + find_rsx_block(new_block, old_block, rsx_calls) + } + (None, None) => false, + _ => true, } } (syn::TraitItem::Type(new_item), syn::TraitItem::Type(old_item)) => { @@ -198,6 +221,9 @@ fn find_rsx_trait( (syn::TraitItem::Macro(new_item), syn::TraitItem::Macro(old_item)) => { old_item != new_item } + (syn::TraitItem::Verbatim(stream), syn::TraitItem::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } { return true; @@ -355,6 +381,11 @@ fn find_rsx_expr( || new_expr.or2_token != old_expr.or2_token || new_expr.output != old_expr.output } + (syn::Expr::Const(new_expr), syn::Expr::Const(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.const_token != old_expr.const_token + } (syn::Expr::Continue(new_expr), syn::Expr::Continue(old_expr)) => old_expr != new_expr, (syn::Expr::Field(new_expr), syn::Expr::Field(old_expr)) => { find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls) @@ -402,6 +433,7 @@ fn find_rsx_expr( || new_expr.attrs != old_expr.attrs || new_expr.bracket_token != old_expr.bracket_token } + (syn::Expr::Infer(new_expr), syn::Expr::Infer(old_expr)) => new_expr != old_expr, (syn::Expr::Let(new_expr), syn::Expr::Let(old_expr)) => { find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) || new_expr.attrs != old_expr.attrs @@ -589,7 +621,9 @@ fn find_rsx_expr( _ => true, } } - (syn::Expr::Verbatim(_), syn::Expr::Verbatim(_)) => false, + (syn::Expr::Verbatim(stream), syn::Expr::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } } diff --git a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs index 0393142b4..7e36dddf9 100644 --- a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs +++ b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs @@ -51,17 +51,24 @@ impl FileMap { fn find_rs_files( root: PathBuf, filter: &mut impl FnMut(&Path) -> bool, - ) -> io::Result { + ) -> FileMapSearchResult { let mut files = HashMap::new(); let mut errors = Vec::new(); if root.is_dir() { - for entry in (fs::read_dir(root)?).flatten() { + let read_dir = match fs::read_dir(root) { + Ok(read_dir) => read_dir, + Err(err) => { + errors.push(err); + return FileMapSearchResult { map: files, errors }; + } + }; + for entry in read_dir.flatten() { let path = entry.path(); if !filter(&path) { let FileMapSearchResult { map, errors: child_errors, - } = find_rs_files(path, filter)?; + } = find_rs_files(path, filter); errors.extend(child_errors); files.extend(map); } @@ -69,14 +76,20 @@ impl FileMap { } else if root.extension().and_then(|s| s.to_str()) == Some("rs") { if let Ok(mut file) = File::open(root.clone()) { let mut src = String::new(); - file.read_to_string(&mut src)?; - files.insert(root, (src, None)); + match file.read_to_string(&mut src) { + Ok(_) => { + files.insert(root, (src, None)); + } + Err(err) => { + errors.push(err); + } + } } } - Ok(FileMapSearchResult { map: files, errors }) + FileMapSearchResult { map: files, errors } } - let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter)?; + let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter); let result = Self { map, in_workspace: HashMap::new(), diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index 6c72f5025..fa764bf2e 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -13,7 +13,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result { } #[allow(dead_code)] // dumb compiler does not see the struct being used... -#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] pub struct IfmtInput { pub source: Option, pub segments: Vec, @@ -27,8 +27,29 @@ impl IfmtInput { } } + pub fn join(mut self, other: Self, separator: &str) -> Self { + if !self.segments.is_empty() { + self.segments.push(Segment::Literal(separator.to_string())); + } + self.segments.extend(other.segments); + self + } + + pub fn push_expr(&mut self, expr: Expr) { + self.segments.push(Segment::Formatted(FormattedSegment { + format_args: String::new(), + segment: FormattedSegmentType::Expr(Box::new(expr)), + })); + } + + pub fn push_str(&mut self, s: &str) { + self.segments.push(Segment::Literal(s.to_string())); + } + pub fn is_static(&self) -> bool { - matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[]) + self.segments + .iter() + .all(|seg| matches!(seg, Segment::Literal(_))) } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f52ecad0e..aee1bb431 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,3 +1,6 @@ +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] + //! Parse the root tokens in the rsx!{} macro //! ========================================= //! @@ -13,6 +16,7 @@ #[macro_use] mod errors; +mod attribute; mod component; mod element; #[cfg(feature = "hot_reload")] @@ -23,6 +27,7 @@ mod node; use std::{fmt::Debug, hash::Hash}; // Re-export the namespaces into each other +pub use attribute::*; pub use component::*; #[cfg(feature = "hot_reload")] use dioxus_core::{Template, TemplateAttribute, TemplateNode}; @@ -190,7 +195,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let mut context = DynamicContext::default(); - let key = match self.roots.get(0) { + let key = match self.roots.first() { Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(), Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(), _ => None, @@ -201,12 +206,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> { None => quote! { None }, }; - let spndbg = format!("{:?}", self.roots[0].span()); - let root_col = spndbg - .rsplit_once("..") - .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before)) - .unwrap_or_default(); - + let root_col = match self.roots.first() { + Some(first_root) => { + let first_root_span = format!("{:?}", first_root.span()); + first_root_span + .rsplit_once("..") + .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before)) + .unwrap_or_default() + .to_string() + } + _ => "0".to_string(), + }; let root_printer = self.roots.iter().enumerate().map(|(idx, root)| { context.current_path.push(idx as u8); let out = context.render_static_node(root); @@ -244,14 +254,13 @@ impl<'a> ToTokens for TemplateRenderer<'a> { node_paths: &[ #(#node_paths),* ], attr_paths: &[ #(#attr_paths),* ], }; - ::dioxus::core::VNode { - parent: None, - key: #key_tokens, - template: std::cell::Cell::new(TEMPLATE), - root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(), - dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), - dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), - } + ::dioxus::core::VNode::new( + #key_tokens, + TEMPLATE, + dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()), + __cx.bump().alloc([ #( #node_printer ),* ]), + __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), + ) }); } } @@ -259,7 +268,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> { #[cfg(feature = "hot_reload")] #[derive(Default, Debug)] struct DynamicMapping { - attribute_to_idx: std::collections::HashMap>, + attribute_to_idx: std::collections::HashMap>, last_attribute_idx: usize, node_to_idx: std::collections::HashMap>, last_element_idx: usize, @@ -275,7 +284,7 @@ impl DynamicMapping { new } - fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option { + fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option { self.attribute_to_idx .get_mut(attr) .and_then(|idxs| idxs.pop()) @@ -285,7 +294,7 @@ impl DynamicMapping { self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop()) } - fn insert_attribute(&mut self, attr: ElementAttr) -> usize { + fn insert_attribute(&mut self, attr: AttributeType) -> usize { let idx = self.last_attribute_idx; self.last_attribute_idx += 1; @@ -306,18 +315,18 @@ impl DynamicMapping { fn add_node(&mut self, node: BodyNode) { match node { BodyNode::Element(el) => { - for attr in el.attributes { - match &attr.attr { - ElementAttr::CustomAttrText { value, .. } - | ElementAttr::AttrText { value, .. } - if value.is_static() => {} - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { - self.insert_attribute(attr.attr); + for attr in el.merged_attributes { + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(input), + .. + }, + .. + }) if input.is_static() => {} + _ => { + self.insert_attribute(attr); } } } @@ -345,7 +354,7 @@ impl DynamicMapping { #[derive(Default, Debug)] pub struct DynamicContext<'a> { dynamic_nodes: Vec<&'a BodyNode>, - dynamic_attributes: Vec<&'a ElementAttrNamed>, + dynamic_attributes: Vec<&'a AttributeType>, current_path: Vec, node_paths: Vec>, @@ -364,9 +373,16 @@ impl<'a> DynamicContext<'a> { let element_name_rust = el.name.to_string(); let mut static_attrs = Vec::new(); - for attr in &el.attributes { - match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + for attr in &el.merged_attributes { + match &attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) if value.is_static() => { let value = value.source.as_ref().unwrap(); let attribute_name_rust = name.to_string(); let (name, namespace) = @@ -379,22 +395,9 @@ impl<'a> DynamicContext<'a> { }) } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); - static_attrs.push(TemplateAttribute::Static { - name: intern(name.value().as_str()), - namespace: None, - value: intern(value.value().as_str()), - }) - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + _ => { let idx = match mapping { - Some(mapping) => mapping.get_attribute_idx(&attr.attr)?, + Some(mapping) => mapping.get_attribute_idx(attr)?, None => self.dynamic_attributes.len(), }; self.dynamic_attributes.push(attr); @@ -464,13 +467,27 @@ impl<'a> DynamicContext<'a> { ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, ElementName::Custom(_) => quote! { None }, }; - let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { + let static_attrs = el.merged_attributes.iter().map(|attr| match attr { + AttributeType::Named(ElementAttrNamed { + attr: + ElementAttr { + value: ElementAttrValue::AttrLiteral(value), + name, + }, + .. + }) if value.is_static() => { let value = value.to_static().unwrap(); - let ns = ns(quote!(#name.1)); - let name = match el_name { - ElementName::Ident(_) => quote! { #el_name::#name.0 }, - ElementName::Custom(_) => { + let ns = { + match name { + ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)), + ElementAttrName::Custom(_) => quote!(None), + } + }; + let name = match (el_name, name) { + (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => { + quote! { #el_name::#name.0 } + } + _ => { let as_string = name.to_string(); quote! { #as_string } } @@ -487,25 +504,7 @@ impl<'a> DynamicContext<'a> { } } - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.to_static().unwrap(); - quote! { - ::dioxus::core::TemplateAttribute::Static { - name: #name, - namespace: None, - value: #value, - - // todo: we don't diff these so we never apply the volatile flag - // volatile: dioxus_elements::#el_name::#name.2, - } - } - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } - | ElementAttr::EventTokens { .. } => { + _ => { let ct = self.dynamic_attributes.len(); self.dynamic_attributes.push(attr); self.attr_paths.push(self.current_path.clone()); diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 1bda081d9..c71611082 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -125,7 +125,10 @@ impl ToTokens for BodyNode { __cx.text_node(#txt) }), BodyNode::RawExpr(exp) => tokens.append_all(quote! { - __cx.make_node(#exp) + { + let ___nodes = (#exp).into_dyn_node(__cx); + ___nodes + } }), BodyNode::ForLoop(exp) => { let ForLoop { @@ -137,16 +140,23 @@ impl ToTokens for BodyNode { location: None, }; + // Signals expose an issue with temporary lifetimes + // We need to directly render out the nodes first to collapse their lifetime to <'a> + // And then we can return them into the dyn loop tokens.append_all(quote! { - __cx.make_node( - (#expr).into_iter().map(|#pat| { #renderer }) - ) + { + let ___nodes =(#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node(__cx); + ___nodes + } }) } BodyNode::IfChain(chain) => { if is_if_chain_terminated(chain) { tokens.append_all(quote! { - __cx.make_node(#chain) + { + let ___nodes = (#chain).into_dyn_node(__cx); + ___nodes + } }); } else { let ExprIf { @@ -200,7 +210,10 @@ impl ToTokens for BodyNode { }); tokens.append_all(quote! { - __cx.make_node(#body) + { + let ___nodes = (#body).into_dyn_node(__cx); + ___nodes + } }); } } @@ -247,7 +260,7 @@ impl Parse for ForLoop { } } -fn is_if_chain_terminated(chain: &ExprIf) -> bool { +pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool { let mut current = chain; loop { if let Some((_, else_block)) = ¤t.else_branch { diff --git a/packages/server-macro/Cargo.toml b/packages/server-macro/Cargo.toml index 5a38acb40..a91cc626a 100644 --- a/packages/server-macro/Cargo.toml +++ b/packages/server-macro/Cargo.toml @@ -17,7 +17,7 @@ proc-macro2 = "^1.0.63" quote = "^1.0.26" syn = { version = "2", features = ["full"] } convert_case = "^0.6.0" -server_fn_macro = "^0.4.6" +server_fn_macro = "^0.5.2" [lib] proc-macro = true diff --git a/packages/server-macro/src/lib.rs b/packages/server-macro/src/lib.rs index d00c009d0..871de33bc 100644 --- a/packages/server-macro/src/lib.rs +++ b/packages/server-macro/src/lib.rs @@ -1,3 +1,7 @@ +// TODO: Create README, uncomment this: #![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")] + use convert_case::{Case, Converter}; use proc_macro::TokenStream; use proc_macro2::Literal; @@ -8,14 +12,14 @@ use syn::{ Ident, ItemFn, Token, }; -/// Declares that a function is a [server function](dioxus_fullstack). This means that +/// Declares that a function is a [server function](https://dioxuslabs.com/learn/0.4/reference/fullstack/server_functions). This means that /// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// /// If you call a server function from the client (i.e., when the `csr` or `hydrate` features /// are enabled), it will instead make a network request to the server. /// /// You can specify one, two, or three arguments to the server function: -/// 1. **Required**: A type name that will be used to identify and register the server function +/// 1. *Optional*: A type name that will be used to identify and register the server function /// (e.g., `MyServerFn`). /// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered /// (e.g., `"/api"`). Defaults to `"/"`. @@ -105,7 +109,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { let upper_cammel_case_name = Converter::new() .from_case(Case::Snake) .to_case(Case::UpperCamel) - .convert(&sig.ident.to_string()); + .convert(sig.ident.to_string()); args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span())); } let struct_name = args.struct_name.as_ref().unwrap(); diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 56e19bbd8..c85fb6d7f 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "dioxus-signals" authors = ["Jonathan Kelley"] -version = "0.0.0" +version = "0.4.3" edition = "2018" +description = "Signals for Dioxus" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "wasm"] +rust-version = "1.60.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,4 +26,4 @@ tokio = { version = "1", features = ["full"] } [features] default = [] -serialize = ["serde"] \ No newline at end of file +serialize = ["serde"] diff --git a/packages/signals/examples/split_subscriptions.rs b/packages/signals/examples/split_subscriptions.rs index 3eb60ed29..bb286b4b7 100644 --- a/packages/signals/examples/split_subscriptions.rs +++ b/packages/signals/examples/split_subscriptions.rs @@ -22,15 +22,9 @@ fn app(cx: Scope) -> Element { use_context_provider(cx, ApplicationData::default); render! { - div { - ReadsFirst {} - } - div { - ReadsSecond {} - } - div { - ReadsManySignals {} - } + div { ReadsFirst {} } + div { ReadsSecond {} } + div { ReadsManySignals {} } } } @@ -107,16 +101,14 @@ fn ReadsManySignals(cx: Scope) -> Element { } button { onclick: move |_| { - if let Some(first) = data.many_signals.read().get(0) { + if let Some(first) = data.many_signals.read().first() { *first.write() += 1; } }, "Increase First Item" } for signal in data.many_signals { - Child { - count: signal, - } + Child { count: signal } } } } diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 9a54188d9..e09e58cf0 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -1,16 +1,28 @@ use core::{self, fmt::Debug}; -use std::cell::RefCell; use std::fmt::{self, Formatter}; -use std::rc::Rc; // use dioxus_core::prelude::*; use crate::use_signal; use crate::{dependency::Dependency, CopyValue}; -#[derive(Default, Clone)] +#[derive(Copy, Clone, PartialEq)] pub(crate) struct EffectStack { - pub(crate) effects: Rc>>, + pub(crate) effects: CopyValue>, +} + +impl Default for EffectStack { + fn default() -> Self { + Self { + effects: CopyValue::new_in_scope(Vec::new(), ScopeId::ROOT), + } + } +} + +impl EffectStack { + pub(crate) fn current(&self) -> Option { + self.effects.read().last().copied() + } } pub(crate) fn get_effect_stack() -> EffectStack { @@ -18,7 +30,7 @@ pub(crate) fn get_effect_stack() -> EffectStack { Some(rt) => rt, None => { let store = EffectStack::default(); - provide_root_context(store.clone()); + provide_root_context(store); store } } @@ -57,6 +69,7 @@ pub fn use_effect_with_dependencies( pub struct Effect { pub(crate) source: ScopeId, pub(crate) callback: CopyValue>, + pub(crate) effect_stack: EffectStack, } impl Debug for Effect { @@ -67,7 +80,7 @@ impl Debug for Effect { impl Effect { pub(crate) fn current() -> Option { - get_effect_stack().effects.borrow().last().copied() + get_effect_stack().effects.read().last().copied() } /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. @@ -77,6 +90,7 @@ impl Effect { let myself = Self { source: current_scope_id().expect("in a virtual dom"), callback: CopyValue::new(Box::new(callback)), + effect_stack: get_effect_stack(), }; myself.try_run(); @@ -86,13 +100,13 @@ impl Effect { /// Run the effect callback immediately. Returns `true` if the effect was run. Returns `false` is the effect is dead. pub fn try_run(&self) { - if let Some(mut callback) = self.callback.try_write() { + if let Ok(mut callback) = self.callback.try_write() { { - get_effect_stack().effects.borrow_mut().push(*self); + self.effect_stack.effects.write().push(*self); } callback(); { - get_effect_stack().effects.borrow_mut().pop(); + self.effect_stack.effects.write().pop(); } } } diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 6427c2e2b..077777c40 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -1,7 +1,7 @@ use crate::rt::CopyValue; use crate::signal::{ReadOnlySignal, Signal, Write}; - -use std::cell::{Ref, RefMut}; +use generational_box::GenerationalRef; +use generational_box::GenerationalRefMut; use std::{ fmt::{Debug, Display}, @@ -38,8 +38,8 @@ macro_rules! read_impls { impl $ty> { /// Read a value from the inner vector. - pub fn get(&self, index: usize) -> Option> { - Ref::filter_map(self.read(), |v| v.get(index)).ok() + pub fn get(&self, index: usize) -> Option> { + GenerationalRef::filter_map(self.read(), |v| v.get(index)) } } @@ -52,9 +52,9 @@ macro_rules! read_impls { self.with(|v| v.clone()).unwrap() } - /// Attemps to read the inner value of the Option. - pub fn as_ref(&self) -> Option> { - Ref::filter_map(self.read(), |v| v.as_ref()).ok() + /// Attempts to read the inner value of the Option. + pub fn as_ref(&self) -> Option> { + GenerationalRef::filter_map(self.read(), |v| v.as_ref()) } } }; @@ -182,19 +182,19 @@ macro_rules! write_impls { } /// Gets the value out of the Option, or inserts the given value if the Option is empty. - pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { + pub fn get_or_insert(&self, default: T) -> GenerationalRef { self.get_or_insert_with(|| default) } /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty. - pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> { + pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> GenerationalRef { let borrow = self.read(); if borrow.is_none() { drop(borrow); self.with_mut(|v| *v = Some(default())); - Ref::map(self.read(), |v| v.as_ref().unwrap()) + GenerationalRef::map(self.read(), |v| v.as_ref().unwrap()) } else { - Ref::map(borrow, |v| v.as_ref().unwrap()) + GenerationalRef::map(borrow, |v| v.as_ref().unwrap()) } } } @@ -238,15 +238,15 @@ impl IntoIterator for CopyValue> { impl CopyValue> { /// Write to an element in the inner vector. - pub fn get_mut(&self, index: usize) -> Option> { - RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() + pub fn get_mut(&self, index: usize) -> Option> { + GenerationalRefMut::filter_map(self.write(), |v| v.get_mut(index)) } } impl CopyValue> { /// Deref the inner value mutably. - pub fn as_mut(&self) -> Option> { - RefMut::filter_map(self.write(), |v| v.as_mut()).ok() + pub fn as_mut(&self) -> Option> { + GenerationalRefMut::filter_map(self.write(), |v| v.as_mut()) } } @@ -281,14 +281,14 @@ impl IntoIterator for Signal> { impl Signal> { /// Returns a reference to an element or `None` if out of bounds. - pub fn get_mut(&self, index: usize) -> Option>> { + pub fn get_mut(&self, index: usize) -> Option>> { Write::filter_map(self.write(), |v| v.get_mut(index)) } } impl Signal> { /// Returns a reference to an element or `None` if out of bounds. - pub fn as_mut(&self) -> Option>> { + pub fn as_mut(&self) -> Option>> { Write::filter_map(self.write(), |v| v.as_mut()) } } diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 1f8bc3297..dce9af0a8 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -1,4 +1,6 @@ #![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")] #![warn(missing_docs)] mod rt; diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index 113fe9b30..b0c09d7f1 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -1,11 +1,13 @@ -use std::cell::{Ref, RefMut}; - +use std::mem::MaybeUninit; +use std::ops::Deref; use std::rc::Rc; use dioxus_core::prelude::*; use dioxus_core::ScopeId; -use generational_box::{GenerationalBox, Owner, Store}; +use generational_box::{ + BorrowError, BorrowMutError, GenerationalBox, GenerationalRef, GenerationalRefMut, Owner, Store, +}; use crate::Effect; @@ -81,6 +83,7 @@ impl CopyValue { /// Create a new CopyValue. The value will be stored in the current component. /// /// Once the component this value is created in is dropped, the value will be dropped. + #[track_caller] pub fn new(value: T) -> Self { let owner = current_owner(); @@ -90,6 +93,22 @@ impl CopyValue { } } + pub(crate) fn new_with_caller( + value: T, + #[cfg(debug_assertions)] caller: &'static std::panic::Location<'static>, + ) -> Self { + let owner = current_owner(); + + Self { + value: owner.insert_with_caller( + value, + #[cfg(debug_assertions)] + caller, + ), + origin_scope: current_scope_id().expect("in a virtual dom"), + } + } + /// Create a new CopyValue. The value will be stored in the given scope. When the specified scope is dropped, the value will be dropped. pub fn new_in_scope(value: T, scope: ScopeId) -> Self { let owner = owner_in_scope(scope); @@ -115,22 +134,26 @@ impl CopyValue { } /// Try to read the value. If the value has been dropped, this will return None. - pub fn try_read(&self) -> Option> { + #[track_caller] + pub fn try_read(&self) -> Result, BorrowError> { self.value.try_read() } /// Read the value. If the value has been dropped, this will panic. - pub fn read(&self) -> Ref<'_, T> { + #[track_caller] + pub fn read(&self) -> GenerationalRef { self.value.read() } /// Try to write the value. If the value has been dropped, this will return None. - pub fn try_write(&self) -> Option> { + #[track_caller] + pub fn try_write(&self) -> Result, BorrowMutError> { self.value.try_write() } /// Write the value. If the value has been dropped, this will panic. - pub fn write(&self) -> RefMut<'_, T> { + #[track_caller] + pub fn write(&self) -> GenerationalRefMut { self.value.write() } @@ -164,3 +187,36 @@ impl PartialEq for CopyValue { self.value.ptr_eq(&other.value) } } + +impl Deref for CopyValue { + type Target = dyn Fn() -> GenerationalRef; + + fn deref(&self) -> &Self::Target { + // https://github.com/dtolnay/case-studies/tree/master/callable-types + + // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). + let uninit_callable = MaybeUninit::::uninit(); + // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. + let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }); + + // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. + let size_of_closure = std::mem::size_of_val(&uninit_closure); + assert_eq!(size_of_closure, std::mem::size_of::()); + + // Then cast the lifetime of the closure to the lifetime of &self. + fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { + b + } + let reference_to_closure = cast_lifetime( + { + // The real closure that we will never use. + &uninit_closure + }, + // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. + unsafe { std::mem::transmute(self) }, + ); + + // Cast the closure to a trait object. + reference_to_closure as &Self::Target + } +} diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs index 346506ff7..556c8437c 100644 --- a/packages/signals/src/selector.rs +++ b/packages/signals/src/selector.rs @@ -21,6 +21,7 @@ use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySig /// render! { "{double}" } /// } /// ``` +#[must_use = "Consider using `use_effect` to rerun a callback when dependencies change"] pub fn use_selector( cx: &ScopeState, f: impl FnMut() -> R + 'static, @@ -44,6 +45,7 @@ pub fn use_selector( /// render! { "{double}" } /// } /// ``` +#[must_use = "Consider using `use_effect` to rerun a callback when dependencies change"] pub fn use_selector_with_dependencies( cx: &ScopeState, dependencies: D, @@ -76,19 +78,21 @@ pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig let effect = Effect { source: current_scope_id().expect("in a virtual dom"), callback: CopyValue::invalid(), + effect_stack: get_effect_stack(), }; { - get_effect_stack().effects.borrow_mut().push(effect); + get_effect_stack().effects.write().push(effect); } state.inner.value.set(SignalData { subscribers: Default::default(), effect_subscribers: Default::default(), update_any: schedule_update_any().expect("in a virtual dom"), value: f(), + effect_stack: get_effect_stack(), }); { - get_effect_stack().effects.borrow_mut().pop(); + get_effect_stack().effects.write().pop(); } effect.callback.value.set(Box::new(move || { diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index c1ae076d9..946a6c380 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,5 +1,6 @@ use std::{ - cell::{Ref, RefCell, RefMut}, + cell::RefCell, + mem::MaybeUninit, ops::{Deref, DerefMut}, rc::Rc, sync::Arc, @@ -9,8 +10,9 @@ use dioxus_core::{ prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, ScopeId, ScopeState, }; +use generational_box::{GenerationalRef, GenerationalRefMut}; -use crate::{CopyValue, Effect}; +use crate::{get_effect_stack, CopyValue, Effect, EffectStack}; /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. /// @@ -43,8 +45,19 @@ use crate::{CopyValue, Effect}; /// } /// } /// ``` +#[track_caller] +#[must_use] pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { - *cx.use_hook(|| Signal::new(f())) + #[cfg(debug_assertions)] + let caller = std::panic::Location::caller(); + + *cx.use_hook(|| { + Signal::new_with_caller( + f(), + #[cfg(debug_assertions)] + caller, + ) + }) } #[derive(Clone)] @@ -80,6 +93,7 @@ pub(crate) struct SignalData { pub(crate) subscribers: Rc>>, pub(crate) effect_subscribers: Rc>>, pub(crate) update_any: Arc, + pub(crate) effect_stack: EffectStack, pub(crate) value: T, } @@ -135,6 +149,7 @@ impl<'de, T: serde::Deserialize<'de> + 'static> serde::Deserialize<'de> for Sign impl Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. + #[track_caller] pub fn new(value: T) -> Self { Self { inner: CopyValue::new(SignalData { @@ -142,10 +157,31 @@ impl Signal { effect_subscribers: Default::default(), update_any: schedule_update_any().expect("in a virtual dom"), value, + effect_stack: get_effect_stack(), }), } } + /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. + fn new_with_caller( + value: T, + #[cfg(debug_assertions)] caller: &'static std::panic::Location<'static>, + ) -> Self { + Self { + inner: CopyValue::new_with_caller( + SignalData { + subscribers: Default::default(), + effect_subscribers: Default::default(), + update_any: schedule_update_any().expect("in a virtual dom"), + value, + effect_stack: get_effect_stack(), + }, + #[cfg(debug_assertions)] + caller, + ), + } + } + /// Create a new signal with a custom owner scope. The signal will be dropped when the owner scope is dropped instead of the current scope. pub fn new_in_scope(value: T, owner: ScopeId) -> Self { Self { @@ -155,6 +191,7 @@ impl Signal { effect_subscribers: Default::default(), update_any: schedule_update_any().expect("in a virtual dom"), value, + effect_stack: get_effect_stack(), }, owner, ), @@ -166,11 +203,13 @@ impl Signal { self.inner.origin_scope() } - /// Get the current value of the signal. This will subscribe the current scope to the signal. + /// Get the current value of the signal. This will subscribe the current scope to the signal. If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead. + /// /// If the signal has been dropped, this will panic. - pub fn read(&self) -> Ref { + #[track_caller] + pub fn read(&self) -> GenerationalRef { let inner = self.inner.read(); - if let Some(effect) = Effect::current() { + if let Some(effect) = inner.effect_stack.current() { let mut effect_subscribers = inner.effect_subscribers.borrow_mut(); if !effect_subscribers.contains(&effect) { effect_subscribers.push(effect); @@ -192,14 +231,24 @@ impl Signal { } } } - Ref::map(inner, |v| &v.value) + GenerationalRef::map(inner, |v| &v.value) + } + + /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.** + /// + /// If the signal has been dropped, this will panic. + pub fn peek(&self) -> GenerationalRef { + let inner = self.inner.read(); + GenerationalRef::map(inner, |v| &v.value) } /// Get a mutable reference to the signal's value. + /// /// If the signal has been dropped, this will panic. - pub fn write(&self) -> Write<'_, T> { + #[track_caller] + pub fn write(&self) -> Write { let inner = self.inner.write(); - let borrow = RefMut::map(inner, |v| &mut v.value); + let borrow = GenerationalRefMut::map(inner, |v| &mut v.value); Write { write: borrow, signal: SignalSubscriberDrop { signal: *self }, @@ -235,12 +284,14 @@ impl Signal { } /// Set the value of the signal. This will trigger an update on all subscribers. + #[track_caller] pub fn set(&self, value: T) { *self.write() = value; } /// Run a closure with a reference to the signal's value. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { let write = self.read(); f(&*write) @@ -248,6 +299,7 @@ impl Signal { /// Run a closure with a mutable reference to the signal's value. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { let mut write = self.write(); f(&mut *write) @@ -257,17 +309,58 @@ impl Signal { impl Signal { /// Get the current value of the signal. This will subscribe the current scope to the signal. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn value(&self) -> T { self.read().clone() } } +impl Signal { + /// Invert the boolean value of the signal. This will trigger an update on all subscribers. + pub fn toggle(&self) { + self.set(!self.value()); + } +} + impl PartialEq for Signal { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } +impl Deref for Signal { + type Target = dyn Fn() -> GenerationalRef; + + fn deref(&self) -> &Self::Target { + // https://github.com/dtolnay/case-studies/tree/master/callable-types + + // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). + let uninit_callable = MaybeUninit::::uninit(); + // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. + let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }); + + // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. + let size_of_closure = std::mem::size_of_val(&uninit_closure); + assert_eq!(size_of_closure, std::mem::size_of::()); + + // Then cast the lifetime of the closure to the lifetime of &self. + fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { + b + } + let reference_to_closure = cast_lifetime( + { + // The real closure that we will never use. + &uninit_closure + }, + // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. + unsafe { std::mem::transmute(self) }, + ); + + // Cast the closure to a trait object. + reference_to_closure as &Self::Target + } +} + struct SignalSubscriberDrop { signal: Signal, } @@ -279,17 +372,17 @@ impl Drop for SignalSubscriberDrop { } /// A mutable reference to a signal's value. -pub struct Write<'a, T: 'static, I: 'static = T> { - write: RefMut<'a, T>, +pub struct Write { + write: GenerationalRefMut, signal: SignalSubscriberDrop, } -impl<'a, T: 'static, I: 'static> Write<'a, T, I> { +impl Write { /// Map the mutable reference to the signal's value to a new type. - pub fn map(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, I> { + pub fn map(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write { let Self { write, signal } = myself; Write { - write: RefMut::map(write, f), + write: GenerationalRefMut::map(write, f), signal, } } @@ -298,14 +391,14 @@ impl<'a, T: 'static, I: 'static> Write<'a, T, I> { pub fn filter_map( myself: Self, f: impl FnOnce(&mut T) -> Option<&mut O>, - ) -> Option> { + ) -> Option> { let Self { write, signal } = myself; - let write = RefMut::filter_map(write, f).ok(); + let write = GenerationalRefMut::filter_map(write, f); write.map(|write| Write { write, signal }) } } -impl<'a, T: 'static> Deref for Write<'a, T> { +impl Deref for Write { type Target = T; fn deref(&self) -> &Self::Target { @@ -313,7 +406,7 @@ impl<'a, T: 'static> Deref for Write<'a, T> { } } -impl DerefMut for Write<'_, T> { +impl DerefMut for Write { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.write } @@ -335,12 +428,23 @@ impl ReadOnlySignal { self.inner.origin_scope() } - /// Get the current value of the signal. This will subscribe the current scope to the signal. - pub fn read(&self) -> Ref { + /// Get the current value of the signal. This will subscribe the current scope to the signal. If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead. + /// + /// If the signal has been dropped, this will panic. + #[track_caller] + pub fn read(&self) -> GenerationalRef { self.inner.read() } + /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.** + /// + /// If the signal has been dropped, this will panic. + pub fn peek(&self) -> GenerationalRef { + self.inner.peek() + } + /// Run a closure with a reference to the signal's value. + #[track_caller] pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { self.inner.with(f) } @@ -358,3 +462,42 @@ impl PartialEq for ReadOnlySignal { self.inner == other.inner } } + +impl Deref for ReadOnlySignal { + type Target = dyn Fn() -> GenerationalRef; + + fn deref(&self) -> &Self::Target { + // https://github.com/dtolnay/case-studies/tree/master/callable-types + + // First we create a closure that captures something with the Same in memory layout as Self (MaybeUninit). + let uninit_callable = MaybeUninit::::uninit(); + // Then move that value into the closure. We assume that the closure now has a in memory layout of Self. + let uninit_closure = move || Self::read(unsafe { &*uninit_callable.as_ptr() }); + + // Check that the size of the closure is the same as the size of Self in case the compiler changed the layout of the closure. + let size_of_closure = std::mem::size_of_val(&uninit_closure); + assert_eq!(size_of_closure, std::mem::size_of::()); + + // Then cast the lifetime of the closure to the lifetime of &self. + fn cast_lifetime<'a, T>(_a: &T, b: &'a T) -> &'a T { + b + } + let reference_to_closure = cast_lifetime( + { + // The real closure that we will never use. + &uninit_closure + }, + // We transmute self into a reference to the closure. This is safe because we know that the closure has the same memory layout as Self so &Closure == &Self. + unsafe { std::mem::transmute(self) }, + ); + + // Cast the closure to a trait object. + reference_to_closure as &Self::Target + } +} + +impl From> for ReadOnlySignal { + fn from(signal: Signal) -> Self { + Self::new(signal) + } +} diff --git a/packages/signals/tests/create.rs b/packages/signals/tests/create.rs index 57d8325af..ecda2f376 100644 --- a/packages/signals/tests/create.rs +++ b/packages/signals/tests/create.rs @@ -29,6 +29,30 @@ fn create_signals_global() { } } +#[test] +fn deref_signal() { + let mut dom = VirtualDom::new(|cx| { + render! { + for _ in 0..10 { + Child {} + } + } + }); + + fn Child(cx: Scope) -> Element { + let signal = Signal::new("hello world".to_string()); + + // You can call signals like functions to get a Ref of their value. + assert_eq!(&*signal(), "hello world"); + + render! { + "hello world" + } + } + + let _edits = dom.rebuild().santize(); +} + #[test] fn drop_signals() { let mut dom = VirtualDom::new(|cx| { diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index f6376452d..1a8d475ee 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] dioxus-core = { workspace = true, features = ["serialize"] } +dioxus-html = { workspace = true, features = ["eval"]} askama_escape = "0.10.3" thiserror = "1.0.23" rustc-hash = "1.1.0" @@ -17,6 +18,8 @@ lru = "0.10.0" tracing = { workspace = true } http = "0.2.9" tokio = { version = "1.28", features = ["full"], optional = true } +async-trait = "0.1.58" +serde_json = { version = "1.0" } [dev-dependencies] dioxus = { workspace = true } diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index bcd6ba71f..11f90058a 100644 --- a/packages/ssr/src/cache.rs +++ b/packages/ssr/src/cache.rs @@ -27,6 +27,10 @@ pub enum Segment { }, /// A marker for where to insert a dynamic inner html InnerHtmlMarker, + /// A marker for where to insert a node id for an attribute + AttributeNodeMarker, + /// A marker for where to insert a node id for a root node + RootNodeMarker, } impl std::fmt::Write for StringChain { @@ -41,13 +45,13 @@ impl std::fmt::Write for StringChain { } impl StringCache { - pub fn from_template(template: &VNode) -> Result { + pub fn from_template(template: &VNode, prerender: bool) -> Result { let mut chain = StringChain::default(); let mut cur_path = vec![]; for (root_idx, root) in template.template.get().roots.iter().enumerate() { - Self::recurse(root, &mut cur_path, root_idx, &mut chain)?; + Self::recurse(root, &mut cur_path, root_idx, true, prerender, &mut chain)?; } Ok(Self { @@ -60,6 +64,8 @@ impl StringCache { root: &TemplateNode, cur_path: &mut Vec, root_idx: usize, + is_root: bool, + prerender: bool, chain: &mut StringChain, ) -> Result<(), std::fmt::Error> { match root { @@ -76,7 +82,7 @@ impl StringCache { // we need to collect the inner html and write it at the end let mut inner_html = None; // we need to keep track of if we have dynamic attrs to know if we need to insert a style and inner_html marker - let mut has_dynamic_attrs = false; + let mut has_dyn_attrs = false; for attr in *attrs { match attr { TemplateAttribute::Static { @@ -97,8 +103,9 @@ impl StringCache { } } TemplateAttribute::Dynamic { id: index } => { - chain.segments.push(Segment::Attr(*index)); - has_dynamic_attrs = true; + let index = *index; + chain.segments.push(Segment::Attr(index)); + has_dyn_attrs = true } } } @@ -113,12 +120,23 @@ impl StringCache { inside_style_tag: true, }); write!(chain, "\"")?; - } else if has_dynamic_attrs { + } else if has_dyn_attrs { chain.segments.push(Segment::StyleMarker { inside_style_tag: false, }); } + // write the id if we are prerendering and this is either a root node or a node with a dynamic attribute + if prerender && (has_dyn_attrs || is_root) { + write!(chain, " data-node-hydration=\"")?; + if has_dyn_attrs { + chain.segments.push(Segment::AttributeNodeMarker); + } else if is_root { + chain.segments.push(Segment::RootNodeMarker); + } + write!(chain, "\"")?; + } + if children.is_empty() && tag_is_self_closing(tag) { write!(chain, "/>")?; } else { @@ -126,12 +144,12 @@ impl StringCache { // Write the static inner html, or insert a marker if dynamic inner html is possible if let Some(inner_html) = inner_html { chain.write_str(inner_html)?; - } else if has_dynamic_attrs { + } else if has_dyn_attrs { chain.segments.push(Segment::InnerHtmlMarker); } for child in *children { - Self::recurse(child, cur_path, root_idx, chain)?; + Self::recurse(child, cur_path, root_idx, false, prerender, chain)?; } write!(chain, "")?; } diff --git a/packages/ssr/src/eval.rs b/packages/ssr/src/eval.rs new file mode 100644 index 000000000..2ab74f4e1 --- /dev/null +++ b/packages/ssr/src/eval.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; +use dioxus_core::ScopeState; +use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; +use std::rc::Rc; + +/// Provides the SSREvalProvider through [`cx.provide_context`]. +pub fn init_eval(cx: &ScopeState) { + let provider: Rc = Rc::new(SSREvalProvider {}); + cx.provide_context(provider); +} + +/// Reprents the ssr-target's provider of evaluators. +pub struct SSREvalProvider; +impl EvalProvider for SSREvalProvider { + fn new_evaluator(&self, _: String) -> Result, EvalError> { + Ok(Rc::new(SSREvaluator) as Rc) + } +} + +/// Represents a ssr-target's JavaScript evaluator. +pub struct SSREvaluator; + +// In ssr rendering we never run or resolve evals. +#[async_trait(?Send)] +impl Evaluator for SSREvaluator { + /// Runs the evaluated JavaScript. + async fn join(&self) -> Result { + std::future::pending::<()>().await; + unreachable!() + } + + /// Sends a message to the evaluated JavaScript. + fn send(&self, _el: serde_json::Value) -> Result<(), EvalError> { + Ok(()) + } + + /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript. + async fn recv(&self) -> Result { + std::future::pending::<()>().await; + unreachable!() + } +} diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index c3bb25314..34ffa3443 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -81,6 +81,7 @@ impl IncrementalRenderer { let mut html_buffer = WriteBuffer { buffer: Vec::new() }; { let mut vdom = VirtualDom::new_with_props(comp, props); + crate::eval::init_eval(vdom.base_scope()); rebuild_with(&mut vdom).await; renderer.render_before_body(&mut *html_buffer)?; diff --git a/packages/ssr/src/incremental_cfg.rs b/packages/ssr/src/incremental_cfg.rs index 1a4ea3418..15a6ad44e 100644 --- a/packages/ssr/src/incremental_cfg.rs +++ b/packages/ssr/src/incremental_cfg.rs @@ -67,6 +67,7 @@ pub struct IncrementalRendererConfig { memory_cache_limit: usize, invalidate_after: Option, map_path: Option, + clear_cache: bool, } impl Default for IncrementalRendererConfig { @@ -83,9 +84,16 @@ impl IncrementalRendererConfig { memory_cache_limit: 10000, invalidate_after: None, map_path: None, + clear_cache: true, } } + /// Clear the cache on startup (default: true) + pub fn clear_cache(mut self, clear_cache: bool) -> Self { + self.clear_cache = clear_cache; + self + } + /// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`. /// The function should return the path to the folder to store the index.html file in. pub fn map_path PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self { @@ -114,7 +122,7 @@ impl IncrementalRendererConfig { /// Build the incremental renderer. pub fn build(self) -> IncrementalRenderer { let static_dir = self.static_dir.clone(); - IncrementalRenderer { + let mut renderer = IncrementalRenderer { static_dir: self.static_dir.clone(), memory_cache: NonZeroUsize::new(self.memory_cache_limit) .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), @@ -129,6 +137,12 @@ impl IncrementalRendererConfig { path }) }), + }; + + if self.clear_cache { + renderer.invalidate_all(); } + + renderer } } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 45e3f59ea..fdc0e9333 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -1,4 +1,6 @@ #![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")] mod cache; pub mod config; @@ -8,6 +10,7 @@ pub mod incremental; #[cfg(feature = "incremental")] mod incremental_cfg; +pub mod eval; pub mod renderer; pub mod template; @@ -40,6 +43,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String { }; let mut dom = VirtualDom::new_with_props(lazy_app, props); + crate::eval::init_eval(dom.base_scope()); _ = dom.rebuild(); Renderer::new().render(&dom) diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 98afef8b7..65ab4d0ed 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -28,6 +28,9 @@ pub struct Renderer { /// A cache of templates that have been rendered template_cache: HashMap<&'static str, Arc>, + + /// The current dynamic node id for hydration + dynamic_node_id: usize, } impl Renderer { @@ -54,6 +57,7 @@ impl Renderer { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() { + self.dynamic_node_id = 0; self.render_template(buf, dom, node)? }; @@ -69,7 +73,10 @@ impl Renderer { let entry = self .template_cache .entry(template.template.get().name) - .or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap())) + .or_insert_with({ + let prerender = self.pre_render; + move || Arc::new(StringCache::from_template(template, prerender).unwrap()) + }) .clone(); let mut inner_html = None; @@ -77,22 +84,36 @@ impl Renderer { // We need to keep track of the dynamic styles so we can insert them into the right place let mut accumulated_dynamic_styles = Vec::new(); + // We need to keep track of the listeners so we can insert them into the right place + let mut accumulated_listeners = Vec::new(); + for segment in entry.segments.iter() { match segment { Segment::Attr(idx) => { let attr = &template.dynamic_attrs[*idx]; - if attr.name == "dangerous_inner_html" { - inner_html = Some(attr); - } else if attr.namespace == Some("style") { - accumulated_dynamic_styles.push(attr); - } else if BOOL_ATTRS.contains(&attr.name) { - if truthy(&attr.value) { - write!(buf, " {}=", attr.name)?; - write_value(buf, &attr.value)?; + attr.attribute_type().try_for_each(|attr| { + if attr.name == "dangerous_inner_html" { + inner_html = Some(attr); + } else if attr.namespace == Some("style") { + accumulated_dynamic_styles.push(attr); + } else if BOOL_ATTRS.contains(&attr.name) { + if truthy(&attr.value) { + write_attribute(buf, attr)?; + } + } else { + write_attribute(buf, attr)?; } - } else { - write_attribute(buf, attr)?; - } + + if self.pre_render { + if let AttributeValue::Listener(_) = &attr.value { + // The onmounted event doesn't need a DOM listener + if attr.name != "onmounted" { + accumulated_listeners.push(attr.name); + } + } + } + Ok(()) + })?; } Segment::Node(idx) => match &template.dynamic_nodes[*idx] { DynamicNode::Component(node) => { @@ -115,7 +136,8 @@ impl Renderer { DynamicNode::Text(text) => { // in SSR, we are concerned that we can't hunt down the right text node since they might get merged if self.pre_render { - write!(buf, "")?; + write!(buf, "", self.dynamic_node_id)?; + self.dynamic_node_id += 1; } write!( @@ -134,9 +156,14 @@ impl Renderer { } } - DynamicNode::Placeholder(_el) => { + DynamicNode::Placeholder(_) => { if self.pre_render { - write!(buf, "
")?;
+                            write!(
+                                buf,
+                                "
",
+                                self.dynamic_node_id
+                            )?;
+                            self.dynamic_node_id += 1;
                         }
                     }
                 },
@@ -175,6 +202,22 @@ impl Renderer {
                         }
                     }
                 }
+
+                Segment::AttributeNodeMarker => {
+                    // first write the id
+                    write!(buf, "{}", self.dynamic_node_id)?;
+                    self.dynamic_node_id += 1;
+                    // then write any listeners
+                    for name in accumulated_listeners.drain(..) {
+                        write!(buf, ",{}:", &name[2..])?;
+                        write!(buf, "{}", dioxus_html::event_bubbles(name) as u8)?;
+                    }
+                }
+
+                Segment::RootNodeMarker => {
+                    write!(buf, "{}", self.dynamic_node_id)?;
+                    self.dynamic_node_id += 1
+                }
             }
         }
 
@@ -192,7 +235,9 @@ fn to_string_works() {
 
         render! {
             div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
-                "Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
+                "Hello world 1 -->"
+                "{dynamic}"
+                "<-- Hello world 2"
                 div { "nest 1" }
                 div {}
                 div { "nest 2" }
@@ -213,7 +258,7 @@ fn to_string_works() {
             assert_eq!(
                 item.1.segments,
                 vec![
-                    PreRendered("
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); + assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); +} + +#[test] +fn empty_for_loop_works() { + use dioxus::prelude::*; + + fn app(cx: Scope) -> Element { + render! { + div { class: "asdasdasd", + for _ in (0..5) { + + } + } + } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!( + item.1.segments, + vec![ + PreRendered("
".into()), + InnerHtmlMarker, + PreRendered("
".into(),), + ] + ); + } + } + + use Segment::*; + + assert_eq!(out, "
"); +} + +#[test] +fn empty_render_works() { + use dioxus::prelude::*; + + fn app(cx: Scope) -> Element { + render! {} + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!(item.1.segments, vec![]); + } + } + assert_eq!(out, ""); +} + +#[test] +fn empty_rsx_works() { + use dioxus::prelude::*; + + fn app(_: Scope) -> Element { + rsx! {}; + None + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + let mut renderer = Renderer::new(); + let out = renderer.render(&dom); + + for item in renderer.template_cache.iter() { + if item.1.segments.len() > 5 { + assert_eq!(item.1.segments, vec![]); + } + } + assert_eq!(out, ""); } pub(crate) const BOOL_ATTRS: &[&str] = &[ @@ -293,16 +426,6 @@ pub(crate) fn write_attribute(buf: &mut impl Write, attr: &Attribute) -> std::fm } } -pub(crate) fn write_value(buf: &mut impl Write, value: &AttributeValue) -> std::fmt::Result { - match value { - AttributeValue::Text(value) => write!(buf, "\"{}\"", value), - AttributeValue::Bool(value) => write!(buf, "{}", value), - AttributeValue::Int(value) => write!(buf, "{}", value), - AttributeValue::Float(value) => write!(buf, "{}", value), - _ => Ok(()), - } -} - pub(crate) fn write_value_unquoted( buf: &mut impl Write, value: &AttributeValue, diff --git a/packages/ssr/tests/hydration.rs b/packages/ssr/tests/hydration.rs new file mode 100644 index 000000000..8c9e9f5c8 --- /dev/null +++ b/packages/ssr/tests/hydration.rs @@ -0,0 +1,199 @@ +use dioxus::prelude::*; + +#[test] +fn root_ids() { + fn app(cx: Scope) -> Element { + render! { div { width: "100px" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); +} + +#[test] +fn dynamic_attributes() { + fn app(cx: Scope) -> Element { + let dynamic = 123; + render! { + div { width: "100px", div { width: "{dynamic}px" } } + } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); +} + +#[test] +fn listeners() { + fn app(cx: Scope) -> Element { + render! { + div { width: "100px", div { onclick: |_| {} } } + } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); + + fn app2(cx: Scope) -> Element { + let dynamic = 123; + render! { + div { width: "100px", div { width: "{dynamic}px", onclick: |_| {} } } + } + } + + let mut dom = VirtualDom::new(app2); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); +} + +#[test] +fn text_nodes() { + fn app(cx: Scope) -> Element { + let dynamic_text = "hello"; + render! { + div { dynamic_text } + } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
hello
"# + ); + + fn app2(cx: Scope) -> Element { + let dynamic = 123; + render! { + div { "{dynamic}", "{1234}" } + } + } + + let mut dom = VirtualDom::new(app2); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
1231234
"# + ); +} + +#[allow(non_snake_case)] +#[test] +fn components_hydrate() { + fn app(cx: Scope) -> Element { + render! { Child {} } + } + + fn Child(cx: Scope) -> Element { + render! { div { "hello" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
hello
"# + ); + + fn app2(cx: Scope) -> Element { + render! { Child2 {} } + } + + fn Child2(cx: Scope) -> Element { + let dyn_text = "hello"; + render! { + div { dyn_text } + } + } + + let mut dom = VirtualDom::new(app2); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
hello
"# + ); + + fn app3(cx: Scope) -> Element { + render! { Child3 {} } + } + + fn Child3(cx: Scope) -> Element { + render! { div { width: "{1}" } } + } + + let mut dom = VirtualDom::new(app3); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); + + fn app4(cx: Scope) -> Element { + render! { Child4 {} } + } + + fn Child4(cx: Scope) -> Element { + render! { + for _ in 0..2 { + render! { + render! { + "{1}" + } + } + } + } + } + + let mut dom = VirtualDom::new(app4); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"11"# + ); +} + +#[test] +fn hello_world_hydrates() { + fn app(cx: Scope) -> Element { + let mut 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!" } + }) + } + + let mut dom = VirtualDom::new(app); + _ = dbg!(dom.rebuild()); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"

High-Five counter: 0

"# + ); +} diff --git a/packages/ssr/tests/simple.rs b/packages/ssr/tests/simple.rs index cdf186a80..a9034df9b 100644 --- a/packages/ssr/tests/simple.rs +++ b/packages/ssr/tests/simple.rs @@ -47,9 +47,7 @@ fn dynamic() { fn components() { #[component] fn MyComponent(cx: Scope, name: i32) -> Element { - render! { - div { "component {name}" } - } + render! { div { "component {name}" } } } assert_eq!( diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 9c2d9b65e..12c2c974a 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { workspace = true, features = ["serialize"] } dioxus-html = { workspace = true, features = ["wasm-bind"] } dioxus-interpreter-js = { workspace = true, features = [ - "sledgehammer", "minimal_bindings", + "web", ] } js-sys = "0.3.56" @@ -66,7 +66,9 @@ hot_reload = [ "web-sys/WebSocket", "web-sys/Location", ] -eval = [] +eval = [ + "dioxus-html/eval", +] [dev-dependencies] dioxus = { workspace = true } diff --git a/packages/web/examples/hydrate.rs b/packages/web/examples/hydrate.rs index 08c0f95cd..8a6cd464e 100644 --- a/packages/web/examples/hydrate.rs +++ b/packages/web/examples/hydrate.rs @@ -4,14 +4,10 @@ use web_sys::window; fn app(cx: Scope) -> Element { cx.render(rsx! { + div { h1 { "thing 1" } } + div { h2 { "thing 2" } } div { - h1 { "thing 1" } - } - div { - h2 { "thing 2"} - } - div { - h2 { "thing 2"} + h2 { "thing 2" } "asd" "asd" Bapp {} @@ -27,14 +23,10 @@ fn app(cx: Scope) -> Element { #[allow(non_snake_case)] fn Bapp(cx: Scope) -> Element { cx.render(rsx! { + div { h1 { "thing 1" } } + div { h2 { "thing 2" } } div { - h1 { "thing 1" } - } - div { - h2 { "thing 2"} - } - div { - h2 { "thing 2"} + h2 { "thing 2" } "asd" "asd" } @@ -60,6 +52,6 @@ fn main() { .unwrap() .set_inner_html(&pre); - // now rehydtrate + // now rehydrate dioxus_web::launch_with_props(app, (), Config::new().hydrate(true)); } diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index e17203a65..bfd0fd197 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ "oncopy", "oncuechange", "oncut", - "ondblclick", + "ondoubleclick", "ondrag", "ondragend", "ondragenter", diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 183eb9c14..9d08f09f4 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,34 +1,32 @@ //! Implementation of a renderer for Dioxus on the web. //! -//! Oustanding todos: -//! - Removing event listeners (delegation) +//! Outstanding todos: //! - Passive event listeners //! - no-op event listener patch for safari //! - tests to ensure dyn_into works for various event types. -//! - Partial delegation?> +//! - Partial delegation? use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; +use dioxus_html::PlatformEventData; +use dioxus_html::{event_bubbles, MountedData}; use dioxus_interpreter_js::{get_node, minimal_bindings, save_template, Channel}; use futures_channel::mpsc; -use js_sys::Array; use rustc_hash::FxHashMap; -use std::sync::Arc; -use std::{any::Any, rc::Rc}; -use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event}; -use crate::Config; +use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter}; pub struct WebsysDom { document: Document, #[allow(dead_code)] pub(crate) root: Element, - templates: FxHashMap, - max_template_id: u32, + templates: FxHashMap, + max_template_id: u16, pub(crate) interpreter: Channel, + #[cfg(feature = "mounted")] event_channel: mpsc::UnboundedSender, } @@ -36,7 +34,7 @@ pub struct UiEvent { pub name: String, pub bubbles: bool, pub element: ElementId, - pub data: Rc, + pub data: PlatformEventData, } impl WebsysDom { @@ -46,7 +44,16 @@ impl WebsysDom { let document = load_document(); let root = match document.get_element_by_id(&cfg.rootname) { Some(root) => root, - None => document.create_element("body").ok().unwrap(), + None => { + web_sys::console::error_1( + &format!( + "element '#{}' not found. mounting to the body.", + cfg.rootname + ) + .into(), + ); + document.create_element("body").ok().unwrap() + } }; let interpreter = Channel::default(); @@ -91,10 +98,11 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initilize( + dioxus_interpreter_js::initialize( root.clone().unchecked_into(), handler.as_ref().unchecked_ref(), ); + dioxus_html::set_event_converter(Box::new(WebEventConverter)); handler.forget(); Self { document, @@ -102,6 +110,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + #[cfg(feature = "mounted")] event_channel, } } @@ -172,11 +181,12 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; let i = &mut self.interpreter; + #[cfg(feature = "mounted")] // we need to apply the mount events last, so we collect them here let mut to_mount = Vec::new(); for edit in &edits { match edit { - AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), + AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u16), AssignId { path, id } => { i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32) } @@ -187,15 +197,15 @@ impl WebsysDom { } LoadTemplate { name, index, id } => { if let Some(tmpl_id) = self.templates.get(*name) { - i.load_template(*tmpl_id, *index as u32, id.0 as u32) + i.load_template(*tmpl_id, *index as u16, id.0 as u32) } } - ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32), + ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u16), ReplacePlaceholder { path, m } => { - i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32) + i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u16) } - InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32), - InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32), + InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u16), + InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u16), SetAttribute { name, value, @@ -227,6 +237,7 @@ impl WebsysDom { match *name { // mounted events are fired immediately after the element is mounted. "mounted" => { + #[cfg(feature = "mounted")] to_mount.push(*id); } _ => { @@ -247,190 +258,25 @@ impl WebsysDom { edits.clear(); i.flush(); + #[cfg(feature = "mounted")] for id in to_mount { - let node = get_node(id.0 as u32); - if let Some(element) = node.dyn_ref::() { - let data: MountedData = element.into(); - let data = Rc::new(data); - let _ = self.event_channel.unbounded_send(UiEvent { - name: "mounted".to_string(), - bubbles: false, - element: id, - data, - }); - } - } - } -} - -// 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. -pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { - use dioxus_html::events::*; - - match event.type_().as_str() { - "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), - "compositionend" | "compositionstart" | "compositionupdate" => { - make_composition_event(&event) - } - "keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)), - "focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}), - - "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - - "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Rc::new(MouseData::from(event)) - } - "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" - | "drop" => { - let mut files = None; - if let Some(event) = event.dyn_ref::() { - if let Some(data) = event.data_transfer() { - #[cfg(feature = "file_engine")] - #[allow(clippy::arc_with_non_send_sync)] - if let Some(file_list) = data.files() { - files = crate::file_engine::WebFileEngine::new(file_list) - .map(|f| Arc::new(f) as Arc); - } - } - } - let mouse = MouseData::from(event); - Rc::new(DragData { mouse, files }) - } - - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Rc::new(PointerData::from(event)) - } - "select" => Rc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)), - - "scroll" => Rc::new(()), - "wheel" => Rc::new(WheelData::from(event)), - "animationstart" | "animationend" | "animationiteration" => { - Rc::new(AnimationData::from(event)) - } - "transitionend" => Rc::new(TransitionData::from(event)), - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}), - "error" => Rc::new(ImageData { load_error: true }), - "load" => Rc::new(ImageData { load_error: false }), - "toggle" => Rc::new(ToggleData {}), - - _ => Rc::new(()), - } -} - -fn make_composition_event(event: &Event) -> Rc { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Rc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -fn read_input_to_data(target: Element) -> Rc { - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - - let value: String = target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - 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()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .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"); - - let mut values = std::collections::HashMap::new(); - - // try to fill in form values - if let Some(form) = target.dyn_ref::() { - let form_data = get_form_data(form); - for value in form_data.entries().into_iter().flatten() { - if let Ok(array) = value.dyn_into::() { - if let Some(name) = array.get(0).as_string() { - if let Ok(item_values) = array.get(1).dyn_into::() { - let item_values = - item_values.iter().filter_map(|v| v.as_string()).collect(); - - values.insert(name, item_values); - } - } - } + self.send_mount_event(id); } } - #[cfg(not(feature = "file_engine"))] - let files = None; - #[cfg(feature = "file_engine")] - let files = target - .dyn_ref() - .and_then(|input: &web_sys::HtmlInputElement| { - input.files().and_then(|files| { - #[allow(clippy::arc_with_non_send_sync)] - crate::file_engine::WebFileEngine::new(files) - .map(|f| std::sync::Arc::new(f) as std::sync::Arc) - }) - }); - - Rc::new(FormData { - value, - values, - files, - }) -} - -// web-sys does not expose the keys api for form data, so we need to manually bind to it -#[wasm_bindgen(inline_js = r#" - export function get_form_data(form) { - let values = new Map(); - const formData = new FormData(form); - - for (let name of formData.keys()) { - values.set(name, formData.getAll(name)); + pub(crate) fn send_mount_event(&self, id: ElementId) { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + let data: MountedData = element.into(); + let data = Box::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data: PlatformEventData::new(data), + }); } - - return values; } -"#)] -extern "C" { - fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; } fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs new file mode 100644 index 000000000..994bf304a --- /dev/null +++ b/packages/web/src/event.rs @@ -0,0 +1,455 @@ +use std::{any::Any, collections::HashMap}; + +use dioxus_html::{ + prelude::FormValue, FileEngine, FormData, HasFormData, HasImageData, HtmlEventConverter, + ImageData, MountedData, PlatformEventData, ScrollData, +}; +use js_sys::Array; +use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; +use web_sys::{Document, Element, Event}; + +pub(crate) struct WebEventConverter; + +#[inline(always)] +fn downcast_event(event: &dioxus_html::PlatformEventData) -> &GenericWebSysEvent { + event + .downcast::() + .expect("event should be a GenericWebSysEvent") +} + +impl HtmlEventConverter for WebEventConverter { + #[inline(always)] + fn convert_animation_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::AnimationData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_clipboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ClipboardData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_composition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::CompositionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_drag_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::DragData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_focus_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FocusData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_form_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::FormData { + let event = downcast_event(event); + FormData::new(WebFormData::new(event.element.clone(), event.raw.clone())) + } + + #[inline(always)] + fn convert_image_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::ImageData { + let event = downcast_event(event); + let error = event.raw.type_() == "error"; + ImageData::new(WebImageEvent::new(event.raw.clone(), error)) + } + + #[inline(always)] + fn convert_keyboard_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::KeyboardData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_media_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MediaData { + downcast_event(event).raw.clone().into() + } + + #[allow(unused_variables)] + #[inline(always)] + fn convert_mounted_data(&self, event: &dioxus_html::PlatformEventData) -> MountedData { + #[cfg(feature = "mounted")] + { + MountedData::from( + event + .downcast::() + .expect("event should be a web_sys::Element"), + ) + } + #[cfg(not(feature = "mounted"))] + { + panic!("mounted events are not supported without the mounted feature on the dioxus-web crate enabled") + } + } + + #[inline(always)] + fn convert_mouse_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::MouseData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_pointer_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::PointerData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_scroll_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ScrollData { + ScrollData::from(downcast_event(event).raw.clone()) + } + + #[inline(always)] + fn convert_selection_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::SelectionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_toggle_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::ToggleData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_touch_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::TouchData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_transition_data( + &self, + event: &dioxus_html::PlatformEventData, + ) -> dioxus_html::TransitionData { + downcast_event(event).raw.clone().into() + } + + #[inline(always)] + fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData { + downcast_event(event).raw.clone().into() + } +} + +/// A extension trait for web-sys events that provides a way to get the event as a web-sys event. +pub trait WebEventExt { + /// Get the event as a web-sys event. + fn web_event(&self) -> &E; +} + +impl WebEventExt for dioxus_html::AnimationData { + fn web_event(&self) -> &web_sys::AnimationEvent { + self.downcast::() + .expect("event should be a WebAnimationEvent") + } +} + +impl WebEventExt for dioxus_html::ClipboardData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a web_sys::Event") + } +} + +impl WebEventExt for dioxus_html::CompositionData { + fn web_event(&self) -> &web_sys::CompositionEvent { + self.downcast::() + .expect("event should be a WebCompositionEvent") + } +} + +impl WebEventExt for dioxus_html::DragData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::FocusData { + fn web_event(&self) -> &web_sys::FocusEvent { + self.downcast::() + .expect("event should be a WebFocusEvent") + } +} + +impl WebEventExt for dioxus_html::FormData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebFormData") + } +} + +impl WebEventExt for dioxus_html::ImageData { + fn web_event(&self) -> &WebImageEvent { + self.downcast::() + .expect("event should be a WebImageEvent") + } +} + +impl WebEventExt for dioxus_html::KeyboardData { + fn web_event(&self) -> &web_sys::KeyboardEvent { + self.downcast::() + .expect("event should be a WebKeyboardEvent") + } +} + +impl WebEventExt for dioxus_html::MediaData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebMediaEvent") + } +} + +impl WebEventExt for MountedData { + fn web_event(&self) -> &web_sys::Element { + self.downcast::() + .expect("event should be a web_sys::Element") + } +} + +impl WebEventExt for dioxus_html::MouseData { + fn web_event(&self) -> &web_sys::MouseEvent { + self.downcast::() + .expect("event should be a WebMouseEvent") + } +} + +impl WebEventExt for dioxus_html::PointerData { + fn web_event(&self) -> &web_sys::PointerEvent { + self.downcast::() + .expect("event should be a WebPointerEvent") + } +} + +impl WebEventExt for ScrollData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebScrollEvent") + } +} + +impl WebEventExt for dioxus_html::SelectionData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebSelectionEvent") + } +} + +impl WebEventExt for dioxus_html::ToggleData { + fn web_event(&self) -> &web_sys::Event { + self.downcast::() + .expect("event should be a WebToggleEvent") + } +} + +impl WebEventExt for dioxus_html::TouchData { + fn web_event(&self) -> &web_sys::TouchEvent { + self.downcast::() + .expect("event should be a WebTouchEvent") + } +} + +impl WebEventExt for dioxus_html::TransitionData { + fn web_event(&self) -> &web_sys::TransitionEvent { + self.downcast::() + .expect("event should be a WebTransitionEvent") + } +} + +impl WebEventExt for dioxus_html::WheelData { + fn web_event(&self) -> &web_sys::WheelEvent { + self.downcast::() + .expect("event should be a WebWheelEvent") + } +} + +struct GenericWebSysEvent { + raw: Event, + element: Element, +} + +// 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. +pub(crate) fn virtual_event_from_websys_event( + event: web_sys::Event, + target: Element, +) -> PlatformEventData { + PlatformEventData::new(Box::new(GenericWebSysEvent { + raw: event, + element: target, + })) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +struct WebImageEvent { + raw: Event, + error: bool, +} + +impl WebImageEvent { + fn new(raw: Event, error: bool) -> Self { + Self { raw, error } + } +} + +impl HasImageData for WebImageEvent { + fn load_error(&self) -> bool { + self.error + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +struct WebFormData { + element: Element, + raw: Event, +} + +impl WebFormData { + fn new(element: Element, raw: Event) -> Self { + Self { element, raw } + } +} + +impl HasFormData for WebFormData { + fn value(&self) -> String { + let target = &self.element; + target + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + 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()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .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") + } + + fn values(&self) -> HashMap { + let mut values = std::collections::HashMap::new(); + + // try to fill in form values + if let Some(form) = self.element.dyn_ref::() { + let form_data = get_form_data(form); + for value in form_data.entries().into_iter().flatten() { + if let Ok(array) = value.dyn_into::() { + if let Some(name) = array.get(0).as_string() { + if let Ok(item_values) = array.get(1).dyn_into::() { + let item_values: Vec = + item_values.iter().filter_map(|v| v.as_string()).collect(); + + values.insert(name, FormValue::VecText(item_values)); + } else if let Ok(item_value) = array.get(1).dyn_into::() { + values.insert(name, FormValue::Text(item_value.as_string().unwrap())); + } + } + } + } + } + + values + } + + fn files(&self) -> Option> { + #[cfg(not(feature = "file_engine"))] + let files = None; + #[cfg(feature = "file_engine")] + let files = self + .element + .dyn_ref() + .and_then(|input: &web_sys::HtmlInputElement| { + input.files().and_then(|files| { + #[allow(clippy::arc_with_non_send_sync)] + crate::file_engine::WebFileEngine::new(files).map(|f| { + std::sync::Arc::new(f) as std::sync::Arc + }) + }) + }); + + files + } + + fn as_any(&self) -> &dyn Any { + &self.raw as &dyn Any + } +} + +// web-sys does not expose the keys api for form data, so we need to manually bind to it +#[wasm_bindgen(inline_js = r#" + export function get_form_data(form) { + let values = new Map(); + const formData = new FormData(form); + + for (let name of formData.keys()) { + const fieldType = target.elements[name].type; + + switch (fieldType) { + case "select-multiple": + contents.values[name] = formData.getAll(name); + break; + + // add cases for fieldTypes that can hold multiple values here + default: + contents.values[name] = formData.get(name); + break; + } + + return values; + } +"#)] +extern "C" { + fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map; +} diff --git a/packages/web/src/hot_reload.rs b/packages/web/src/hot_reload.rs index 6256cc935..e8b4e42e4 100644 --- a/packages/web/src/hot_reload.rs +++ b/packages/web/src/hot_reload.rs @@ -5,7 +5,6 @@ use futures_channel::mpsc::UnboundedReceiver; use dioxus_core::Template; pub(crate) fn init() -> UnboundedReceiver> { - use std::convert::TryInto; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use web_sys::{MessageEvent, WebSocket}; @@ -31,14 +30,12 @@ pub(crate) fn init() -> UnboundedReceiver> { // change the rsx when new data is received let cl = Closure::wrap(Box::new(move |e: MessageEvent| { if let Ok(text) = e.data().dyn_into::() { - let text: Result = text.try_into(); - if let Ok(string) = text { - let val = serde_json::from_str::(&string).unwrap(); - // leak the value - let val: &'static serde_json::Value = Box::leak(Box::new(val)); - let template: Template<'_> = Template::deserialize(val).unwrap(); - tx.unbounded_send(template).unwrap(); - } + let string: String = text.into(); + let val = serde_json::from_str::(&string).unwrap(); + // leak the value + let val: &'static serde_json::Value = Box::leak(Box::new(val)); + let template: Template<'_> = Template::deserialize(val).unwrap(); + tx.unbounded_send(template).unwrap(); } }) as Box); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 60f84de37..72a85fc90 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -1,3 +1,5 @@ +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] //! Dioxus WebSys @@ -53,7 +55,10 @@ // - Do the VDOM work during the idlecallback // - Do DOM work in the next requestAnimationFrame callback +use std::rc::Rc; + pub use crate::cfg::Config; +#[cfg(feature = "file_engine")] pub use crate::file_engine::WebFileEngineExt; use dioxus_core::{Element, Scope, VirtualDom}; use futures_util::{ @@ -66,6 +71,8 @@ mod cfg; mod dom; #[cfg(feature = "eval")] mod eval; +mod event; +pub use event::*; #[cfg(feature = "file_engine")] mod file_engine; #[cfg(all(feature = "hot_reload", debug_assertions))] @@ -213,14 +220,16 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // todo: we need to split rebuild and initialize into two phases // it's a waste to produce edits just to get the vdom loaded - let templates = dom.rebuild().templates; - websys_dom.load_templates(&templates); - + { + let mutations = dom.rebuild(); + web_sys::console::log_1(&format!("mutations: {:#?}", mutations).into()); + let templates = mutations.templates; + websys_dom.load_templates(&templates); + websys_dom.interpreter.flush(); + } if let Err(err) = websys_dom.rehydrate(&dom) { - tracing::error!( - "Rehydration failed {:?}. Rebuild DOM into element from scratch", - &err - ); + tracing::error!("Rehydration failed. {:?}", err); + tracing::error!("Rebuild DOM into element from scratch"); websys_dom.root.set_text_content(None); let edits = dom.rebuild(); @@ -249,13 +258,6 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop pin_mut!(work); #[cfg(all(feature = "hot_reload", debug_assertions))] - // futures_util::select! { - // _ = work => (None, None), - // new_template = hotreload_rx.next() => { - // (None, new_template) - // } - // evt = rx.next() => - // } match select(work, select(hotreload_rx.next(), rx.next())).await { Either::Left((_, _)) => (None, None), Either::Right((Either::Left((new_template, _)), _)) => (None, new_template), @@ -275,7 +277,12 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // Dequeue all of the events from the channel in send order // todo: we should re-order these if possible while let Some(evt) = res { - dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles); + dom.handle_event( + evt.name.as_str(), + Rc::new(evt.data), + evt.element, + evt.bubbles, + ); res = rx.try_next().transpose().unwrap().ok(); } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 4415ceb37..e50da9e2b 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,105 +1,63 @@ use crate::dom::WebsysDom; -use dioxus_core::{ - AttributeValue, DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom, -}; -use dioxus_html::event_bubbles; -use wasm_bindgen::JsCast; -use web_sys::{Comment, Node}; +use dioxus_core::AttributeValue; +use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug)] pub enum RehydrationError { - NodeTypeMismatch, - NodeNotFound, VNodeNotInitialized, } -use RehydrationError::*; -fn set_node(hydrated: &mut Vec, id: ElementId, node: Node) { - let idx = id.0; - if idx >= hydrated.len() { - hydrated.resize(idx + 1, false); - } - if !hydrated[idx] { - dioxus_interpreter_js::set_node(idx as u32, node); - hydrated[idx] = true; - } -} +use RehydrationError::*; impl WebsysDom { // we're streaming in patches, but the nodes already exist // so we're just going to write the correct IDs to the node and load them in pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> { - let mut root = self - .root - .clone() - .dyn_into::() - .map_err(|_| NodeTypeMismatch)? - .first_child() - .ok_or(NodeNotFound); - let root_scope = dom.base_scope(); - - let mut hydrated = vec![true]; - - let mut last_node_was_static_text = false; + let mut ids = Vec::new(); + let mut to_mount = Vec::new(); // Recursively rehydrate the dom from the VirtualDom - self.rehydrate_scope( - root_scope, - &mut root, - &mut hydrated, - dom, - &mut last_node_was_static_text, - )?; + self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?; + + dioxus_interpreter_js::hydrate(ids); + + for id in to_mount { + self.send_mount_event(id); + } - self.interpreter.flush(); Ok(()) } fn rehydrate_scope( &mut self, scope: &ScopeState, - current_child: &mut Result, - hydrated: &mut Vec, dom: &VirtualDom, - last_node_was_static_text: &mut bool, + ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { let vnode = match scope.root_node() { dioxus_core::RenderReturn::Ready(ready) => ready, _ => return Err(VNodeNotInitialized), }; - self.rehydrate_vnode( - current_child, - hydrated, - dom, - vnode, - last_node_was_static_text, - ) + self.rehydrate_vnode(dom, vnode, ids, to_mount) } fn rehydrate_vnode( &mut self, - current_child: &mut Result, - hydrated: &mut Vec, dom: &VirtualDom, vnode: &VNode, - last_node_was_static_text: &mut bool, + ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { for (i, root) in vnode.template.get().roots.iter().enumerate() { - // make sure we set the root node ids even if the node is not dynamic - set_node( - hydrated, - *vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?, - current_child.clone()?, - ); - self.rehydrate_template_node( - current_child, - hydrated, dom, vnode, root, - last_node_was_static_text, + ids, + to_mount, + Some(*vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?), )?; } Ok(()) @@ -107,182 +65,78 @@ impl WebsysDom { fn rehydrate_template_node( &mut self, - current_child: &mut Result, - hydrated: &mut Vec, dom: &VirtualDom, vnode: &VNode, node: &TemplateNode, - last_node_was_static_text: &mut bool, + ids: &mut Vec, + to_mount: &mut Vec, + root_id: Option, ) -> Result<(), RehydrationError> { tracing::trace!("rehydrate template node: {:?}", node); - if let Ok(current_child) = current_child { - if tracing::event_enabled!(tracing::Level::TRACE) { - web_sys::console::log_1(¤t_child.clone().into()); - } - } match node { TemplateNode::Element { children, attrs, .. } => { - let mut mounted_id = None; + let mut mounted_id = root_id; for attr in *attrs { if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { let attribute = &vnode.dynamic_attrs[*id]; - let value = &attribute.value; let id = attribute.mounted_element(); - mounted_id = Some(id); - let name = attribute.name; - if let AttributeValue::Listener(_) = value { - let event_name = &name[2..]; - self.interpreter.new_event_listener( - event_name, - id.0 as u32, - event_bubbles(event_name) as u8, - ); - } + attribute.attribute_type().for_each(|attribute| { + let value = &attribute.value; + mounted_id = Some(id); + if let AttributeValue::Listener(_) = value { + if attribute.name == "onmounted" { + to_mount.push(id); + } + } + }); } } if let Some(id) = mounted_id { - set_node(hydrated, id, current_child.clone()?); + ids.push(id.0 as u32); } if !children.is_empty() { - let mut children_current_child = current_child - .as_mut() - .map_err(|e| *e)? - .first_child() - .ok_or(NodeNotFound)? - .dyn_into::() - .map_err(|_| NodeTypeMismatch); for child in *children { - self.rehydrate_template_node( - &mut children_current_child, - hydrated, - dom, - vnode, - child, - last_node_was_static_text, - )?; + self.rehydrate_template_node(dom, vnode, child, ids, to_mount, None)?; } } - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - *last_node_was_static_text = false; - } - TemplateNode::Text { .. } => { - // if the last node was static text, it got merged with this one - if !*last_node_was_static_text { - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - } - *last_node_was_static_text = true; } TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { - self.rehydrate_dynamic_node( - current_child, - hydrated, - dom, - &vnode.dynamic_nodes[*id], - last_node_was_static_text, - )?; + self.rehydrate_dynamic_node(dom, &vnode.dynamic_nodes[*id], ids, to_mount)?; } + _ => {} } Ok(()) } fn rehydrate_dynamic_node( &mut self, - current_child: &mut Result, - hydrated: &mut Vec, dom: &VirtualDom, dynamic: &DynamicNode, - last_node_was_static_text: &mut bool, + ids: &mut Vec, + to_mount: &mut Vec, ) -> Result<(), RehydrationError> { tracing::trace!("rehydrate dynamic node: {:?}", dynamic); - if let Ok(current_child) = current_child { - if tracing::event_enabled!(tracing::Level::TRACE) { - web_sys::console::log_1(¤t_child.clone().into()); - } - } match dynamic { dioxus_core::DynamicNode::Text(text) => { - let id = text.mounted_element(); - // skip comment separator before node - if cfg!(debug_assertions) { - assert!(current_child - .as_mut() - .map_err(|e| *e)? - .has_type::()); - } - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - - set_node( - hydrated, - id.ok_or(VNodeNotInitialized)?, - current_child.clone()?, - ); - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - - // skip comment separator after node - if cfg!(debug_assertions) { - assert!(current_child - .as_mut() - .map_err(|e| *e)? - .has_type::()); - } - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - - *last_node_was_static_text = false; + ids.push(text.mounted_element().ok_or(VNodeNotInitialized)?.0 as u32); } dioxus_core::DynamicNode::Placeholder(placeholder) => { - set_node( - hydrated, - placeholder.mounted_element().ok_or(VNodeNotInitialized)?, - current_child.clone()?, - ); - *current_child = current_child - .as_mut() - .map_err(|e| *e)? - .next_sibling() - .ok_or(NodeNotFound); - *last_node_was_static_text = false; + ids.push(placeholder.mounted_element().ok_or(VNodeNotInitialized)?.0 as u32); } dioxus_core::DynamicNode::Component(comp) => { let scope = comp.mounted_scope().ok_or(VNodeNotInitialized)?; self.rehydrate_scope( - dom.get_scope(scope).unwrap(), - current_child, - hydrated, + dom.get_scope(scope).ok_or(VNodeNotInitialized)?, dom, - last_node_was_static_text, + ids, + to_mount, )?; } dioxus_core::DynamicNode::Fragment(fragment) => { for vnode in *fragment { - self.rehydrate_vnode( - current_child, - hydrated, - dom, - vnode, - last_node_was_static_text, - )?; + self.rehydrate_vnode(dom, vnode, ids, to_mount)?; } } } diff --git a/playwright-tests/playwright.config.js b/playwright-tests/playwright.config.js index 54fe810a7..dcee8f0e4 100644 --- a/playwright-tests/playwright.config.js +++ b/playwright-tests/playwright.config.js @@ -76,23 +76,23 @@ module.exports = defineConfig({ command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test", port: 3030, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), "web"), - command: "cargo run --package dioxus-cli -- serve", + command: "cargo run --package dioxus-cli --release -- serve --skip-assets", port: 8080, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { cwd: path.join(process.cwd(), 'fullstack'), - command: 'cargo run --package dioxus-cli -- build --features web --release\ncargo run --release --features ssr', + command: 'cargo run --package dioxus-cli --release -- build --features web --release --skip-assets\ncargo run --release --features ssr', port: 3333, - timeout: 10 * 60 * 1000, + timeout: 20 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", },