浏览代码

Merge branch 'master' into jk/faster-cache-ci

Jonathan Kelley 1 年之前
父节点
当前提交
5a3d4f9bff
共有 100 个文件被更改,包括 2437 次插入539 次删除
  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. 11 2
      .github/workflows/main.yml
  6. 4 4
      .github/workflows/miri.yml
  7. 3 2
      .github/workflows/playwright.yml
  8. 1 0
      .gitignore
  9. 13 5
      Cargo.toml
  10. 72 4
      Makefile.toml
  11. 2 1
      README.md
  12. 0 1
      examples/README.md
  13. 4 7
      examples/all_events.rs
  14. 0 18
      examples/button.rs
  15. 3 2
      examples/calculator.rs
  16. 1 1
      examples/clock.rs
  17. 4 18
      examples/compose.rs
  18. 1 1
      examples/counter.rs
  19. 27 63
      examples/crm.rs
  20. 1 1
      examples/custom_assets.rs
  21. 27 0
      examples/dynamic_asset.rs
  22. 11 16
      examples/error_handle.rs
  23. 9 14
      examples/eval.rs
  24. 2 1
      examples/file_explorer.rs
  25. 2 2
      examples/file_upload.rs
  26. 2 2
      examples/form.rs
  27. 17 20
      examples/login_form.rs
  28. 1 1
      examples/mobile_demo/Cargo.toml
  29. 1 3
      examples/multiwindow.rs
  30. 3 0
      examples/openid_connect_demo/.gitignore
  31. 25 0
      examples/openid_connect_demo/Cargo.toml
  32. 47 0
      examples/openid_connect_demo/Dioxus.toml
  33. 13 0
      examples/openid_connect_demo/README.md
  34. 2 0
      examples/openid_connect_demo/src/constants.rs
  35. 20 0
      examples/openid_connect_demo/src/errors.rs
  36. 60 0
      examples/openid_connect_demo/src/main.rs
  37. 1 0
      examples/openid_connect_demo/src/model/mod.rs
  38. 7 0
      examples/openid_connect_demo/src/model/user.rs
  39. 125 0
      examples/openid_connect_demo/src/oidc.rs
  40. 20 0
      examples/openid_connect_demo/src/props/client.rs
  41. 1 0
      examples/openid_connect_demo/src/props/mod.rs
  42. 17 0
      examples/openid_connect_demo/src/router.rs
  43. 38 0
      examples/openid_connect_demo/src/storage.rs
  44. 250 0
      examples/openid_connect_demo/src/views/header.rs
  45. 5 0
      examples/openid_connect_demo/src/views/home.rs
  46. 86 0
      examples/openid_connect_demo/src/views/login.rs
  47. 4 0
      examples/openid_connect_demo/src/views/mod.rs
  48. 7 0
      examples/openid_connect_demo/src/views/not_found.rs
  49. 12 0
      examples/optional_props.rs
  50. 2 4
      examples/overlay.rs
  51. 1 1
      examples/pattern_model.rs
  52. 1 0
      examples/query_segments_demo/Cargo.toml
  53. 21 7
      examples/query_segments_demo/src/main.rs
  54. 5 0
      examples/rsx_usage.rs
  55. 1 1
      examples/shared_state.rs
  56. 36 0
      examples/spread.rs
  57. 1 1
      examples/tailwind/Cargo.toml
  58. 1 1
      examples/tailwind/Dioxus.toml
  59. 1 1
      examples/tailwind/README.md
  60. 1 0
      examples/tailwind/dist/tailwind3531548035813279582.css
  61. 8 6
      examples/tailwind/src/main.rs
  62. 1 1
      examples/textarea.rs
  63. 133 79
      examples/todomvc.rs
  64. 188 0
      examples/video_stream.rs
  65. 1 1
      examples/window_focus.rs
  66. 2 4
      examples/window_zoom.rs
  67. 1 1
      examples/xss_safety.rs
  68. 247 0
      flake.lock
  69. 63 0
      flake.nix
  70. 6 5
      packages/autofmt/src/buffer.rs
  71. 63 33
      packages/autofmt/src/element.rs
  72. 3 3
      packages/autofmt/src/expr.rs
  73. 108 0
      packages/autofmt/src/indent.rs
  74. 12 15
      packages/autofmt/src/lib.rs
  75. 56 38
      packages/autofmt/src/writer.rs
  76. 1 1
      packages/autofmt/tests/samples.rs
  77. 1 1
      packages/autofmt/tests/samples/simple.rsx
  78. 10 5
      packages/autofmt/tests/wrong.rs
  79. 0 0
      packages/autofmt/tests/wrong/comments-4sp.rsx
  80. 0 0
      packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
  81. 7 0
      packages/autofmt/tests/wrong/comments-tab.rsx
  82. 5 0
      packages/autofmt/tests/wrong/comments-tab.wrong.rsx
  83. 0 0
      packages/autofmt/tests/wrong/multi-4sp.rsx
  84. 0 0
      packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
  85. 3 0
      packages/autofmt/tests/wrong/multi-tab.rsx
  86. 5 0
      packages/autofmt/tests/wrong/multi-tab.wrong.rsx
  87. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.rsx
  88. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
  89. 8 0
      packages/autofmt/tests/wrong/multiexpr-tab.rsx
  90. 5 0
      packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
  91. 7 4
      packages/cli/Cargo.toml
  92. 5 5
      packages/cli/README.md
  93. 2 2
      packages/cli/src/assets/autoreload.js
  94. 1 1
      packages/cli/src/assets/dioxus.toml
  95. 126 27
      packages/cli/src/builder.rs
  96. 147 76
      packages/cli/src/cli/autoformat.rs
  97. 54 12
      packages/cli/src/cli/build.rs
  98. 27 3
      packages/cli/src/cli/bundle.rs
  99. 83 9
      packages/cli/src/cli/cfg.rs
  100. 6 0
      packages/cli/src/cli/clean.rs

+ 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 }}
           targets: ${{ matrix.platform.target }}
 
+      - uses: ilammy/setup-nasm@v1
+
       # Setup the Github Actions Cache for the CLI package
       - name: Setup cache
         uses: Swatinem/rust-cache@v2

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

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

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

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

+ 11 - 2
.github/workflows/main.yml

@@ -44,6 +44,7 @@ jobs:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - uses: mozilla-actions/sccache-action@v0.0.3
+      - uses: ilammy/setup-nasm@v1
       - run: sudo apt-get update
       - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
       - uses: actions/checkout@v4
@@ -61,8 +62,9 @@ jobs:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - uses: mozilla-actions/sccache-action@v0.0.3
+      - uses: ilammy/setup-nasm@v1
       - run: sudo apt-get update
-      - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
+      - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
       - uses: davidB/rust-cargo-make@v1
       - uses: browser-actions/setup-firefox@latest
       - uses: jetli/wasm-pack-action@v0.4.0
@@ -81,6 +83,7 @@ jobs:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - uses: mozilla-actions/sccache-action@v0.0.3
+      - uses: ilammy/setup-nasm@v1
       - run: rustup component add rustfmt
       - uses: actions/checkout@v4
       - run: cargo fmt --all -- --check
@@ -97,6 +100,7 @@ jobs:
     steps:
       - uses: dtolnay/rust-toolchain@stable
       - uses: mozilla-actions/sccache-action@v0.0.3
+      - uses: ilammy/setup-nasm@v1
       - run: sudo apt-get update
       - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev
       - run: rustup component add clippy
@@ -149,7 +153,7 @@ jobs:
 
     steps:
       - uses: actions/checkout@v4
-
+      - uses: ilammy/setup-nasm@v1
       - name: install stable
         uses: dtolnay/rust-toolchain@master
         with:
@@ -165,6 +169,11 @@ jobs:
           workspaces: core -> ../target
           save-if: ${{ matrix.features.key == 'all' }}
 
+      - name: Install rustfmt
+        run: rustup component add rustfmt
+
+      - uses: actions/checkout@v4
+
       - name: test
         run: |
           ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}

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

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

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

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

+ 1 - 0
.gitignore

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

+ 13 - 5
Cargo.toml

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

+ 72 - 4
Makefile.toml

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

+ 2 - 1
README.md

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

+ 0 - 1
examples/README.md

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

+ 4 - 7
examples/all_events.rs

@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
     };
 
     cx.render(rsx! (
-        div {
-            style: "{CONTAINER_STYLE}",
+        div { style: "{CONTAINER_STYLE}",
             div {
                 style: "{RECT_STYLE}",
                 // focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
 
                 onmousemove: move |event| log_event(Event::MouseMove(event)),
                 onclick: move |event| log_event(Event::MouseClick(event)),
-                ondblclick: move |event| log_event(Event::MouseDoubleClick(event)),
+                ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
                 onmousedown: move |event| log_event(Event::MouseDown(event)),
                 onmouseup: move |event| log_event(Event::MouseUp(event)),
 
@@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
 
                 "Hover, click, type or scroll to see the info down below"
             }
-            div {
-                events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
-            },
-        },
+            div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
+        }
     ))
 }

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

+ 1 - 1
examples/clock.rs

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

+ 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.
 
 use dioxus::prelude::*;
-use dioxus_desktop::use_window;
 use futures_util::StreamExt;
 
 fn main() {
@@ -9,7 +8,6 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
-    let window = use_window(cx);
     let emails_sent = use_ref(cx, Vec::new);
 
     let tx = use_coroutine(cx, |mut rx: UnboundedReceiver<String>| {
@@ -27,14 +25,8 @@ fn app(cx: Scope) -> Element {
 
             button {
                 onclick: move |_| {
-                    let dom = VirtualDom::new_with_props(compose, ComposeProps {
-                        app_tx: tx.clone()
-                    });
-
-                    // this returns a weak reference to the other window
-                    // Be careful not to keep a strong reference to the other window or it will never be dropped
-                    // and the window will never close.
-                    window.new_window(dom, Default::default());
+                    let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
+                    dioxus_desktop::window().new_window(dom, Default::default());
                 },
                 "Click to compose a new email"
             }
@@ -57,7 +49,6 @@ struct ComposeProps {
 
 fn compose(cx: Scope<ComposeProps>) -> Element {
     let user_input = use_state(cx, String::new);
-    let window = use_window(cx);
 
     cx.render(rsx! {
         div {
@@ -66,17 +57,12 @@ fn compose(cx: Scope<ComposeProps>) -> Element {
             button {
                 onclick: move |_| {
                     cx.props.app_tx.send(user_input.get().clone());
-                    window.close();
+                    dioxus_desktop::window().close();
                 },
                 "Click to send"
             }
 
-            input {
-                oninput: move |e| {
-                    user_input.set(e.value.clone());
-                },
-                value: "{user_input}"
-            }
+            input { oninput: move |e| user_input.set(e.value()), value: "{user_input}" }
         }
     })
 }

+ 1 - 1
examples/counter.rs

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

+ 27 - 63
examples/crm.rs

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

+ 1 - 1
examples/custom_assets.rs

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

+ 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() {
     dioxus_desktop::launch(App);
@@ -6,30 +6,25 @@ fn main() {
 
 #[component]
 fn App(cx: Scope) -> Element {
-    let val = use_state(cx, || "0.0001");
-
-    let num = match val.parse::<f32>() {
-        Err(_) => return cx.render(rsx!("Parsing failed")),
-        Ok(num) => num,
-    };
-
     cx.render(rsx! {
-        h1 { "The parsed value is {num}" }
-        button {
-            onclick: move |_| val.set("invalid"),
-            "Set an invalid number"
+        ErrorBoundary {
+            handle_error: |error: CapturedError| rsx! {"Found error {error}"},
+            DemoC {
+                x: 1
+            }
         }
-        (0..5).map(|i| rsx! {
-            DemoC { x: i }
-        })
     })
 }
 
 #[component]
 fn DemoC(cx: Scope, x: i32) -> Element {
+    let result = Err("Error");
+
+    result.throw()?;
+
     cx.render(rsx! {
         h1 {
-            "asdasdasdasd {x}"
+            "{x}"
         }
     })
 }

+ 9 - 14
examples/eval.rs

@@ -5,26 +5,21 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
-    let eval_provider = use_eval(cx);
-
-    let future = use_future(cx, (), |_| {
-        to_owned![eval_provider];
-        async move {
-            let eval = eval_provider(
-                r#"
+    let future = use_future(cx, (), |_| async move {
+        let eval = eval(
+            r#"
                 dioxus.send("Hi from JS!");
                 let msg = await dioxus.recv();
                 console.log(msg);
                 return "hello world";
             "#,
-            )
-            .unwrap();
+        )
+        .unwrap();
 
-            eval.send("Hi from Rust!".into()).unwrap();
-            let res = eval.recv().await.unwrap();
-            println!("{:?}", eval.await);
-            res
-        }
+        eval.send("Hi from Rust!".into()).unwrap();
+        let res = eval.recv().await.unwrap();
+        println!("{:?}", eval.await);
+        res
     });
 
     match future.value() {

+ 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 {
     let files = use_ref(cx, Files::new);
 
     cx.render(rsx! {
         div {
             link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", }
-            style { include_str!("./assets/fileexplorer.css") }
             header {
                 i { class: "material-icons icon-menu", "menu" }
                 h1 { "Files: ", files.read().current() }

+ 2 - 2
examples/file_upload.rs

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

+ 2 - 2
examples/form.rs

@@ -14,8 +14,8 @@ fn app(cx: Scope) -> Element {
         div {
             h1 { "Form" }
             form {
-                onsubmit: move |ev| println!("Submitted {:?}", ev.values),
-                oninput: move |ev| println!("Input {:?}", ev.values),
+                onsubmit: move |ev| println!("Submitted {:?}", ev.values()),
+                oninput: move |ev| println!("Input {:?}", ev.values()),
                 input { r#type: "text", name: "username" }
                 input { r#type: "text", name: "full-name" }
                 input { r#type: "password", name: "password" }

+ 17 - 20
examples/login_form.rs

@@ -8,33 +8,30 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
-    let onsubmit = move |evt: FormEvent| {
-        cx.spawn(async move {
-            let resp = reqwest::Client::new()
-                .post("http://localhost:8080/login")
-                .form(&[
-                    ("username", &evt.values["username"]),
-                    ("password", &evt.values["password"]),
-                ])
-                .send()
-                .await;
+    let onsubmit = move |evt: FormEvent| async move {
+        let resp = reqwest::Client::new()
+            .post("http://localhost:8080/login")
+            .form(&[
+                ("username", &evt.values()["username"]),
+                ("password", &evt.values()["password"]),
+            ])
+            .send()
+            .await;
 
-            match resp {
-                // Parse data from here, such as storing a response token
-                Ok(_data) => println!("Login successful!"),
+        match resp {
+            // Parse data from here, such as storing a response token
+            Ok(_data) => println!("Login successful!"),
 
-                //Handle any errors from the fetch here
-                Err(_err) => {
-                    println!("Login failed - you need a login server running on localhost:8080.")
-                }
+            //Handle any errors from the fetch here
+            Err(_err) => {
+                println!("Login failed - you need a login server running on localhost:8080.")
             }
-        });
+        }
     };
 
     cx.render(rsx! {
         h1 { "Login" }
-        form {
-            onsubmit: onsubmit,
+        form { onsubmit: onsubmit,
             input { r#type: "text", id: "username", name: "username" }
             label { "Username" }
             br {}

+ 1 - 1
examples/mobile_demo/Cargo.toml

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

+ 1 - 3
examples/multiwindow.rs

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

+ 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(),
             c: "asd".to_string(),
             d: Some("asd".to_string()),
+            e: Some("asd".to_string()),
+        }
+        Button {
+            a: "asd".to_string(),
+            b: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
             e: "asd".to_string(),
         }
+        Button {
+            a: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
+        }
     })
 }
 

+ 2 - 4
examples/overlay.rs

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

+ 1 - 1
examples/pattern_model.rs

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

+ 1 - 0
examples/query_segments_demo/Cargo.toml

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

+ 21 - 7
examples/query_segments_demo/src/main.rs

@@ -14,29 +14,35 @@ use dioxus_router::prelude::*;
 #[derive(Routable, Clone)]
 #[rustfmt::skip]
 enum Route {
-    // segments that start with ?: are query segments
-    #[route("/blog?:query_params")]
+    // segments that start with ?:.. are query segments that capture the entire query
+    #[route("/blog?:..query_params")]
     BlogPost {
         // You must include query segments in child variants
-        query_params: BlogQuerySegments,
+        query_params: ManualBlogQuerySegments,
+    },
+    // segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
+    #[route("/autoblog?:name&:surname")]
+    AutomaticBlogPost {
+        name: String,
+        surname: String,
     },
 }
 
 #[derive(Debug, Clone, PartialEq)]
-struct BlogQuerySegments {
+struct ManualBlogQuerySegments {
     name: String,
     surname: String,
 }
 
 /// The display impl needs to display the query in a way that can be parsed:
-impl Display for BlogQuerySegments {
+impl Display for ManualBlogQuerySegments {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "name={}&surname={}", self.name, self.surname)
     }
 }
 
 /// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
-impl FromQuery for BlogQuerySegments {
+impl FromQuery for ManualBlogQuerySegments {
     fn from_query(query: &str) -> Self {
         let mut name = None;
         let mut surname = None;
@@ -57,13 +63,21 @@ impl FromQuery for BlogQuerySegments {
 }
 
 #[component]
-fn BlogPost(cx: Scope, query_params: BlogQuerySegments) -> Element {
+fn BlogPost(cx: Scope, query_params: ManualBlogQuerySegments) -> Element {
     render! {
         div{"This is your blogpost with a query segment:"}
         div{format!("{:?}", query_params)}
     }
 }
 
+#[component]
+fn AutomaticBlogPost(cx: Scope, name: String, surname: String) -> Element {
+    render! {
+        div{"This is your blogpost with a query segment:"}
+        div{format!("name={}&surname={}", name, surname)}
+    }
+}
+
 #[component]
 fn App(cx: Scope) -> Element {
     render! { Router::<Route>{} }

+ 5 - 0
examples/rsx_usage.rs

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

+ 1 - 1
examples/shared_state.rs

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

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

+ 1 - 1
examples/tailwind/Cargo.toml

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

+ 1 - 1
examples/tailwind/Dioxus.toml

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

+ 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:
 
 ```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

+ 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::*;
 
+const _STYLE: &str = mg!(file("./public/tailwind.css"));
+
 fn main() {
     #[cfg(not(target_arch = "wasm32"))]
-    dioxus_desktop::launch_cfg(
-        app,
-        dioxus_desktop::Config::new()
-            .with_custom_head(r#"<link rel="stylesheet" href="public/tailwind.css">"#.to_string()),
-    );
+    dioxus_desktop::launch(app);
     #[cfg(target_arch = "wasm32")]
     dioxus_web::launch(app);
 }
 
 pub fn app(cx: Scope) -> Element {
+    let grey_background = true;
     cx.render(rsx!(
         div {
-            header { class: "text-gray-400 bg-gray-900 body-font",
+            header {
+                class: "text-gray-400 body-font",
+                // you can use optional attributes to optionally apply a tailwind class
+                class: if grey_background { "bg-gray-900" },
                 div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
                     a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
                         StacksIcon {}

+ 1 - 1
examples/textarea.rs

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

+ 133 - 79
examples/todomvc.rs

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

+ 2 - 4
examples/window_zoom.rs

@@ -1,12 +1,10 @@
 use dioxus::prelude::*;
-use dioxus_desktop::use_window;
 
 fn main() {
     dioxus_desktop::launch(app);
 }
 
 fn app(cx: Scope) -> Element {
-    let window = use_window(cx);
     let level = use_state(cx, || 1.0);
 
     cx.render(rsx! {
@@ -14,9 +12,9 @@ fn app(cx: Scope) -> Element {
             r#type: "number",
             value: "{level}",
             oninput: |e| {
-                if let Ok(new_zoom) = e.value.parse::<f64>() {
+                if let Ok(new_zoom) = e.value().parse::<f64>() {
                     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 {
                 value: "{contents}",
                 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 crate::write_ifmt;
+use crate::{indent::IndentOptions, write_ifmt};
 
 /// The output buffer that tracks indent and string
 #[derive(Debug, Default)]
 pub struct Buffer {
     pub buf: String,
-    pub indent: usize,
+    pub indent_level: usize,
+    pub indent: IndentOptions,
 }
 
 impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
     }
 
     pub fn tab(&mut self) -> Result {
-        self.write_tabs(self.indent)
+        self.write_tabs(self.indent_level)
     }
 
     pub fn indented_tab(&mut self) -> Result {
-        self.write_tabs(self.indent + 1)
+        self.write_tabs(self.indent_level + 1)
     }
 
     pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
         for _ in 0..num {
-            write!(self.buf, "    ")?
+            write!(self.buf, "{}", self.indent.indent_str())?
         }
         Ok(())
     }

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

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

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

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

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

+ 12 - 15
packages/autofmt/src/lib.rs

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

+ 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 quote::ToTokens;
 use std::{
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
 
     // Push out the indent level and write each component, line by line
     pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
-        self.out.indent += 1;
+        self.out.indent_level += 1;
 
         self.write_body_no_indent(children)?;
 
-        self.out.indent -= 1;
+        self.out.indent_level -= 1;
         Ok(())
     }
 
@@ -132,12 +132,45 @@ impl<'a> Writer<'a> {
         Ok(())
     }
 
-    pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
+    pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize {
+        match value {
+            ElementAttrValue::AttrOptionalExpr { condition, value } => {
+                let condition_len = self.retrieve_formatted_expr(condition).len();
+                let value_len = self.attr_value_len(value);
+
+                condition_len + value_len + 6
+            }
+            ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
+            ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
+            ElementAttrValue::EventTokens(tokens) => {
+                let location = Location::new(tokens.span().start());
+
+                let len = if let std::collections::hash_map::Entry::Vacant(e) =
+                    self.cached_formats.entry(location)
+                {
+                    let formatted = prettyplease::unparse_expr(tokens);
+                    let len = if formatted.contains('\n') {
+                        10000
+                    } else {
+                        formatted.len()
+                    };
+                    e.insert(formatted);
+                    len
+                } else {
+                    self.cached_formats[&location].len()
+                };
+
+                len
+            }
+        }
+    }
+
+    pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize {
         let mut total = 0;
 
         for attr in attributes {
-            if self.current_span_is_primary(attr.attr.start()) {
-                'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() {
+            if self.current_span_is_primary(attr.start()) {
+                'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
                     match (line.trim().starts_with("//"), line.is_empty()) {
                         (true, _) => return 100000,
                         (_, true) => continue 'line,
@@ -146,40 +179,25 @@ impl<'a> Writer<'a> {
                 }
             }
 
-            total += match &attr.attr {
-                ElementAttr::AttrText { value, name } => {
-                    ifmt_to_string(value).len() + name.span().line_length() + 6
-                }
-                ElementAttr::AttrExpression { name, value } => {
-                    value.span().line_length() + name.span().line_length() + 6
-                }
-                ElementAttr::CustomAttrText { value, name } => {
-                    ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
-                }
-                ElementAttr::CustomAttrExpression { name, value } => {
-                    name.to_token_stream().to_string().len() + value.span().line_length() + 6
-                }
-                ElementAttr::EventTokens { tokens, name } => {
-                    let location = Location::new(tokens.span().start());
-
-                    let len = if let std::collections::hash_map::Entry::Vacant(e) =
-                        self.cached_formats.entry(location)
-                    {
-                        let formatted = prettyplease::unparse_expr(tokens);
-                        let len = if formatted.contains('\n') {
-                            10000
-                        } else {
-                            formatted.len()
-                        };
-                        e.insert(formatted);
-                        len
-                    } else {
-                        self.cached_formats[&location].len()
+            match attr {
+                AttributeType::Named(attr) => {
+                    let name_len = match &attr.attr.name {
+                        dioxus_rsx::ElementAttrName::BuiltIn(name) => {
+                            let name = name.to_string();
+                            name.len()
+                        }
+                        dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
                     };
-
-                    len + name.span().line_length() + 6
+                    total += name_len;
+                    total += self.attr_value_len(&attr.attr.value);
+                }
+                AttributeType::Spread(expr) => {
+                    let expr_len = self.retrieve_formatted_expr(expr).len();
+                    total += expr_len + 3;
                 }
             };
+
+            total += 6;
         }
 
         total
@@ -218,7 +236,7 @@ impl<'a> Writer<'a> {
     }
 }
 
-trait SpanLength {
+pub(crate) trait SpanLength {
     fn line_length(&self) -> usize;
 }
 impl SpanLength for Span {

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

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

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

@@ -33,7 +33,7 @@ rsx! {
     }
 
     // No children, minimal props
-    img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
+    img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" }
 
     // One level compression
     div {

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

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

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

+ 7 - 4
packages/cli/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "dioxus-cli"
-version = "0.4.1"
+version = "0.4.3"
 authors = ["Jonathan Kelley"]
 edition = "2021"
 description = "CLI tool for developing, testing, and publishing Dioxus apps"
@@ -30,7 +30,7 @@ cargo_metadata = "0.15.0"
 tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
 atty = "0.2.14"
 chrono = "0.4.19"
-anyhow = "1.0.53"
+anyhow = "1"
 hyper = "0.14.17"
 hyper-rustls = "0.23.2"
 indicatif = "0.17.5"
@@ -72,8 +72,10 @@ cargo-generate = "0.18"
 toml_edit = "0.19.11"
 
 # bundling
-tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] }
-tauri-utils = "=1.4.*"
+tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
+tauri-utils = "=1.5.*"
+
+manganis-cli-support= { git = "https://github.com/DioxusLabs/collect-assets", features = ["webp", "html"] }
 
 dioxus-autofmt = { workspace = true }
 dioxus-check = { workspace = true }
@@ -83,6 +85,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
 dioxus-core = { workspace = true, features = ["serialize"] }
 dioxus-hot-reload = { workspace = true }
 interprocess-docfix = { version = "1.2.2" }
+gitignore = "1.0.8"
 
 [features]
 default = []

+ 5 - 5
packages/cli/README.md

@@ -10,8 +10,8 @@ It handles building, bundling, development and publishing to simplify developmen
 
 ### Install the stable version (recommended)
 
-```
-cargo install dioxus-cli --locked
+```shell
+cargo install dioxus-cli
 ```
 
 ### Install the latest development build through git
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
 However, this is not fully tested.
 That means you're probably going to have more bugs despite having the latest bug fixes.
 
-```
+```shell
 cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
 ```
 
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
 
 ### Install from local folder
 
-```
+```shell
 cargo install --path . --debug
 ```
 
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
 
 Alternatively, you can specify the template path:
 
-```
+```shell
 dx create hello --template gh:dioxuslabs/dioxus-template
 ```
 

+ 2 - 2
packages/cli/src/assets/autoreload.js

@@ -1,5 +1,5 @@
 // Dioxus-CLI
-// https://github.com/DioxusLabs/cli
+// https://github.com/DioxusLabs/dioxus/tree/master/packages/cli
 
 (function () {
   var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -22,4 +22,4 @@
       }
   };
   ws.onclose = reload_upon_connect;
-})()
+})()

+ 1 - 1
packages/cli/src/assets/dioxus.toml

@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
 
 index_on_404 = true
 
-watch_path = ["src"]
+watch_path = ["src", "examples"]
 
 # include `assets` in web platform
 [web.resource]

+ 126 - 27
packages/cli/src/builder.rs

@@ -2,31 +2,35 @@ use crate::{
     config::{CrateConfig, ExecutableType},
     error::{Error, Result},
     tools::Tool,
-    DioxusConfig,
 };
 use cargo_metadata::{diagnostic::Diagnostic, Message};
 use indicatif::{ProgressBar, ProgressStyle};
+use lazy_static::lazy_static;
+use manganis_cli_support::AssetManifestExt;
 use serde::Serialize;
 use std::{
     fs::{copy, create_dir_all, File},
-    io::Read,
+    io::{Read, Write},
     panic,
     path::PathBuf,
     time::Duration,
 };
 use wasm_bindgen_cli_support::Bindgen;
 
+lazy_static! {
+    static ref PROGRESS_BARS: indicatif::MultiProgress = indicatif::MultiProgress::new();
+}
+
 #[derive(Serialize, Debug, Clone)]
 pub struct BuildResult {
     pub warnings: Vec<Diagnostic>,
     pub elapsed_time: u128,
 }
 
-#[allow(unused)]
-pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
+pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildResult> {
     // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
     // [2] Generate the appropriate build folders
-    // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
+    // [3] Wasm-bindgen the .wasm file, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
     // [4] Wasm-opt the .wasm file with whatever optimizations need to be done
     // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
     // [6] Link up the html page to the wasm module
@@ -41,6 +45,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
         ..
     } = config;
 
+    let _gaurd = WebAssetConfigDropGuard::new();
+
     // start to build the assets
     let ignore_files = build_assets(config)?;
 
@@ -48,14 +54,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
 
     // [1] Build the .wasm module
     log::info!("🚅 Running build command...");
-    let cmd = subprocess::Exec::cmd("cargo");
-    let cmd = cmd
+
+    let wasm_check_command = std::process::Command::new("rustup")
+        .args(["show"])
+        .output()?;
+    let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
+    if !wasm_check_output.contains("wasm32-unknown-unknown") {
+        log::info!("wasm32-unknown-unknown target not detected, installing..");
+        let _ = std::process::Command::new("rustup")
+            .args(["target", "add", "wasm32-unknown-unknown"])
+            .output()?;
+    }
+
+    let cmd = subprocess::Exec::cmd("cargo")
+        .env("CARGO_TARGET_DIR", target_dir)
         .cwd(crate_dir)
         .arg("build")
         .arg("--target")
         .arg("wasm32-unknown-unknown")
-        .arg("--message-format=json")
-        .arg("--quiet");
+        .arg("--message-format=json");
 
     let cmd = if config.release {
         cmd.arg("--release")
@@ -65,7 +82,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
     let cmd = if config.verbose {
         cmd.arg("--verbose")
     } else {
-        cmd
+        cmd.arg("--quiet")
     };
 
     let cmd = if config.custom_profile.is_some() {
@@ -82,6 +99,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
         cmd
     };
 
+    let cmd = cmd.args(&config.cargo_args);
+
     let cmd = match executable {
         ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -239,19 +258,28 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
         }
     }
 
+    if !skip_assets {
+        process_assets(config)?;
+    }
+
     Ok(BuildResult {
         warnings: warning_messages,
         elapsed_time: t_start.elapsed().as_millis(),
     })
 }
 
-pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
+pub fn build_desktop(
+    config: &CrateConfig,
+    _is_serve: bool,
+    skip_assets: bool,
+) -> Result<BuildResult> {
     log::info!("🚅 Running build [Desktop] command...");
 
     let t_start = std::time::Instant::now();
     let ignore_files = build_assets(config)?;
 
     let mut cmd = subprocess::Exec::cmd("cargo")
+        .env("CARGO_TARGET_DIR", &config.target_dir)
         .cwd(&config.crate_dir)
         .arg("build")
         .arg("--message-format=json");
@@ -261,6 +289,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     }
     if config.verbose {
         cmd = cmd.arg("--verbose");
+    } else {
+        cmd = cmd.arg("--quiet");
     }
 
     if config.custom_profile.is_some() {
@@ -273,6 +303,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
         cmd = cmd.arg("--features").arg(features_str);
     }
 
+    if let Some(target) = &config.target {
+        cmd = cmd.arg("--target").arg(target);
+    }
+
+    let target_platform = config.target.as_deref().unwrap_or("");
+
+    cmd = cmd.args(&config.cargo_args);
+
     let cmd = match &config.executable {
         crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -290,12 +328,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     let mut res_path = match &config.executable {
         crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
             file_name = name.clone();
-            config.target_dir.join(release_type).join(name)
+            config
+                .target_dir
+                .join(target_platform)
+                .join(release_type)
+                .join(name)
         }
         crate::ExecutableType::Example(name) => {
             file_name = name.clone();
             config
                 .target_dir
+                .join(target_platform)
                 .join(release_type)
                 .join("examples")
                 .join(name)
@@ -347,6 +390,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
         }
     }
 
+    if !skip_assets {
+        // Collect assets
+        process_assets(config)?;
+        // Create the __assets_head.html file for bundling
+        create_assets_head(config)?;
+    }
+
     log::info!(
         "🚩 Build completed: [./{}]",
         config
@@ -366,11 +416,19 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     })
 }
 
+fn create_assets_head(config: &CrateConfig) -> Result<()> {
+    let manifest = config.asset_manifest();
+    let mut file = File::create(config.out_dir.join("__assets_head.html"))?;
+    file.write_all(manifest.head().as_bytes())?;
+    Ok(())
+}
+
 fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
     let mut warning_messages: Vec<Diagnostic> = vec![];
 
-    let pb = ProgressBar::new_spinner();
+    let mut pb = ProgressBar::new_spinner();
     pb.enable_steady_tick(Duration::from_millis(200));
+    pb = PROGRESS_BARS.add(pb);
     pb.set_style(
         ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
             .unwrap()
@@ -378,14 +436,6 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
     );
     pb.set_message("💼 Waiting to start build the project...");
 
-    struct StopSpinOnDrop(ProgressBar);
-
-    impl Drop for StopSpinOnDrop {
-        fn drop(&mut self) {
-            self.0.finish_and_clear();
-        }
-    }
-
     let stdout = cmd.detached().stream_stdout()?;
     let reader = std::io::BufReader::new(stdout);
 
@@ -421,13 +471,17 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
                     std::process::exit(1);
                 }
             }
-            _ => (), // Unknown message
+            _ => {
+                // Unknown message
+            }
         }
     }
     Ok(warning_messages)
 }
 
-pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
+pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: bool) -> String {
+    let _gaurd = WebAssetConfigDropGuard::new();
+
     let crate_root = crate::cargo::crate_root().unwrap();
     let custom_html_file = crate_root.join("index.html");
     let mut html = if custom_html_file.is_file() {
@@ -442,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
         String::from(include_str!("./assets/index.html"))
     };
 
-    let resources = config.web.resource.clone();
+    let resources = config.dioxus_config.web.resource.clone();
 
     let mut style_list = resources.style.unwrap_or_default();
     let mut script_list = resources.script.unwrap_or_default();
@@ -462,13 +516,18 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
         ))
     }
     if config
+        .dioxus_config
         .application
         .tools
         .clone()
         .unwrap_or_default()
         .contains_key("tailwindcss")
     {
-        style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
+        style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
+    }
+    if !skip_assets {
+        let manifest = config.asset_manifest();
+        style_str.push_str(&manifest.head());
     }
 
     replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
@@ -490,11 +549,11 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
         );
     }
 
-    let base_path = match &config.web.app.base_path {
+    let base_path = match &config.dioxus_config.web.app.base_path {
         Some(path) => path,
         None => ".",
     };
-    let app_name = &config.application.name;
+    let app_name = &config.dioxus_config.application.name;
     // Check if a script already exists
     if html.contains("{app_name}") && html.contains("{base_path}") {
         html = html.replace("{app_name}", app_name);
@@ -519,6 +578,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
     }
 
     let title = config
+        .dioxus_config
         .web
         .app
         .title
@@ -688,3 +748,42 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
 
     Ok(result)
 }
+
+/// Process any assets collected from the binary
+fn process_assets(config: &CrateConfig) -> anyhow::Result<()> {
+    let manifest = config.asset_manifest();
+
+    let static_asset_output_dir = PathBuf::from(
+        config
+            .dioxus_config
+            .web
+            .app
+            .base_path
+            .clone()
+            .unwrap_or_default(),
+    );
+    let static_asset_output_dir = config.out_dir.join(static_asset_output_dir);
+
+    manifest.copy_static_assets_to(static_asset_output_dir)?;
+
+    Ok(())
+}
+
+pub(crate) struct WebAssetConfigDropGuard;
+
+impl WebAssetConfigDropGuard {
+    pub fn new() -> Self {
+        // Set up the collect asset config
+        manganis_cli_support::Config::default()
+            .with_assets_serve_location("/")
+            .save();
+        Self {}
+    }
+}
+
+impl Drop for WebAssetConfigDropGuard {
+    fn drop(&mut self) {
+        // Reset the config
+        manganis_cli_support::Config::default().save();
+    }
+}

+ 147 - 76
packages/cli/src/cli/autoformat.rs

@@ -1,3 +1,4 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
 use futures::{stream::FuturesUnordered, StreamExt};
 use std::{fs, path::Path, process::exit};
 
@@ -26,16 +27,19 @@ pub struct Autoformat {
 impl Autoformat {
     // Todo: autoformat the entire crate
     pub async fn autoformat(self) -> Result<()> {
+        let Autoformat { check, raw, file } = self;
+
         // Default to formatting the project
-        if self.raw.is_none() && self.file.is_none() {
-            if let Err(e) = autoformat_project(self.check).await {
+        if raw.is_none() && file.is_none() {
+            if let Err(e) = autoformat_project(check).await {
                 eprintln!("error formatting project: {}", e);
                 exit(1);
             }
         }
 
-        if let Some(raw) = self.raw {
-            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
+        if let Some(raw) = raw {
+            let indent = indentation_for(".")?;
+            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
                 println!("{}", inner);
             } else {
                 // exit process with error
@@ -45,43 +49,90 @@ impl Autoformat {
         }
 
         // Format single file
-        if let Some(file) = self.file {
-            let file_content = if file == "-" {
-                let mut contents = String::new();
-                std::io::stdin().read_to_string(&mut contents)?;
-                Ok(contents)
-            } else {
-                fs::read_to_string(&file)
-            };
-
-            match file_content {
-                Ok(s) => {
-                    let edits = dioxus_autofmt::fmt_file(&s);
-                    let out = dioxus_autofmt::apply_formats(&s, edits);
-                    if file == "-" {
-                        print!("{}", out);
-                    } else {
-                        match fs::write(&file, out) {
-                            Ok(_) => {
-                                println!("formatted {}", file);
-                            }
-                            Err(e) => {
-                                eprintln!("failed to write formatted content to file: {}", e);
-                            }
-                        }
-                    }
-                }
-                Err(e) => {
-                    eprintln!("failed to open file: {}", e);
-                    exit(1);
-                }
-            }
+        if let Some(file) = file {
+            refactor_file(file)?;
         }
 
         Ok(())
     }
 }
 
+fn refactor_file(file: String) -> Result<(), Error> {
+    let indent = indentation_for(".")?;
+    let file_content = if file == "-" {
+        let mut contents = String::new();
+        std::io::stdin().read_to_string(&mut contents)?;
+        Ok(contents)
+    } else {
+        fs::read_to_string(&file)
+    };
+    let Ok(s) = file_content else {
+        eprintln!("failed to open file: {}", file_content.unwrap_err());
+        exit(1);
+    };
+    let edits = dioxus_autofmt::fmt_file(&s, indent);
+    let out = dioxus_autofmt::apply_formats(&s, edits);
+
+    if file == "-" {
+        print!("{}", out);
+    } else if let Err(e) = fs::write(&file, out) {
+        eprintln!("failed to write formatted content to file: {e}",);
+    } else {
+        println!("formatted {}", file);
+    }
+
+    Ok(())
+}
+
+fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
+    let mut files = vec![];
+
+    let gitignore_path = config.crate_dir.join(".gitignore");
+    if gitignore_path.is_file() {
+        let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
+        if let Ok(git_files) = gitigno.included_files() {
+            let git_files = git_files
+                .into_iter()
+                .filter(|f| f.ends_with(".rs") && !is_target_dir(f));
+            files.extend(git_files)
+        };
+    } else {
+        collect_rs_files(&config.crate_dir, &mut files);
+    }
+
+    files
+}
+
+fn is_target_dir(file: &Path) -> bool {
+    let stripped = if let Ok(cwd) = std::env::current_dir() {
+        file.strip_prefix(cwd).unwrap_or(file)
+    } else {
+        file
+    };
+    if let Some(first) = stripped.components().next() {
+        first.as_os_str() == "target"
+    } else {
+        false
+    }
+}
+
+async fn format_file(
+    path: impl AsRef<Path>,
+    indent: IndentOptions,
+) -> Result<usize, tokio::io::Error> {
+    let contents = tokio::fs::read_to_string(&path).await?;
+
+    let edits = dioxus_autofmt::fmt_file(&contents, indent);
+    let len = edits.len();
+
+    if !edits.is_empty() {
+        let out = dioxus_autofmt::apply_formats(&contents, edits);
+        tokio::fs::write(path, out).await?;
+    }
+
+    Ok(len)
+}
+
 /// Read every .rs file accessible when considering the .gitignore and try to format it
 ///
 /// Runs using Tokio for multithreading, so it should be really really fast
@@ -90,42 +141,27 @@ impl Autoformat {
 async fn autoformat_project(check: bool) -> Result<()> {
     let crate_config = crate::CrateConfig::new(None)?;
 
-    let mut files_to_format = vec![];
-    collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
+    let files_to_format = get_project_files(&crate_config);
+
+    if files_to_format.is_empty() {
+        return Ok(());
+    }
+
+    let indent = indentation_for(&files_to_format[0])?;
 
     let counts = files_to_format
         .into_iter()
-        .filter(|file| {
-            if file.components().any(|f| f.as_os_str() == "target") {
-                return false;
-            }
-
-            true
-        })
         .map(|path| async {
-            let _path = path.clone();
-            let res = tokio::spawn(async move {
-                let contents = tokio::fs::read_to_string(&path).await?;
-
-                let edits = dioxus_autofmt::fmt_file(&contents);
-                let len = edits.len();
-
-                if !edits.is_empty() {
-                    let out = dioxus_autofmt::apply_formats(&contents, edits);
-                    tokio::fs::write(&path, out).await?;
-                }
-
-                Ok(len) as Result<usize, tokio::io::Error>
-            })
-            .await;
+            let path_clone = path.clone();
+            let res = tokio::spawn(format_file(path, indent.clone())).await;
 
             match res {
                 Err(err) => {
-                    eprintln!("error formatting file: {}\n{err}", _path.display());
+                    eprintln!("error formatting file: {}\n{err}", path_clone.display());
                     None
                 }
                 Ok(Err(err)) => {
-                    eprintln!("error formatting file: {}\n{err}", _path.display());
+                    eprintln!("error formatting file: {}\n{err}", path_clone.display());
                     None
                 }
                 Ok(Ok(res)) => Some(res),
@@ -135,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
         .collect::<Vec<_>>()
         .await;
 
-    let files_formatted: usize = counts
-        .into_iter()
-        .map(|f| match f {
-            Some(res) => res,
-            _ => 0,
-        })
-        .sum();
+    let files_formatted: usize = counts.into_iter().flatten().sum();
 
     if files_formatted > 0 && check {
         eprintln!("{} files needed formatting", files_formatted);
@@ -151,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
     Ok(())
 }
 
-fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
-    let Ok(folder) = folder.read_dir() else {
+fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
+    let out = std::process::Command::new("cargo")
+        .args(["fmt", "--", "--print-config", "current"])
+        .arg(file_or_dir.as_ref())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::inherit())
+        .output()?;
+    if !out.status.success() {
+        return Err(Error::CargoError("cargo fmt failed".into()));
+    }
+
+    let config = String::from_utf8_lossy(&out.stdout);
+
+    let hard_tabs = config
+        .lines()
+        .find(|line| line.starts_with("hard_tabs "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim() == "true")
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
+        })?;
+    let tab_spaces = config
+        .lines()
+        .find(|line| line.starts_with("tab_spaces "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim().parse::<usize>())
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
+        })?
+        .map_err(|_| {
+            Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
+        })?;
+
+    Ok(IndentOptions::new(
+        if hard_tabs {
+            IndentType::Tabs
+        } else {
+            IndentType::Spaces
+        },
+        tab_spaces,
+    ))
+}
+
+fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
+    if is_target_dir(folder.as_ref()) {
+        return;
+    }
+    let Ok(folder) = folder.as_ref().read_dir() else {
         return;
     };
-
     // load the gitignore
-
     for entry in folder {
         let Ok(entry) = entry else {
             continue;
         };
-
         let path = entry.path();
-
         if path.is_dir() {
             collect_rs_files(&path, files);
         }
-
         if let Some(ext) = path.extension() {
-            if ext == "rs" {
+            if ext == "rs" && !is_target_dir(&path) {
                 files.push(path);
             }
         }

+ 54 - 12
packages/cli/src/cli/build.rs

@@ -1,6 +1,8 @@
-use crate::cfg::Platform;
 #[cfg(feature = "plugin")]
 use crate::plugin::PluginManager;
+use crate::server::fullstack::FullstackServerEnvGuard;
+use crate::server::fullstack::FullstackWebEnvGuard;
+use crate::{cfg::Platform, WebAssetConfigDropGuard};
 
 use super::*;
 
@@ -13,23 +15,26 @@ pub struct Build {
 }
 
 impl Build {
-    pub fn build(self, bin: Option<PathBuf>) -> Result<()> {
+    pub fn build(self, bin: Option<PathBuf>, target_dir: Option<&std::path::Path>) -> Result<()> {
         let mut crate_config = crate::CrateConfig::new(bin)?;
+        if let Some(target_dir) = target_dir {
+            crate_config.target_dir = target_dir.to_path_buf();
+        }
 
         // change the release state.
         crate_config.with_release(self.build.release);
         crate_config.with_verbose(self.build.verbose);
 
         if self.build.example.is_some() {
-            crate_config.as_example(self.build.example.unwrap());
+            crate_config.as_example(self.build.example.clone().unwrap());
         }
 
         if self.build.profile.is_some() {
-            crate_config.set_profile(self.build.profile.unwrap());
+            crate_config.set_profile(self.build.profile.clone().unwrap());
         }
 
         if self.build.features.is_some() {
-            crate_config.set_features(self.build.features.unwrap());
+            crate_config.set_features(self.build.features.clone().unwrap());
         }
 
         let platform = self
@@ -37,19 +42,56 @@ impl Build {
             .platform
             .unwrap_or(crate_config.dioxus_config.application.default_platform);
 
-        #[cfg(feature = "plugin")]
-        let _ = PluginManager::on_build_start(&crate_config, &platform);
+        if let Some(target) = self.build.target.clone() {
+            crate_config.set_target(target);
+        }
+
+        crate_config.set_cargo_args(self.build.cargo_args.clone());
+
+        // #[cfg(feature = "plugin")]
+        // let _ = PluginManager::on_build_start(&crate_config, &platform);
 
         match platform {
             Platform::Web => {
-                crate::builder::build(&crate_config, true)?;
+                crate::builder::build(&crate_config, false, self.build.skip_assets)?;
             }
             Platform::Desktop => {
-                crate::builder::build_desktop(&crate_config, false)?;
+                crate::builder::build_desktop(&crate_config, false, self.build.skip_assets)?;
+            }
+            Platform::Fullstack => {
+                // Fullstack mode must be built with web configs on the desktop (server) binary as well as the web binary
+                let _config = WebAssetConfigDropGuard::new();
+                {
+                    let mut web_config = crate_config.clone();
+                    let _gaurd = FullstackWebEnvGuard::new(&self.build);
+                    let web_feature = self.build.client_feature;
+                    let features = &mut web_config.features;
+                    match features {
+                        Some(features) => {
+                            features.push(web_feature);
+                        }
+                        None => web_config.features = Some(vec![web_feature]),
+                    };
+                    crate::builder::build(&crate_config, false, self.build.skip_assets)?;
+                }
+                {
+                    let mut desktop_config = crate_config.clone();
+                    let desktop_feature = self.build.server_feature;
+                    let features = &mut desktop_config.features;
+                    match features {
+                        Some(features) => {
+                            features.push(desktop_feature);
+                        }
+                        None => desktop_config.features = Some(vec![desktop_feature]),
+                    };
+                    let _gaurd =
+                        FullstackServerEnvGuard::new(self.build.force_debug, self.build.release);
+                    crate::builder::build_desktop(&desktop_config, false, self.build.skip_assets)?;
+                }
             }
         }
 
-        let temp = gen_page(&crate_config.dioxus_config, false);
+        let temp = gen_page(&crate_config, false, self.build.skip_assets);
 
         let mut file = std::fs::File::create(
             crate_config
@@ -66,8 +108,8 @@ impl Build {
         )?;
         file.write_all(temp.as_bytes())?;
 
-        #[cfg(feature = "plugin")]
-        let _ = PluginManager::on_build_finish(&crate_config, &platform);
+        // #[cfg(feature = "plugin")]
+        // let _ = PluginManager::on_build_finish(&crate_config, &platform);
 
         Ok(())
     }

+ 27 - 3
packages/cli/src/cli/bundle.rs

@@ -76,8 +76,14 @@ impl Bundle {
             crate_config.set_profile(self.build.profile.unwrap());
         }
 
+        if let Some(target) = &self.build.target {
+            crate_config.set_target(target.to_string());
+        }
+
+        crate_config.set_cargo_args(self.build.cargo_args);
+
         // build the desktop app
-        build_desktop(&crate_config, false)?;
+        build_desktop(&crate_config, false, false)?;
 
         // copy the binary to the out dir
         let package = crate_config.manifest.package.unwrap();
@@ -128,6 +134,19 @@ impl Bundle {
             }
         }
 
+        // Add all assets from collect assets to the bundle
+        {
+            let config = manganis_cli_support::Config::current();
+            let location = config.assets_serve_location().to_string();
+            let location = format!("./{}", location);
+            println!("Adding assets from {} to bundle", location);
+            if let Some(resources) = &mut bundle_settings.resources {
+                resources.push(location);
+            } else {
+                bundle_settings.resources = Some(vec![location]);
+            }
+        }
+
         let mut settings = SettingsBuilder::new()
             .project_out_directory(crate_config.out_dir)
             .package_settings(PackageSettings {
@@ -148,6 +167,11 @@ impl Bundle {
                     .collect(),
             );
         }
+
+        if let Some(target) = &self.build.target {
+            settings = settings.target(target.to_string());
+        }
+
         let settings = settings.build();
 
         // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
@@ -156,9 +180,9 @@ impl Bundle {
 
         tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
             #[cfg(target_os = "macos")]
-            panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
+            panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
             #[cfg(not(target_os = "macos"))]
-            panic!("Failed to bundle project: {}", err);
+            panic!("Failed to bundle project: {:#?}", err);
         });
 
         Ok(())

+ 83 - 9
packages/cli/src/cli/cfg.rs

@@ -6,15 +6,16 @@ use super::*;
 /// Config options for the build system.
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub struct ConfigOptsBuild {
-    /// The index HTML file to drive the bundling process [default: index.html]
-    #[arg(long)]
-    pub target: Option<PathBuf>,
-
     /// Build in release mode [default: false]
     #[clap(long)]
     #[serde(default)]
     pub release: bool,
 
+    /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
+    #[clap(long)]
+    #[serde(default)]
+    pub force_debug: bool,
+
     // Use verbose output [default: false]
     #[clap(long)]
     #[serde(default)]
@@ -32,17 +33,53 @@ pub struct ConfigOptsBuild {
     #[clap(long, value_enum)]
     pub platform: Option<Platform>,
 
+    /// Skip collecting assets from dependencies [default: false]
+    #[clap(long)]
+    #[serde(default)]
+    pub skip_assets: bool,
+
     /// Space separated list of features to activate
     #[clap(long)]
     pub features: Option<Vec<String>>,
+
+    /// The feature to use for the client in a fullstack app [default: "web"]
+    #[clap(long, default_value_t = { "web".to_string() })]
+    pub client_feature: String,
+
+    /// The feature to use for the server in a fullstack app [default: "ssr"]
+    #[clap(long, default_value_t = { "ssr".to_string() })]
+    pub server_feature: String,
+
+    /// Rustc platform triple
+    #[clap(long)]
+    pub target: Option<String>,
+
+    /// Extra arguments passed to cargo build
+    #[clap(last = true)]
+    pub cargo_args: Vec<String>,
+}
+
+impl From<ConfigOptsServe> for ConfigOptsBuild {
+    fn from(serve: ConfigOptsServe) -> Self {
+        Self {
+            target: serve.target,
+            release: serve.release,
+            verbose: serve.verbose,
+            example: serve.example,
+            profile: serve.profile,
+            platform: serve.platform,
+            features: serve.features,
+            client_feature: serve.client_feature,
+            server_feature: serve.server_feature,
+            skip_assets: serve.skip_assets,
+            force_debug: serve.force_debug,
+            cargo_args: serve.cargo_args,
+        }
+    }
 }
 
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub struct ConfigOptsServe {
-    /// The index HTML file to drive the bundling process [default: index.html]
-    #[arg(short, long)]
-    pub target: Option<PathBuf>,
-
     /// Port of dev server
     #[clap(long)]
     #[clap(default_value_t = 8080)]
@@ -62,6 +99,11 @@ pub struct ConfigOptsServe {
     #[serde(default)]
     pub release: bool,
 
+    /// This flag only applies to fullstack builds. By default fullstack builds will run with something in between debug and release mode. This flag will force the build to run in debug mode. [default: false]
+    #[clap(long)]
+    #[serde(default)]
+    pub force_debug: bool,
+
     // Use verbose output [default: false]
     #[clap(long)]
     #[serde(default)]
@@ -71,7 +113,7 @@ pub struct ConfigOptsServe {
     #[clap(long)]
     pub profile: Option<String>,
 
-    /// Build platform: support Web & Desktop [default: "default_platform"]
+    /// Build platform: support Web, Desktop, and Fullstack [default: "default_platform"]
     #[clap(long, value_enum)]
     pub platform: Option<Platform>,
 
@@ -89,6 +131,27 @@ pub struct ConfigOptsServe {
     /// Space separated list of features to activate
     #[clap(long)]
     pub features: Option<Vec<String>>,
+
+    /// Skip collecting assets from dependencies [default: false]
+    #[clap(long)]
+    #[serde(default)]
+    pub skip_assets: bool,
+
+    /// The feature to use for the client in a fullstack app [default: "web"]
+    #[clap(long, default_value_t = { "web".to_string() })]
+    pub client_feature: String,
+
+    /// The feature to use for the server in a fullstack app [default: "ssr"]
+    #[clap(long, default_value_t = { "ssr".to_string() })]
+    pub server_feature: String,
+
+    /// Rustc platform triple
+    #[clap(long)]
+    pub target: Option<String>,
+
+    /// Extra arguments passed to cargo build
+    #[clap(last = true)]
+    pub cargo_args: Vec<String>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -99,6 +162,9 @@ pub enum Platform {
     #[clap(name = "desktop")]
     #[serde(rename = "desktop")]
     Desktop,
+    #[clap(name = "fullstack")]
+    #[serde(rename = "fullstack")]
+    Fullstack,
 }
 
 /// Config options for the bundling system.
@@ -129,4 +195,12 @@ pub struct ConfigOptsBundle {
     /// Space separated list of features to activate
     #[clap(long)]
     pub features: Option<Vec<String>>,
+
+    /// Rustc platform triple
+    #[clap(long)]
+    pub target: Option<String>,
+
+    /// Extra arguments passed to cargo build
+    #[clap(last = true)]
+    pub cargo_args: Vec<String>,
 }

+ 6 - 0
packages/cli/src/cli/clean.rs

@@ -28,6 +28,12 @@ impl Clean {
             remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
         }
 
+        let fullstack_out_dir = crate_config.crate_dir.join(".dioxus");
+
+        if fullstack_out_dir.is_dir() {
+            remove_dir_all(fullstack_out_dir)?;
+        }
+
         Ok(())
     }
 }

部分文件因为文件数量过多而无法显示