Browse Source

Merge branch 'master' into add-file-data-drag-event

Evan Almloff 1 year ago
parent
commit
56798b3d1c
100 changed files with 2077 additions and 650 deletions
  1. 5 0
      .cargo/config.toml
  2. 2 0
      .github/workflows/cli_release.yml
  3. 1 1
      .github/workflows/docs stable.yml
  4. 1 1
      .github/workflows/docs.yml
  5. 40 7
      .github/workflows/main.yml
  6. 4 4
      .github/workflows/miri.yml
  7. 3 2
      .github/workflows/playwright.yml
  8. 1 0
      .gitignore
  9. 0 171
      CHANGELOG.md
  10. 13 5
      Cargo.toml
  11. 72 4
      Makefile.toml
  12. 2 1
      README.md
  13. 0 1
      examples/README.md
  14. 4 7
      examples/all_events.rs
  15. 0 18
      examples/button.rs
  16. 3 2
      examples/calculator.rs
  17. 1 1
      examples/clock.rs
  18. 4 18
      examples/compose.rs
  19. 1 1
      examples/counter.rs
  20. 27 63
      examples/crm.rs
  21. 1 1
      examples/custom_assets.rs
  22. 27 0
      examples/dynamic_asset.rs
  23. 11 16
      examples/error_handle.rs
  24. 9 14
      examples/eval.rs
  25. 2 1
      examples/file_explorer.rs
  26. 2 2
      examples/file_upload.rs
  27. 2 2
      examples/form.rs
  28. 17 20
      examples/login_form.rs
  29. 1 1
      examples/mobile_demo/.gitignore
  30. 1 1
      examples/mobile_demo/Cargo.toml
  31. 1 1
      examples/mobile_demo/README.md
  32. 1 3
      examples/multiwindow.rs
  33. 3 0
      examples/openid_connect_demo/.gitignore
  34. 25 0
      examples/openid_connect_demo/Cargo.toml
  35. 47 0
      examples/openid_connect_demo/Dioxus.toml
  36. 13 0
      examples/openid_connect_demo/README.md
  37. 2 0
      examples/openid_connect_demo/src/constants.rs
  38. 20 0
      examples/openid_connect_demo/src/errors.rs
  39. 60 0
      examples/openid_connect_demo/src/main.rs
  40. 1 0
      examples/openid_connect_demo/src/model/mod.rs
  41. 7 0
      examples/openid_connect_demo/src/model/user.rs
  42. 125 0
      examples/openid_connect_demo/src/oidc.rs
  43. 20 0
      examples/openid_connect_demo/src/props/client.rs
  44. 1 0
      examples/openid_connect_demo/src/props/mod.rs
  45. 17 0
      examples/openid_connect_demo/src/router.rs
  46. 38 0
      examples/openid_connect_demo/src/storage.rs
  47. 250 0
      examples/openid_connect_demo/src/views/header.rs
  48. 5 0
      examples/openid_connect_demo/src/views/home.rs
  49. 86 0
      examples/openid_connect_demo/src/views/login.rs
  50. 4 0
      examples/openid_connect_demo/src/views/mod.rs
  51. 7 0
      examples/openid_connect_demo/src/views/not_found.rs
  52. 12 0
      examples/optional_props.rs
  53. 2 4
      examples/overlay.rs
  54. 1 1
      examples/pattern_model.rs
  55. 1 0
      examples/query_segments_demo/Cargo.toml
  56. 22 8
      examples/query_segments_demo/src/main.rs
  57. 5 0
      examples/rsx_usage.rs
  58. 1 1
      examples/shared_state.rs
  59. 23 1
      examples/signals.rs
  60. 36 0
      examples/spread.rs
  61. 33 0
      examples/streams.rs
  62. 1 1
      examples/tailwind/Cargo.toml
  63. 1 1
      examples/tailwind/Dioxus.toml
  64. 1 1
      examples/tailwind/README.md
  65. 1 0
      examples/tailwind/dist/tailwind3531548035813279582.css
  66. 8 6
      examples/tailwind/src/main.rs
  67. 1 1
      examples/textarea.rs
  68. 133 79
      examples/todomvc.rs
  69. 188 0
      examples/video_stream.rs
  70. 1 1
      examples/window_focus.rs
  71. 2 4
      examples/window_zoom.rs
  72. 1 1
      examples/xss_safety.rs
  73. 247 0
      flake.lock
  74. 63 0
      flake.nix
  75. 6 5
      packages/autofmt/src/buffer.rs
  76. 63 33
      packages/autofmt/src/element.rs
  77. 3 3
      packages/autofmt/src/expr.rs
  78. 108 0
      packages/autofmt/src/indent.rs
  79. 16 15
      packages/autofmt/src/lib.rs
  80. 56 38
      packages/autofmt/src/writer.rs
  81. 1 1
      packages/autofmt/tests/samples.rs
  82. 1 1
      packages/autofmt/tests/samples/simple.rsx
  83. 10 5
      packages/autofmt/tests/wrong.rs
  84. 0 0
      packages/autofmt/tests/wrong/comments-4sp.rsx
  85. 0 0
      packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
  86. 7 0
      packages/autofmt/tests/wrong/comments-tab.rsx
  87. 5 0
      packages/autofmt/tests/wrong/comments-tab.wrong.rsx
  88. 0 0
      packages/autofmt/tests/wrong/multi-4sp.rsx
  89. 0 0
      packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
  90. 3 0
      packages/autofmt/tests/wrong/multi-tab.rsx
  91. 5 0
      packages/autofmt/tests/wrong/multi-tab.wrong.rsx
  92. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.rsx
  93. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
  94. 8 0
      packages/autofmt/tests/wrong/multiexpr-tab.rsx
  95. 5 0
      packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
  96. 0 3
      packages/check/Cargo.toml
  97. 2 2
      packages/check/README.md
  98. 4 0
      packages/check/src/lib.rs
  99. 0 31
      packages/cli/.github/workflows/build.yml
  100. 0 34
      packages/cli/.github/workflows/docs.yml

+ 5 - 0
.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 = ""

+ 2 - 0
.github/workflows/cli_release.yml

@@ -36,6 +36,8 @@ jobs:
           toolchain: ${{ matrix.platform.toolchain }}
           toolchain: ${{ matrix.platform.toolchain }}
           targets: ${{ matrix.platform.target }}
           targets: ${{ matrix.platform.target }}
 
 
+      - uses: ilammy/setup-nasm@v1
+
       # Setup the Github Actions Cache for the CLI package
       # Setup the Github Actions Cache for the CLI package
       - name: Setup cache
       - name: Setup cache
         uses: Swatinem/rust-cache@v2
         uses: Swatinem/rust-cache@v2

+ 1 - 1
.github/workflows/docs stable.yml

@@ -33,7 +33,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
 
       - name: Deploy 🚀
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.3
+        uses: JamesIves/github-pages-deploy-action@v4.5.0
         with:
         with:
           branch: gh-pages # The branch the action should deploy to.
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.
           folder: docs/nightly # The folder the action should deploy.

+ 1 - 1
.github/workflows/docs.yml

@@ -39,7 +39,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
 
       - name: Deploy 🚀
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.3
+        uses: JamesIves/github-pages-deploy-action@v4.5.0
         with:
         with:
           branch: gh-pages # The branch the action should deploy to.
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.
           folder: docs/nightly # The folder the action should deploy.

+ 40 - 7
.github/workflows/main.yml

@@ -36,9 +36,15 @@ jobs:
     if: github.event.pull_request.draft == false
     if: github.event.pull_request.draft == false
     name: Check
     name: Check
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    env:
+      CARGO_TERM_COLOR: always
+      CARGO_INCREMENTAL: 0
+      SCCACHE_GHA_ENABLED: "true"
+      RUSTC_WRAPPER: "sccache"
     steps:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - 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-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
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -48,11 +54,17 @@ jobs:
     if: github.event.pull_request.draft == false
     if: github.event.pull_request.draft == false
     name: Test Suite
     name: Test Suite
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    env:
+      CARGO_TERM_COLOR: always
+      CARGO_INCREMENTAL: 0
+      SCCACHE_GHA_ENABLED: "true"
+      RUSTC_WRAPPER: "sccache"
     steps:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - 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-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: davidB/rust-cargo-make@v1
       - uses: browser-actions/setup-firefox@latest
       - uses: browser-actions/setup-firefox@latest
       - uses: jetli/wasm-pack-action@v0.4.0
       - uses: jetli/wasm-pack-action@v0.4.0
@@ -63,9 +75,15 @@ jobs:
     if: github.event.pull_request.draft == false
     if: github.event.pull_request.draft == false
     name: Rustfmt
     name: Rustfmt
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    env:
+      CARGO_TERM_COLOR: always
+      CARGO_INCREMENTAL: 0
+      SCCACHE_GHA_ENABLED: "true"
+      RUSTC_WRAPPER: "sccache"
     steps:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - 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
       - run: rustup component add rustfmt
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
       - run: cargo fmt --all -- --check
       - run: cargo fmt --all -- --check
@@ -74,9 +92,15 @@ jobs:
     if: github.event.pull_request.draft == false
     if: github.event.pull_request.draft == false
     name: Clippy
     name: Clippy
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    env:
+      CARGO_TERM_COLOR: always
+      CARGO_INCREMENTAL: 0
+      SCCACHE_GHA_ENABLED: "true"
+      RUSTC_WRAPPER: "sccache"
     steps:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - 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-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
       - run: rustup component add clippy
       - run: rustup component add clippy
@@ -86,6 +110,10 @@ jobs:
   matrix_test:
   matrix_test:
     runs-on: ${{ matrix.platform.os }}
     runs-on: ${{ matrix.platform.os }}
     env:
     env:
+      CARGO_TERM_COLOR: always
+      CARGO_INCREMENTAL: 0
+      SCCACHE_GHA_ENABLED: "true"
+      RUSTC_WRAPPER: "sccache"
       RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
       RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
     strategy:
     strategy:
       matrix:
       matrix:
@@ -125,7 +153,7 @@ jobs:
 
 
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
-
+      - uses: ilammy/setup-nasm@v1
       - name: install stable
       - name: install stable
         uses: dtolnay/rust-toolchain@master
         uses: dtolnay/rust-toolchain@master
         with:
         with:
@@ -136,11 +164,16 @@ jobs:
         if: ${{ matrix.platform.cross == true }}
         if: ${{ matrix.platform.cross == true }}
         uses: taiki-e/install-action@cross
         uses: taiki-e/install-action@cross
 
 
-      - uses: Swatinem/rust-cache@v2
+      - uses: mozilla-actions/sccache-action@v0.0.3
         with:
         with:
           workspaces: core -> ../target
           workspaces: core -> ../target
           save-if: ${{ matrix.features.key == 'all' }}
           save-if: ${{ matrix.features.key == 'all' }}
 
 
+      - name: Install rustfmt
+        run: rustup component add rustfmt
+
+      - uses: actions/checkout@v4
+
       - name: test
       - name: test
         run: |
         run: |
           ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
           ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}

+ 4 - 4
.github/workflows/miri.yml

@@ -26,8 +26,8 @@ env:
   RUST_BACKTRACE: 1
   RUST_BACKTRACE: 1
   # Change to specific Rust release to pin
   # Change to specific Rust release to pin
   rust_stable: stable
   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:
   # When updating this, also update:
   # - README.md
   # - README.md
   # - tokio/README.md
   # - tokio/README.md
@@ -70,6 +70,7 @@ jobs:
         run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV
         run: echo "MIRIFLAGS=-Zmiri-tag-gc=1" >> $GITHUB_ENV
 
 
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
+      - uses: ilammy/setup-nasm@v1
       - name: Install Rust ${{ env.rust_nightly }}
       - name: Install Rust ${{ env.rust_nightly }}
         uses: dtolnay/rust-toolchain@master
         uses: dtolnay/rust-toolchain@master
         with:
         with:
@@ -86,8 +87,7 @@ jobs:
 
 
         # working-directory: tokio
         # working-directory: tokio
         env:
         env:
-          #  todo: disable memory leaks ignore
-          MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
+          MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
           PROPTEST_CASES: 10
           PROPTEST_CASES: 10
 
 
       # Cache the global cargo directory, but NOT the local `target` directory which
       # Cache the global cargo directory, but NOT the local `target` directory which

+ 3 - 2
.github/workflows/playwright.yml

@@ -20,7 +20,8 @@ jobs:
     steps:
     steps:
       # Do our best to cache the toolchain and node install steps
       # Do our best to cache the toolchain and node install steps
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
-      - uses: actions/setup-node@v3
+      - uses: ilammy/setup-nasm@v1
+      - uses: actions/setup-node@v4
         with:
         with:
           node-version: 16
           node-version: 16
       - name: Install Rust
       - name: Install Rust
@@ -43,7 +44,7 @@ jobs:
       #     args: --path packages/cli
       #     args: --path packages/cli
       - name: Run Playwright tests
       - name: Run Playwright tests
         run: npx playwright test
         run: npx playwright test
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         if: always()
         if: always()
         with:
         with:
           name: playwright-report
           name: playwright-report

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@
 /dist
 /dist
 Cargo.lock
 Cargo.lock
 .DS_Store
 .DS_Store
+/examples/assets/test_video.mp4
 
 
 .vscode/*
 .vscode/*
 !.vscode/settings.json
 !.vscode/settings.json

+ 0 - 171
CHANGELOG.md

@@ -1,171 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## Unreleased
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 1 commit contributed to the release over the course of 7 calendar days.
- - 0 commits where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
-</details>
-
-## v0.1.7 (2022-01-08)
-
-### Bug Fixes
-
- - <csr-id-bd341f5571580cdf5e495379b49ca988fd9211c3/> tests
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 1 commit contributed to the release over the course of 2 calendar days.
- - 1 commit where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - tests ([`bd341f5`](https://github.comgit//DioxusLabs/dioxus/commit/bd341f5571580cdf5e495379b49ca988fd9211c3))
-</details>
-
-## v0.1.1 (2021-12-15)
-
-### Documentation
-
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-## v0.1.0 (2021-12-15)
-
-### Documentation
-
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-## v0.0.1 (2022-01-03)
-
-### Documentation
-
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 40 commits contributed to the release over the course of 193 calendar days.
- - 38 commits where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - remove runner on hook and then update docs ([`d156045`](https://github.comgit//DioxusLabs/dioxus/commit/d1560450bac55f9566e00e00ea405bd1c70b57e5))
-    - polish some more things ([`1496102`](https://github.comgit//DioxusLabs/dioxus/commit/14961023f927b3a8bde83cfc7883aa8bfcca9e85))
-    - upgrade hooks ([`b3ac2ee`](https://github.comgit//DioxusLabs/dioxus/commit/b3ac2ee3f76549cd1c7b6f9eee7e3382b07d873c))
-    - docs ([`8814977`](https://github.comgit//DioxusLabs/dioxus/commit/8814977eeebe06748a3b9677a8070e42a037ebd7))
-    - prepare to change our fragment pattern. Add some more docs ([`2c3a046`](https://github.comgit//DioxusLabs/dioxus/commit/2c3a0464264fa11e8100df025d863931f9606cdb))
-    - it compiles once more ([`8acdd2e`](https://github.comgit//DioxusLabs/dioxus/commit/8acdd2ea830b995b608d8bac2ef527db8d40e662))
-    - some docs and suspense ([`93d4b8c`](https://github.comgit//DioxusLabs/dioxus/commit/93d4b8ca7c1b133e5dba2a8dc9a310dbe1357001))
-    - docs and router ([`a5f05d7`](https://github.comgit//DioxusLabs/dioxus/commit/a5f05d73acc0e47b05cff64a373482519414bc7c))
-    - Merge branch 'master' into jk/remove_node_safety ([`db00047`](https://github.comgit//DioxusLabs/dioxus/commit/db0004758c77331cc3b93ea8cf227c060028e12e))
-    - improve docs ([`7800744`](https://github.comgit//DioxusLabs/dioxus/commit/78007445f944f259170307d840e0f16242b7b4b6))
-    - Various typos/grammar/rewording ([`5747e00`](https://github.comgit//DioxusLabs/dioxus/commit/5747e00b27b1b69c4f9c2820e7e78030feaff71e))
-    - bubbling in progress ([`a21020e`](https://github.comgit//DioxusLabs/dioxus/commit/a21020ea575e467ba0d608737269fe1b0792dba7))
-    - update local examples and docs to support new syntaxes ([`4de16c4`](https://github.comgit//DioxusLabs/dioxus/commit/4de16c4779648e591b3869b5df31271ae603c812))
-    - massage lifetimes ([`9726a06`](https://github.comgit//DioxusLabs/dioxus/commit/9726a065b0d4fb1ede5b53a2ddd58c855e51539f))
-    - major cleanups to scheduler ([`2933e4b`](https://github.comgit//DioxusLabs/dioxus/commit/2933e4bc11b3074c2bde8d76ec55364fca841988))
-    - threadsafe ([`82953f2`](https://github.comgit//DioxusLabs/dioxus/commit/82953f2ac37913f83a822333acd0c47e20777d31))
-    - move macro crate out of core ([`7bdad1e`](https://github.comgit//DioxusLabs/dioxus/commit/7bdad1e2e6f67e74c9f67dde2150140cf8a090e8))
-    - amazingly awesome error handling ([`4a72b31`](https://github.comgit//DioxusLabs/dioxus/commit/4a72b3140bd244da602deada1eeecded65ff5848))
-    - some ideas ([`05c909f`](https://github.comgit//DioxusLabs/dioxus/commit/05c909f320765aec1bf4c1c55ca59ffd5525a2c7))
-    - big updates to the reference ([`583fdfa`](https://github.comgit//DioxusLabs/dioxus/commit/583fdfa5618e11d660985b97e570d4503be2ff49))
-    - docs, html! macro, more ([`caf772c`](https://github.comgit//DioxusLabs/dioxus/commit/caf772cf249d2f56c8d0b0fa2737ad48e32c6e82))
-    - cleanup workspace ([`8f0bb5d`](https://github.comgit//DioxusLabs/dioxus/commit/8f0bb5dc5bfa3e775af567c4b569622cdd932af1))
-    - svgs working in webview ([`3bedcb9`](https://github.comgit//DioxusLabs/dioxus/commit/3bedcb93cacec5bdf134adc38ff02eadbf96c1c6))
-    - mvoe away from compound context ([`a2c7d17`](https://github.comgit//DioxusLabs/dioxus/commit/a2c7d17b0595769f60bc1c2bbf7cbe32cec37486))
-    - more suspended nodes! ([`de9f61b`](https://github.comgit//DioxusLabs/dioxus/commit/de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2))
-    - add aria ([`4091846`](https://github.comgit//DioxusLabs/dioxus/commit/4091846934b4b3b2bc03d3ca8aaf7712aebd4e36))
-    - more examples ([`56e7eb8`](https://github.comgit//DioxusLabs/dioxus/commit/56e7eb83a97ebd6d5bcd23464cfb9d718e5ac26d))
-    - more refactor for async ([`975fa56`](https://github.comgit//DioxusLabs/dioxus/commit/975fa566f9809f8fa2bb0bdb07fbfc7f855dcaeb))
-    - enable arbitrary body in rsx! macro ([`7aec40d`](https://github.comgit//DioxusLabs/dioxus/commit/7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2))
-    - move CLI into its own "studio" app ([`fd79335`](https://github.comgit//DioxusLabs/dioxus/commit/fd7933561fe81922e4d5d77f6ac3b6f19efb5a90))
-    - move some examples around ([`98a0933`](https://github.comgit//DioxusLabs/dioxus/commit/98a09339fd3190799ea4dd316908f0a53fdf2413))
-    - fix issues with lifetimes ([`a38a81e`](https://github.comgit//DioxusLabs/dioxus/commit/a38a81e1290375cae685f7c49d3745e4298fab26))
-    - more examples ([`11f89e5`](https://github.comgit//DioxusLabs/dioxus/commit/11f89e5d338d14a7aeece0a6275c24ae65913ce7))
-    - lib.rs docs ([`bf21c82`](https://github.comgit//DioxusLabs/dioxus/commit/bf21c82de04e25daee60a06232b9a16b640508f2))
-    - rename ctx to cx ([`81382e7`](https://github.comgit//DioxusLabs/dioxus/commit/81382e7044fb3dba61d4abb1e6086b7b29143116))
-    - move around examples ([`70cd46d`](https://github.comgit//DioxusLabs/dioxus/commit/70cd46dbb2a689ae2d512e142b8aee9c80798430))
-    - start moving events to rc<event> ([`b9ff95f`](https://github.comgit//DioxusLabs/dioxus/commit/b9ff95fa12c46365fe73b64a4926a506d5da2342))
-    - rename recoil to atoms ([`36ea39a`](https://github.comgit//DioxusLabs/dioxus/commit/36ea39ae30aa3f1fb2d718c0fdf08850c6bfd3ac))
-    - more examples and docs ([`7fbaf69`](https://github.comgit//DioxusLabs/dioxus/commit/7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9))
-    - docs ([`f5683a2`](https://github.comgit//DioxusLabs/dioxus/commit/f5683a23464992ecace463a61414795b5a2c58c8))
-</details>
-

+ 13 - 5
Cargo.toml

@@ -9,6 +9,7 @@ members = [
     "packages/extension",
     "packages/extension",
     "packages/router",
     "packages/router",
     "packages/html",
     "packages/html",
+    "packages/html-internal-macro",
     "packages/hooks",
     "packages/hooks",
     "packages/web",
     "packages/web",
     "packages/ssr",
     "packages/ssr",
@@ -41,6 +42,7 @@ members = [
     "examples/tailwind",
     "examples/tailwind",
     "examples/PWA-example",
     "examples/PWA-example",
     "examples/query_segments_demo",
     "examples/query_segments_demo",
+    "examples/openid_connect_demo",
     # Playwright tests
     # Playwright tests
     "playwright-tests/liveview",
     "playwright-tests/liveview",
     "playwright-tests/web",
     "playwright-tests/web",
@@ -49,7 +51,7 @@ members = [
 exclude = ["examples/mobile_demo"]
 exclude = ["examples/mobile_demo"]
 
 
 [workspace.package]
 [workspace.package]
-version = "0.4.2"
+version = "0.4.3"
 
 
 # dependencies that are shared across packages
 # dependencies that are shared across packages
 [workspace.dependencies]
 [workspace.dependencies]
@@ -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-core-macro = { path = "packages/core-macro", version = "0.4.0"  }
 dioxus-router = { path = "packages/router", version = "0.4.1"  }
 dioxus-router = { path = "packages/router", version = "0.4.1"  }
 dioxus-router-macro = { path = "packages/router-macro", 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-hooks = { path = "packages/hooks", version = "0.4.0" }
 dioxus-web = { path = "packages/web", version = "0.4.0"  }
 dioxus-web = { path = "packages/web", version = "0.4.0"  }
 dioxus-ssr = { path = "packages/ssr", 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" }
 dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
 rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
 rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
 dioxus-signals = { path = "packages/signals" }
 dioxus-signals = { path = "packages/signals" }
-generational-box = { path = "packages/generational-box" }
+generational-box = { path = "packages/generational-box", version = "0.4.3" }
 dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
 dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
 dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1"  }
 dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1"  }
 dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
 dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
@@ -87,7 +90,7 @@ slab = "0.4.2"
 futures-channel = "0.3.21"
 futures-channel = "0.3.21"
 futures-util = { version = "0.3", default-features = false }
 futures-util = { version = "0.3", default-features = false }
 rustc-hash = "1.1.0"
 rustc-hash = "1.1.0"
-wasm-bindgen = "0.2.87"
+wasm-bindgen = "0.2.88"
 html_parser = "0.7.0"
 html_parser = "0.7.0"
 thiserror = "1.0.40"
 thiserror = "1.0.40"
 prettyplease = { package = "prettier-please", version = "0.2", features = [
 prettyplease = { package = "prettier-please", version = "0.2", features = [
@@ -98,7 +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
 # It is not meant to be published, but is used so "cargo run --example XYZ" works properly
 [package]
 [package]
 name = "dioxus-examples"
 name = "dioxus-examples"
-version = "0.0.0"
+version = "0.4.3"
 authors = ["Jonathan Kelley"]
 authors = ["Jonathan Kelley"]
 edition = "2021"
 edition = "2021"
 description = "Top level crate for the Dioxus repository"
 description = "Top level crate for the Dioxus repository"
@@ -132,3 +135,8 @@ fern = { version = "0.6.0", features = ["colored"] }
 env_logger = "0.10.0"
 env_logger = "0.10.0"
 simple_logger = "4.0.0"
 simple_logger = "4.0.0"
 thiserror = { workspace = true }
 thiserror = { workspace = true }
+
+
+[dependencies]
+tracing-subscriber = "0.3.17"
+http-range = "0.1.5"

+ 72 - 4
Makefile.toml

@@ -24,12 +24,64 @@ script = [
 ]
 ]
 script_runner = "@duckscript"
 script_runner = "@duckscript"
 
 
+[tasks.format]
+command = "cargo"
+args = ["fmt", "--all"]
+
+[tasks.check]
+command = "cargo"
+args = ["check", "--workspace", "--examples", "--tests"]
+
+[tasks.clippy]
+command = "cargo"
+args = [
+  "clippy",
+  "--workspace",
+  "--examples",
+  "--tests",
+  "--",
+  "-D",
+  "warnings",
+]
+
+[tasks.tidy]
+category = "Formatting"
+dependencies = ["format", "check", "clippy"]
+description = "Format and Check workspace"
+
+[tasks.install-miri]
+toolchain = "nightly"
+install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
+private = true
+
+[tasks.miri-native]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = [
+  "miri",
+  "test",
+  "--package",
+  "dioxus-native-core",
+  "--test",
+  "miri_native",
+]
+
+[tasks.miri-stress]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
+
+[tasks.miri]
+dependencies = ["miri-native", "miri-stress"]
+
 [tasks.tests]
 [tasks.tests]
 category = "Testing"
 category = "Testing"
 dependencies = ["tests-setup"]
 dependencies = ["tests-setup"]
 description = "Run all tests"
 description = "Run all tests"
-env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
-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]
 [tasks.build]
 command = "cargo"
 command = "cargo"
@@ -42,10 +94,26 @@ private = true
 [tasks.test]
 [tasks.test]
 dependencies = ["build"]
 dependencies = ["build"]
 command = "cargo"
 command = "cargo"
-args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"]
+args = [
+  "test",
+  "--lib",
+  "--bins",
+  "--tests",
+  "--examples",
+  "--workspace",
+  "--exclude",
+  "dioxus-router",
+  "--exclude",
+  "dioxus-desktop",
+  "--exclude",
+  "dioxus-mobile",
+]
 private = true
 private = true
 
 
 [tasks.test-with-browser]
 [tasks.test-with-browser]
-env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] }
+env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
+  "**/packages/router",
+  "**/packages/desktop",
+] }
 private = true
 private = true
 workspace = true
 workspace = true

+ 2 - 1
README.md

@@ -159,8 +159,9 @@ So... Dioxus is great, but why won't it work for me?
 
 
 
 
 ## Contributing
 ## Contributing
+- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
-- Join the discord and ask questions!
+- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
 
 
 
 
 <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
 <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">

+ 0 - 1
examples/README.md

@@ -139,7 +139,6 @@ Missing Features
 Missing examples
 Missing examples
 - Shared state
 - Shared state
 - Root-less element groups
 - Root-less element groups
-- Spread props
 - Custom elements
 - Custom elements
 - Component Children: Pass children into child components
 - Component Children: Pass children into child components
 - Render To string: Render a mounted virtualdom to a string
 - Render To string: Render a mounted virtualdom to a string

+ 4 - 7
examples/all_events.rs

@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
     };
     };
 
 
     cx.render(rsx! (
     cx.render(rsx! (
-        div {
-            style: "{CONTAINER_STYLE}",
+        div { style: "{CONTAINER_STYLE}",
             div {
             div {
                 style: "{RECT_STYLE}",
                 style: "{RECT_STYLE}",
                 // focusing is necessary to catch keyboard events
                 // focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
 
 
                 onmousemove: move |event| log_event(Event::MouseMove(event)),
                 onmousemove: move |event| log_event(Event::MouseMove(event)),
                 onclick: move |event| log_event(Event::MouseClick(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)),
                 onmousedown: move |event| log_event(Event::MouseDown(event)),
                 onmouseup: move |event| log_event(Event::MouseUp(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"
                 "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:?}" } )) }
+        }
     ))
     ))
 }
 }

+ 0 - 18
examples/button.rs

@@ -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!"
-        }
-    })
-}

+ 3 - 2
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::events::*;
 use dioxus::html::input_data::keyboard_types::Key;
 use dioxus::html::input_data::keyboard_types::Key;
 use dioxus::prelude::*;
 use dioxus::prelude::*;
-use dioxus_desktop::{Config, WindowBuilder};
+use dioxus_desktop::{Config, LogicalSize, WindowBuilder};
 
 
 fn main() {
 fn main() {
     let config = Config::new().with_window(
     let config = Config::new().with_window(
         WindowBuilder::default()
         WindowBuilder::default()
             .with_title("Calculator")
             .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);
     dioxus_desktop::launch_cfg(app, config);
@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
         div { id: "wrapper",
         div { id: "wrapper",
             div { class: "app",
             div { class: "app",
                 div { class: "calculator",
                 div { class: "calculator",
+                    tabindex: "0",
                     onkeydown: handle_key_down_event,
                     onkeydown: handle_key_down_event,
                     div { class: "calculator-display", val.to_string() }
                     div { class: "calculator-display", val.to_string() }
                     div { class: "calculator-keypad",
                     div { class: "calculator-keypad",

+ 1 - 1
examples/clock.rs

@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
 
 
     use_future!(cx, || async move {
     use_future!(cx, || async move {
         loop {
         loop {
-            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
             count += 1;
             count += 1;
             println!("current: {count}");
             println!("current: {count}");
         }
         }

+ 4 - 18
examples/compose.rs

@@ -1,7 +1,6 @@
 //! This example shows how to create a popup window and send data back to the parent window.
 //! This example shows how to create a popup window and send data back to the parent window.
 
 
 use dioxus::prelude::*;
 use dioxus::prelude::*;
-use dioxus_desktop::use_window;
 use futures_util::StreamExt;
 use futures_util::StreamExt;
 
 
 fn main() {
 fn main() {
@@ -9,7 +8,6 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
-    let window = use_window(cx);
     let emails_sent = use_ref(cx, Vec::new);
     let emails_sent = use_ref(cx, Vec::new);
 
 
     let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
     let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
@@ -27,14 +25,8 @@ fn app(cx: Scope) -> Element {
 
 
             button {
             button {
                 onclick: move |_| {
                 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"
                 "Click to compose a new email"
             }
             }
@@ -57,7 +49,6 @@ struct ComposeProps {
 
 
 fn compose(cx: Scope<ComposeProps>) -> Element {
 fn compose(cx: Scope<ComposeProps>) -> Element {
     let user_input = use_state(cx, String::new);
     let user_input = use_state(cx, String::new);
-    let window = use_window(cx);
 
 
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
@@ -66,17 +57,12 @@ fn compose(cx: Scope<ComposeProps>) -> Element {
             button {
             button {
                 onclick: move |_| {
                 onclick: move |_| {
                     cx.props.app_tx.send(user_input.get().clone());
                     cx.props.app_tx.send(user_input.get().clone());
-                    window.close();
+                    dioxus_desktop::window().close();
                 },
                 },
                 "Click to send"
                 "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}" }
         }
         }
     })
     })
 }
 }

+ 1 - 1
examples/counter.rs

@@ -22,7 +22,7 @@ fn app(cx: Scope) -> Element {
                     input {
                     input {
                         value: "{counter}",
                         value: "{counter}",
                         oninput: move |e| {
                         oninput: move |e| {
-                            if let Ok(value) = e.value.parse::<usize>() {
+                            if let Ok(value) = e.value().parse::<usize>() {
                                 counters.make_mut()[i] = value;
                                 counters.make_mut()[i] = value;
                             }
                             }
                         }
                         }

+ 27 - 63
examples/crm.rs

@@ -35,14 +35,16 @@ fn App(cx: Scope) -> Element {
             rel: "stylesheet",
             rel: "stylesheet",
             href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
             href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
             integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
             integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
-            crossorigin: "anonymous",
+            crossorigin: "anonymous"
         }
         }
 
 
-        style { "
+        style {
+            "
             .red {{
             .red {{
                 background-color: rgb(202, 60, 60) !important;
                 background-color: rgb(202, 60, 60) !important;
             }}
             }}
-        " }
+        "
+        }
 
 
         h1 { "Dioxus CRM Example" }
         h1 { "Dioxus CRM Example" }
 
 
@@ -57,16 +59,8 @@ fn ClientList(cx: Scope) -> Element {
     cx.render(rsx! {
     cx.render(rsx! {
         h2 { "List of Clients" }
         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! {
         clients.read().iter().map(|client| rsx! {
             div {
             div {
@@ -87,8 +81,6 @@ fn ClientAdd(cx: Scope) -> Element {
     let last_name = use_state(cx, String::new);
     let last_name = use_state(cx, String::new);
     let description = use_state(cx, String::new);
     let description = use_state(cx, String::new);
 
 
-    let navigator = use_navigator(cx);
-
     cx.render(rsx! {
     cx.render(rsx! {
         h2 { "Add new Client" }
         h2 { "Add new Client" }
 
 
@@ -96,79 +88,55 @@ fn ClientAdd(cx: Scope) -> Element {
             class: "pure-form pure-form-aligned",
             class: "pure-form pure-form-aligned",
             onsubmit: move |_| {
             onsubmit: move |_| {
                 let mut clients = clients.write();
                 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 {
             fieldset {
-                div {
-                    class: "pure-control-group",
-                    label {
-                        "for": "first_name",
-                        "First Name"
-                    }
+                div { class: "pure-control-group",
+                    label { "for": "first_name", "First Name" }
                     input {
                     input {
                         id: "first_name",
                         id: "first_name",
                         "type": "text",
                         "type": "text",
                         placeholder: "First Name…",
                         placeholder: "First Name…",
                         required: "",
                         required: "",
                         value: "{first_name}",
                         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 {
                     input {
                         id: "last_name",
                         id: "last_name",
                         "type": "text",
                         "type": "text",
                         placeholder: "Last Name…",
                         placeholder: "Last Name…",
                         required: "",
                         required: "",
                         value: "{last_name}",
                         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 {
                     textarea {
                         id: "description",
                         id: "description",
                         placeholder: "Description…",
                         placeholder: "Description…",
                         value: "{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"
             "Remove all Clients"
         }
         }
 
 
-        Link {
-            to: Route::ClientList {},
-            class: "pure-button",
-            "Go back"
-        }
+        Link { to: Route::ClientList {}, class: "pure-button", "Go back" }
     })
     })
 }
 }

+ 1 - 1
examples/custom_assets.rs

@@ -10,7 +10,7 @@ fn app(cx: Scope) -> Element {
             p {
             p {
                 "This should show an image:"
                 "This should show an image:"
             }
             }
-            img { src: "examples/assets/logo.png" }
+            img { src: mg!(image("examples/assets/logo.png").format(ImageType::Avif)).to_string() }
         }
         }
     })
     })
 }
 }

+ 27 - 0
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"
+            }
+        }
+    })
+}

+ 11 - 16
examples/error_handle.rs

@@ -1,4 +1,4 @@
-use dioxus::prelude::*;
+use dioxus::{core::CapturedError, prelude::*};
 
 
 fn main() {
 fn main() {
     dioxus_desktop::launch(App);
     dioxus_desktop::launch(App);
@@ -6,30 +6,25 @@ fn main() {
 
 
 #[component]
 #[component]
 fn App(cx: Scope) -> Element {
 fn App(cx: Scope) -> Element {
-    let val = use_state(cx, || "0.0001");
-
-    let num = match val.parse::<f32>() {
-        Err(_) => return cx.render(rsx!("Parsing failed")),
-        Ok(num) => num,
-    };
-
     cx.render(rsx! {
     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]
 #[component]
 fn DemoC(cx: Scope, x: i32) -> Element {
 fn DemoC(cx: Scope, x: i32) -> Element {
+    let result = Err("Error");
+
+    result.throw()?;
+
     cx.render(rsx! {
     cx.render(rsx! {
         h1 {
         h1 {
-            "asdasdasdasd {x}"
+            "{x}"
         }
         }
     })
     })
 }
 }

+ 9 - 14
examples/eval.rs

@@ -5,26 +5,21 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 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!");
                 dioxus.send("Hi from JS!");
                 let msg = await dioxus.recv();
                 let msg = await dioxus.recv();
                 console.log(msg);
                 console.log(msg);
                 return "hello world";
                 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() {
     match future.value() {

+ 2 - 1
examples/file_explorer.rs

@@ -18,13 +18,14 @@ fn main() {
     );
     );
 }
 }
 
 
+const _STYLE: &str = mg!(file("./examples/assets/fileexplorer.css"));
+
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
     let files = use_ref(cx, Files::new);
     let files = use_ref(cx, Files::new);
 
 
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
             link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
             link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
-            style { include_str!("./assets/fileexplorer.css") }
             header {
             header {
                 i { class: "material-icons icon-menu", "menu" }
                 i { class: "material-icons icon-menu", "menu" }
                 h1 { "Files: ", files.read().current() }
                 h1 { "Files: ", files.read().current() }

+ 2 - 2
examples/file_upload.rs

@@ -16,7 +16,7 @@ fn App(cx: Scope) -> Element {
                 r#type: "checkbox",
                 r#type: "checkbox",
                 checked: "{enable_directory_upload}",
                 checked: "{enable_directory_upload}",
                 oninput: move |evt| {
                 oninput: move |evt| {
-                    enable_directory_upload.set(evt.value.parse().unwrap());
+                    enable_directory_upload.set(evt.value().parse().unwrap());
                 },
                 },
             },
             },
             "Enable directory upload"
             "Enable directory upload"
@@ -30,7 +30,7 @@ fn App(cx: Scope) -> Element {
             onchange: |evt| {
             onchange: |evt| {
                 to_owned![files_uploaded];
                 to_owned![files_uploaded];
                 async move {
                 async move {
-                    if let Some(file_engine) = &evt.files {
+                    if let Some(file_engine) = &evt.files() {
                         let files = file_engine.files();
                         let files = file_engine.files();
                         for file_name in files {
                         for file_name in files {
                             sleep(std::time::Duration::from_secs(1)).await;
                             sleep(std::time::Duration::from_secs(1)).await;

+ 2 - 2
examples/form.rs

@@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element {
         div {
         div {
             h1 { "Form" }
             h1 { "Form" }
             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: "username" }
                 input { r#type: "text", name: "full-name" }
                 input { r#type: "text", name: "full-name" }
                 input { r#type: "password", name: "password" }
                 input { r#type: "password", name: "password" }

+ 17 - 20
examples/login_form.rs

@@ -8,33 +8,30 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 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! {
     cx.render(rsx! {
         h1 { "Login" }
         h1 { "Login" }
-        form {
-            onsubmit: onsubmit,
+        form { onsubmit: onsubmit,
             input { r#type: "text", id: "username", name: "username" }
             input { r#type: "text", id: "username", name: "username" }
             label { "Username" }
             label { "Username" }
             br {}
             br {}

+ 1 - 1
examples/mobile_demo/.gitignore

@@ -2,7 +2,7 @@
 target/
 target/
 **/*.rs.bk
 **/*.rs.bk
 
 
-# tauri-mobile
+# cargo-mobile2
 .cargo/
 .cargo/
 /gen
 /gen
 
 

+ 1 - 1
examples/mobile_demo/Cargo.toml

@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
 [dependencies]
 [dependencies]
 anyhow = "1.0.56"
 anyhow = "1.0.56"
 log = "0.4.11"
 log = "0.4.11"
-wry = "0.28.0"
+wry = "0.35.0"
 dioxus = { path = "../../packages/dioxus" }
 dioxus = { path = "../../packages/dioxus" }
 dioxus-desktop = { path = "../../packages/desktop", features = [
 dioxus-desktop = { path = "../../packages/desktop", features = [
     "tokio_runtime",
     "tokio_runtime",

+ 1 - 1
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.
 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
 ## Running on iOS
 
 

+ 1 - 3
examples/multiwindow.rs

@@ -5,14 +5,12 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
-    let window = dioxus_desktop::use_window(cx);
-
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
             button {
             button {
                 onclick: move |_| {
                 onclick: move |_| {
                     let dom = VirtualDom::new(popup);
                     let dom = VirtualDom::new(popup);
-                    window.new_window(dom, Default::default());
+                    dioxus_desktop::window().new_window(dom, Default::default());
                 },
                 },
                 "New Window"
                 "New Window"
             }
             }

+ 3 - 0
examples/openid_connect_demo/.gitignore

@@ -0,0 +1,3 @@
+/target
+/dist
+.env

+ 25 - 0
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"] }

+ 47 - 0
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 = []

+ 13 - 0
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`

+ 2 - 0
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";

+ 20 - 0
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<openidconnect::reqwest::Error<reqwest::Error>>,
+    ),
+    #[error("Parsing error: {0}")]
+    Parse(#[from] url::ParseError),
+    #[error("Request token error: {0}")]
+    RequestToken(
+        #[from]
+        RequestTokenError<
+            openidconnect::reqwest::Error<reqwest::Error>,
+            StandardErrorResponse<CoreErrorResponseType>,
+        >,
+    ),
+}

+ 60 - 0
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<ClientState> = AtomRef(|_| ClientState::default());
+
+// An option is required to prevent the component from being constantly refreshed
+pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
+pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
+
+pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
+pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
+pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
+
+fn App(cx: Scope) -> Element {
+    use_init_atom_root(cx);
+
+    // Retrieve the value stored in the browser's storage
+    let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
+        .ok()
+        .unwrap_or(AuthTokenState::default());
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    if fermi_auth_token.read().is_none() {
+        *fermi_auth_token.write() = Some(stored_auth_token);
+    }
+
+    let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
+        .ok()
+        .unwrap_or(AuthRequestState::default());
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    if fermi_auth_request.read().is_none() {
+        *fermi_auth_request.write() = Some(stored_auth_request);
+    }
+    render! { Router::<Route> {} }
+}
+
+fn main() {
+    dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
+    console_error_panic_hook::set_once();
+    log::info!("starting app");
+    dioxus_web::launch(App);
+}

+ 1 - 0
examples/openid_connect_demo/src/model/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod user;

+ 7 - 0
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,
+}

+ 125 - 0
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<ClientProps>,
+}
+
+/// State that holds the nonce and authorization url and the nonce generated to log in an user
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct AuthRequestState {
+    pub auth_request: Option<AuthRequest>,
+}
+
+#[derive(Clone, Deserialize, Serialize)]
+pub struct AuthRequest {
+    pub nonce: Nonce,
+    pub authorize_url: String,
+}
+
+/// State the tokens returned once the user is authenticated
+#[derive(Debug, Deserialize, Serialize, Default, Clone)]
+pub struct AuthTokenState {
+    /// Token used to identify the user
+    pub id_token: Option<CoreIdToken>,
+    /// Token used to refresh the tokens if they expire
+    pub refresh_token: Option<RefreshToken>,
+}
+
+pub fn email(
+    client: CoreClient,
+    id_token: CoreIdToken,
+    nonce: Nonce,
+) -> Result<String, ClaimsVerificationError> {
+    match id_token.claims(&client.id_token_verifier(), &nonce) {
+        Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
+        Err(error) => Err(error),
+    }
+}
+
+pub fn authorize_url(client: CoreClient) -> AuthRequest {
+    let (authorize_url, _csrf_state, nonce) = client
+        .authorize_url(
+            AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
+            CsrfToken::new_random,
+            Nonce::new_random,
+        )
+        .add_scope(openidconnect::Scope::new("email".to_string()))
+        .add_scope(openidconnect::Scope::new("profile".to_string()))
+        .url();
+    AuthRequest {
+        authorize_url: authorize_url.to_string(),
+        nonce,
+    }
+}
+
+pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
+    let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
+    Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
+}
+
+pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
+    let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
+    let provider_metadata = init_provider_metadata().await?;
+    let client_secret = None;
+    let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
+
+    Ok((
+        client_id.clone(),
+        CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
+            .set_redirect_uri(redirect_url),
+    ))
+}
+
+///TODO: Add pkce_pacifier
+pub async fn token_response(
+    oidc_client: CoreClient,
+    code: String,
+) -> Result<CoreTokenResponse, crate::errors::Error> {
+    // let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
+    Ok(oidc_client
+        .exchange_code(AuthorizationCode::new(code.clone()))
+        // .set_pkce_verifier(pkce_verifier)
+        .request_async(async_http_client)
+        .await?)
+}
+
+pub async fn exchange_refresh_token(
+    oidc_client: CoreClient,
+    refresh_token: RefreshToken,
+) -> Result<
+    CoreTokenResponse,
+    RequestTokenError<
+        openidconnect::reqwest::Error<reqwest::Error>,
+        StandardErrorResponse<CoreErrorResponseType>,
+    >,
+> {
+    oidc_client
+        .exchange_refresh_token(&refresh_token)
+        .request_async(async_http_client)
+        .await
+}
+
+pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
+    let provider_metadata = init_provider_metadata().await?;
+    let end_session_url = provider_metadata
+        .additional_metadata()
+        .clone()
+        .end_session_endpoint
+        .unwrap();
+    let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
+    Ok(logout_request
+        .set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
+        .set_id_token_hint(&id_token_hint)
+        .http_get_url())
+}

+ 20 - 0
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 }
+    }
+}

+ 1 - 0
examples/openid_connect_demo/src/props/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod client;

+ 17 - 0
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<String> },
+}

+ 38 - 0
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<T> {
+    pub key: String,
+    pub value: T,
+}
+
+pub trait PersistentWrite<T: Serialize + Clone> {
+    fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
+}
+
+impl PersistentWrite<AuthTokenState> for AuthTokenState {
+    fn persistent_set(
+        atom_ref: &UseAtomRef<Option<AuthTokenState>>,
+        entry: Option<AuthTokenState>,
+    ) {
+        *atom_ref.write() = entry.clone();
+        LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
+    }
+}
+
+impl PersistentWrite<AuthRequestState> for AuthRequestState {
+    fn persistent_set(
+        atom_ref: &UseAtomRef<Option<AuthRequestState>>,
+        entry: Option<AuthRequestState>,
+    ) {
+        *atom_ref.write() = entry.clone();
+        LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
+    }
+}

+ 250 - 0
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<ClientProps>) -> Element {
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_token_read = fermi_auth_token.read().clone();
+    let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
+    cx.render(match fermi_auth_token_read {
+        Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
+            Some(id_token) => match log_out_url_state.get() {
+                Some(log_out_url_result) => match log_out_url_result {
+                    Some(uri) => match uri {
+                        Ok(uri) => {
+                            rsx! {
+                                Link {
+                                    onclick: move |_| {
+                                        {
+                                            AuthTokenState::persistent_set(
+                                                fermi_auth_token,
+                                                Some(AuthTokenState::default()),
+                                            );
+                                        }
+                                    },
+                                    to: uri.to_string(),
+                                    "Log out"
+                                }
+                            }
+                        }
+                        Err(error) => {
+                            rsx! {
+                                div { format!{"Failed to load disconnection url: {:?}", error} }
+                            }
+                        }
+                    },
+                    None => {
+                        rsx! { div { "Loading... Please wait" } }
+                    }
+                },
+                None => {
+                    let logout_url_task = move || {
+                        cx.spawn({
+                            let log_out_url_state = log_out_url_state.to_owned();
+                            async move {
+                                let logout_url = log_out_url(id_token).await;
+                                let logout_url_option = Some(logout_url);
+                                log_out_url_state.set(Some(logout_url_option));
+                            }
+                        })
+                    };
+                    logout_url_task();
+                    rsx! { div{"Loading log out url... Please wait"}}
+                }
+            },
+            None => {
+                rsx! {{}}
+            }
+        },
+        None => {
+            rsx! {{}}
+        }
+    })
+}
+
+#[component]
+pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let fermi_auth_token_read = fermi_auth_token.read().clone();
+    cx.render(match fermi_auth_token_read {
+        Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
+            Some(refresh_token) => {
+                let fermi_auth_token = fermi_auth_token.to_owned();
+                let fermi_auth_request = fermi_auth_request.to_owned();
+                let client = cx.props.client.clone();
+                let exchange_refresh_token_spawn = move || {
+                    cx.spawn({
+                        async move {
+                            let exchange_refresh_token =
+                                exchange_refresh_token(client, refresh_token).await;
+                            match exchange_refresh_token {
+                                Ok(response_token) => {
+                                    AuthTokenState::persistent_set(
+                                        &fermi_auth_token,
+                                        Some(AuthTokenState {
+                                            id_token: response_token.id_token().cloned(),
+                                            refresh_token: response_token.refresh_token().cloned(),
+                                        }),
+                                    );
+                                }
+                                Err(_error) => {
+                                    AuthTokenState::persistent_set(
+                                        &fermi_auth_token,
+                                        Some(AuthTokenState::default()),
+                                    );
+                                    AuthRequestState::persistent_set(
+                                        &fermi_auth_request,
+                                        Some(AuthRequestState::default()),
+                                    );
+                                }
+                            }
+                        }
+                    })
+                };
+                exchange_refresh_token_spawn();
+                rsx! { div { "Refreshing session, please wait" } }
+            }
+            None => {
+                rsx! { div { "Id token expired and no refresh token found" } }
+            }
+        },
+        None => {
+            rsx! {{}}
+        }
+    })
+}
+
+#[component]
+pub fn LoadClient(cx: Scope) -> Element {
+    let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
+    let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
+    cx.render(match init_client_future.value() {
+        Some(client_props) => match client_props {
+            Ok((client_id, client)) => {
+                *fermi_client.write() = ClientState {
+                    oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
+                };
+                rsx! {
+                    div { "Client successfully loaded" }
+                    Outlet::<Route> {}
+                }
+            }
+            Err(error) => {
+                rsx! {
+                    div { format!{"Failed to load client: {:?}", error} }
+                    log::info!{"Failed to load client: {:?}", error},
+                    Outlet::<Route> {}
+                }
+            }
+        },
+        None => {
+            rsx! {
+                div {
+                    div { "Loading client, please wait" }
+                    Outlet::<Route> {}
+                }
+            }
+        }
+    })
+}
+
+#[component]
+pub fn AuthHeader(cx: Scope) -> Element {
+    let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
+    let client = fermi_client.read().oidc_client.clone();
+    let auth_request_read = fermi_auth_request.read().clone();
+    let auth_token_read = auth_token.read().clone();
+    cx.render(match (client, auth_request_read, auth_token_read) {
+        // We have everything we need to attempt to authenticate the user
+        (Some(client_props), Some(auth_request), Some(auth_token)) => {
+            match auth_request.auth_request {
+                Some(auth_request) => {
+                    match auth_token.id_token {
+                        Some(id_token) => {
+                            match email(
+                                client_props.client.clone(),
+                                id_token.clone(),
+                                auth_request.nonce.clone(),
+                            ) {
+                                Ok(email) => {
+                                    rsx! {
+                                        div {
+                                            div { email }
+                                            LogOut { client_id: client_props.client_id, client: client_props.client }
+                                            Outlet::<Route> {}
+                                        }
+                                    }
+                                }
+                                // Id token failed to be decoded
+                                Err(error) => match error {
+                                    // Id token failed to be decoded because it expired, we refresh it
+                                    openidconnect::ClaimsVerificationError::Expired(_message) => {
+                                        log::info!("Token expired");
+                                        rsx! {
+                                            div {
+                                                RefreshToken {client_id: client_props.client_id, client: client_props.client}
+                                                Outlet::<Route> {}
+                                            }
+                                        }
+                                    }
+                                    // Other issue with token decoding
+                                    _ => {
+                                        log::info!("Other issue with token");
+                                        rsx! {
+                                            div {
+                                                div { error.to_string() }
+                                                Outlet::<Route> {}
+                                            }
+                                        }
+                                    }
+                                },
+                            }
+                        }
+                        // User is not logged in
+                        None => {
+                            rsx! {
+                                div {
+                                    Link { to: auth_request.authorize_url.clone(), "Log in" }
+                                    Outlet::<Route> {}
+                                }
+                            }
+                        }
+                    }
+                }
+                None => {
+                    let auth_request = authorize_url(client_props.client);
+                    AuthRequestState::persistent_set(
+                        fermi_auth_request,
+                        Some(AuthRequestState {
+                            auth_request: Some(auth_request),
+                        }),
+                    );
+                    rsx! { div { "Loading nonce" } }
+                }
+            }
+        }
+        // Client is not initialized yet, we need it for everything
+        (None, _, _) => {
+            rsx! { LoadClient {} }
+        }
+        // We need everything loaded before doing anything
+        (_client, _auth_request, _auth_token) => {
+            rsx! {{}}
+        }
+    })
+}

+ 5 - 0
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" } }
+}

+ 86 - 0
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<Route> = DIOXUS_FRONT_URL.parse().unwrap();
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let client = fermi_client.read().oidc_client.clone();
+    let auth_token_read = fermi_auth_token.read().clone();
+    cx.render(match (client, auth_token_read) {
+        (Some(client_props), Some(auth_token_read)) => {
+            match (auth_token_read.id_token, auth_token_read.refresh_token) {
+                (Some(_id_token), Some(_refresh_token)) => {
+                    rsx! {
+                        div { "Sign in successful" }
+                        Link { to: home_url, "Go back home" }
+                    }
+                }
+                // If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
+                (None, Some(_)) | (Some(_), None) => {
+                    rsx! {
+                        div { "Error while attempting to log in" }
+                        Link {
+                            to: home_url,
+                            onclick: move |_| {
+                                AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
+                                AuthRequestState::persistent_set(
+                                    fermi_auth_request,
+                                    Some(AuthRequestState::default()),
+                                );
+                            },
+                            "Go back home"
+                        }
+                    }
+                }
+                (None, None) => {
+                    let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
+                let code_pair = query_pairs.find(|(key, _value)| key == "code");
+                match code_pair {
+                    Some((_key, code)) => {
+                        let auth_code = code.to_string();
+                        let token_response_spawn = move ||{
+                            cx.spawn({
+                                let fermi_auth_token = fermi_auth_token.to_owned();
+                                async move {
+                                    let token_response_result = token_response(client_props.client, auth_code).await;
+                                    match token_response_result{
+                                        Ok(token_response) => {
+                                            let id_token = token_response.id_token().unwrap();
+                                            AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
+                                                id_token: Some(id_token.clone()),
+                                                refresh_token: token_response.refresh_token().cloned()
+                                            }));
+                                        }
+                                        Err(error) => {
+                                            log::warn!{"{error}"};
+                                        }
+                                    }
+                                }
+                            })
+                        };
+                        token_response_spawn();
+                        rsx!{ div {} }
+                    }
+                    None => {
+                        rsx! { div { "No code provided" } }
+                    }
+                }
+                }
+            }
+        }
+        (_, _) => {
+            rsx! {{}}
+        }
+    })
+}

+ 4 - 0
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;

+ 7 - 0
examples/openid_connect_demo/src/views/not_found.rs

@@ -0,0 +1,7 @@
+use dioxus::prelude::*;
+
+#[component]
+pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
+    let routes = route.join("");
+    render! {rsx! {div{routes}}}
+}

+ 12 - 0
examples/optional_props.rs

@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
             a: "asd".to_string(),
             a: "asd".to_string(),
             c: "asd".to_string(),
             c: "asd".to_string(),
             d: Some("asd".to_string()),
             d: Some("asd".to_string()),
+            e: Some("asd".to_string()),
+        }
+        Button {
+            a: "asd".to_string(),
+            b: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
             e: "asd".to_string(),
             e: "asd".to_string(),
         }
         }
+        Button {
+            a: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
+        }
     })
     })
 }
 }
 
 

+ 2 - 4
examples/overlay.rs

@@ -1,13 +1,11 @@
 use dioxus::prelude::*;
 use dioxus::prelude::*;
-use dioxus_desktop::{tao::dpi::PhysicalPosition, use_window, LogicalSize, WindowBuilder};
+use dioxus_desktop::{tao::dpi::PhysicalPosition, LogicalSize, WindowBuilder};
 
 
 fn main() {
 fn main() {
     dioxus_desktop::launch_cfg(app, make_config());
     dioxus_desktop::launch_cfg(app, make_config());
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
-    let window = use_window(cx);
-
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
             width: "100%",
             width: "100%",
@@ -19,7 +17,7 @@ fn app(cx: Scope) -> Element {
                 width: "100%",
                 width: "100%",
                 height: "10px",
                 height: "10px",
                 background_color: "black",
                 background_color: "black",
-                onmousedown: move |_| window.drag(),
+                onmousedown: move |_| dioxus_desktop::window().drag(),
             }
             }
 
 
             "This is an overlay!"
             "This is an overlay!"

+ 1 - 1
examples/pattern_model.rs

@@ -21,7 +21,7 @@ use dioxus::events::*;
 use dioxus::html::input_data::keyboard_types::Key;
 use dioxus::html::input_data::keyboard_types::Key;
 use dioxus::html::MouseEvent;
 use dioxus::html::MouseEvent;
 use dioxus::prelude::*;
 use dioxus::prelude::*;
-use dioxus_desktop::wry::application::dpi::LogicalSize;
+use dioxus_desktop::tao::dpi::LogicalSize;
 use dioxus_desktop::{Config, WindowBuilder};
 use dioxus_desktop::{Config, WindowBuilder};
 
 
 fn main() {
 fn main() {

+ 1 - 0
examples/query_segments_demo/Cargo.toml

@@ -2,6 +2,7 @@
 name = "query_segments_demo"
 name = "query_segments_demo"
 version = "0.1.0"
 version = "0.1.0"
 edition = "2021"
 edition = "2021"
+publish = false
 
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 

+ 22 - 8
examples/query_segments_demo/src/main.rs

@@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
 #[derive(Routable, Clone)]
 #[derive(Routable, Clone)]
 #[rustfmt::skip]
 #[rustfmt::skip]
 enum Route {
 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 {
     BlogPost {
         // You must include query segments in child variants
         // 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)]
 #[derive(Debug, Clone, PartialEq)]
-struct BlogQuerySegments {
+struct ManualBlogQuerySegments {
     name: String,
     name: String,
     surname: String,
     surname: String,
 }
 }
 
 
 /// The display impl needs to display the query in a way that can be parsed:
 /// 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 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "name={}&surname={}", self.name, self.surname)
         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 <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 ManualBlogQuerySegments {
     fn from_query(query: &str) -> Self {
     fn from_query(query: &str) -> Self {
         let mut name = None;
         let mut name = None;
         let mut surname = None;
         let mut surname = None;
@@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
 }
 }
 
 
 #[component]
 #[component]
-fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element {
+fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
     render! {
     render! {
         div{"This is your blogpost with a query segment:"}
         div{"This is your blogpost with a query segment:"}
         div{format!("{:?}", query_params)}
         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]
 #[component]
 fn App(cx: Scope) -> Element {
 fn App(cx: Scope) -> Element {
     render! { Router::<Route>{} }
     render! { Router::<Route>{} }

+ 5 - 0
examples/rsx_usage.rs

@@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
     let formatting = "formatting!";
     let formatting = "formatting!";
     let formatting_tuple = ("a", "b");
     let formatting_tuple = ("a", "b");
     let lazy_fmt = format_args!("lazily formatted text");
     let lazy_fmt = format_args!("lazily formatted text");
+    let asd = 123;
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
             // Elements
             // Elements
@@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
                 // pass simple rust expressions in
                 // pass simple rust expressions in
                 class: lazy_fmt,
                 class: lazy_fmt,
                 id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
                 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 {
                 div {
                     class: {
                     class: {
                         const WORD: &str = "expressions";
                         const WORD: &str = "expressions";

+ 1 - 1
examples/shared_state.rs

@@ -64,7 +64,7 @@ fn DataEditor(cx: Scope, id: usize) -> Element {
 fn DataView(cx: Scope, id: usize) -> Element {
 fn DataView(cx: Scope, id: usize) -> Element {
     let cool_data = use_shared_state::<CoolData>(cx).unwrap();
     let cool_data = use_shared_state::<CoolData>(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 cool_data = cool_data.read();
     let my_data = &cool_data.view(id).unwrap();
     let my_data = &cool_data.view(id).unwrap();

+ 23 - 1
examples/signals.rs

@@ -6,11 +6,17 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
+    let running = dioxus_signals::use_signal(cx, || true);
     let mut count = dioxus_signals::use_signal(cx, || 0);
     let mut count = dioxus_signals::use_signal(cx, || 0);
+    let saved_values = dioxus_signals::use_signal(cx, || vec![0.to_string()]);
 
 
+    // Signals can be used in async functions without an explicit clone since they're 'static and Copy
+    // Signals are backed by a runtime that is designed to deeply integrate with Dioxus apps
     use_future!(cx, || async move {
     use_future!(cx, || async move {
         loop {
         loop {
-            count += 1;
+            if running.value() {
+                count += 1;
+            }
             tokio::time::sleep(Duration::from_millis(400)).await;
             tokio::time::sleep(Duration::from_millis(400)).await;
         }
         }
     });
     });
@@ -19,9 +25,25 @@ fn app(cx: Scope) -> Element {
         h1 { "High-Five counter: {count}" }
         h1 { "High-Five counter: {count}" }
         button { onclick: move |_| count += 1, "Up high!" }
         button { onclick: move |_| count += 1, "Up high!" }
         button { onclick: move |_| count -= 1, "Down low!" }
         button { onclick: move |_| count -= 1, "Down low!" }
+        button { onclick: move |_| running.toggle(), "Toggle counter" }
+        button { onclick: move |_| saved_values.push(count.value().to_string()), "Save this value" }
+        button { onclick: move |_| saved_values.write().clear(), "Clear saved values" }
 
 
+        // We can do boolean operations on the current signal value
         if count.value() > 5 {
         if count.value() > 5 {
             rsx!{ h2 { "High five!" } }
             rsx!{ h2 { "High five!" } }
         }
         }
+
+        // We can cleanly map signals with iterators
+        for value in saved_values.read().iter() {
+            h3 { "Saved value: {value}" }
+        }
+
+        // We can also use the signal value as a slice
+        if let [ref first, .., ref last] = saved_values.read().as_slice() {
+            rsx! { li { "First and last: {first}, {last}" } }
+        } else {
+            rsx! { "No saved values" }
+        }
     })
     })
 }
 }

+ 36 - 0
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<Attribute<'a>>,
+    extra_data: &'a str,
+    extra_data2: &'a str,
+}

+ 33 - 0
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<dyn Stream<Item = i32>>> {
+    Box::pin(
+        stream::once(future::ready(0)).chain(stream::iter(1..).then(|second| async move {
+            tokio::time::sleep(Duration::from_secs(1)).await;
+            second
+        })),
+    )
+}

+ 1 - 1
examples/tailwind/Cargo.toml

@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
 dioxus-desktop = { path = "../../packages/desktop" }
 dioxus-desktop = { path = "../../packages/desktop" }
 
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-dioxus-web = { path = "../../packages/web" }
+dioxus-web = { path = "../../packages/web" }

+ 1 - 1
examples/tailwind/Dioxus.toml

@@ -30,7 +30,7 @@ watch_path = ["src", "public"]
 [web.resource]
 [web.resource]
 
 
 # CSS style file
 # CSS style file
-style = ["/tailwind.css"]
+style = []
 
 
 # Javascript code file
 # Javascript code file
 script = []
 script = []

+ 1 - 1
examples/tailwind/README.md

@@ -7,7 +7,7 @@ This example shows how an app might be styled with TailwindCSS.
 1. Install the Dioxus CLI:
 1. Install the Dioxus CLI:
 
 
 ```bash
 ```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
 2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm

+ 1 - 0
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}}

+ 8 - 6
examples/tailwind/src/main.rs

@@ -2,21 +2,23 @@
 
 
 use dioxus::prelude::*;
 use dioxus::prelude::*;
 
 
+const _STYLE: &str = mg!(file("./public/tailwind.css"));
+
 fn main() {
 fn main() {
     #[cfg(not(target_arch = "wasm32"))]
     #[cfg(not(target_arch = "wasm32"))]
-    dioxus_desktop::launch_cfg(
-        app,
-        dioxus_desktop::Config::new()
-            .with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
-    );
+    dioxus_desktop::launch(app);
     #[cfg(target_arch = "wasm32")]
     #[cfg(target_arch = "wasm32")]
     dioxus_web::launch(app);
     dioxus_web::launch(app);
 }
 }
 
 
 pub fn app(cx: Scope) -> Element {
 pub fn app(cx: Scope) -> Element {
+    let grey_background = true;
     cx.render(rsx!(
     cx.render(rsx!(
         div {
         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",
                 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",
                     a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
                         StacksIcon {}
                         StacksIcon {}

+ 1 - 1
examples/textarea.rs

@@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
             rows: "10",
             rows: "10",
             cols: "80",
             cols: "80",
             value: "{model}",
             value: "{model}",
-            oninput: move |e| model.set(e.value.clone()),
+            oninput: move |e| model.set(e.value().clone()),
         }
         }
     })
     })
 }
 }

+ 133 - 79
examples/todomvc.rs

@@ -7,6 +7,8 @@ fn main() {
     dioxus_desktop::launch(app);
     dioxus_desktop::launch(app);
 }
 }
 
 
+const _STYLE: &str = mg!(file("./examples/assets/todomvc.css"));
+
 #[derive(PartialEq, Eq, Clone, Copy)]
 #[derive(PartialEq, Eq, Clone, Copy)]
 pub enum FilterState {
 pub enum FilterState {
     All,
     All,
@@ -24,8 +26,6 @@ pub struct TodoItem {
 pub fn app(cx: Scope<()>) -> Element {
 pub fn app(cx: Scope<()>) -> Element {
     let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
     let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
     let filter = use_state(cx, || FilterState::All);
     let filter = use_state(cx, || FilterState::All);
-    let draft = use_state(cx, || "".to_string());
-    let todo_id = use_state(cx, || 0);
 
 
     // Filter the todos based on the filter state
     // Filter the todos based on the filter state
     let mut filtered_todos = todos
     let mut filtered_todos = todos
@@ -47,45 +47,10 @@ pub fn app(cx: Scope<()>) -> Element {
 
 
     let show_clear_completed = todos.values().any(|todo| todo.checked);
     let show_clear_completed = todos.values().any(|todo| todo.checked);
 
 
-    let selected = |state| {
-        if *filter == state {
-            "selected"
-        } else {
-            "false"
-        }
-    };
-
     cx.render(rsx! {
     cx.render(rsx! {
         section { class: "todoapp",
         section { class: "todoapp",
-            style { include_str!("./assets/todomvc.css") }
-            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() {
                 if !todos.is_empty() {
                     rsx! {
                     rsx! {
                         input {
                         input {
@@ -111,43 +76,58 @@ pub fn app(cx: Scope<()>) -> Element {
                     }))
                     }))
                 }
                 }
                 (!todos.is_empty()).then(|| rsx!(
                 (!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<im_rc::HashMap<u32, TodoItem>>,
+}
+
+pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
+    let draft = use_state(cx, || "".to_string());
+    let todo_id = use_state(cx, || 0);
+
+    cx.render(rsx! {
+        header { class: "header",
+            h1 { "todos" }
+            input {
+                class: "new-todo",
+                placeholder: "What needs to be done?",
+                value: "{draft}",
+                autofocus: "true",
+                oninput: move |evt| {
+                    draft.set(evt.value().clone());
+                },
+                onkeydown: move |evt| {
+                    if evt.key() == Key::Enter && !draft.is_empty() {
+                        cx.props
+                            .todos
+                            .make_mut()
+                            .insert(
+                                **todo_id,
+                                TodoItem {
+                                    id: **todo_id,
+                                    checked: false,
+                                    contents: draft.to_string(),
+                                },
+                            );
+                        *todo_id.make_mut() += 1;
+                        draft.set("".to_string());
+                    }
+                }
+            }
         }
         }
     })
     })
 }
 }
@@ -167,8 +147,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
     let editing = if **is_editing { "editing" } else { "" };
     let editing = if **is_editing { "editing" } else { "" };
 
 
     cx.render(rsx!{
     cx.render(rsx!{
-        li {
-            class: "{completed} {editing}",
+        li { class: "{completed} {editing}",
             div { class: "view",
             div { class: "view",
                 input {
                 input {
                     class: "toggle",
                     class: "toggle",
@@ -176,26 +155,28 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                     id: "cbg-{todo.id}",
                     id: "cbg-{todo.id}",
                     checked: "{todo.checked}",
                     checked: "{todo.checked}",
                     oninput: move |evt| {
                     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 {
                 label {
                     r#for: "cbg-{todo.id}",
                     r#for: "cbg-{todo.id}",
-                    ondblclick: move |_| is_editing.set(true),
+                    ondoubleclick: move |_| is_editing.set(true),
                     prevent_default: "onclick",
                     prevent_default: "onclick",
                     "{todo.contents}"
                     "{todo.contents}"
                 }
                 }
                 button {
                 button {
                     class: "destroy",
                     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!{
             is_editing.then(|| rsx!{
                 input {
                 input {
                     class: "edit",
                     class: "edit",
                     value: "{todo.contents}",
                     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",
                     autofocus: "true",
                     onfocusout: move |_| is_editing.set(false),
                     onfocusout: move |_| is_editing.set(false),
                     onkeydown: move |evt| {
                     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<im_rc::HashMap<u32, TodoItem>>,
+    active_todo_count: usize,
+    active_todo_text: &'a str,
+    show_clear_completed: bool,
+    filter: &'a UseState<FilterState>,
+}
+
+pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
+    let active_todo_count = cx.props.active_todo_count;
+    let active_todo_text = cx.props.active_todo_text;
+
+    let selected = |state| {
+        if *cx.props.filter == state {
+            "selected"
+        } else {
+            "false"
+        }
+    };
+
+    cx.render(rsx! {
+        footer { class: "footer",
+            span { class: "todo-count",
+                strong { "{active_todo_count} " }
+                span { "{active_todo_text} left" }
+            }
+            ul { class: "filters",
+                for (state , state_text , url) in [
+    (FilterState::All, "All", "#/"),
+    (FilterState::Active, "Active", "#/active"),
+    (FilterState::Completed, "Completed", "#/completed"),
+] {
+                    li {
+                        a {
+                            href: url,
+                            class: selected(state),
+                            onclick: move |_| cx.props.filter.set(state),
+                            prevent_default: "onclick",
+                            state_text
+                        }
+                    }
+                }
+            }
+            if cx.props.show_clear_completed {
+                cx.render(rsx! {
+                    button {
+                        class: "clear-completed",
+                        onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked),
+                        "Clear completed"
+                    }
+                })
+            }
+        }
+    })
+}
+
+pub fn PageFooter(cx: Scope) -> Element {
+    cx.render(rsx! {
+        footer { class: "info",
+            p { "Double-click to edit a todo" }
+            p {
+                "Created by "
+                a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
+            }
+            p {
+                "Part of "
+                a { href: "http://todomvc.com", "TodoMVC" }
+            }
+        }
+    })
+}

+ 188 - 0
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<Response<Vec<u8>>, Box<dyn std::error::Error>> {
+    // 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 <start-end>, example: 0-499
+                .map(|r| (r.start, r.start + r.length - 1))
+                .collect::<Vec<_>>()
+        } 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::<Vec<_>>();
+
+            let boundary = format!("{:x}", rand::random::<u64>());
+            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)
+}

+ 1 - 1
examples/window_focus.rs

@@ -1,7 +1,7 @@
 use dioxus::prelude::*;
 use dioxus::prelude::*;
+use dioxus_desktop::tao::event::Event as WryEvent;
 use dioxus_desktop::tao::event::WindowEvent;
 use dioxus_desktop::tao::event::WindowEvent;
 use dioxus_desktop::use_wry_event_handler;
 use dioxus_desktop::use_wry_event_handler;
-use dioxus_desktop::wry::application::event::Event as WryEvent;
 use dioxus_desktop::{Config, WindowCloseBehaviour};
 use dioxus_desktop::{Config, WindowCloseBehaviour};
 
 
 fn main() {
 fn main() {

+ 2 - 4
examples/window_zoom.rs

@@ -1,12 +1,10 @@
 use dioxus::prelude::*;
 use dioxus::prelude::*;
-use dioxus_desktop::use_window;
 
 
 fn main() {
 fn main() {
     dioxus_desktop::launch(app);
     dioxus_desktop::launch(app);
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
-    let window = use_window(cx);
     let level = use_state(cx, || 1.0);
     let level = use_state(cx, || 1.0);
 
 
     cx.render(rsx! {
     cx.render(rsx! {
@@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element {
             r#type: "number",
             r#type: "number",
             value: "{level}",
             value: "{level}",
             oninput: |e| {
             oninput: |e| {
-                if let Ok(new_zoom) = e.value.parse::<f64>() {
+                if let Ok(new_zoom) = e.value().parse::<f64>() {
                     level.set(new_zoom);
                     level.set(new_zoom);
-                    window.webview.zoom(new_zoom);
+                    dioxus_desktop::window().webview.zoom(new_zoom);
                 }
                 }
             }
             }
         }
         }

+ 1 - 1
examples/xss_safety.rs

@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
             input {
             input {
                 value: "{contents}",
                 value: "{contents}",
                 r#type: "text",
                 r#type: "text",
-                oninput: move |e| contents.set(e.value.clone()),
+                oninput: move |e| contents.set(e.value()),
             }
             }
         }
         }
     })
     })

+ 247 - 0
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
+}

+ 63 - 0
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";
+            '';
+          };
+        };
+    };
+}

+ 6 - 5
packages/autofmt/src/buffer.rs

@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
 
 
 use dioxus_rsx::IfmtInput;
 use dioxus_rsx::IfmtInput;
 
 
-use crate::write_ifmt;
+use crate::{indent::IndentOptions, write_ifmt};
 
 
 /// The output buffer that tracks indent and string
 /// The output buffer that tracks indent and string
 #[derive(Debug, Default)]
 #[derive(Debug, Default)]
 pub struct Buffer {
 pub struct Buffer {
     pub buf: String,
     pub buf: String,
-    pub indent: usize,
+    pub indent_level: usize,
+    pub indent: IndentOptions,
 }
 }
 
 
 impl Buffer {
 impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
     }
     }
 
 
     pub fn tab(&mut self) -> Result {
     pub fn tab(&mut self) -> Result {
-        self.write_tabs(self.indent)
+        self.write_tabs(self.indent_level)
     }
     }
 
 
     pub fn indented_tab(&mut self) -> Result {
     pub fn indented_tab(&mut self) -> Result {
-        self.write_tabs(self.indent + 1)
+        self.write_tabs(self.indent_level + 1)
     }
     }
 
 
     pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
     pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
         for _ in 0..num {
         for _ in 0..num {
-            write!(self.buf, "    ")?
+            write!(self.buf, "{}", self.indent.indent_str())?
         }
         }
         Ok(())
         Ok(())
     }
     }

+ 63 - 33
packages/autofmt/src/element.rs

@@ -49,6 +49,7 @@ impl Writer<'_> {
             attributes,
             attributes,
             children,
             children,
             brace,
             brace,
+            ..
         } = el;
         } = el;
 
 
         /*
         /*
@@ -66,7 +67,7 @@ impl Writer<'_> {
 
 
         // check if we have a lot of attributes
         // check if we have a lot of attributes
         let attr_len = self.is_short_attrs(attributes);
         let attr_len = self.is_short_attrs(attributes);
-        let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
+        let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
         let children_len = self.is_short_children(children);
         let children_len = self.is_short_children(children);
         let is_small_children = children_len.is_some();
         let is_small_children = children_len.is_some();
 
 
@@ -86,7 +87,7 @@ impl Writer<'_> {
 
 
         // if we have few children and few attributes, make it a one-liner
         // if we have few children and few attributes, make it a one-liner
         if is_short_attr_list && is_small_children {
         if is_short_attr_list && is_small_children {
-            if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
+            if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
                 opt_level = ShortOptimization::Oneliner;
                 opt_level = ShortOptimization::Oneliner;
             } else {
             } else {
                 opt_level = ShortOptimization::PropsOnTop;
                 opt_level = ShortOptimization::PropsOnTop;
@@ -165,7 +166,7 @@ impl Writer<'_> {
 
 
     fn write_attributes(
     fn write_attributes(
         &mut self,
         &mut self,
-        attributes: &[ElementAttrNamed],
+        attributes: &[AttributeType],
         key: &Option<IfmtInput>,
         key: &Option<IfmtInput>,
         sameline: bool,
         sameline: bool,
     ) -> Result {
     ) -> Result {
@@ -185,11 +186,11 @@ impl Writer<'_> {
         }
         }
 
 
         while let Some(attr) = attr_iter.next() {
         while let Some(attr) = attr_iter.next() {
-            self.out.indent += 1;
+            self.out.indent_level += 1;
             if !sameline {
             if !sameline {
-                self.write_comments(attr.attr.start())?;
+                self.write_comments(attr.start())?;
             }
             }
-            self.out.indent -= 1;
+            self.out.indent_level -= 1;
 
 
             if !sameline {
             if !sameline {
                 self.out.indented_tabbed_line()?;
                 self.out.indented_tabbed_line()?;
@@ -209,12 +210,34 @@ impl Writer<'_> {
         Ok(())
         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 out = prettyplease::unparse_expr(value);
                 let mut lines = out.split('\n').peekable();
                 let mut lines = out.split('\n').peekable();
                 let first = lines.next().unwrap();
                 let first = lines.next().unwrap();
@@ -222,9 +245,9 @@ impl Writer<'_> {
                 // a one-liner for whatever reason
                 // a one-liner for whatever reason
                 // Does not need a new line
                 // Does not need a new line
                 if lines.peek().is_none() {
                 if lines.peek().is_none() {
-                    write!(self.out, "{name}: {first}")?;
+                    write!(self.out, "{first}")?;
                 } else {
                 } else {
-                    writeln!(self.out, "{name}: {first}")?;
+                    writeln!(self.out, "{first}")?;
 
 
                     while let Some(line) = lines.next() {
                     while let Some(line) = lines.next() {
                         self.out.indented_tab()?;
                         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 out = self.retrieve_formatted_expr(tokens).to_string();
 
 
                 let mut lines = out.split('\n').peekable();
                 let mut lines = out.split('\n').peekable();
@@ -261,9 +269,9 @@ impl Writer<'_> {
                 // a one-liner for whatever reason
                 // a one-liner for whatever reason
                 // Does not need a new line
                 // Does not need a new line
                 if lines.peek().is_none() {
                 if lines.peek().is_none() {
-                    write!(self.out, "{name}: {first}")?;
+                    write!(self.out, "{first}")?;
                 } else {
                 } else {
-                    writeln!(self.out, "{name}: {first}")?;
+                    writeln!(self.out, "{first}")?;
 
 
                     while let Some(line) = lines.next() {
                     while let Some(line) = lines.next() {
                         self.out.indented_tab()?;
                         self.out.indented_tab()?;
@@ -281,6 +289,28 @@ impl Writer<'_> {
         Ok(())
         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.
     // make sure the comments are actually relevant to this element.
     // test by making sure this element is the primary element on this line
     // test by making sure this element is the primary element on this line
     pub fn current_span_is_primary(&self, location: Span) -> bool {
     pub fn current_span_is_primary(&self, location: Span) -> bool {
@@ -398,14 +428,14 @@ impl Writer<'_> {
         for idx in start.line..end.line {
         for idx in start.line..end.line {
             let line = &self.src[idx];
             let line = &self.src[idx];
             if line.trim().starts_with("//") {
             if line.trim().starts_with("//") {
-                for _ in 0..self.out.indent + 1 {
+                for _ in 0..self.out.indent_level + 1 {
                     write!(self.out, "    ")?
                     write!(self.out, "    ")?
                 }
                 }
                 writeln!(self.out, "{}", line.trim()).unwrap();
                 writeln!(self.out, "{}", line.trim()).unwrap();
             }
             }
         }
         }
 
 
-        for _ in 0..self.out.indent {
+        for _ in 0..self.out.indent_level {
             write!(self.out, "    ")?
             write!(self.out, "    ")?
         }
         }
 
 

+ 3 - 3
packages/autofmt/src/expr.rs

@@ -29,7 +29,7 @@ impl Writer<'_> {
         let first_line = &self.src[start.line - 1];
         let first_line = &self.src[start.line - 1];
         write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
         write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
 
 
-        let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
+        let prev_block_indent_level = self.out.indent.count_indents(first_line);
 
 
         for (id, line) in self.src[start.line..end.line].iter().enumerate() {
         for (id, line) in self.src[start.line..end.line].iter().enumerate() {
             writeln!(self.out)?;
             writeln!(self.out)?;
@@ -43,9 +43,9 @@ impl Writer<'_> {
             };
             };
 
 
             // trim the leading whitespace
             // trim the leading whitespace
-            let previous_indent = crate::leading_whitespaces(line) / 4;
+            let previous_indent = self.out.indent.count_indents(line);
             let offset = previous_indent.saturating_sub(prev_block_indent_level);
             let offset = previous_indent.saturating_sub(prev_block_indent_level);
-            let required_indent = self.out.indent + offset;
+            let required_indent = self.out.indent_level + offset;
             self.out.write_tabs(required_indent)?;
             self.out.write_tabs(required_indent)?;
 
 
             let line = line.trim_start();
             let line = line.trim_start();

+ 108 - 0
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
+        );
+    }
+}

+ 16 - 15
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 std::fmt::{Display, Write};
 
 
 use crate::writer::*;
 use crate::writer::*;
@@ -12,8 +16,11 @@ mod collect_macros;
 mod component;
 mod component;
 mod element;
 mod element;
 mod expr;
 mod expr;
+mod indent;
 mod writer;
 mod writer;
 
 
+pub use indent::{IndentOptions, IndentType};
+
 /// A modification to the original file to be applied by an IDE
 /// A modification to the original file to be applied by an IDE
 ///
 ///
 /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
 /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@@ -43,7 +50,7 @@ pub struct FormattedBlock {
 /// back to the file precisely.
 /// back to the file precisely.
 ///
 ///
 /// Nested blocks of RSX will be handled automatically
 /// Nested blocks of RSX will be handled automatically
-pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
+pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
     let mut formatted_blocks = Vec::new();
     let mut formatted_blocks = Vec::new();
 
 
     let parsed = syn::parse_file(contents).unwrap();
     let parsed = syn::parse_file(contents).unwrap();
@@ -57,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
     }
     }
 
 
     let mut writer = Writer::new(contents);
     let mut writer = Writer::new(contents);
+    writer.out.indent = indent;
 
 
     // Don't parse nested macros
     // Don't parse nested macros
     let mut end_span = LineColumn { column: 0, line: 0 };
     let mut end_span = LineColumn { column: 0, line: 0 };
@@ -72,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
 
 
         let rsx_start = macro_path.span().start();
         let rsx_start = macro_path.span().start();
 
 
-        writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
+        writer.out.indent_level = writer
+            .out
+            .indent
+            .count_indents(writer.src[rsx_start.line - 1]);
 
 
         write_body(&mut writer, &body);
         write_body(&mut writer, &body);
 
 
@@ -155,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
     buf.consume()
     buf.consume()
 }
 }
 
 
-pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
+pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
     let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
     let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
 
 
     let mut buf = Writer::new(block);
     let mut buf = Writer::new(block);
 
 
-    buf.out.indent = indent_level;
+    buf.out.indent = indent;
+    buf.out.indent_level = indent_level;
 
 
     write_body(&mut buf, &body);
     write_body(&mut buf, &body);
 
 
@@ -226,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
     let display = DisplayIfmt(input);
     let display = DisplayIfmt(input);
     write!(writable, "{}", display)
     write!(writable, "{}", display)
 }
 }
-
-pub fn leading_whitespaces(input: &str) -> usize {
-    input
-        .chars()
-        .map_while(|c| match c {
-            ' ' => Some(1),
-            '\t' => Some(4),
-            _ => None,
-        })
-        .sum()
-}

+ 56 - 38
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 proc_macro2::{LineColumn, Span};
 use quote::ToTokens;
 use quote::ToTokens;
 use std::{
 use std::{
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
 
 
     // Push out the indent level and write each component, line by line
     // Push out the indent level and write each component, line by line
     pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
     pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
-        self.out.indent += 1;
+        self.out.indent_level += 1;
 
 
         self.write_body_no_indent(children)?;
         self.write_body_no_indent(children)?;
 
 
-        self.out.indent -= 1;
+        self.out.indent_level -= 1;
         Ok(())
         Ok(())
     }
     }
 
 
@@ -132,12 +132,45 @@ impl<'a> Writer<'a> {
         Ok(())
         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;
         let mut total = 0;
 
 
         for attr in attributes {
         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()) {
                     match (line.trim().starts_with("//"), line.is_empty()) {
                         (true, _) => return 100000,
                         (true, _) => return 100000,
                         (_, true) => continue 'line,
                         (_, 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
         total
@@ -218,7 +236,7 @@ impl<'a> Writer<'a> {
     }
     }
 }
 }
 
 
-trait SpanLength {
+pub(crate) trait SpanLength {
     fn line_length(&self) -> usize;
     fn line_length(&self) -> usize;
 }
 }
 impl SpanLength for Span {
 impl SpanLength for Span {

+ 1 - 1
packages/autofmt/tests/samples.rs

@@ -12,7 +12,7 @@ macro_rules! twoway {
             #[test]
             #[test]
             fn $name() {
             fn $name() {
                 let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
                 let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
-                let formatted = dioxus_autofmt::fmt_file(src);
+                let formatted = dioxus_autofmt::fmt_file(src, Default::default());
                 let out = dioxus_autofmt::apply_formats(src, formatted);
                 let out = dioxus_autofmt::apply_formats(src, formatted);
                 // normalize line endings
                 // normalize line endings
                 let out = out.replace("\r", "");
                 let out = out.replace("\r", "");

+ 1 - 1
packages/autofmt/tests/samples/simple.rsx

@@ -33,7 +33,7 @@ rsx! {
     }
     }
 
 
     // No children, minimal props
     // 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
     // One level compression
     div {
     div {

+ 10 - 5
packages/autofmt/tests/wrong.rs

@@ -1,10 +1,12 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
+
 macro_rules! twoway {
 macro_rules! twoway {
-    ($val:literal => $name:ident) => {
+    ($val:literal => $name:ident ($indent:expr)) => {
         #[test]
         #[test]
         fn $name() {
         fn $name() {
             let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
             let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
             let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
             let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
-            let formatted = dioxus_autofmt::fmt_file(src_wrong);
+            let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
             let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
             let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
 
 
             // normalize line endings
             // normalize line endings
@@ -16,8 +18,11 @@ macro_rules! twoway {
     };
     };
 }
 }
 
 
-twoway!("comments" => comments);
+twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
 
-twoway!("multi" => multi);
+twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
 
-twoway!("multiexpr" => multiexpr);
+twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));

+ 0 - 0
packages/autofmt/tests/wrong/comments.rsx → packages/autofmt/tests/wrong/comments-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/comments.wrong.rsx → packages/autofmt/tests/wrong/comments-4sp.wrong.rsx


+ 7 - 0
packages/autofmt/tests/wrong/comments-tab.rsx

@@ -0,0 +1,7 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd",
+		"hello world"
+	}
+}

+ 5 - 0
packages/autofmt/tests/wrong/comments-tab.wrong.rsx

@@ -0,0 +1,5 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd", "hello world" }
+}

+ 0 - 0
packages/autofmt/tests/wrong/multi.rsx → packages/autofmt/tests/wrong/multi-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multi.wrong.rsx → packages/autofmt/tests/wrong/multi-4sp.wrong.rsx


+ 3 - 0
packages/autofmt/tests/wrong/multi-tab.rsx

@@ -0,0 +1,3 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! { div { "hello world" } })
+}

+ 5 - 0
packages/autofmt/tests/wrong/multi-tab.wrong.rsx

@@ -0,0 +1,5 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! {
+		div {"hello world" }
+	})
+}

+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.wrong.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx


+ 8 - 0
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
+		}
+	})
+}

+ 5 - 0
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 }
+	})
+}

+ 0 - 3
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
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 
 [dependencies]
 [dependencies]
-dioxus-rsx = { workspace = true }
 proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
 proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
 quote = "1.0"
 quote = "1.0"
 syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
 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"] }
 owo-colors = { version = "3.5.0", features = ["supports-colors"] }
-prettyplease = { workspace = true }
 
 
 [dev-dependencies]
 [dev-dependencies]
 indoc = "2.0.3"
 indoc = "2.0.3"

+ 2 - 2
packages/check/README.md

@@ -6,7 +6,7 @@
 [![Discord chat][discord-badge]][discord-url]
 [![Discord chat][discord-badge]][discord-url]
 
 
 [crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
 [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-badge]: https://img.shields.io/badge/license-MIT-blue.svg
 [mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
 [mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
 [actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
 [actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
@@ -16,7 +16,7 @@
 
 
 [Website](https://dioxuslabs.com) |
 [Website](https://dioxuslabs.com) |
 [Guides](https://dioxuslabs.com/learn/0.4/) |
 [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)
 [Chat](https://discord.gg/XgGxMSkvUM)
 
 
 ## Overview
 ## Overview

+ 4 - 0
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 check;
 mod issues;
 mod issues;
 mod metadata;
 mod metadata;

+ 0 - 31
packages/cli/.github/workflows/build.yml

@@ -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 }}

+ 0 - 34
packages/cli/.github/workflows/docs.yml

@@ -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

Some files were not shown because too many files changed in this diff