浏览代码

Merge branch 'master' into binary-protocal

Jonathan Kelley 1 年之前
父节点
当前提交
a2f44be2a2
共有 76 个文件被更改,包括 1921 次插入628 次删除
  1. 1 1
      .github/workflows/playwright.yml
  2. 1 0
      .gitignore
  3. 1 0
      Cargo.toml
  4. 1 1
      README.md
  5. 4 7
      examples/all_events.rs
  6. 1 7
      examples/compose.rs
  7. 29 0
      examples/dynamic_asset.rs
  8. 1 1
      examples/mobile_demo/Cargo.toml
  9. 48 41
      examples/todomvc.rs
  10. 184 0
      examples/video_stream.rs
  11. 1 0
      packages/cli/Cargo.toml
  12. 4 4
      packages/cli/README.md
  13. 18 2
      packages/cli/src/builder.rs
  14. 95 80
      packages/cli/src/cli/autoformat.rs
  15. 6 0
      packages/cli/src/cli/build.rs
  16. 13 2
      packages/cli/src/cli/bundle.rs
  17. 24 8
      packages/cli/src/cli/cfg.rs
  18. 6 0
      packages/cli/src/cli/serve.rs
  19. 16 0
      packages/cli/src/config.rs
  20. 16 13
      packages/cli/src/main.rs
  21. 5 1
      packages/cli/src/server/desktop/mod.rs
  22. 9 1
      packages/cli/src/server/web/mod.rs
  23. 3 0
      packages/core-macro/src/component_body_deserializers/component.rs
  24. 63 59
      packages/core/src/arena.rs
  25. 80 45
      packages/core/src/create.rs
  26. 149 65
      packages/core/src/diff.rs
  27. 2 1
      packages/core/src/fragment.rs
  28. 1 1
      packages/core/src/lazynodes.rs
  29. 36 4
      packages/core/src/nodes.rs
  30. 11 7
      packages/core/src/scheduler/task.rs
  31. 1 0
      packages/core/src/scope_arena.rs
  32. 1 11
      packages/core/src/scope_context.rs
  33. 3 2
      packages/core/src/scopes.rs
  34. 84 76
      packages/core/src/virtual_dom.rs
  35. 69 0
      packages/core/tests/event_propagation.rs
  36. 29 30
      packages/core/tests/fuzzing.rs
  37. 2 2
      packages/desktop/Cargo.toml
  38. 21 18
      packages/desktop/headless_tests/events.rs
  39. 10 0
      packages/desktop/src/cfg.rs
  40. 20 0
      packages/desktop/src/desktop_context.rs
  41. 5 1
      packages/desktop/src/lib.rs
  42. 195 9
      packages/desktop/src/protocol.rs
  43. 93 13
      packages/desktop/src/webview.rs
  44. 6 14
      packages/dioxus-tui/examples/all_terminal_events.rs
  45. 1 1
      packages/fullstack/Cargo.toml
  46. 1 1
      packages/fullstack/examples/salvo-hello-world/Cargo.toml
  47. 2 1
      packages/fullstack/src/hooks/server_cached.rs
  48. 4 2
      packages/generational-box/src/lib.rs
  49. 8 10
      packages/hooks/src/use_effect.rs
  50. 2 1
      packages/html/Cargo.toml
  51. 53 3
      packages/html/src/elements.rs
  52. 17 4
      packages/html/src/events/mouse.rs
  53. 46 0
      packages/html/src/global_attributes.rs
  54. 2 0
      packages/html/src/lib.rs
  55. 13 2
      packages/liveview/Cargo.toml
  56. 1 0
      packages/liveview/README.md
  57. 76 0
      packages/liveview/examples/rocket.rs
  58. 25 0
      packages/liveview/src/adapters/rocket_adapter.rs
  59. 5 0
      packages/liveview/src/lib.rs
  60. 24 26
      packages/native-core/tests/fuzzing.rs
  61. 2 2
      packages/rink/src/layout.rs
  62. 1 0
      packages/rsx-rosetta/Cargo.toml
  63. 26 10
      packages/rsx-rosetta/src/lib.rs
  64. 33 0
      packages/rsx-rosetta/tests/h-tags.rs
  65. 21 0
      packages/rsx-rosetta/tests/raw.rs
  66. 0 4
      packages/rsx-rosetta/tests/simple.rs
  67. 21 0
      packages/rsx-rosetta/tests/web-component.rs
  68. 18 14
      packages/rsx/src/lib.rs
  69. 1 1
      packages/server-macro/src/lib.rs
  70. 6 0
      packages/signals/src/signal.rs
  71. 15 1
      packages/ssr/src/incremental_cfg.rs
  72. 88 0
      packages/ssr/src/renderer.rs
  73. 1 0
      packages/web/Cargo.toml
  74. 1 1
      packages/web/src/cache.rs
  75. 25 12
      packages/web/src/dom.rs
  76. 14 5
      packages/web/src/rehydrate.rs

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

@@ -43,7 +43,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

+ 1 - 0
Cargo.toml

@@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
 env_logger = "0.10.0"
 simple_logger = "4.0.0"
 thiserror = { workspace = true }
+http-range = "0.1.5"

+ 1 - 1
README.md

@@ -161,7 +161,7 @@ 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">

+ 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:?}" } )) }
+        }
     ))
 }

+ 1 - 7
examples/compose.rs

@@ -27,13 +27,7 @@ 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.
+                    let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
                     window.new_window(dom, Default::default());
                 },
                 "Click to compose a new email"

+ 29 - 0
examples/dynamic_asset.rs

@@ -0,0 +1,29 @@
+use dioxus::prelude::*;
+use dioxus_desktop::wry::http::Response;
+use dioxus_desktop::{use_asset_handler, AssetRequest};
+use std::path::Path;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    use_asset_handler(cx, |request: &AssetRequest| {
+        let path = request.path().to_path_buf();
+        async move {
+            if path != Path::new("logo.png") {
+                return None;
+            }
+            let image_data: &[u8] = include_bytes!("./assets/logo.png");
+            Some(Response::new(image_data.into()))
+        }
+    });
+
+    cx.render(rsx! {
+        div {
+            img {
+                src: "logo.png"
+            }
+        }
+    })
+}

+ 1 - 1
examples/mobile_demo/Cargo.toml

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

+ 48 - 41
examples/todomvc.rs

@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
     cx.render(rsx! {
         section { class: "todoapp",
             style { include_str!("./assets/todomvc.css") }
-            TodoHeader {
-                todos: todos,
-            }
-            section {
-                class: "main",
+            TodoHeader { todos: todos }
+            section { class: "main",
                 if !todos.is_empty() {
                     rsx! {
                         input {
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
 
     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());
+            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());
+                    }
                 }
             }
         }
-    }
     })
 }
 
@@ -146,8 +146,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",
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                 }
                 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!{
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
     cx.render(rsx! {
         footer { class: "footer",
             span { class: "todo-count",
-                strong {"{active_todo_count} "}
-                span {"{active_todo_text} left"}
+                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"),
-                ] {
+                for (state , state_text , url) in [
+    (FilterState::All, "All", "#/"),
+    (FilterState::Active, "Active", "#/active"),
+    (FilterState::Completed, "Completed", "#/completed"),
+] {
                     li {
                         a {
                             href: url,
@@ -250,8 +251,14 @@ 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" }}
+            p {
+                "Created by "
+                a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
+            }
+            p {
+                "Part of "
+                a { href: "http://todomvc.com", "TodoMVC" }
+            }
         }
     })
 }

+ 184 - 0
examples/video_stream.rs

@@ -0,0 +1,184 @@
+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::borrow::Cow;
+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, move |request: &AssetRequest| {
+        let request = request.clone();
+        async move {
+            let video_file = PathBuf::from(VIDEO_PATH);
+            let mut file = tokio::fs::File::open(&video_file).await.unwrap();
+            let response: Option<Response<Cow<'static, [u8]>>> =
+                match get_stream_response(&mut file, &request).await {
+                    Ok(response) => Some(response.map(Cow::Owned)),
+                    Err(err) => {
+                        eprintln!("Error: {}", err);
+                        None
+                    }
+                };
+            response
+        }
+    });
+
+    render! {
+        div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
+    }
+}
+
+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 - 0
packages/cli/Cargo.toml

@@ -83,6 +83,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 = []

+ 4 - 4
packages/cli/README.md

@@ -10,7 +10,7 @@ It handles building, bundling, development and publishing to simplify developmen
 
 ### Install the stable version (recommended)
 
-```
+```shell
 cargo install dioxus-cli
 ```
 
@@ -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
 ```
 

+ 18 - 2
packages/cli/src/builder.rs

@@ -93,6 +93,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),
@@ -265,7 +267,6 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     let mut cmd = subprocess::Exec::cmd("cargo")
         .cwd(&config.crate_dir)
         .arg("build")
-        .arg("--quiet")
         .arg("--message-format=json");
 
     if config.release {
@@ -273,6 +274,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() {
@@ -285,6 +288,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),
@@ -302,12 +313,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)

+ 95 - 80
packages/cli/src/cli/autoformat.rs

@@ -27,15 +27,17 @@ 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(raw) = raw {
             let indent = indentation_for(".")?;
             if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
                 println!("{}", inner);
@@ -47,47 +49,90 @@ impl Autoformat {
         }
 
         // Format single file
-        if let Some(file) = self.file {
-            let file_content;
-            let indent;
-            if file == "-" {
-                indent = indentation_for(".")?;
-                let mut contents = String::new();
-                std::io::stdin().read_to_string(&mut contents)?;
-                file_content = Ok(contents);
-            } else {
-                indent = indentation_for(".")?;
-                file_content = fs::read_to_string(&file);
-            };
-
-            match file_content {
-                Ok(s) => {
-                    let edits = dioxus_autofmt::fmt_file(&s, indent);
-                    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
@@ -96,8 +141,7 @@ 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(());
@@ -107,38 +151,17 @@ async fn autoformat_project(check: bool) -> Result<()> {
 
     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 _indent = indent.clone();
-            let res = tokio::spawn(async move {
-                let contents = tokio::fs::read_to_string(&path).await?;
-
-                let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone());
-                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),
@@ -148,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);
@@ -207,26 +224,24 @@ fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
     ))
 }
 
-fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
-    let Ok(folder) = folder.read_dir() else {
+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);
             }
         }

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

@@ -37,6 +37,12 @@ impl Build {
             .platform
             .unwrap_or(crate_config.dioxus_config.application.default_platform);
 
+        if let Some(target) = self.build.target {
+            crate_config.set_target(target);
+        }
+
+        crate_config.set_cargo_args(self.build.cargo_args);
+
         // #[cfg(feature = "plugin")]
         // let _ = PluginManager::on_build_start(&crate_config, &platform);
 

+ 13 - 2
packages/cli/src/cli/bundle.rs

@@ -76,6 +76,12 @@ 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)?;
 
@@ -148,6 +154,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 +167,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(())

+ 24 - 8
packages/cli/src/cli/cfg.rs

@@ -6,10 +6,6 @@ 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)]
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
     /// 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>,
 }
 
 #[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)]
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
     /// 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>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -129,4 +137,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/serve.rs

@@ -34,6 +34,12 @@ impl Serve {
         // Subdirectories don't work with the server
         crate_config.dioxus_config.web.app.base_path = None;
 
+        if let Some(target) = self.serve.target {
+            crate_config.set_target(target);
+        }
+
+        crate_config.set_cargo_args(self.serve.cargo_args);
+
         let platform = self
             .serve
             .platform

+ 16 - 0
packages/cli/src/config.rs

@@ -211,6 +211,8 @@ pub struct CrateConfig {
     pub verbose: bool,
     pub custom_profile: Option<String>,
     pub features: Option<Vec<String>>,
+    pub target: Option<String>,
+    pub cargo_args: Vec<String>,
 }
 
 #[derive(Debug, Clone)]
@@ -278,6 +280,8 @@ impl CrateConfig {
         let verbose = false;
         let custom_profile = None;
         let features = None;
+        let target = None;
+        let cargo_args = vec![];
 
         Ok(Self {
             out_dir,
@@ -294,6 +298,8 @@ impl CrateConfig {
             custom_profile,
             features,
             verbose,
+            target,
+            cargo_args,
         })
     }
 
@@ -331,6 +337,16 @@ impl CrateConfig {
         self.features = Some(features);
         self
     }
+
+    pub fn set_target(&mut self, target: String) -> &mut Self {
+        self.target = Some(target);
+        self
+    }
+
+    pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
+        self.cargo_args = cargo_args;
+        self
+    }
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]

+ 16 - 13
packages/cli/src/main.rs

@@ -42,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
 
     set_up_logging();
 
-    let bin = get_bin(args.bin)?;
+    let bin = get_bin(args.bin);
 
-    let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
+    if let Ok(bin) = &bin {
+        let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
         .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
         .unwrap_or_else(|| {
             log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
             DioxusConfig::default()
         });
 
-    #[cfg(feature = "plugin")]
-    PluginManager::init(_dioxus_config.plugin)
-        .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
+        #[cfg(feature = "plugin")]
+        PluginManager::init(_dioxus_config.plugin)
+            .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
+    }
 
     match args.action {
         Translate(opts) => opts
             .translate()
             .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
 
-        Build(opts) => opts
-            .build(Some(bin.clone()))
+        Build(opts) if bin.is_ok() => opts
+            .build(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
 
-        Clean(opts) => opts
-            .clean(Some(bin.clone()))
+        Clean(opts) if bin.is_ok() => opts
+            .clean(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
 
-        Serve(opts) => opts
-            .serve(Some(bin.clone()))
+        Serve(opts) if bin.is_ok() => opts
+            .serve(Some(bin.unwrap().clone()))
             .await
             .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
 
@@ -81,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
             .config()
             .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
 
-        Bundle(opts) => opts
-            .bundle(Some(bin.clone()))
+        Bundle(opts) if bin.is_ok() => opts
+            .bundle(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
 
         #[cfg(feature = "plugin")]
@@ -107,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
 
             Ok(())
         }
+        _ => Err(anyhow::anyhow!(bin.unwrap_err())),
     }
 }

+ 5 - 1
packages/cli/src/server/desktop/mod.rs

@@ -154,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
                                 println!("Connected to hot reloading 🚀");
                             }
                             Err(err) => {
-                                if err.kind() != std::io::ErrorKind::WouldBlock {
+                                let error_string = err.to_string();
+                                // Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
+                                let display_error = err.kind() != std::io::ErrorKind::WouldBlock
+                                    && !error_string.contains("Waiting for a process");
+                                if display_error {
                                     println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
                                 }
                             }

+ 9 - 1
packages/cli/src/server/web/mod.rs

@@ -11,6 +11,7 @@ use axum::{
     body::{Full, HttpBody},
     extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
     http::{
+        self,
         header::{HeaderName, HeaderValue},
         Method, Response, StatusCode,
     },
@@ -262,7 +263,7 @@ async fn setup_router(
         .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
         .and_then(
             move |response: Response<ServeFileSystemResponseBody>| async move {
-                let response = if file_service_config
+                let mut response = if file_service_config
                     .dioxus_config
                     .web
                     .watcher
@@ -290,6 +291,13 @@ async fn setup_router(
                 } else {
                     response.map(|body| body.boxed())
                 };
+                let headers = response.headers_mut();
+                headers.insert(
+                    http::header::CACHE_CONTROL,
+                    HeaderValue::from_static("no-cache"),
+                );
+                headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
+                headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
                 Ok(response)
             },
         )

+ 3 - 0
packages/core-macro/src/component_body_deserializers/component.rs

@@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
         let comp_fn = &self.comp_fn;
         let props_struct = &self.props_struct;
+        let fn_ident = &comp_fn.sig.ident;
 
+        let doc = format!("Properties for the [`{fn_ident}`] component.");
         tokens.append_all(quote! {
+            #[doc = #doc]
             #props_struct
             #[allow(non_snake_case)]
             #comp_fn

+ 63 - 59
packages/core/src/arena.rs

@@ -13,62 +13,50 @@ use crate::{
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
 pub struct ElementId(pub usize);
 
-pub(crate) struct ElementRef {
+/// An Element that can be bubbled to's unique identifier.
+///
+/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
+/// unmounted, then the `BubbleId` will be reused for a new component.
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
+pub struct VNodeId(pub usize);
+
+#[derive(Debug, Clone, Copy)]
+pub struct ElementRef {
     // the pathway of the real element inside the template
-    pub path: ElementPath,
+    pub(crate) path: ElementPath,
 
     // The actual template
-    pub template: Option<NonNull<VNode<'static>>>,
+    pub(crate) template: VNodeId,
 
     // The scope the element belongs to
-    pub scope: ScopeId,
+    pub(crate) scope: ScopeId,
 }
 
 #[derive(Clone, Copy, Debug)]
-pub enum ElementPath {
-    Deep(&'static [u8]),
-    Root(usize),
-}
-
-impl ElementRef {
-    pub(crate) fn none() -> Self {
-        Self {
-            template: None,
-            path: ElementPath::Root(0),
-            scope: ScopeId::ROOT,
-        }
-    }
+pub struct ElementPath {
+    pub(crate) path: &'static [u8],
 }
 
 impl VirtualDom {
-    pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
-        self.next_reference(template, ElementPath::Deep(path))
-    }
-
-    pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
-        self.next_reference(template, ElementPath::Root(path))
+    pub(crate) fn next_element(&mut self) -> ElementId {
+        ElementId(self.elements.insert(None))
     }
 
-    pub(crate) fn next_null(&mut self) -> ElementId {
-        let entry = self.elements.vacant_entry();
-        let id = entry.key();
-
-        entry.insert(ElementRef::none());
-        ElementId(id)
-    }
-
-    fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
-        let entry = self.elements.vacant_entry();
-        let id = entry.key();
-        let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT);
+    pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
+        let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
+            std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
+        })));
+
+        // Set this id to be dropped when the scope is rerun
+        if let Some(scope) = self.runtime.current_scope_id() {
+            self.scopes[scope.0]
+                .element_refs_to_drop
+                .borrow_mut()
+                .push(new_id);
+        }
 
-        entry.insert(ElementRef {
-            // We know this is non-null because it comes from a reference
-            template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
-            path,
-            scope,
-        });
-        ElementId(id)
+        new_id
     }
 
     pub(crate) fn reclaim(&mut self, el: ElementId) {
@@ -76,7 +64,7 @@ impl VirtualDom {
             .unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
     }
 
-    pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
+    pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
         if el.0 == 0 {
             panic!(
                 "Cannot reclaim the root element - {:#?}",
@@ -84,12 +72,12 @@ impl VirtualDom {
             );
         }
 
-        self.elements.try_remove(el.0)
+        self.elements.try_remove(el.0).map(|_| ())
     }
 
-    pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
-        let node: *const VNode = node as *const _;
-        self.elements[el.0].template = unsafe { std::mem::transmute(node) };
+    pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
+        self.element_refs[id.0] =
+            Some(unsafe { std::mem::transmute::<NonNull<VNode>, _>(vnode.into()) });
     }
 
     // Drop a scope and all its children
@@ -101,6 +89,15 @@ impl VirtualDom {
             id,
         });
 
+        // Remove all VNode ids from the scope
+        for id in self.scopes[id.0]
+            .element_refs_to_drop
+            .borrow_mut()
+            .drain(..)
+        {
+            self.element_refs.try_remove(id.0);
+        }
+
         self.ensure_drop_safety(id);
 
         if recursive {
@@ -145,14 +142,25 @@ impl VirtualDom {
     }
 
     /// Descend through the tree, removing any borrowed props and listeners
-    pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
+    pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) {
         let scope = &self.scopes[scope_id.0];
 
+        {
+            // Drop all element refs that could be invalidated when the component was rerun
+            let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut();
+            let element_refs_slab = &mut self.element_refs;
+            for element_ref in element_refs.drain(..) {
+                if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) {
+                    *element_ref = None;
+                }
+            }
+        }
+
         // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
         // run the hooks (which hold an &mut Reference)
         // recursively call ensure_drop_safety on all children
-        let mut props = scope.borrowed_props.borrow_mut();
-        props.drain(..).for_each(|comp| {
+        let props = { scope.borrowed_props.borrow_mut().clone() };
+        for comp in props {
             let comp = unsafe { &*comp };
             match comp.scope.get() {
                 Some(child) if child != scope_id => self.ensure_drop_safety(child),
@@ -161,7 +169,9 @@ impl VirtualDom {
             if let Ok(mut props) = comp.props.try_borrow_mut() {
                 *props = None;
             }
-        });
+        }
+        let scope = &self.scopes[scope_id.0];
+        scope.borrowed_props.borrow_mut().clear();
 
         // Now that all the references are gone, we can safely drop our own references in our listeners.
         let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
@@ -176,18 +186,12 @@ impl VirtualDom {
 
 impl ElementPath {
     pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
-        match *self {
-            ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
-            ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
-        }
+        small.len() <= self.path.len() && *small == &self.path[..small.len()]
     }
 }
 
 impl PartialEq<&[u8]> for ElementPath {
     fn eq(&self, other: &&[u8]) -> bool {
-        match *self {
-            ElementPath::Deep(deep) => deep.eq(*other),
-            ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
-        }
+        self.path.eq(*other)
     }
 }

+ 80 - 45
packages/core/src/create.rs

@@ -1,5 +1,7 @@
 use crate::any_props::AnyProps;
-use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
+use crate::innerlude::{
+    BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
+};
 use crate::mutations::Mutation;
 use crate::mutations::Mutation::*;
 use crate::nodes::VNode;
@@ -94,6 +96,9 @@ impl<'b> VirtualDom {
             nodes_mut.resize(len, ElementId::default());
         };
 
+        // Set this node id
+        node.stable_id.set(Some(self.next_vnode_ref(node)));
+
         // The best renderers will have templates prehydrated and registered
         // Just in case, let's create the template using instructions anyways
         self.register_template(node.template.get());
@@ -181,15 +186,30 @@ impl<'b> VirtualDom {
         use DynamicNode::*;
         match &template.dynamic_nodes[idx] {
             node @ Component { .. } | node @ Fragment(_) => {
-                self.create_dynamic_node(template, node, idx)
+                let template_ref = ElementRef {
+                    path: ElementPath {
+                        path: template.template.get().node_paths[idx],
+                    },
+                    template: template.stable_id().unwrap(),
+                    scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+                };
+                self.create_dynamic_node(template_ref, node)
             }
-            Placeholder(VPlaceholder { id }) => {
-                let id = self.set_slot(template, id, idx);
+            Placeholder(VPlaceholder { id, parent }) => {
+                let template_ref = ElementRef {
+                    path: ElementPath {
+                        path: template.template.get().node_paths[idx],
+                    },
+                    template: template.stable_id().unwrap(),
+                    scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+                };
+                parent.set(Some(template_ref));
+                let id = self.set_slot(id);
                 self.mutations.push(CreatePlaceholder { id });
                 1
             }
             Text(VText { id, value }) => {
-                let id = self.set_slot(template, id, idx);
+                let id = self.set_slot(id);
                 self.create_static_text(value, id);
                 1
             }
@@ -205,7 +225,7 @@ impl<'b> VirtualDom {
         });
     }
 
-    /// We write all the descndent data for this element
+    /// We write all the descendent data for this element
     ///
     /// Elements can contain other nodes - and those nodes can be dynamic or static
     ///
@@ -265,7 +285,14 @@ impl<'b> VirtualDom {
             .map(|sorted_index| dynamic_nodes[sorted_index].0);
 
         for idx in reversed_iter {
-            let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
+            let boundary_ref = ElementRef {
+                path: ElementPath {
+                    path: template.template.get().node_paths[idx],
+                },
+                template: template.stable_id().unwrap(),
+                scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+            };
+            let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]);
             if m > 0 {
                 // The path is one shorter because the top node is the root
                 let path = &template.template.get().node_paths[idx][1..];
@@ -279,15 +306,15 @@ impl<'b> VirtualDom {
         attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
         root_idx: u8,
         root: ElementId,
-        node: &VNode,
+        node: &'b VNode<'b>,
     ) {
         while let Some((mut attr_id, path)) =
             attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
         {
-            let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
+            let id = self.assign_static_node_as_dynamic(path, root);
 
             loop {
-                self.write_attribute(&node.dynamic_attrs[attr_id], id);
+                self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
 
                 // Only push the dynamic attributes forward if they match the current path (same element)
                 match attrs.next_if(|(_, p)| *p == path) {
@@ -298,7 +325,13 @@ impl<'b> VirtualDom {
         }
     }
 
-    fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
+    fn write_attribute(
+        &mut self,
+        template: &'b VNode<'b>,
+        idx: usize,
+        attribute: &'b crate::Attribute<'b>,
+        id: ElementId,
+    ) {
         // Make sure we set the attribute's associated id
         attribute.mounted_element.set(id);
 
@@ -307,6 +340,13 @@ impl<'b> VirtualDom {
 
         match &attribute.value {
             AttributeValue::Listener(_) => {
+                let path = &template.template.get().attr_paths[idx];
+                let element_ref = ElementRef {
+                    path: ElementPath { path },
+                    template: template.stable_id().unwrap(),
+                    scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
+                };
+                self.elements[id.0] = Some(element_ref);
                 self.mutations.push(NewEventListener {
                     // all listeners start with "on"
                     name: &unbounded_name[2..],
@@ -330,7 +370,7 @@ impl<'b> VirtualDom {
 
     fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
         // Get an ID for this root since it's a real root
-        let this_id = self.next_root(template, root_idx);
+        let this_id = self.next_element();
         template.root_ids.borrow_mut()[root_idx] = this_id;
 
         self.mutations.push(LoadTemplate {
@@ -353,8 +393,6 @@ impl<'b> VirtualDom {
         &mut self,
         path: &'static [u8],
         this_id: ElementId,
-        template: &VNode,
-        attr_id: usize,
     ) -> ElementId {
         if path.len() == 1 {
             return this_id;
@@ -362,7 +400,7 @@ impl<'b> VirtualDom {
 
         // if attribute is on a root node, then we've already created the element
         // Else, it's deep in the template and we should create a new id for it
-        let id = self.next_element(template, template.template.get().attr_paths[attr_id]);
+        let id = self.next_element();
 
         self.mutations.push(Mutation::AssignId {
             path: &path[1..],
@@ -405,6 +443,7 @@ impl<'b> VirtualDom {
     #[allow(unused_mut)]
     pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
         let (path, byte_index) = template.name.rsplit_once(':').unwrap();
+
         let byte_index = byte_index.parse::<usize>().unwrap();
         // First, check if we've already seen this template
         if self
@@ -439,27 +478,21 @@ impl<'b> VirtualDom {
 
     pub(crate) fn create_dynamic_node(
         &mut self,
-        template: &'b VNode<'b>,
+        parent: ElementRef,
         node: &'b DynamicNode<'b>,
-        idx: usize,
     ) -> usize {
         use DynamicNode::*;
         match node {
-            Text(text) => self.create_dynamic_text(template, text, idx),
-            Placeholder(place) => self.create_placeholder(place, template, idx),
-            Component(component) => self.create_component_node(template, component),
-            Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
+            Text(text) => self.create_dynamic_text(parent, text),
+            Placeholder(place) => self.create_placeholder(place, parent),
+            Component(component) => self.create_component_node(Some(parent), component),
+            Fragment(frag) => self.create_children(*frag, Some(parent)),
         }
     }
 
-    fn create_dynamic_text(
-        &mut self,
-        template: &'b VNode<'b>,
-        text: &'b VText<'b>,
-        idx: usize,
-    ) -> usize {
+    fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
         // Allocate a dynamic element reference for this text node
-        let new_id = self.next_element(template, template.template.get().node_paths[idx]);
+        let new_id = self.next_element();
 
         // Make sure the text node is assigned to the correct element
         text.id.set(Some(new_id));
@@ -470,7 +503,7 @@ impl<'b> VirtualDom {
         // Add the mutation to the list
         self.mutations.push(HydrateText {
             id: new_id,
-            path: &template.template.get().node_paths[idx][1..],
+            path: &parent.path.path[1..],
             value,
         });
 
@@ -481,18 +514,20 @@ impl<'b> VirtualDom {
     pub(crate) fn create_placeholder(
         &mut self,
         placeholder: &VPlaceholder,
-        template: &'b VNode<'b>,
-        idx: usize,
+        parent: ElementRef,
     ) -> usize {
         // Allocate a dynamic element reference for this text node
-        let id = self.next_element(template, template.template.get().node_paths[idx]);
+        let id = self.next_element();
 
         // Make sure the text node is assigned to the correct element
         placeholder.id.set(Some(id));
 
+        // Assign the placeholder's parent
+        placeholder.parent.set(Some(parent));
+
         // Assign the ID to the existing node in the template
         self.mutations.push(AssignId {
-            path: &template.template.get().node_paths[idx][1..],
+            path: &parent.path.path[1..],
             id,
         });
 
@@ -502,7 +537,7 @@ impl<'b> VirtualDom {
 
     pub(super) fn create_component_node(
         &mut self,
-        template: &'b VNode<'b>,
+        parent: Option<ElementRef>,
         component: &'b VComponent<'b>,
     ) -> usize {
         use RenderReturn::*;
@@ -514,8 +549,11 @@ impl<'b> VirtualDom {
 
         match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
             // Create the component's root element
-            Ready(t) => self.create_scope(scope, t),
-            Aborted(t) => self.mount_aborted(template, t),
+            Ready(t) => {
+                self.assign_boundary_ref(parent, t);
+                self.create_scope(scope, t)
+            }
+            Aborted(t) => self.mount_aborted(t, parent),
         }
     }
 
@@ -531,20 +569,17 @@ impl<'b> VirtualDom {
             .unwrap_or_else(|| component.scope.get().unwrap())
     }
 
-    fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
-        let id = self.next_element(parent, &[]);
+    fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option<ElementRef>) -> usize {
+        let id = self.next_element();
         self.mutations.push(Mutation::CreatePlaceholder { id });
         placeholder.id.set(Some(id));
+        placeholder.parent.set(parent);
+
         1
     }
 
-    fn set_slot(
-        &mut self,
-        template: &'b VNode<'b>,
-        slot: &'b Cell<Option<ElementId>>,
-        id: usize,
-    ) -> ElementId {
-        let id = self.next_element(template, template.template.get().node_paths[id]);
+    fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
+        let id = self.next_element();
         slot.set(Some(id));
         id
     }

+ 149 - 65
packages/core/src/diff.rs

@@ -1,7 +1,10 @@
 use crate::{
     any_props::AnyProps,
     arena::ElementId,
-    innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
+    innerlude::{
+        BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
+        VText,
+    },
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::{DynamicNode, VNode},
@@ -39,19 +42,27 @@ impl<'b> VirtualDom {
                 (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
 
                 // Just move over the placeholder
-                (Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
+                (Aborted(l), Aborted(r)) => {
+                    r.id.set(l.id.get());
+                    r.parent.set(l.parent.get())
+                }
 
                 // Placeholder becomes something
                 // We should also clear the error now
-                (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
+                (Aborted(l), Ready(r)) => self.replace_placeholder(
+                    l,
+                    [r],
+                    l.parent.get().expect("root node should not be none"),
+                ),
             };
         }
         self.runtime.scope_stack.borrow_mut().pop();
     }
 
     fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
-        let id = self.next_null();
+        let id = self.next_element();
         p.id.set(Some(id));
+        p.parent.set(l.parent.get());
         self.mutations.push(Mutation::CreatePlaceholder { id });
 
         let pre_edits = self.mutations.edits.len();
@@ -81,12 +92,24 @@ impl<'b> VirtualDom {
                 if let Some(&template) = map.get(&byte_index) {
                     right_template.template.set(template);
                     if template != left_template.template.get() {
-                        return self.replace(left_template, [right_template]);
+                        let parent = left_template.parent.take();
+                        return self.replace(left_template, [right_template], parent);
                     }
                 }
             }
         }
 
+        // Copy over the parent
+        {
+            right_template.parent.set(left_template.parent.get());
+        }
+
+        // Update the bubble id pointer
+        right_template.stable_id.set(left_template.stable_id.get());
+        if let Some(bubble_id) = right_template.stable_id.get() {
+            self.set_template(bubble_id, right_template);
+        }
+
         // If the templates are the same, we don't need to do anything, nor do we want to
         if templates_are_the_same(left_template, right_template) {
             return;
@@ -105,12 +128,8 @@ impl<'b> VirtualDom {
             .zip(right_template.dynamic_attrs.iter())
             .for_each(|(left_attr, right_attr)| {
                 // Move over the ID from the old to the new
-                right_attr
-                    .mounted_element
-                    .set(left_attr.mounted_element.get());
-
-                // We want to make sure anything that gets pulled is valid
-                self.update_template(left_attr.mounted_element.get(), right_template);
+                let mounted_element = left_attr.mounted_element.get();
+                right_attr.mounted_element.set(mounted_element);
 
                 // If the attributes are different (or volatile), we need to update them
                 if left_attr.value != right_attr.value || left_attr.volatile {
@@ -123,8 +142,16 @@ impl<'b> VirtualDom {
             .dynamic_nodes
             .iter()
             .zip(right_template.dynamic_nodes.iter())
-            .for_each(|(left_node, right_node)| {
-                self.diff_dynamic_node(left_node, right_node, right_template);
+            .enumerate()
+            .for_each(|(dyn_node_idx, (left_node, right_node))| {
+                let current_ref = ElementRef {
+                    template: right_template.stable_id().unwrap(),
+                    path: ElementPath {
+                        path: left_template.template.get().node_paths[dyn_node_idx],
+                    },
+                    scope: self.runtime.scope_stack.borrow().last().copied().unwrap(),
+                };
+                self.diff_dynamic_node(left_node, right_node, current_ref);
             });
 
         // Make sure the roots get transferred over while we're here
@@ -135,30 +162,24 @@ impl<'b> VirtualDom {
                 right.push(element);
             }
         }
-
-        let root_ids = right_template.root_ids.borrow();
-
-        // Update the node refs
-        for i in 0..root_ids.len() {
-            if let Some(root_id) = root_ids.get(i) {
-                self.update_template(*root_id, right_template);
-            }
-        }
     }
 
     fn diff_dynamic_node(
         &mut self,
         left_node: &'b DynamicNode<'b>,
         right_node: &'b DynamicNode<'b>,
-        node: &'b VNode<'b>,
+        parent: ElementRef,
     ) {
         match (left_node, right_node) {
-            (Text(left), Text(right)) => self.diff_vtext(left, right, node),
-            (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
-            (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
-            (Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
-            (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
-            (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
+            (Text(left), Text(right)) => self.diff_vtext(left, right),
+            (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent),
+            (Placeholder(left), Placeholder(right)) => {
+                right.id.set(left.id.get());
+                right.parent.set(left.parent.get());
+            },
+            (Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)),
+            (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent),
+            (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent),
             _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
         };
     }
@@ -179,7 +200,7 @@ impl<'b> VirtualDom {
         &mut self,
         left: &'b VComponent<'b>,
         right: &'b VComponent<'b>,
-        right_template: &'b VNode<'b>,
+        parent: Option<ElementRef>,
     ) {
         if std::ptr::eq(left, right) {
             return;
@@ -187,7 +208,7 @@ impl<'b> VirtualDom {
 
         // Replace components that have different render fns
         if left.render_fn != right.render_fn {
-            return self.replace_vcomponent(right_template, right, left);
+            return self.replace_vcomponent(right, left, parent);
         }
 
         // Make sure the new vcomponent has the right scopeid associated to it
@@ -228,11 +249,11 @@ impl<'b> VirtualDom {
 
     fn replace_vcomponent(
         &mut self,
-        right_template: &'b VNode<'b>,
         right: &'b VComponent<'b>,
         left: &'b VComponent<'b>,
+        parent: Option<ElementRef>,
     ) {
-        let m = self.create_component_node(right_template, right);
+        let m = self.create_component_node(parent, right);
 
         let pre_edits = self.mutations.edits.len();
 
@@ -287,11 +308,12 @@ impl<'b> VirtualDom {
     /// }
     /// ```
     fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
+        let parent = left.parent.take();
         match matching_components(left, right) {
-            None => self.replace(left, [right]),
+            None => self.replace(left, [right], parent),
             Some(components) => components
                 .into_iter()
-                .for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
+                .for_each(|(l, r)| self.diff_vcomponent(l, r, parent)),
         }
     }
 
@@ -299,11 +321,8 @@ impl<'b> VirtualDom {
     ///
     /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
     /// different.
-    fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
-        let id = left
-            .id
-            .get()
-            .unwrap_or_else(|| self.next_element(node, &[0]));
+    fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
+        let id = left.id.get().unwrap_or_else(|| self.next_element());
 
         right.id.set(Some(id));
         if left.value != right.value {
@@ -312,7 +331,12 @@ impl<'b> VirtualDom {
         }
     }
 
-    fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
+    fn diff_non_empty_fragment(
+        &mut self,
+        old: &'b [VNode<'b>],
+        new: &'b [VNode<'b>],
+        parent: ElementRef,
+    ) {
         let new_is_keyed = new[0].key.is_some();
         let old_is_keyed = old[0].key.is_some();
         debug_assert!(
@@ -325,9 +349,9 @@ impl<'b> VirtualDom {
         );
 
         if new_is_keyed && old_is_keyed {
-            self.diff_keyed_children(old, new);
+            self.diff_keyed_children(old, new, parent);
         } else {
-            self.diff_non_keyed_children(old, new);
+            self.diff_non_keyed_children(old, new, parent);
         }
     }
 
@@ -339,7 +363,12 @@ impl<'b> VirtualDom {
     //     [... parent]
     //
     // the change list stack is in the same state when this function returns.
-    fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
+    fn diff_non_keyed_children(
+        &mut self,
+        old: &'b [VNode<'b>],
+        new: &'b [VNode<'b>],
+        parent: ElementRef,
+    ) {
         use std::cmp::Ordering;
 
         // Handled these cases in `diff_children` before calling this function.
@@ -348,7 +377,9 @@ impl<'b> VirtualDom {
 
         match old.len().cmp(&new.len()) {
             Ordering::Greater => self.remove_nodes(&old[new.len()..]),
-            Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
+            Ordering::Less => {
+                self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent)
+            }
             Ordering::Equal => {}
         }
 
@@ -373,7 +404,12 @@ impl<'b> VirtualDom {
     // https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
     //
     // The stack is empty upon entry.
-    fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
+    fn diff_keyed_children(
+        &mut self,
+        old: &'b [VNode<'b>],
+        new: &'b [VNode<'b>],
+        parent: ElementRef,
+    ) {
         if cfg!(debug_assertions) {
             let mut keys = rustc_hash::FxHashSet::default();
             let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
@@ -401,7 +437,7 @@ impl<'b> VirtualDom {
         //
         // `shared_prefix_count` is the count of how many nodes at the start of
         // `new` and `old` share the same keys.
-        let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) {
+        let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) {
             Some(count) => count,
             None => return,
         };
@@ -427,18 +463,18 @@ impl<'b> VirtualDom {
             if left_offset == 0 {
                 // insert at the beginning of the old list
                 let foothold = &old[old.len() - right_offset];
-                self.create_and_insert_before(new_middle, foothold);
+                self.create_and_insert_before(new_middle, foothold, parent);
             } else if right_offset == 0 {
                 // insert at the end  the old list
                 let foothold = old.last().unwrap();
-                self.create_and_insert_after(new_middle, foothold);
+                self.create_and_insert_after(new_middle, foothold, parent);
             } else {
                 // inserting in the middle
                 let foothold = &old[left_offset - 1];
-                self.create_and_insert_after(new_middle, foothold);
+                self.create_and_insert_after(new_middle, foothold, parent);
             }
         } else {
-            self.diff_keyed_middle(old_middle, new_middle);
+            self.diff_keyed_middle(old_middle, new_middle, parent);
         }
     }
 
@@ -451,6 +487,7 @@ impl<'b> VirtualDom {
         &mut self,
         old: &'b [VNode<'b>],
         new: &'b [VNode<'b>],
+        parent: ElementRef,
     ) -> Option<(usize, usize)> {
         let mut left_offset = 0;
 
@@ -466,7 +503,7 @@ impl<'b> VirtualDom {
         // If that was all of the old children, then create and append the remaining
         // new children and we're finished.
         if left_offset == old.len() {
-            self.create_and_insert_after(&new[left_offset..], old.last().unwrap());
+            self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent);
             return None;
         }
 
@@ -505,7 +542,12 @@ impl<'b> VirtualDom {
     //
     // Upon exit from this function, it will be restored to that same self.
     #[allow(clippy::too_many_lines)]
-    fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
+    fn diff_keyed_middle(
+        &mut self,
+        old: &'b [VNode<'b>],
+        new: &'b [VNode<'b>],
+        parent: ElementRef,
+    ) {
         /*
         1. Map the old keys into a numerical ordering based on indices.
         2. Create a map of old key to its index
@@ -562,7 +604,7 @@ impl<'b> VirtualDom {
         if shared_keys.is_empty() {
             if old.first().is_some() {
                 self.remove_nodes(&old[1..]);
-                self.replace(&old[0], new);
+                self.replace(&old[0], new, Some(parent));
             } else {
                 // I think this is wrong - why are we appending?
                 // only valid of the if there are no trailing elements
@@ -739,20 +781,38 @@ impl<'b> VirtualDom {
             .sum()
     }
 
-    fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize {
+    pub(crate) fn create_children(
+        &mut self,
+        nodes: impl IntoIterator<Item = &'b VNode<'b>>,
+        parent: Option<ElementRef>,
+    ) -> usize {
         nodes
             .into_iter()
-            .fold(0, |acc, child| acc + self.create(child))
+            .map(|child| {
+                self.assign_boundary_ref(parent, child);
+                self.create(child)
+            })
+            .sum()
     }
 
-    fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) {
-        let m = self.create_children(new);
+    fn create_and_insert_before(
+        &mut self,
+        new: &'b [VNode<'b>],
+        before: &'b VNode<'b>,
+        parent: ElementRef,
+    ) {
+        let m = self.create_children(new, Some(parent));
         let id = self.find_first_element(before);
         self.mutations.push(Mutation::InsertBefore { id, m })
     }
 
-    fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) {
-        let m = self.create_children(new);
+    fn create_and_insert_after(
+        &mut self,
+        new: &'b [VNode<'b>],
+        after: &'b VNode<'b>,
+        parent: ElementRef,
+    ) {
+        let m = self.create_children(new, Some(parent));
         let id = self.find_last_element(after);
         self.mutations.push(Mutation::InsertAfter { id, m })
     }
@@ -762,15 +822,21 @@ impl<'b> VirtualDom {
         &mut self,
         l: &'b VPlaceholder,
         r: impl IntoIterator<Item = &'b VNode<'b>>,
+        parent: ElementRef,
     ) {
-        let m = self.create_children(r);
+        let m = self.create_children(r, Some(parent));
         let id = l.id.get().unwrap();
         self.mutations.push(Mutation::ReplaceWith { id, m });
         self.reclaim(id);
     }
 
-    fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
-        let m = self.create_children(right);
+    fn replace(
+        &mut self,
+        left: &'b VNode<'b>,
+        right: impl IntoIterator<Item = &'b VNode<'b>>,
+        parent: Option<ElementRef>,
+    ) {
+        let m = self.create_children(right, parent);
 
         let pre_edits = self.mutations.edits.len();
 
@@ -789,11 +855,12 @@ impl<'b> VirtualDom {
         };
     }
 
-    fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
+    fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) {
         // Create the placeholder first, ensuring we get a dedicated ID for the placeholder
-        let placeholder = self.next_element(&l[0], &[]);
+        let placeholder = self.next_element();
 
         r.id.set(Some(placeholder));
+        r.parent.set(Some(parent));
 
         self.mutations
             .push(Mutation::CreatePlaceholder { id: placeholder });
@@ -831,6 +898,16 @@ impl<'b> VirtualDom {
         // Clean up the roots, assuming we need to generate mutations for these
         // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)
         self.reclaim_roots(node, gen_muts);
+
+        // Clean up the vnode id
+        self.reclaim_vnode_id(node);
+    }
+
+    fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) {
+        // Clean up the vnode id
+        if let Some(id) = node.stable_id() {
+            self.element_refs.remove(id.0);
+        }
     }
 
     fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
@@ -989,6 +1066,13 @@ impl<'b> VirtualDom {
             }
         }
     }
+
+    pub(crate) fn assign_boundary_ref(&mut self, parent: Option<ElementRef>, child: &'b VNode<'b>) {
+        if let Some(parent) = parent {
+            // assign the parent of the child
+            child.parent.set(Some(parent));
+        }
+    }
 }
 
 /// Are the templates the same?

+ 2 - 1
packages/core/src/fragment.rs

@@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
     let children = cx.props.0.as_ref()?;
     Some(VNode {
         key: children.key,
-        parent: children.parent,
+        parent: children.parent.clone(),
+        stable_id: children.stable_id.clone(),
         template: children.template.clone(),
         root_ids: children.root_ids.clone(),
         dynamic_nodes: children.dynamic_nodes,

+ 1 - 1
packages/core/src/lazynodes.rs

@@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState};
 
 /// A concrete type provider for closures that build [`VNode`] structures.
 ///
-/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
+/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
 /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
 ///
 ///

+ 36 - 4
packages/core/src/nodes.rs

@@ -1,3 +1,4 @@
+use crate::innerlude::{ElementRef, VNodeId};
 use crate::{
     any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
 };
@@ -47,7 +48,10 @@ pub struct VNode<'a> {
     pub key: Option<&'a str>,
 
     /// When rendered, this template will be linked to its parent manually
-    pub parent: Option<ElementId>,
+    pub(crate) parent: Cell<Option<ElementRef>>,
+
+    /// The bubble id assigned to the child that we need to update and drop when diffing happens
+    pub(crate) stable_id: Cell<Option<VNodeId>>,
 
     /// The static nodes and static descriptor of the template
     pub template: Cell<Template<'static>>,
@@ -68,7 +72,8 @@ impl<'a> VNode<'a> {
     pub fn empty(cx: &'a ScopeState) -> Element<'a> {
         Some(VNode {
             key: None,
-            parent: None,
+            parent: Default::default(),
+            stable_id: Default::default(),
             root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
             dynamic_nodes: &[],
             dynamic_attrs: &[],
@@ -81,6 +86,30 @@ impl<'a> VNode<'a> {
         })
     }
 
+    /// Create a new VNode
+    pub fn new(
+        key: Option<&'a str>,
+        template: Template<'static>,
+        root_ids: bumpalo::collections::Vec<'a, ElementId>,
+        dynamic_nodes: &'a [DynamicNode<'a>],
+        dynamic_attrs: &'a [Attribute<'a>],
+    ) -> Self {
+        Self {
+            key,
+            parent: Cell::new(None),
+            stable_id: Cell::new(None),
+            template: Cell::new(template),
+            root_ids: RefCell::new(root_ids),
+            dynamic_nodes,
+            dynamic_attrs,
+        }
+    }
+
+    /// Get the stable id of this node used for bubbling events
+    pub(crate) fn stable_id(&self) -> Option<VNodeId> {
+        self.stable_id.get()
+    }
+
     /// Load a dynamic root at the given index
     ///
     /// Returns [`None`] if the root is actually a static node (Element/Text)
@@ -319,7 +348,7 @@ pub struct VComponent<'a> {
 
     /// The function pointer of the component, known at compile time
     ///
-    /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
+    /// It is possible that components get folded at compile time, so these shouldn't be really used as a key
     pub(crate) render_fn: *const (),
 
     pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
@@ -372,6 +401,8 @@ impl<'a> VText<'a> {
 pub struct VPlaceholder {
     /// The ID of this node in the real DOM
     pub(crate) id: Cell<Option<ElementId>>,
+    /// The parent of this node
+    pub(crate) parent: Cell<Option<ElementRef>>,
 }
 
 impl VPlaceholder {
@@ -722,7 +753,8 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> {
 impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
     fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
         DynamicNode::Fragment(_cx.bump().alloc([VNode {
-            parent: self.parent,
+            parent: self.parent.clone(),
+            stable_id: self.stable_id.clone(),
             template: self.template.clone(),
             root_ids: self.root_ids.clone(),
             key: self.key,

+ 11 - 7
packages/core/src/scheduler/task.rs

@@ -19,7 +19,7 @@ pub struct TaskId(pub usize);
 /// the task itself is the waker
 pub(crate) struct LocalTask {
     pub scope: ScopeId,
-    pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
+    pub task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
     pub waker: Waker,
 }
 
@@ -48,11 +48,15 @@ impl Scheduler {
             })),
         };
 
-        entry.insert(task);
+        let mut cx = std::task::Context::from_waker(&task.waker);
+
+        if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
+            self.sender
+                .unbounded_send(SchedulerMsg::TaskNotified(task_id))
+                .expect("Scheduler should exist");
+        }
 
-        self.sender
-            .unbounded_send(SchedulerMsg::TaskNotified(task_id))
-            .expect("Scheduler should exist");
+        entry.insert(task);
 
         task_id
     }
@@ -60,8 +64,8 @@ impl Scheduler {
     /// Drop the future with the given TaskId
     ///
     /// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
-    pub fn remove(&self, id: TaskId) {
-        self.tasks.borrow_mut().try_remove(id.0);
+    pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
+        self.tasks.borrow_mut().try_remove(id.0)
     }
 }
 

+ 1 - 0
packages/core/src/scope_arena.rs

@@ -36,6 +36,7 @@ impl VirtualDom {
 
             borrowed_props: Default::default(),
             attributes_to_drop_before_render: Default::default(),
+            element_refs_to_drop: Default::default(),
         }));
 
         let context =

+ 1 - 11
packages/core/src/scope_context.rs

@@ -230,17 +230,7 @@ impl ScopeContext {
     /// This is good for tasks that need to be run after the component has been dropped.
     pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
         // The root scope will never be unmounted so we can just add the task at the top of the app
-        let id = self.tasks.spawn(ScopeId::ROOT, fut);
-
-        // wake up the scheduler if it is sleeping
-        self.tasks
-            .sender
-            .unbounded_send(SchedulerMsg::TaskNotified(id))
-            .expect("Scheduler should exist");
-
-        self.spawned_tasks.borrow_mut().insert(id);
-
-        id
+        self.tasks.spawn(ScopeId::ROOT, fut)
     }
 
     /// Informs the scheduler that this task is no longer needed and should be removed.

+ 3 - 2
packages/core/src/scopes.rs

@@ -3,7 +3,7 @@ use crate::{
     any_props::VProps,
     bump_frame::BumpFrame,
     innerlude::ErrorBoundary,
-    innerlude::{DynamicNode, EventHandler, VComponent, VText},
+    innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText},
     lazynodes::LazyNodes,
     nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
     runtime::Runtime,
@@ -94,6 +94,7 @@ pub struct ScopeState {
     pub(crate) hook_idx: Cell<usize>,
 
     pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
+    pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
     pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
 
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
@@ -466,7 +467,7 @@ impl<'src> ScopeState {
             render_fn: component as *const (),
             static_props: P::IS_STATIC,
             props: RefCell::new(Some(extended)),
-            scope: Cell::new(None),
+            scope: Default::default(),
         })
     }
 

+ 84 - 76
packages/core/src/virtual_dom.rs

@@ -4,19 +4,19 @@
 
 use crate::{
     any_props::VProps,
-    arena::{ElementId, ElementRef},
-    innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
+    arena::ElementId,
+    innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::{Template, TemplateId},
     runtime::{Runtime, RuntimeGuard},
     scopes::{ScopeId, ScopeState},
-    AttributeValue, Element, Event, Scope,
+    AttributeValue, Element, Event, Scope, VNode,
 };
 use futures_util::{pin_mut, StreamExt};
 use rustc_hash::{FxHashMap, FxHashSet};
 use slab::Slab;
-use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
+use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc};
 
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
@@ -186,7 +186,10 @@ pub struct VirtualDom {
     pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
 
     // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
-    pub(crate) elements: Slab<ElementRef>,
+    pub(crate) element_refs: Slab<Option<NonNull<VNode<'static>>>>,
+
+    // The element ids that are used in the renderer
+    pub(crate) elements: Slab<Option<ElementRef>>,
 
     pub(crate) mutations: Mutations<'static>,
 
@@ -263,6 +266,7 @@ impl VirtualDom {
             dirty_scopes: Default::default(),
             templates: Default::default(),
             elements: Default::default(),
+            element_refs: Default::default(),
             mutations: Mutations::default(),
             suspended_scopes: Default::default(),
         };
@@ -276,7 +280,7 @@ impl VirtualDom {
         root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT)));
 
         // the root element is always given element ID 0 since it's the container for the entire tree
-        dom.elements.insert(ElementRef::none());
+        dom.elements.insert(None);
 
         dom
     }
@@ -314,9 +318,9 @@ impl VirtualDom {
         }
     }
 
-    /// Call a listener inside the VirtualDom with data from outside the VirtualDom.
+    /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.**
     ///
-    /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
+    /// This method will identify the appropriate element. The data must match up with the listener declared. Note that
     /// this method does not give any indication as to the success of the listener call. If the listener is not found,
     /// nothing will happen.
     ///
@@ -353,7 +357,15 @@ impl VirtualDom {
         | | |       <-- no, broke early
         |           <-- no, broke early
         */
-        let mut parent_path = self.elements.get(element.0);
+        let parent_path = match self.elements.get(element.0) {
+            Some(Some(el)) => el,
+            _ => return,
+        };
+        let mut parent_node = self
+            .element_refs
+            .get(parent_path.template.0)
+            .cloned()
+            .map(|el| (*parent_path, el));
         let mut listeners = vec![];
 
         // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
@@ -365,82 +377,81 @@ impl VirtualDom {
         // If the event bubbles, we traverse through the tree until we find the target element.
         if bubbles {
             // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
-            while let Some(el_ref) = parent_path {
+            while let Some((path, el_ref)) = parent_node {
                 // safety: we maintain references of all vnodes in the element slab
-                if let Some(template) = el_ref.template {
-                    let template = unsafe { template.as_ref() };
-                    let node_template = template.template.get();
-                    let target_path = el_ref.path;
-
-                    for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
-                        let this_path = node_template.attr_paths[idx];
-
-                        // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
-                        if attr.name.trim_start_matches("on") == name
-                            && target_path.is_decendant(&this_path)
-                        {
-                            listeners.push(&attr.value);
-
-                            // Break if this is the exact target element.
-                            // This means we won't call two listeners with the same name on the same element. This should be
-                            // documented, or be rejected from the rsx! macro outright
-                            if target_path == this_path {
-                                break;
-                            }
+                let template = unsafe { el_ref.unwrap().as_ref() };
+                let node_template = template.template.get();
+                let target_path = path.path;
+
+                for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
+                    let this_path = node_template.attr_paths[idx];
+
+                    // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
+                    if attr.name.trim_start_matches("on") == name
+                        && target_path.is_decendant(&this_path)
+                    {
+                        listeners.push(&attr.value);
+
+                        // Break if this is the exact target element.
+                        // This means we won't call two listeners with the same name on the same element. This should be
+                        // documented, or be rejected from the rsx! macro outright
+                        if target_path == this_path {
+                            break;
                         }
                     }
+                }
 
-                    // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
-                    // We check the bubble state between each call to see if the event has been stopped from bubbling
-                    for listener in listeners.drain(..).rev() {
-                        if let AttributeValue::Listener(listener) = listener {
-                            let origin = el_ref.scope;
-                            self.runtime.scope_stack.borrow_mut().push(origin);
-                            self.runtime.rendering.set(false);
-                            if let Some(cb) = listener.borrow_mut().as_deref_mut() {
-                                cb(uievent.clone());
-                            }
-                            self.runtime.scope_stack.borrow_mut().pop();
-                            self.runtime.rendering.set(true);
+                // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
+                // We check the bubble state between each call to see if the event has been stopped from bubbling
+                for listener in listeners.drain(..).rev() {
+                    if let AttributeValue::Listener(listener) = listener {
+                        let origin = path.scope;
+                        self.runtime.scope_stack.borrow_mut().push(origin);
+                        self.runtime.rendering.set(false);
+                        if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                            cb(uievent.clone());
+                        }
+                        self.runtime.scope_stack.borrow_mut().pop();
+                        self.runtime.rendering.set(true);
 
-                            if !uievent.propagates.get() {
-                                return;
-                            }
+                        if !uievent.propagates.get() {
+                            return;
                         }
                     }
-
-                    parent_path = template.parent.and_then(|id| self.elements.get(id.0));
-                } else {
-                    break;
                 }
+
+                parent_node = template.parent.get().and_then(|element_ref| {
+                    self.element_refs
+                        .get(element_ref.template.0)
+                        .cloned()
+                        .map(|el| (element_ref, el))
+                });
             }
         } else {
             // Otherwise, we just call the listener on the target element
-            if let Some(el_ref) = parent_path {
+            if let Some((path, el_ref)) = parent_node {
                 // safety: we maintain references of all vnodes in the element slab
-                if let Some(template) = el_ref.template {
-                    let template = unsafe { template.as_ref() };
-                    let node_template = template.template.get();
-                    let target_path = el_ref.path;
-
-                    for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
-                        let this_path = node_template.attr_paths[idx];
-
-                        // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
-                        // Only call the listener if this is the exact target element.
-                        if attr.name.trim_start_matches("on") == name && target_path == this_path {
-                            if let AttributeValue::Listener(listener) = &attr.value {
-                                let origin = el_ref.scope;
-                                self.runtime.scope_stack.borrow_mut().push(origin);
-                                self.runtime.rendering.set(false);
-                                if let Some(cb) = listener.borrow_mut().as_deref_mut() {
-                                    cb(uievent.clone());
-                                }
-                                self.runtime.scope_stack.borrow_mut().pop();
-                                self.runtime.rendering.set(true);
-
-                                break;
+                let template = unsafe { el_ref.unwrap().as_ref() };
+                let node_template = template.template.get();
+                let target_path = path.path;
+
+                for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
+                    let this_path = node_template.attr_paths[idx];
+
+                    // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
+                    // Only call the listener if this is the exact target element.
+                    if attr.name.trim_start_matches("on") == name && target_path == this_path {
+                        if let AttributeValue::Listener(listener) = &attr.value {
+                            let origin = path.scope;
+                            self.runtime.scope_stack.borrow_mut().push(origin);
+                            self.runtime.rendering.set(false);
+                            if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                                cb(uievent.clone());
                             }
+                            self.runtime.scope_stack.borrow_mut().pop();
+                            self.runtime.rendering.set(true);
+
+                            break;
                         }
                     }
                 }
@@ -563,7 +574,7 @@ impl VirtualDom {
             // If an error occurs, we should try to render the default error component and context where the error occured
             RenderReturn::Aborted(placeholder) => {
                 tracing::debug!("Ran into suspended or aborted scope during rebuild");
-                let id = self.next_null();
+                let id = self.next_element();
                 placeholder.id.set(Some(id));
                 self.mutations.push(Mutation::CreatePlaceholder { id });
             }
@@ -595,15 +606,12 @@ impl VirtualDom {
     /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
     pub async fn wait_for_suspense(&mut self) {
         loop {
-            // println!("waiting for suspense {:?}", self.suspended_scopes);
             if self.suspended_scopes.is_empty() {
                 return;
             }
 
-            // println!("waiting for suspense");
             self.wait_for_work().await;
 
-            // println!("Rendered immediately");
             _ = self.render_immediate();
         }
     }

+ 69 - 0
packages/core/tests/event_propagation.rs

@@ -0,0 +1,69 @@
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use std::{rc::Rc, sync::Mutex};
+
+static CLICKS: Mutex<usize> = Mutex::new(0);
+
+#[test]
+fn events_propagate() {
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+
+    // Top-level click is registered
+    dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
+    assert_eq!(*CLICKS.lock().unwrap(), 1);
+
+    // break reference....
+    for _ in 0..5 {
+        dom.mark_dirty(ScopeId(0));
+        _ = dom.render_immediate();
+    }
+
+    // Lower click is registered
+    dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
+    assert_eq!(*CLICKS.lock().unwrap(), 3);
+
+    // break reference....
+    for _ in 0..5 {
+        dom.mark_dirty(ScopeId(0));
+        _ = dom.render_immediate();
+    }
+
+    // Stop propagation occurs
+    dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
+    assert_eq!(*CLICKS.lock().unwrap(), 3);
+}
+
+fn app(cx: Scope) -> Element {
+    render! {
+        div {
+            onclick: move |_| {
+                println!("top clicked");
+                *CLICKS.lock().unwrap() += 1;
+            },
+
+            vec![
+                render! {
+                    problematic_child {}
+                }
+            ].into_iter()
+        }
+    }
+}
+
+fn problematic_child(cx: Scope) -> Element {
+    render! {
+        button {
+            onclick: move |evt| {
+                println!("bottom clicked");
+                let mut clicks = CLICKS.lock().unwrap();
+
+                if *clicks == 3 {
+                    evt.stop_propagation();
+                } else {
+                    *clicks += 1;
+                }
+            }
+        }
+    }
+}

+ 29 - 30
packages/core/tests/fuzzing.rs

@@ -2,7 +2,7 @@
 
 use dioxus::prelude::Props;
 use dioxus_core::*;
-use std::{cell::Cell, collections::HashSet};
+use std::{cfg, collections::HashSet};
 
 fn random_ns() -> Option<&'static str> {
     let namespace = rand::random::<u8>() % 2;
@@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
     let range = if depth > 5 { 1 } else { 4 };
     match rand::random::<u8>() % range {
         0 => DynamicNode::Placeholder(Default::default()),
-        1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
-            key: None,
-            parent: Default::default(),
-            template: Cell::new(Template {
-                name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
-                roots: &[TemplateNode::Dynamic { id: 0 }],
-                node_paths: &[&[0]],
-                attr_paths: &[],
-            }),
-            root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
-            dynamic_nodes: cx.bump().alloc([cx.component(
-                create_random_element,
-                DepthProps { depth, root: false },
-                "create_random_element",
-            )]),
-            dynamic_attrs: &[],
+        1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
+            VNode::new(
+                None,
+                Template {
+                    name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
+                    roots: &[TemplateNode::Dynamic { id: 0 }],
+                    node_paths: &[&[0]],
+                    attr_paths: &[],
+                },
+                bumpalo::collections::Vec::new_in(cx.bump()),
+                cx.bump().alloc([cx.component(
+                    create_random_element,
+                    DepthProps { depth, root: false },
+                    "create_random_element",
+                )]),
+                &[],
+            )
         })),
         2 => cx.component(
             create_random_element,
@@ -271,13 +272,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                 )
                 .into_boxed_str(),
             ));
-            // println!("{template:#?}");
-            let node = VNode {
-                key: None,
-                parent: None,
-                template: Cell::new(template),
-                root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
-                dynamic_nodes: {
+            let node = VNode::new(
+                None,
+                template,
+                bumpalo::collections::Vec::new_in(cx.bump()),
+                {
                     let dynamic_nodes: Vec<_> = dynamic_node_types
                         .iter()
                         .map(|ty| match ty {
@@ -291,12 +290,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                         .collect();
                     cx.bump().alloc(dynamic_nodes)
                 },
-                dynamic_attrs: cx.bump().alloc(
+                cx.bump().alloc(
                     (0..template.attr_paths.len())
                         .map(|_| create_random_dynamic_attr(cx))
                         .collect::<Vec<_>>(),
                 ),
-            };
+            );
             Some(node)
         }
         _ => None,
@@ -306,10 +305,10 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
 }
 
 // test for panics when creating random nodes and templates
-#[cfg(not(miri))]
 #[test]
 fn create() {
-    for _ in 0..1000 {
+    let repeat_count = if cfg!(miri) { 100 } else { 1000 };
+    for _ in 0..repeat_count {
         let mut vdom =
             VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
         let _ = vdom.rebuild();
@@ -318,10 +317,10 @@ fn create() {
 
 // test for panics when diffing random nodes
 // This test will change the template every render which is not very realistic, but it helps stress the system
-#[cfg(not(miri))]
 #[test]
 fn diff() {
-    for _ in 0..100000 {
+    let repeat_count = if cfg!(miri) { 100 } else { 1000 };
+    for _ in 0..repeat_count {
         let mut vdom =
             VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
         let _ = vdom.rebuild();

+ 2 - 2
packages/desktop/Cargo.toml

@@ -18,8 +18,8 @@ dioxus-hot-reload = { workspace = true, optional = true }
 serde = "1.0.136"
 serde_json = "1.0.79"
 thiserror = { workspace = true }
-wry = { version = "0.33.0", default-features = false, features = ["protocol", "file-drop", "tao"] }
 tracing = { workspace = true }
+wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
 futures-channel = { workspace = true }
 tokio = { workspace = true, features = [
     "sync",
@@ -42,7 +42,7 @@ crossbeam-channel = "0.5.8"
 
 
 [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
-rfd = "0.11.3"
+rfd = "0.12"
 global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
 
 [target.'cfg(target_os = "ios")'.dependencies]

+ 21 - 18
packages/desktop/headless_tests/events.rs

@@ -232,6 +232,12 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
                     received_events.modify(|x| *x + 1)
                 },
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Primary),
+                    );
+                    recieved_events.modify(|x| *x + 1)
+                }
             }
             div {
                 id: "mouse_move_div",
@@ -240,7 +246,7 @@ fn app(cx: Scope) -> Element {
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
                     received_events.modify(|x| *x + 1)
-                },
+                }
             }
             div {
                 id: "mouse_click_div",
@@ -250,11 +256,11 @@ fn app(cx: Scope) -> Element {
                     assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
                     assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
                     received_events.modify(|x| *x + 1)
-                },
+                }
             }
-            div{
+            div {
                 id: "mouse_dblclick_div",
-                ondblclick: move |event| {
+                ondoubleclick: move |event| {
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
@@ -263,7 +269,7 @@ fn app(cx: Scope) -> Element {
                     received_events.modify(|x| *x + 1)
                 }
             }
-            div{
+            div {
                 id: "mouse_down_div",
                 onmousedown: move |event| {
                     println!("{:?}", event.data);
@@ -273,7 +279,7 @@ fn app(cx: Scope) -> Element {
                     received_events.modify(|x| *x + 1)
                 }
             }
-            div{
+            div {
                 id: "mouse_up_div",
                 onmouseup: move |event| {
                     println!("{:?}", event.data);
@@ -283,21 +289,20 @@ fn app(cx: Scope) -> Element {
                     received_events.modify(|x| *x + 1)
                 }
             }
-            div{
+            div {
                 id: "wheel_div",
                 width: "100px",
                 height: "100px",
                 background_color: "red",
                 onwheel: move |event| {
                     println!("{:?}", event.data);
-                    let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
-                        panic!("Expected delta to be in pixels")
-                    };
+                    let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
+                    panic!("Expected delta to be in pixels") };
                     assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
                     received_events.modify(|x| *x + 1)
                 }
             }
-            input{
+            input {
                 id: "key_down_div",
                 onkeydown: move |event| {
                     println!("{:?}", event.data);
@@ -306,11 +311,11 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert!(event.data.is_auto_repeating());
-
                     received_events.modify(|x| *x + 1)
+
                 }
             }
-            input{
+            input {
                 id: "key_up_div",
                 onkeyup: move |event| {
                     println!("{:?}", event.data);
@@ -319,11 +324,10 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert!(!event.data.is_auto_repeating());
-
                     received_events.modify(|x| *x + 1)
                 }
             }
-            input{
+            input {
                 id: "key_press_div",
                 onkeypress: move |event| {
                     println!("{:?}", event.data);
@@ -332,18 +336,17 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert!(!event.data.is_auto_repeating());
-
                     received_events.modify(|x| *x + 1)
                 }
             }
-            input{
+            input {
                 id: "focus_in_div",
                 onfocusin: move |event| {
                     println!("{:?}", event.data);
                     received_events.modify(|x| *x + 1)
                 }
             }
-            input{
+            input {
                 id: "focus_out_div",
                 onfocusout: move |event| {
                     println!("{:?}", event.data);

+ 10 - 0
packages/desktop/src/cfg.rs

@@ -35,6 +35,7 @@ pub struct Config {
     pub(crate) root_name: String,
     pub(crate) background_color: Option<(u8, u8, u8, u8)>,
     pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
+    pub(crate) enable_default_menu_bar: bool,
 }
 
 type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@@ -64,9 +65,18 @@ impl Config {
             root_name: "main".to_string(),
             background_color: None,
             last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
+            enable_default_menu_bar: true,
         }
     }
 
+    /// Set whether the default menu bar should be enabled.
+    ///
+    /// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`.
+    pub fn with_default_menu_bar(mut self, enable: bool) -> Self {
+        self.enable_default_menu_bar = enable;
+        self
+    }
+
     /// set the directory from which assets will be searched in release mode
     pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
         self.resource_dir = Some(path.into());

+ 20 - 0
packages/desktop/src/desktop_context.rs

@@ -1,7 +1,10 @@
 use crate::create_new_window;
 use crate::events::IpcMessage;
+use crate::protocol::AssetFuture;
+use crate::protocol::AssetHandlerRegistry;
 use crate::query::QueryEngine;
 use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
+use crate::AssetHandler;
 use crate::Config;
 use crate::WebviewHandler;
 use dioxus_core::ScopeState;
@@ -114,6 +117,7 @@ pub struct DesktopService {
     pub(crate) max_template_count: AtomicU16,
 
     pub(crate) channel: RefCell<Channel>,
+    pub(crate) asset_handlers: AssetHandlerRegistry,
 
     #[cfg(target_os = "ios")]
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -140,6 +144,7 @@ impl DesktopService {
         event_handlers: WindowEventHandlers,
         shortcut_manager: ShortcutRegistry,
         edit_queue: EditQueue,
+        asset_handlers: AssetHandlerRegistry,
     ) -> Self {
         Self {
             webview: Rc::new(webview),
@@ -153,6 +158,7 @@ impl DesktopService {
             templates: Default::default(),
             max_template_count: Default::default(),
             channel: Default::default(),
+            asset_handlers,
             #[cfg(target_os = "ios")]
             views: Default::default(),
         }
@@ -303,6 +309,20 @@ impl DesktopService {
         self.shortcut_manager.remove_all()
     }
 
+    /// Provide a callback to handle asset loading yourself.
+    ///
+    /// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
+    pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
+        self.asset_handlers.register_handler(f).await
+    }
+
+    /// Removes an asset handler by its identifier.
+    ///
+    /// Returns `None` if the handler did not exist.
+    pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
+        self.asset_handlers.remove_handler(id).await
+    }
+
     /// Push an objc view to the window
     #[cfg(target_os = "ios")]
     pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {

+ 5 - 1
packages/desktop/src/lib.rs

@@ -34,6 +34,7 @@ use element::DesktopElement;
 use eval::init_eval;
 use futures_util::{pin_mut, FutureExt};
 use rustc_hash::FxHashMap;
+pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
 use shortcut::ShortcutRegistry;
 pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
 use std::cell::Cell;
@@ -48,6 +49,7 @@ use tao::{
     event::{Event, StartCause, WindowEvent},
     event_loop::ControlFlow,
 };
+pub use webview::build_default_menu_bar;
 pub use wry;
 pub use wry::application as tao;
 use wry::application::event_loop::EventLoopBuilder;
@@ -401,7 +403,8 @@ fn create_new_window(
     event_handlers: &WindowEventHandlers,
     shortcut_manager: ShortcutRegistry,
 ) -> WebviewHandler {
-    let (webview, web_context, edit_queue) = webview::build(&mut cfg, event_loop, proxy.clone());
+    let (webview, web_context, asset_handlers, edit_queue) =
+        webview::build(&mut cfg, event_loop, proxy.clone());
     let desktop_context = Rc::from(DesktopService::new(
         webview,
         proxy.clone(),
@@ -409,6 +412,7 @@ fn create_new_window(
         queue.clone(),
         event_handlers.clone(),
         shortcut_manager,
+        asset_handlers,
         edit_queue,
     ));
 

+ 195 - 9
packages/desktop/src/protocol.rs

@@ -1,18 +1,58 @@
+use dioxus_core::ScopeState;
+use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
+use slab::Slab;
 use std::{
     borrow::Cow,
+    future::Future,
+    ops::Deref,
     path::{Path, PathBuf},
+    pin::Pin,
+    rc::Rc,
+    sync::Arc,
+};
+use tokio::{
+    runtime::Handle,
+    sync::{OnceCell, RwLock},
 };
 use wry::{
     http::{status::StatusCode, Request, Response},
     webview::RequestAsyncResponder,
     Result,
 };
+use crate::{use_window, DesktopContext};
 
 use crate::desktop_context::EditQueue;
 
 static MINIFIED: &str = include_str!("./minified.js");
 
 fn module_loader(root_name: &str, headless: bool) -> String {
+    let js = INTERPRETER_JS.replace(
+        "/*POST_HANDLE_EDITS*/",
+        r#"// Prevent file inputs from opening the file dialog on click
+    let inputs = document.querySelectorAll("input");
+    for (let input of inputs) {
+      if (!input.getAttribute("data-dioxus-file-listener")) {
+        // prevent file inputs from opening the file dialog on click
+        const type = input.getAttribute("type");
+        if (type === "file") {
+          input.setAttribute("data-dioxus-file-listener", true);
+          input.addEventListener("click", (event) => {
+            let target = event.target;
+            let target_id = find_real_id(target);
+            if (target_id !== null) {
+              const send = (event_name) => {
+                const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
+                window.ipc.postMessage(message);
+              };
+              send("change&input");
+            }
+            event.preventDefault();
+          });
+        }
+      }
+    }"#,
+    );
+  
     format!(
         r#"
 <script type="module">
@@ -32,15 +72,158 @@ fn module_loader(root_name: &str, headless: bool) -> String {
     )
 }
 
-pub(super) fn desktop_handler(
-    request: &Request<Vec<u8>>,
-    responder: RequestAsyncResponder,
+/// An arbitrary asset is an HTTP response containing a binary body.
+pub type AssetResponse = Response<Cow<'static, [u8]>>;
+
+/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
+/// so it must be [`Send`], [`Sync`], and `'static`.
+pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
+impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
+
+#[derive(Debug, Clone)]
+/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
+pub struct AssetRequest {
+    path: PathBuf,
+    request: Arc<Request<Vec<u8>>>,
+}
+
+impl AssetRequest {
+    /// Get the path the asset request is for
+    pub fn path(&self) -> &Path {
+        &self.path
+    }
+}
+
+impl From<Request<Vec<u8>>> for AssetRequest {
+    fn from(request: Request<Vec<u8>>) -> Self {
+        let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
+            .expect("expected URL to be UTF-8 encoded");
+        let path = PathBuf::from(&*decoded);
+        Self {
+            request: Arc::new(request),
+            path,
+        }
+    }
+}
+
+impl Deref for AssetRequest {
+    type Target = Request<Vec<u8>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.request
+    }
+}
+
+/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
+/// This handler is stashed indefinitely in a context object, so it must be `'static`.
+pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
+    /// Handle an asset request, returning a future that either loads the asset, or returns `None`
+    fn handle_request(&self, request: &AssetRequest) -> F;
+}
+
+impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
+    fn handle_request(&self, request: &AssetRequest) -> F {
+        self(request)
+    }
+}
+
+type AssetHandlerRegistryInner =
+    Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
+
+#[derive(Clone)]
+pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
+
+impl AssetHandlerRegistry {
+    pub fn new() -> Self {
+        AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
+    }
+
+    pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
+        let mut registry = self.0.write().await;
+        registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
+    }
+
+    pub async fn remove_handler(&self, id: usize) -> Option<()> {
+        let mut registry = self.0.write().await;
+        registry.try_remove(id).map(|_| ())
+    }
+
+    pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
+        let registry = self.0.read().await;
+        for (_, handler) in registry.iter() {
+            if let Some(response) = handler(req).await {
+                return Some(response);
+            }
+        }
+        None
+    }
+}
+
+/// A handle to a registered asset handler.
+pub struct AssetHandlerHandle {
+    desktop: DesktopContext,
+    handler_id: Rc<OnceCell<usize>>,
+}
+
+impl AssetHandlerHandle {
+    /// Returns the ID for this handle.
+    ///
+    /// Because registering an ID is asynchronous, this may return `None` if the
+    /// registration has not completed yet.
+    pub fn handler_id(&self) -> Option<usize> {
+        self.handler_id.get().copied()
+    }
+}
+
+impl Drop for AssetHandlerHandle {
+    fn drop(&mut self) {
+        let cell = Rc::clone(&self.handler_id);
+        let desktop = Rc::clone(&self.desktop);
+        tokio::task::block_in_place(move || {
+            Handle::current().block_on(async move {
+                if let Some(id) = cell.get() {
+                    desktop.asset_handlers.remove_handler(*id).await;
+                }
+            })
+        });
+    }
+}
+
+/// Provide a callback to handle asset loading yourself.
+///
+/// The callback takes a path as requested by the web view, and it should return `Some(response)`
+/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
+pub fn use_asset_handler<F: AssetFuture>(
+    cx: &ScopeState,
+    handler: impl AssetHandler<F>,
+) -> &AssetHandlerHandle {
+    let desktop = Rc::clone(use_window(cx));
+    cx.use_hook(|| {
+        let handler_id = Rc::new(OnceCell::new());
+        let handler_id_ref = Rc::clone(&handler_id);
+        let desktop_ref = Rc::clone(&desktop);
+        cx.push_future(async move {
+            let id = desktop.asset_handlers.register_handler(handler).await;
+            handler_id.set(id).unwrap();
+        });
+        AssetHandlerHandle {
+            desktop: desktop_ref,
+            handler_id: handler_id_ref,
+        }
+    })
+}
+
+pub(super) async fn desktop_handler(
+    request: Request<Vec<u8>>,
     custom_head: Option<String>,
     custom_index: Option<String>,
     root_name: &str,
+    asset_handlers: &AssetHandlerRegistry,
     edit_queue: &EditQueue,
     headless: bool,
-) {
+) -> Result<AssetResponse> {
+    let request = AssetRequest::from(request);
+
     // If the request is for the root, we'll serve the index.html file.
     if request.uri().path() == "/" {
         // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
@@ -86,18 +269,21 @@ pub(super) fn desktop_handler(
         return;
     }
 
+    // If the user provided a custom asset handler, then call it and return the response
+    // if the request was handled.
+    if let Some(response) = asset_handlers.try_handlers(&request).await {
+        return Ok(response);
+    }
+
     // Else, try to serve a file from the filesystem.
-    let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
-        .expect("expected URL to be UTF-8 encoded");
-    let path = PathBuf::from(&*decoded);
 
     // If the path is relative, we'll try to serve it from the assets directory.
     let mut asset = get_asset_root()
         .unwrap_or_else(|| Path::new(".").to_path_buf())
-        .join(&path);
+        .join(&request.path);
 
     if !asset.exists() {
-        asset = PathBuf::from("/").join(path);
+        asset = PathBuf::from("/").join(request.path);
     }
 
     if asset.exists() {

+ 93 - 13
packages/desktop/src/webview.rs

@@ -1,17 +1,19 @@
 use crate::desktop_context::{EditQueue, EventData};
-use crate::protocol;
+use crate::protocol::{self, AssetHandlerRegistry};
 use crate::{desktop_context::UserWindowEvent, Config};
 use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
 pub use wry;
 pub use wry::application as tao;
+use wry::application::menu::{MenuBar, MenuItem};
 use wry::application::window::Window;
+use wry::http::Response;
 use wry::webview::{WebContext, WebView, WebViewBuilder};
 
 pub(crate) fn build(
     cfg: &mut Config,
     event_loop: &EventLoopWindowTarget<UserWindowEvent>,
     proxy: EventLoopProxy<UserWindowEvent>,
-) -> (WebView, WebContext, EditQueue) {
+) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
     let builder = cfg.window.clone();
     let window = builder.with_visible(false).build(event_loop).unwrap();
     let file_handler = cfg.file_drop_handler.take();
@@ -19,6 +21,12 @@ pub(crate) fn build(
     let index_file = cfg.custom_index.clone();
     let root_name = cfg.root_name.clone();
 
+    if cfg.enable_default_menu_bar {
+        builder = builder.with_menu(build_default_menu_bar());
+    }
+
+    let window = builder.with_visible(false).build(event_loop).unwrap();
+
     // We assume that if the icon is None in cfg, then the user just didnt set it
     if cfg.window.window.window_icon.is_none() {
         window.set_window_icon(Some(
@@ -34,6 +42,8 @@ pub(crate) fn build(
     let mut web_context = WebContext::new(cfg.data_dir.clone());
     let edit_queue = EditQueue::default();
     let headless = !cfg.window.window.visible;
+    let asset_handlers = AssetHandlerRegistry::new();
+    let asset_handlers_ref = asset_handlers.clone();
 
     let mut webview = WebViewBuilder::new(window)
         .unwrap()
@@ -46,19 +56,22 @@ pub(crate) fn build(
                 _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
             }
         })
-        .with_asynchronous_custom_protocol("dioxus".into(), {
-            let edit_queue = edit_queue.clone();
-            move |r, responder| {
-                protocol::desktop_handler(
-                    &r,
-                    responder,
+        .with_asynchronous_custom_protocol(String::from("dioxus"), move |request, responder| {
+            let custom_head = custom_head.clone();
+            let index_file = index_file.clone();
+            let root_name = root_name.clone();
+            let asset_handlers_ref = asset_handlers_ref.clone();
+            tokio::spawn(async move {
+                let response_res = protocol::desktop_handler(
+                    request,
                     custom_head.clone(),
                     index_file.clone(),
                     &root_name,
-                    &edit_queue,
-                    headless,
+                    &asset_handlers_ref,
                 )
-            }
+                .await;
+                responder.respond(response);
+            });
         })
         .with_file_drop_handler(move |window, evet| {
             file_handler
@@ -84,7 +97,16 @@ pub(crate) fn build(
     // .with_web_context(&mut web_context);
 
     for (name, handler) in cfg.protocols.drain(..) {
-        webview = webview.with_custom_protocol(name, handler)
+        webview = webview.with_custom_protocol(name, move |r| match handler(&r) {
+            Ok(response) => response,
+            Err(err) => {
+                tracing::error!("Error: {}", err);
+                Response::builder()
+                    .status(500)
+                    .body(err.to_string().into_bytes().into())
+                    .unwrap()
+            }
+        })
     }
 
     if cfg.disable_context_menu {
@@ -107,5 +129,63 @@ pub(crate) fn build(
         webview = webview.with_devtools(true);
     }
 
-    (webview.build().unwrap(), web_context, edit_queue)
+    (webview.build().unwrap(), web_context, asset_handlers, edit_queue)
+}
+
+/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
+/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
+/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
+/// > The menu bar differs per platform because of constraints introduced
+/// > by [`MenuItem`](tao::menu::MenuItem).
+pub fn build_default_menu_bar() -> MenuBar {
+    let mut menu_bar = MenuBar::new();
+
+    // since it is uncommon on windows to have an "application menu"
+    // we add a "window" menu to be more consistent across platforms with the standard menu
+    let mut window_menu = MenuBar::new();
+    #[cfg(target_os = "macos")]
+    {
+        window_menu.add_native_item(MenuItem::EnterFullScreen);
+        window_menu.add_native_item(MenuItem::Zoom);
+        window_menu.add_native_item(MenuItem::Separator);
+    }
+
+    window_menu.add_native_item(MenuItem::Hide);
+
+    #[cfg(target_os = "macos")]
+    {
+        window_menu.add_native_item(MenuItem::HideOthers);
+        window_menu.add_native_item(MenuItem::ShowAll);
+    }
+
+    window_menu.add_native_item(MenuItem::Minimize);
+    window_menu.add_native_item(MenuItem::CloseWindow);
+    window_menu.add_native_item(MenuItem::Separator);
+    window_menu.add_native_item(MenuItem::Quit);
+    menu_bar.add_submenu("Window", true, window_menu);
+
+    // since tao supports none of the below items on linux we should only add them on macos/windows
+    #[cfg(not(target_os = "linux"))]
+    {
+        let mut edit_menu = MenuBar::new();
+        #[cfg(target_os = "macos")]
+        {
+            edit_menu.add_native_item(MenuItem::Undo);
+            edit_menu.add_native_item(MenuItem::Redo);
+            edit_menu.add_native_item(MenuItem::Separator);
+        }
+
+        edit_menu.add_native_item(MenuItem::Cut);
+        edit_menu.add_native_item(MenuItem::Copy);
+        edit_menu.add_native_item(MenuItem::Paste);
+
+        #[cfg(target_os = "macos")]
+        {
+            edit_menu.add_native_item(MenuItem::Separator);
+            edit_menu.add_native_item(MenuItem::SelectAll);
+        }
+        menu_bar.add_submenu("Edit", true, edit_menu);
+    }
+
+    menu_bar
 }

+ 6 - 14
packages/dioxus-tui/examples/all_terminal_events.rs

@@ -37,7 +37,7 @@ fn app(cx: Scope) -> Element {
         // todo: remove
         let mut trimmed = format!("{event:?}");
         trimmed.truncate(200);
-        rsx!(p { "{trimmed}" })
+        rsx!( p { "{trimmed}" } )
     });
 
     let log_event = move |event: Event| {
@@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element {
     };
 
     cx.render(rsx! {
-        div {
-            width: "100%",
-            height: "100%",
-            flex_direction: "column",
+        div { width: "100%", height: "100%", flex_direction: "column",
             div {
                 width: "80%",
                 height: "50%",
@@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element {
 
                 onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
                 onclick: move |event| log_event(Event::MouseClick(event.inner().clone())),
-                ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
+                ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
                 onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
                 onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())),
 
@@ -73,13 +70,8 @@ fn app(cx: Scope) -> Element {
                 onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
 
                 "Hover, click, type or scroll to see the info down below"
-            },
-            div {
-                width: "80%",
-                height: "50%",
-                flex_direction: "column",
-                events_rendered,
-            },
-        },
+            }
+            div { width: "80%", height: "50%", flex_direction: "column", events_rendered }
+        }
     })
 }

+ 1 - 1
packages/fullstack/Cargo.toml

@@ -26,7 +26,7 @@ tower = { version = "0.4.13", features = ["util"], optional = true }
 axum-macros = "0.3.7"
 
 # salvo
-salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] }
+salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
 serde = "1.0.159"
 
 # Dioxus + SSR

+ 1 - 1
packages/fullstack/examples/salvo-hello-world/Cargo.toml

@@ -12,7 +12,7 @@ dioxus = { workspace = true }
 dioxus-fullstack = { workspace = true }
 tokio = { workspace = true, features = ["full"], optional = true }
 serde = "1.0.159"
-salvo = { version = "0.37.9", optional = true }
+salvo = { version = "0.63.0", optional = true }
 execute = "0.2.12"
 reqwest = "0.11.18"
 simple_logger = "4.2.0"

+ 2 - 1
packages/fullstack/src/hooks/server_cached.rs

@@ -12,9 +12,10 @@ use serde::{de::DeserializeOwned, Serialize};
 /// use dioxus_fullstack::prelude::*;
 ///
 /// fn app(cx: Scope) -> Element {
-///    let state1 = use_state(cx, || from_server(|| {
+///    let state1 = use_state(cx, || server_cached(|| {
 ///       1234
 ///    }));
+///    todo!()
 /// }
 /// ```
 pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {

+ 4 - 2
packages/generational-box/src/lib.rs

@@ -700,14 +700,16 @@ impl Owner {
     /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
     pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
         let location = self.store.claim();
-        GenerationalBox {
+        let key = GenerationalBox {
             raw: location,
             #[cfg(any(debug_assertions, feature = "check_generation"))]
             generation: location.0.generation.get(),
             #[cfg(any(debug_assertions, feature = "debug_ownership"))]
             created_at: std::panic::Location::caller(),
             _marker: PhantomData,
-        }
+        };
+        self.owned.borrow_mut().push(location);
+        key
     }
 }
 

+ 8 - 10
packages/hooks/src/use_effect.rs

@@ -99,9 +99,8 @@ where
         // Create the new future
         let return_value = future(dependencies.out());
 
-        if let Some(task) = return_value.apply(state.cleanup.clone(), cx) {
-            state.task.set(Some(task));
-        }
+        let task = return_value.apply(state.cleanup.clone(), cx);
+        state.task.set(Some(task));
     }
 }
 
@@ -109,15 +108,15 @@ type UseEffectCleanup = Rc<RefCell<Option<Box<dyn FnOnce()>>>>;
 
 /// Something that can be returned from a `use_effect` hook.
 pub trait UseEffectReturn<T> {
-    fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId>;
+    fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId;
 }
 
 impl<T> UseEffectReturn<()> for T
 where
     T: Future<Output = ()> + 'static,
 {
-    fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
-        Some(cx.push_future(self))
+    fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> TaskId {
+        cx.push_future(self)
     }
 }
 
@@ -128,12 +127,11 @@ where
     T: Future<Output = F> + 'static,
     F: FnOnce() + 'static,
 {
-    fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
-        let task = cx.push_future(async move {
+    fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId {
+        cx.push_future(async move {
             let cleanup = self.await;
             *oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box<dyn FnOnce()>);
-        });
-        Some(task)
+        })
     }
 }
 

+ 2 - 1
packages/html/Cargo.toml

@@ -21,7 +21,7 @@ keyboard-types = "0.7"
 async-trait = "0.1.58"
 serde-value = "0.7.0"
 tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
-rfd = { version = "0.11.3", optional = true }
+rfd = { version = "0.12", optional = true }
 async-channel = "1.8.0"
 serde_json = { version = "1", optional = true }
 
@@ -68,3 +68,4 @@ mounted = [
 wasm-bind = ["web-sys", "wasm-bindgen"]
 native-bind = ["tokio"]
 hot-reload-context = ["dioxus-rsx"]
+html-to-rsx = []

+ 53 - 3
packages/html/src/elements.rs

@@ -79,6 +79,25 @@ macro_rules! impl_attribute_match {
     };
 }
 
+#[cfg(feature = "html-to-rsx")]
+macro_rules! impl_html_to_rsx_attribute_match {
+    (
+        $attr:ident $fil:ident $name:literal
+    ) => {
+        if $attr == $name {
+            return Some(stringify!($fil));
+        }
+    };
+
+    (
+        $attr:ident $fil:ident $_:tt
+    ) => {
+        if $attr == stringify!($fil) {
+            return Some(stringify!($fil));
+        }
+    };
+}
+
 macro_rules! impl_element {
     (
         $(#[$attr:meta])*
@@ -316,6 +335,38 @@ macro_rules! builder_constructors {
             }
         }
 
+        #[cfg(feature = "html-to-rsx")]
+        pub fn map_html_attribute_to_rsx(html: &str) -> Option<&'static str> {
+            $(
+                $(
+                    impl_html_to_rsx_attribute_match!(
+                        html $fil $extra
+                    );
+                )*
+            )*
+
+            if let Some(name) = crate::map_html_global_attributes_to_rsx(html) {
+                return Some(name);
+            }
+
+            if let Some(name) = crate::map_html_svg_attributes_to_rsx(html) {
+                return Some(name);
+            }
+
+            None
+        }
+
+        #[cfg(feature = "html-to-rsx")]
+        pub fn map_html_element_to_rsx(html: &str) -> Option<&'static str> {
+            $(
+                if html == stringify!($name) {
+                    return Some(stringify!($name));
+                }
+            )*
+
+            None
+        }
+
         $(
             impl_element!(
                 $(#[$attr])*
@@ -998,9 +1049,8 @@ builder_constructors! {
         src: Uri DEFAULT,
         text: String DEFAULT,
 
-        // r#async: Bool,
-        // r#type: String, // TODO could be an enum
-        r#type: String "type",
+        r#async: Bool "async",
+        r#type: String "type", // TODO could be an enum
         r#script: String "script",
     };
 

+ 17 - 4
packages/html/src/events/mouse.rs

@@ -119,10 +119,7 @@ impl_event! {
     /// oncontextmenu
     oncontextmenu
 
-    /// ondoubleclick
-    ondoubleclick
-
-    /// ondoubleclick
+    #[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
     ondblclick
 
     /// onmousedown
@@ -149,6 +146,22 @@ impl_event! {
     onmouseup
 }
 
+/// ondoubleclick
+#[inline]
+pub fn ondoubleclick<'a, E: crate::EventReturn<T>, T>(
+    _cx: &'a ::dioxus_core::ScopeState,
+    mut _f: impl FnMut(::dioxus_core::Event<MouseData>) -> E + 'a,
+) -> ::dioxus_core::Attribute<'a> {
+    ::dioxus_core::Attribute::new(
+        "ondblclick",
+        _cx.listener(move |e: ::dioxus_core::Event<MouseData>| {
+            _f(e).spawn(_cx);
+        }),
+        None,
+        false,
+    )
+}
+
 impl MouseData {
     /// Construct MouseData with the specified properties
     ///

+ 46 - 0
packages/html/src/global_attributes.rs

@@ -33,12 +33,44 @@ macro_rules! trait_method_mapping {
     };
 }
 
+#[cfg(feature = "html-to-rsx")]
+macro_rules! html_to_rsx_attribute_mapping {
+    (
+        $matching:ident;
+        $(#[$attr:meta])*
+        $name:ident;
+    ) => {
+        if $matching == stringify!($name) {
+            return Some(stringify!($name));
+        }
+    };
+    (
+        $matching:ident;
+        $(#[$attr:meta])*
+        $name:ident: $lit:literal;
+    ) => {
+        if $matching == stringify!($lit) {
+            return Some(stringify!($name));
+        }
+    };
+    (
+        $matching:ident;
+        $(#[$attr:meta])*
+        $name:ident: $lit:literal, $ns:literal;
+    ) => {
+        if $matching == stringify!($lit) {
+            return Some(stringify!($name));
+        }
+    };
+}
+
 macro_rules! trait_methods {
     (
         @base
         $(#[$trait_attr:meta])*
         $trait:ident;
         $fn:ident;
+        $fn_html_to_rsx:ident;
         $(
             $(#[$attr:meta])*
             $name:ident $(: $($arg:literal),*)*;
@@ -62,6 +94,18 @@ macro_rules! trait_methods {
             )*
             None
         }
+
+        #[cfg(feature = "html-to-rsx")]
+        #[doc = "Converts an HTML attribute to an RSX attribute"]
+        pub(crate) fn $fn_html_to_rsx(html: &str) -> Option<&'static str> {
+            $(
+                html_to_rsx_attribute_mapping! {
+                    html;
+                    $name$(: $($arg),*)*;
+                }
+            )*
+            None
+        }
     };
 
     // Rename the incoming ident and apply a custom namespace
@@ -79,6 +123,7 @@ trait_methods! {
 
     GlobalAttributes;
     map_global_attributes;
+    map_html_global_attributes_to_rsx;
 
     /// Prevent the default action for this element.
     ///
@@ -1593,6 +1638,7 @@ trait_methods! {
     @base
     SvgAttributes;
     map_svg_attributes;
+    map_html_svg_attributes_to_rsx;
 
     /// Prevent the default action for this element.
     ///

+ 2 - 0
packages/html/src/lib.rs

@@ -19,6 +19,8 @@
 mod elements;
 #[cfg(feature = "hot-reload-context")]
 pub use elements::HtmlCtx;
+#[cfg(feature = "html-to-rsx")]
+pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 pub mod events;
 pub mod geometry;
 mod global_attributes;

+ 13 - 2
packages/liveview/Cargo.toml

@@ -35,10 +35,14 @@ warp = { version = "0.3.3", optional = true }
 axum = { version = "0.6.1", optional = true, features = ["ws"] }
 
 # salvo
-salvo = { version = "0.44.1", optional = true, features = ["ws"] }
+salvo = { version = "0.63.0", optional = true, features = ["websocket"] }
 once_cell = "1.17.1"
 async-trait = "0.1.71"
 
+# rocket
+rocket = { version = "0.5.0", optional = true }
+rocket_ws = { version = "0.1.0", optional = true }
+
 # actix is ... complicated?
 # actix-files = { version = "0.6.2", optional = true }
 # actix-web = { version = "4.2.1", optional = true }
@@ -50,7 +54,9 @@ tokio = { workspace = true, features = ["full"] }
 dioxus = { workspace = true }
 warp = "0.3.3"
 axum = { version = "0.6.1", features = ["ws"] }
-salvo = { version = "0.44.1", features = ["affix", "ws"] }
+salvo = { version = "0.63.0", features = ["affix", "websocket"] }
+rocket = "0.5.0"
+rocket_ws = "0.1.0"
 tower = "0.4.13"
 
 [build-dependencies]
@@ -61,6 +67,7 @@ minify-js = "0.5.6"
 default = ["hot-reload"]
 # actix = ["actix-files", "actix-web", "actix-ws"]
 hot-reload = ["dioxus-hot-reload"]
+rocket = ["dep:rocket", "dep:rocket_ws"]
 
 [[example]]
 name = "axum"
@@ -77,3 +84,7 @@ required-features = ["salvo"]
 [[example]]
 name = "warp"
 required-features = ["warp"]
+
+[[example]]
+name = "rocket"
+required-features = ["rocket"]

+ 1 - 0
packages/liveview/README.md

@@ -28,6 +28,7 @@ The current backend frameworks supported include:
 - Axum
 - Warp
 - Salvo
+- Rocket
 
 Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
 

+ 76 - 0
packages/liveview/examples/rocket.rs

@@ -0,0 +1,76 @@
+#[macro_use]
+extern crate rocket;
+
+use dioxus::prelude::*;
+use dioxus_liveview::LiveViewPool;
+use rocket::response::content::RawHtml;
+use rocket::{Config, Rocket, State};
+use rocket_ws::{Channel, WebSocket};
+
+fn app(cx: Scope) -> Element {
+    let mut num = use_state(cx, || 0);
+
+    cx.render(rsx! {
+        div {
+            "hello Rocket! {num}"
+            button { onclick: move |_| num += 1, "Increment" }
+        }
+    })
+}
+
+fn index_page_with_glue(glue: &str) -> RawHtml<String> {
+    RawHtml(format!(
+        r#"
+        <!DOCTYPE html>
+        <html>
+            <head> <title>Dioxus LiveView with Rocket</title>  </head>
+            <body> <div id="main"></div> </body>
+            {glue}
+        </html>
+        "#,
+        glue = glue
+    ))
+}
+
+#[get("/")]
+async fn index(config: &Config) -> RawHtml<String> {
+    index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!(
+        "ws://{addr}:{port}/ws",
+        addr = config.address,
+        port = config.port,
+    )))
+}
+
+#[get("/as-path")]
+async fn as_path() -> RawHtml<String> {
+    index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws"))
+}
+
+#[get("/ws")]
+fn ws(ws: WebSocket, pool: &State<LiveViewPool>) -> Channel<'static> {
+    let pool = pool.inner().to_owned();
+
+    ws.channel(move |stream| {
+        Box::pin(async move {
+            let _ = pool
+                .launch(dioxus_liveview::rocket_socket(stream), app)
+                .await;
+            Ok(())
+        })
+    })
+}
+
+#[tokio::main]
+async fn main() {
+    let view = dioxus_liveview::LiveViewPool::new();
+
+    Rocket::build()
+        .manage(view)
+        .mount("/", routes![index, as_path, ws])
+        .ignite()
+        .await
+        .expect("Failed to ignite rocket")
+        .launch()
+        .await
+        .expect("Failed to launch rocket");
+}

+ 25 - 0
packages/liveview/src/adapters/rocket_adapter.rs

@@ -0,0 +1,25 @@
+use crate::{LiveViewError, LiveViewSocket};
+use rocket::futures::{SinkExt, StreamExt};
+use rocket_ws::{result::Error, stream::DuplexStream, Message};
+
+/// Convert a rocket websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the rocket web framework
+pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
+    stream
+        .map(transform_rx)
+        .with(transform_tx)
+        .sink_map_err(|_| LiveViewError::SendingFailed)
+}
+
+fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
+    message
+        .map_err(|_| LiveViewError::SendingFailed)?
+        .into_text()
+        .map(|s| s.into_bytes())
+        .map_err(|_| LiveViewError::SendingFailed)
+}
+
+async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
+    Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
+}

+ 5 - 0
packages/liveview/src/lib.rs

@@ -18,6 +18,11 @@ pub mod adapters {
 
     #[cfg(feature = "salvo")]
     pub use salvo_adapter::*;
+
+    #[cfg(feature = "rocket")]
+    pub mod rocket_adapter;
+    #[cfg(feature = "rocket")]
+    pub use rocket_adapter::*;
 }
 
 pub use adapters::*;

+ 24 - 26
packages/native-core/tests/fuzzing.rs

@@ -3,7 +3,6 @@ use dioxus_core::*;
 use dioxus_native_core::prelude::*;
 use dioxus_native_core_macro::partial_derive_state;
 use shipyard::Component;
-use std::cell::Cell;
 
 fn random_ns() -> Option<&'static str> {
     let namespace = rand::random::<u8>() % 2;
@@ -178,22 +177,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
     let range = if depth > 3 { 1 } else { 3 };
     match rand::random::<u8>() % range {
         0 => DynamicNode::Placeholder(Default::default()),
-        1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
-            key: None,
-            parent: Default::default(),
-            template: Cell::new(Template {
-                name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
-                roots: &[TemplateNode::Dynamic { id: 0 }],
-                node_paths: &[&[0]],
-                attr_paths: &[],
-            }),
-            root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
-            dynamic_nodes: cx.bump().alloc([cx.component(
-                create_random_element,
-                DepthProps { depth, root: false },
-                "create_random_element",
-            )]),
-            dynamic_attrs: &[],
+        1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
+            VNode::new(
+                None,
+                Template {
+                    name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
+                    roots: &[TemplateNode::Dynamic { id: 0 }],
+                    node_paths: &[&[0]],
+                    attr_paths: &[],
+                },
+                dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()),
+                cx.bump().alloc([cx.component(
+                    create_random_element,
+                    DepthProps { depth, root: false },
+                    "create_random_element",
+                )]),
+                &[],
+            )
         })),
         2 => cx.component(
             create_random_element,
@@ -253,13 +253,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                 .into_boxed_str(),
             ));
             println!("{template:#?}");
-            let node = VNode {
-                key: None,
-                parent: None,
-                template: Cell::new(template),
-                root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump())
-                    .into(),
-                dynamic_nodes: {
+            let node = VNode::new(
+                None,
+                template,
+                dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()),
+                {
                     let dynamic_nodes: Vec<_> = dynamic_node_types
                         .iter()
                         .map(|ty| match ty {
@@ -273,12 +271,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                         .collect();
                     cx.bump().alloc(dynamic_nodes)
                 },
-                dynamic_attrs: cx.bump().alloc(
+                cx.bump().alloc(
                     (0..template.attr_paths.len())
                         .map(|_| create_random_dynamic_attr(cx))
                         .collect::<Vec<_>>(),
                 ),
-            };
+            );
             Some(node)
         }
         _ => None,

+ 2 - 2
packages/rink/src/layout.rs

@@ -92,10 +92,10 @@ impl State for TaffyLayout {
                     attribute, value, ..
                 } in attributes
                 {
-                    if let Some(text) = value.as_text() {
+                    if value.as_custom().is_none() {
                         apply_layout_attributes_cfg(
                             &attribute.name,
-                            text,
+                            &value.to_string(),
                             &mut style,
                             &LayoutConfigeration {
                                 border_widths: BorderWidths {

+ 1 - 0
packages/rsx-rosetta/Cargo.toml

@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
 [dependencies]
 dioxus-autofmt = { workspace = true }
 dioxus-rsx = { workspace = true }
+dioxus-html = { workspace = true, features = ["html-to-rsx"]}
 html_parser.workspace = true
 proc-macro2 = "1.0.49"
 quote = "1.0.23"

+ 26 - 10
packages/rsx-rosetta/src/lib.rs

@@ -3,6 +3,7 @@
 #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
 
 use convert_case::{Case, Casing};
+use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 use dioxus_rsx::{
     BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
 };
@@ -24,26 +25,41 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
     match node {
         Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
         Node::Element(el) => {
-            let el_name = el.name.to_case(Case::Snake);
-            let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site()));
+            let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) {
+                ElementName::Ident(Ident::new(name, Span::call_site()))
+            } else {
+                // if we don't recognize it and it has a dash, we assume it's a web component
+                if el.name.contains('-') {
+                    ElementName::Custom(LitStr::new(&el.name, Span::call_site()))
+                } else {
+                    // otherwise, it might be an element that isn't supported yet
+                    ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site()))
+                }
+            };
 
             let mut attributes: Vec<_> = el
                 .attributes
                 .iter()
                 .map(|(name, value)| {
-                    let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") {
-                        Ident::new_raw(name.as_str(), Span::call_site())
+                    let value = ifmt_from_text(value.as_deref().unwrap_or("false"));
+                    let attr = if let Some(name) = map_html_attribute_to_rsx(name) {
+                        let ident = if let Some(name) = name.strip_prefix("r#") {
+                            Ident::new_raw(name, Span::call_site())
+                        } else {
+                            Ident::new(name, Span::call_site())
+                        };
+                        ElementAttr::AttrText { value, name: ident }
                     } else {
-                        let new_name = name.to_case(Case::Snake);
-                        Ident::new(new_name.as_str(), Span::call_site())
+                        // If we don't recognize the attribute, we assume it's a custom attribute
+                        ElementAttr::CustomAttrText {
+                            value,
+                            name: LitStr::new(name, Span::call_site()),
+                        }
                     };
 
                     ElementAttrNamed {
                         el_name: el_name.clone(),
-                        attr: ElementAttr::AttrText {
-                            value: ifmt_from_text(value.as_deref().unwrap_or("false")),
-                            name: ident,
-                        },
+                        attr,
                     }
                 })
                 .collect();

+ 33 - 0
packages/rsx-rosetta/tests/h-tags.rs

@@ -0,0 +1,33 @@
+use html_parser::Dom;
+
+#[test]
+fn h_tags_translate() {
+    let html = r#"
+    <div>
+        <h1>hello world!</h1>
+        <h2>hello world!</h2>
+        <h3>hello world!</h3>
+        <h4>hello world!</h4>
+        <h5>hello world!</h5>
+        <h6>hello world!</h6>
+    </div>
+    "#
+    .trim();
+
+    let dom = Dom::parse(html).unwrap();
+
+    let body = rsx_rosetta::rsx_from_html(&dom);
+
+    let out = dioxus_autofmt::write_block_out(body).unwrap();
+
+    let expected = r#"
+    div {
+        h1 { "hello world!" }
+        h2 { "hello world!" }
+        h3 { "hello world!" }
+        h4 { "hello world!" }
+        h5 { "hello world!" }
+        h6 { "hello world!" }
+    }"#;
+    pretty_assertions::assert_eq!(&out, &expected);
+}

+ 21 - 0
packages/rsx-rosetta/tests/raw.rs

@@ -0,0 +1,21 @@
+use html_parser::Dom;
+
+#[test]
+fn raw_attribute() {
+    let html = r#"
+    <div>
+        <div unrecognizedattribute="asd">hello world!</div>
+    </div>
+    "#
+    .trim();
+
+    let dom = Dom::parse(html).unwrap();
+
+    let body = rsx_rosetta::rsx_from_html(&dom);
+
+    let out = dioxus_autofmt::write_block_out(body).unwrap();
+
+    let expected = r#"
+    div { div { "unrecognizedattribute": "asd", "hello world!" } }"#;
+    pretty_assertions::assert_eq!(&out, &expected);
+}

+ 0 - 4
packages/rsx-rosetta/tests/simple.rs

@@ -9,8 +9,6 @@ fn simple_elements() {
         <div id="asd">hello world!</div>
         <div for="asd">hello world!</div>
         <div async="asd">hello world!</div>
-        <div LargeThing="asd">hello world!</div>
-        <ai-is-awesome>hello world!</ai-is-awesome>
     </div>
     "#
     .trim();
@@ -28,8 +26,6 @@ fn simple_elements() {
         div { id: "asd", "hello world!" }
         div { r#for: "asd", "hello world!" }
         div { r#async: "asd", "hello world!" }
-        div { large_thing: "asd", "hello world!" }
-        ai_is_awesome { "hello world!" }
     }"#;
     pretty_assertions::assert_eq!(&out, &expected);
 }

+ 21 - 0
packages/rsx-rosetta/tests/web-component.rs

@@ -0,0 +1,21 @@
+use html_parser::Dom;
+
+#[test]
+fn web_components_translate() {
+    let html = r#"
+    <div>
+       <my-component></my-component>
+    </div>
+    "#
+    .trim();
+
+    let dom = Dom::parse(html).unwrap();
+
+    let body = rsx_rosetta::rsx_from_html(&dom);
+
+    let out = dioxus_autofmt::write_block_out(body).unwrap();
+
+    let expected = r#"
+    div { my-component {} }"#;
+    pretty_assertions::assert_eq!(&out, &expected);
+}

+ 18 - 14
packages/rsx/src/lib.rs

@@ -204,12 +204,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
             None => quote! { None },
         };
 
-        let spndbg = format!("{:?}", self.roots[0].span());
-        let root_col = spndbg
-            .rsplit_once("..")
-            .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
-            .unwrap_or_default();
-
+        let root_col = match self.roots.first() {
+            Some(first_root) => {
+                let first_root_span = format!("{:?}", first_root.span());
+                first_root_span
+                    .rsplit_once("..")
+                    .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
+                    .unwrap_or_default()
+                    .to_string()
+            }
+            _ => "0".to_string(),
+        };
         let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
             context.current_path.push(idx as u8);
             let out = context.render_static_node(root);
@@ -247,14 +252,13 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
                 node_paths: &[ #(#node_paths),* ],
                 attr_paths: &[ #(#attr_paths),* ],
             };
-            ::dioxus::core::VNode {
-                parent: None,
-                key: #key_tokens,
-                template: std::cell::Cell::new(TEMPLATE),
-                root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(),
-                dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
-                dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
-            }
+            ::dioxus::core::VNode::new(
+                #key_tokens,
+                TEMPLATE,
+                dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()),
+                __cx.bump().alloc([ #( #node_printer ),* ]),
+                __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
+            )
         });
     }
 }

+ 1 - 1
packages/server-macro/src/lib.rs

@@ -19,7 +19,7 @@ use syn::{
 /// are enabled), it will instead make a network request to the server.
 ///
 /// You can specify one, two, or three arguments to the server function:
-/// 1. **Required**: A type name that will be used to identify and register the server function
+/// 1. *Optional*: A type name that will be used to identify and register the server function
 ///   (e.g., `MyServerFn`).
 /// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
 ///   (e.g., `"/api"`). Defaults to `"/"`.

+ 6 - 0
packages/signals/src/signal.rs

@@ -495,3 +495,9 @@ impl<T> Deref for ReadOnlySignal<T> {
         reference_to_closure as &Self::Target
     }
 }
+
+impl<T> From<Signal<T>> for ReadOnlySignal<T> {
+    fn from(signal: Signal<T>) -> Self {
+        Self::new(signal)
+    }
+}

+ 15 - 1
packages/ssr/src/incremental_cfg.rs

@@ -67,6 +67,7 @@ pub struct IncrementalRendererConfig {
     memory_cache_limit: usize,
     invalidate_after: Option<Duration>,
     map_path: Option<PathMapFn>,
+    clear_cache: bool,
 }
 
 impl Default for IncrementalRendererConfig {
@@ -83,9 +84,16 @@ impl IncrementalRendererConfig {
             memory_cache_limit: 10000,
             invalidate_after: None,
             map_path: None,
+            clear_cache: true,
         }
     }
 
+    /// Clear the cache on startup (default: true)
+    pub fn clear_cache(mut self, clear_cache: bool) -> Self {
+        self.clear_cache = clear_cache;
+        self
+    }
+
     /// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`.
     /// The function should return the path to the folder to store the index.html file in.
     pub fn map_path<F: Fn(&str) -> PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self {
@@ -114,7 +122,7 @@ impl IncrementalRendererConfig {
     /// Build the incremental renderer.
     pub fn build(self) -> IncrementalRenderer {
         let static_dir = self.static_dir.clone();
-        IncrementalRenderer {
+        let mut renderer = IncrementalRenderer {
             static_dir: self.static_dir.clone(),
             memory_cache: NonZeroUsize::new(self.memory_cache_limit)
                 .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
@@ -129,6 +137,12 @@ impl IncrementalRendererConfig {
                     path
                 })
             }),
+        };
+
+        if self.clear_cache {
+            renderer.invalidate_all();
         }
+
+        renderer
     }
 }

+ 88 - 0
packages/ssr/src/renderer.rs

@@ -238,6 +238,94 @@ fn to_string_works() {
     assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
 }
 
+#[test]
+fn empty_for_loop_works() {
+    use dioxus::prelude::*;
+
+    fn app(cx: Scope) -> Element {
+        render! {
+            div { class: "asdasdasd",
+                for _ in (0..5) {
+
+                }
+            }
+        }
+    }
+
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+
+    let mut renderer = Renderer::new();
+    let out = renderer.render(&dom);
+
+    for item in renderer.template_cache.iter() {
+        if item.1.segments.len() > 5 {
+            assert_eq!(
+                item.1.segments,
+                vec![
+                    PreRendered("<div class=\"asdasdasd\"".into(),),
+                    Attr(0,),
+                    StyleMarker {
+                        inside_style_tag: false,
+                    },
+                    PreRendered(">".into()),
+                    InnerHtmlMarker,
+                    PreRendered("</div>".into(),),
+                ]
+            );
+        }
+    }
+
+    use Segment::*;
+
+    assert_eq!(out, "<div class=\"asdasdasd\"></div>");
+}
+
+#[test]
+fn empty_render_works() {
+    use dioxus::prelude::*;
+
+    fn app(cx: Scope) -> Element {
+        render! {}
+    }
+
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+
+    let mut renderer = Renderer::new();
+    let out = renderer.render(&dom);
+
+    for item in renderer.template_cache.iter() {
+        if item.1.segments.len() > 5 {
+            assert_eq!(item.1.segments, vec![]);
+        }
+    }
+    assert_eq!(out, "");
+}
+
+#[test]
+fn empty_rsx_works() {
+    use dioxus::prelude::*;
+
+    fn app(_: Scope) -> Element {
+        rsx! {};
+        None
+    }
+
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+
+    let mut renderer = Renderer::new();
+    let out = renderer.render(&dom);
+
+    for item in renderer.template_cache.iter() {
+        if item.1.segments.len() > 5 {
+            assert_eq!(item.1.segments, vec![]);
+        }
+    }
+    assert_eq!(out, "");
+}
+
 pub(crate) const BOOL_ATTRS: &[&str] = &[
     "allowfullscreen",
     "allowpaymentrequest",

+ 1 - 0
packages/web/Cargo.toml

@@ -42,6 +42,7 @@ features = [
     "HtmlFormElement",
     "Text",
     "Window",
+    "console",
 ]
 
 [features]

+ 1 - 1
packages/web/src/cache.rs

@@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
     "oncopy",
     "oncuechange",
     "oncut",
-    "ondblclick",
+    "ondoubleclick",
     "ondrag",
     "ondragend",
     "ondragenter",

+ 25 - 12
packages/web/src/dom.rs

@@ -45,7 +45,16 @@ impl WebsysDom {
         let document = load_document();
         let root = match document.get_element_by_id(&cfg.rootname) {
             Some(root) => root,
-            None => document.create_element("body").ok().unwrap(),
+            None => {
+                web_sys::console::error_1(
+                    &format!(
+                        "element '#{}' not found. mounting to the body.",
+                        cfg.rootname
+                    )
+                    .into(),
+                );
+                document.create_element("body").ok().unwrap()
+            }
         };
         let interpreter = Channel::default();
 
@@ -247,17 +256,21 @@ impl WebsysDom {
         i.flush();
 
         for id in to_mount {
-            let node = get_node(id.0 as u32);
-            if let Some(element) = node.dyn_ref::<Element>() {
-                let data: MountedData = element.into();
-                let data = Rc::new(data);
-                let _ = self.event_channel.unbounded_send(UiEvent {
-                    name: "mounted".to_string(),
-                    bubbles: false,
-                    element: id,
-                    data,
-                });
-            }
+            self.send_mount_event(id);
+        }
+    }
+
+    pub(crate) fn send_mount_event(&self, id: ElementId) {
+        let node = get_node(id.0 as u32);
+        if let Some(element) = node.dyn_ref::<Element>() {
+            let data: MountedData = element.into();
+            let data = Rc::new(data);
+            let _ = self.event_channel.unbounded_send(UiEvent {
+                name: "mounted".to_string(),
+                bubbles: false,
+                element: id,
+                data,
+            });
         }
     }
 }

+ 14 - 5
packages/web/src/rehydrate.rs

@@ -125,6 +125,7 @@ impl WebsysDom {
                 children, attrs, ..
             } => {
                 let mut mounted_id = None;
+                let mut should_send_mount_event = true;
                 for attr in *attrs {
                     if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
                         let attribute = &vnode.dynamic_attrs[*id];
@@ -134,16 +135,24 @@ impl WebsysDom {
                         let name = attribute.name;
                         if let AttributeValue::Listener(_) = value {
                             let event_name = &name[2..];
-                            self.interpreter.new_event_listener(
-                                event_name,
-                                id.0 as u32,
-                                event_bubbles(event_name) as u8,
-                            );
+                            match event_name {
+                                "mounted" => should_send_mount_event = true,
+                                _ => {
+                                    self.interpreter.new_event_listener(
+                                        event_name,
+                                        id.0 as u32,
+                                        event_bubbles(event_name) as u8,
+                                    );
+                                }
+                            }
                         }
                     }
                 }
                 if let Some(id) = mounted_id {
                     set_node(hydrated, id, current_child.clone()?);
+                    if should_send_mount_event {
+                        self.send_mount_event(id);
+                    }
                 }
                 if !children.is_empty() {
                     let mut children_current_child = current_child