Browse Source

Merge branch 'master' into fix-event-bubbling

Jonathan Kelley 1 year ago
parent
commit
922d9c8c05
59 changed files with 1468 additions and 397 deletions
  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. 29 0
      examples/dynamic_asset.rs
  7. 1 1
      examples/mobile_demo/Cargo.toml
  8. 48 41
      examples/todomvc.rs
  9. 184 0
      examples/video_stream.rs
  10. 16 1
      packages/cli/src/builder.rs
  11. 6 0
      packages/cli/src/cli/build.rs
  12. 13 2
      packages/cli/src/cli/bundle.rs
  13. 24 8
      packages/cli/src/cli/cfg.rs
  14. 6 0
      packages/cli/src/cli/serve.rs
  15. 16 0
      packages/cli/src/config.rs
  16. 5 1
      packages/cli/src/server/desktop/mod.rs
  17. 9 1
      packages/cli/src/server/web/mod.rs
  18. 3 0
      packages/core-macro/src/component_body_deserializers/component.rs
  19. 2 1
      packages/core/src/create.rs
  20. 1 1
      packages/core/src/lazynodes.rs
  21. 1 11
      packages/core/src/scope_context.rs
  22. 4 4
      packages/desktop/Cargo.toml
  23. 62 29
      packages/desktop/headless_tests/events.rs
  24. 10 0
      packages/desktop/src/cfg.rs
  25. 27 16
      packages/desktop/src/desktop_context.rs
  26. 18 9
      packages/desktop/src/lib.rs
  27. 54 20
      packages/desktop/src/mobile_shortcut.rs
  28. 168 8
      packages/desktop/src/protocol.rs
  29. 149 158
      packages/desktop/src/shortcut.rs
  30. 97 7
      packages/desktop/src/webview.rs
  31. 6 14
      packages/dioxus-tui/examples/all_terminal_events.rs
  32. 1 1
      packages/fullstack/Cargo.toml
  33. 1 1
      packages/fullstack/examples/salvo-hello-world/Cargo.toml
  34. 4 2
      packages/generational-box/src/lib.rs
  35. 2 1
      packages/html/Cargo.toml
  36. 53 3
      packages/html/src/elements.rs
  37. 17 4
      packages/html/src/events/mouse.rs
  38. 46 0
      packages/html/src/global_attributes.rs
  39. 2 0
      packages/html/src/lib.rs
  40. 13 2
      packages/liveview/Cargo.toml
  41. 1 0
      packages/liveview/README.md
  42. 76 0
      packages/liveview/examples/rocket.rs
  43. 25 0
      packages/liveview/src/adapters/rocket_adapter.rs
  44. 5 0
      packages/liveview/src/lib.rs
  45. 2 2
      packages/rink/src/layout.rs
  46. 1 0
      packages/rsx-rosetta/Cargo.toml
  47. 26 10
      packages/rsx-rosetta/src/lib.rs
  48. 33 0
      packages/rsx-rosetta/tests/h-tags.rs
  49. 21 0
      packages/rsx-rosetta/tests/raw.rs
  50. 0 4
      packages/rsx-rosetta/tests/simple.rs
  51. 21 0
      packages/rsx-rosetta/tests/web-component.rs
  52. 11 6
      packages/rsx/src/lib.rs
  53. 1 1
      packages/server-macro/src/lib.rs
  54. 6 0
      packages/signals/src/signal.rs
  55. 15 1
      packages/ssr/src/incremental_cfg.rs
  56. 88 0
      packages/ssr/src/renderer.rs
  57. 1 1
      packages/web/src/cache.rs
  58. 15 11
      packages/web/src/dom.rs
  59. 14 5
      packages/web/src/rehydrate.rs

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

@@ -43,7 +43,7 @@ jobs:
       #     args: --path packages/cli
       #     args: --path packages/cli
       - name: Run Playwright tests
       - name: Run Playwright tests
         run: npx playwright test
         run: npx playwright test
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         if: always()
         if: always()
         with:
         with:
           name: playwright-report
           name: playwright-report

+ 1 - 0
.gitignore

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

+ 1 - 0
Cargo.toml

@@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
 env_logger = "0.10.0"
 env_logger = "0.10.0"
 simple_logger = "4.0.0"
 simple_logger = "4.0.0"
 thiserror = { workspace = true }
 thiserror = { workspace = true }
+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
 ## Contributing
 - Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
 - Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
-- Join the discord and ask questions!
+- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
 
 
 
 
 <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
 <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">

+ 4 - 7
examples/all_events.rs

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

+ 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]
 [dependencies]
 anyhow = "1.0.56"
 anyhow = "1.0.56"
 log = "0.4.11"
 log = "0.4.11"
-wry = "0.28.0"
+wry = "0.34.0"
 dioxus = { path = "../../packages/dioxus" }
 dioxus = { path = "../../packages/dioxus" }
 dioxus-desktop = { path = "../../packages/desktop", features = [
 dioxus-desktop = { path = "../../packages/desktop", features = [
     "tokio_runtime",
     "tokio_runtime",

+ 48 - 41
examples/todomvc.rs

@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
     cx.render(rsx! {
     cx.render(rsx! {
         section { class: "todoapp",
         section { class: "todoapp",
             style { include_str!("./assets/todomvc.css") }
             style { include_str!("./assets/todomvc.css") }
-            TodoHeader {
-                todos: todos,
-            }
-            section {
-                class: "main",
+            TodoHeader { todos: todos }
+            section { class: "main",
                 if !todos.is_empty() {
                 if !todos.is_empty() {
                     rsx! {
                     rsx! {
                         input {
                         input {
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
 
 
     cx.render(rsx! {
     cx.render(rsx! {
         header { class: "header",
         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 { "" };
     let editing = if **is_editing { "editing" } else { "" };
 
 
     cx.render(rsx!{
     cx.render(rsx!{
-        li {
-            class: "{completed} {editing}",
+        li { class: "{completed} {editing}",
             div { class: "view",
             div { class: "view",
                 input {
                 input {
                     class: "toggle",
                     class: "toggle",
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                 }
                 }
                 label {
                 label {
                     r#for: "cbg-{todo.id}",
                     r#for: "cbg-{todo.id}",
-                    ondblclick: move |_| is_editing.set(true),
+                    ondoubleclick: move |_| is_editing.set(true),
                     prevent_default: "onclick",
                     prevent_default: "onclick",
                     "{todo.contents}"
                     "{todo.contents}"
                 }
                 }
                 button {
                 button {
                     class: "destroy",
                     class: "destroy",
-                    onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
-                    prevent_default: "onclick",
+                    onclick: move |_| {
+                        cx.props.todos.make_mut().remove(&todo.id);
+                    },
+                    prevent_default: "onclick"
                 }
                 }
             }
             }
             is_editing.then(|| rsx!{
             is_editing.then(|| rsx!{
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
     cx.render(rsx! {
     cx.render(rsx! {
         footer { class: "footer",
         footer { class: "footer",
             span { class: "todo-count",
             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",
             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 {
                     li {
                         a {
                         a {
                             href: url,
                             href: url,
@@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
     cx.render(rsx! {
     cx.render(rsx! {
         footer { class: "info",
         footer { class: "info",
             p { "Double-click to edit a todo" }
             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)
+}

+ 16 - 1
packages/cli/src/builder.rs

@@ -93,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
         cmd
         cmd
     };
     };
 
 
+    let cmd = cmd.args(&config.cargo_args);
+
     let cmd = match executable {
     let cmd = match executable {
         ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
         ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -286,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
         cmd = cmd.arg("--features").arg(features_str);
         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 {
     let cmd = match &config.executable {
         crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
         crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -303,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     let mut res_path = match &config.executable {
     let mut res_path = match &config.executable {
         crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
         crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
             file_name = name.clone();
             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) => {
         crate::ExecutableType::Example(name) => {
             file_name = name.clone();
             file_name = name.clone();
             config
             config
                 .target_dir
                 .target_dir
+                .join(target_platform)
                 .join(release_type)
                 .join(release_type)
                 .join("examples")
                 .join("examples")
                 .join(name)
                 .join(name)

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

@@ -37,6 +37,12 @@ impl Build {
             .platform
             .platform
             .unwrap_or(crate_config.dioxus_config.application.default_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")]
         // #[cfg(feature = "plugin")]
         // let _ = PluginManager::on_build_start(&crate_config, &platform);
         // 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());
             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 the desktop app
         build_desktop(&crate_config, false)?;
         build_desktop(&crate_config, false)?;
 
 
@@ -148,6 +154,11 @@ impl Bundle {
                     .collect(),
                     .collect(),
             );
             );
         }
         }
+
+        if let Some(target) = &self.build.target {
+            settings = settings.target(target.to_string());
+        }
+
         let settings = settings.build();
         let settings = settings.build();
 
 
         // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
         // 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|{
         tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
             #[cfg(target_os = "macos")]
             #[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"))]
             #[cfg(not(target_os = "macos"))]
-            panic!("Failed to bundle project: {}", err);
+            panic!("Failed to bundle project: {:#?}", err);
         });
         });
 
 
         Ok(())
         Ok(())

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

@@ -6,10 +6,6 @@ use super::*;
 /// Config options for the build system.
 /// Config options for the build system.
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub struct ConfigOptsBuild {
 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]
     /// Build in release mode [default: false]
     #[clap(long)]
     #[clap(long)]
     #[serde(default)]
     #[serde(default)]
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
     /// Space separated list of features to activate
     /// Space separated list of features to activate
     #[clap(long)]
     #[clap(long)]
     pub features: Option<Vec<String>>,
     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)]
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 pub struct ConfigOptsServe {
 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
     /// Port of dev server
     #[clap(long)]
     #[clap(long)]
     #[clap(default_value_t = 8080)]
     #[clap(default_value_t = 8080)]
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
     /// Space separated list of features to activate
     /// Space separated list of features to activate
     #[clap(long)]
     #[clap(long)]
     pub features: Option<Vec<String>>,
     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)]
 #[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
     /// Space separated list of features to activate
     #[clap(long)]
     #[clap(long)]
     pub features: Option<Vec<String>>,
     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
         // Subdirectories don't work with the server
         crate_config.dioxus_config.web.app.base_path = None;
         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
         let platform = self
             .serve
             .serve
             .platform
             .platform

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

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

+ 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 🚀");
                                 println!("Connected to hot reloading 🚀");
                             }
                             }
                             Err(err) => {
                             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);
                                     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},
     body::{Full, HttpBody},
     extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
     extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
     http::{
     http::{
+        self,
         header::{HeaderName, HeaderValue},
         header::{HeaderName, HeaderValue},
         Method, Response, StatusCode,
         Method, Response, StatusCode,
     },
     },
@@ -262,7 +263,7 @@ async fn setup_router(
         .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
         .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
         .and_then(
         .and_then(
             move |response: Response<ServeFileSystemResponseBody>| async move {
             move |response: Response<ServeFileSystemResponseBody>| async move {
-                let response = if file_service_config
+                let mut response = if file_service_config
                     .dioxus_config
                     .dioxus_config
                     .web
                     .web
                     .watcher
                     .watcher
@@ -290,6 +291,13 @@ async fn setup_router(
                 } else {
                 } else {
                     response.map(|body| body.boxed())
                     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)
                 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) {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
         let comp_fn = &self.comp_fn;
         let comp_fn = &self.comp_fn;
         let props_struct = &self.props_struct;
         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! {
         tokens.append_all(quote! {
+            #[doc = #doc]
             #props_struct
             #props_struct
             #[allow(non_snake_case)]
             #[allow(non_snake_case)]
             #comp_fn
             #comp_fn

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

@@ -225,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
     /// Elements can contain other nodes - and those nodes can be dynamic or static
     ///
     ///
@@ -443,6 +443,7 @@ impl<'b> VirtualDom {
     #[allow(unused_mut)]
     #[allow(unused_mut)]
     pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
     pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
         let (path, byte_index) = template.name.rsplit_once(':').unwrap();
         let (path, byte_index) = template.name.rsplit_once(':').unwrap();
+
         let byte_index = byte_index.parse::<usize>().unwrap();
         let byte_index = byte_index.parse::<usize>().unwrap();
         // First, check if we've already seen this template
         // First, check if we've already seen this template
         if self
         if self

+ 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.
 /// 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.
 /// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
 ///
 ///
 ///
 ///

+ 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.
     /// 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 {
     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
         // 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.
     /// Informs the scheduler that this task is no longer needed and should be removed.

+ 4 - 4
packages/desktop/Cargo.toml

@@ -18,8 +18,8 @@ dioxus-hot-reload = { workspace = true, optional = true }
 serde = "1.0.136"
 serde = "1.0.136"
 serde_json = "1.0.79"
 serde_json = "1.0.79"
 thiserror = { workspace = true }
 thiserror = { workspace = true }
-wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] }
 tracing = { workspace = true }
 tracing = { workspace = true }
+wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
 futures-channel = { workspace = true }
 futures-channel = { workspace = true }
 tokio = { workspace = true, features = [
 tokio = { workspace = true, features = [
     "sync",
     "sync",
@@ -37,10 +37,12 @@ slab = { workspace = true }
 futures-util = { workspace = true }
 futures-util = { workspace = true }
 urlencoding = "2.1.2"
 urlencoding = "2.1.2"
 async-trait = "0.1.68"
 async-trait = "0.1.68"
+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]
 [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]
 [target.'cfg(target_os = "ios")'.dependencies]
 objc = "0.2.7"
 objc = "0.2.7"
@@ -56,8 +58,6 @@ tokio_runtime = ["tokio"]
 fullscreen = ["wry/fullscreen"]
 fullscreen = ["wry/fullscreen"]
 transparent = ["wry/transparent"]
 transparent = ["wry/transparent"]
 devtools = ["wry/devtools"]
 devtools = ["wry/devtools"]
-tray = ["wry/tray"]
-dox = ["wry/dox"]
 hot-reload = ["dioxus-hot-reload"]
 hot-reload = ["dioxus-hot-reload"]
 gnu = []
 gnu = []
 
 

+ 62 - 29
packages/desktop/headless_tests/events.rs

@@ -229,75 +229,111 @@ fn app(cx: Scope) -> Element {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.held_buttons().is_empty());
                     assert!(event.data.held_buttons().is_empty());
-                    assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Primary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
-                },
+                }
             }
             }
             div {
             div {
                 id: "mouse_move_div",
                 id: "mouse_move_div",
                 onmousemove: move |event| {
                 onmousemove: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
+                    assert!(
+                        event
+                            .data
+                            .held_buttons()
+                            .contains(dioxus_html::input_data::MouseButton::Secondary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
-                },
+                }
             }
             }
             div {
             div {
                 id: "mouse_click_div",
                 id: "mouse_click_div",
                 onclick: move |event| {
                 onclick: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
-                    assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
+                    assert!(
+                        event
+                            .data
+                            .held_buttons()
+                            .contains(dioxus_html::input_data::MouseButton::Secondary),
+                    );
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Secondary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
-                },
+                }
             }
             }
-            div{
+            div {
                 id: "mouse_dblclick_div",
                 id: "mouse_dblclick_div",
-                ondblclick: move |event| {
+                ondoubleclick: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
-                    assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
-                    assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
+                    assert!(
+                        event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
+                    );
+                    assert!(
+                        event
+                            .data
+                            .held_buttons()
+                            .contains(dioxus_html::input_data::MouseButton::Secondary),
+                    );
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Secondary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            div{
+            div {
                 id: "mouse_down_div",
                 id: "mouse_down_div",
                 onmousedown: move |event| {
                 onmousedown: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
-                    assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
+                    assert!(
+                        event
+                            .data
+                            .held_buttons()
+                            .contains(dioxus_html::input_data::MouseButton::Secondary),
+                    );
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Secondary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            div{
+            div {
                 id: "mouse_up_div",
                 id: "mouse_up_div",
                 onmouseup: move |event| {
                 onmouseup: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.modifiers().is_empty());
                     assert!(event.data.held_buttons().is_empty());
                     assert!(event.data.held_buttons().is_empty());
-                    assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
+                    assert_eq!(
+                        event.data.trigger_button(),
+                        Some(dioxus_html::input_data::MouseButton::Primary),
+                    );
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            div{
+            div {
                 id: "wheel_div",
                 id: "wheel_div",
                 width: "100px",
                 width: "100px",
                 height: "100px",
                 height: "100px",
                 background_color: "red",
                 background_color: "red",
                 onwheel: move |event| {
                 onwheel: move |event| {
                     println!("{:?}", event.data);
                     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));
                     assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            input{
+            input {
                 id: "key_down_div",
                 id: "key_down_div",
                 onkeydown: move |event| {
                 onkeydown: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
@@ -306,11 +342,10 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert_eq!(event.data.location, 0);
                     assert!(event.data.is_auto_repeating());
                     assert!(event.data.is_auto_repeating());
-
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            input{
+            input {
                 id: "key_up_div",
                 id: "key_up_div",
                 onkeyup: move |event| {
                 onkeyup: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
@@ -319,11 +354,10 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert_eq!(event.data.location, 0);
                     assert!(!event.data.is_auto_repeating());
                     assert!(!event.data.is_auto_repeating());
-
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            input{
+            input {
                 id: "key_press_div",
                 id: "key_press_div",
                 onkeypress: move |event| {
                 onkeypress: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
@@ -332,18 +366,17 @@ fn app(cx: Scope) -> Element {
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.code().to_string(), "KeyA");
                     assert_eq!(event.data.location, 0);
                     assert_eq!(event.data.location, 0);
                     assert!(!event.data.is_auto_repeating());
                     assert!(!event.data.is_auto_repeating());
-
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            input{
+            input {
                 id: "focus_in_div",
                 id: "focus_in_div",
                 onfocusin: move |event| {
                 onfocusin: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);
                     recieved_events.modify(|x| *x + 1)
                     recieved_events.modify(|x| *x + 1)
                 }
                 }
             }
             }
-            input{
+            input {
                 id: "focus_out_div",
                 id: "focus_out_div",
                 onfocusout: move |event| {
                 onfocusout: move |event| {
                     println!("{:?}", event.data);
                     println!("{:?}", event.data);

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

@@ -36,6 +36,7 @@ pub struct Config {
     pub(crate) root_name: String,
     pub(crate) root_name: String,
     pub(crate) background_color: Option<(u8, u8, u8, u8)>,
     pub(crate) background_color: Option<(u8, u8, u8, u8)>,
     pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
     pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
+    pub(crate) enable_default_menu_bar: bool,
 }
 }
 
 
 type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
 type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@@ -65,9 +66,18 @@ impl Config {
             root_name: "main".to_string(),
             root_name: "main".to_string(),
             background_color: None,
             background_color: None,
             last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
             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
     /// set the directory from which assets will be searched in release mode
     pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
     pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
         self.resource_dir = Some(path.into());
         self.resource_dir = Some(path.into());

+ 27 - 16
packages/desktop/src/desktop_context.rs

@@ -4,10 +4,11 @@ use std::rc::Weak;
 
 
 use crate::create_new_window;
 use crate::create_new_window;
 use crate::events::IpcMessage;
 use crate::events::IpcMessage;
+use crate::protocol::AssetFuture;
+use crate::protocol::AssetHandlerRegistry;
 use crate::query::QueryEngine;
 use crate::query::QueryEngine;
-use crate::shortcut::ShortcutId;
-use crate::shortcut::ShortcutRegistry;
-use crate::shortcut::ShortcutRegistryError;
+use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
+use crate::AssetHandler;
 use crate::Config;
 use crate::Config;
 use crate::WebviewHandler;
 use crate::WebviewHandler;
 use dioxus_core::ScopeState;
 use dioxus_core::ScopeState;
@@ -15,7 +16,6 @@ use dioxus_core::VirtualDom;
 #[cfg(all(feature = "hot-reload", debug_assertions))]
 #[cfg(all(feature = "hot-reload", debug_assertions))]
 use dioxus_hot_reload::HotReloadMsg;
 use dioxus_hot_reload::HotReloadMsg;
 use slab::Slab;
 use slab::Slab;
-use wry::application::accelerator::Accelerator;
 use wry::application::event::Event;
 use wry::application::event::Event;
 use wry::application::event_loop::EventLoopProxy;
 use wry::application::event_loop::EventLoopProxy;
 use wry::application::event_loop::EventLoopWindowTarget;
 use wry::application::event_loop::EventLoopWindowTarget;
@@ -67,6 +67,8 @@ pub struct DesktopService {
 
 
     pub(crate) shortcut_manager: ShortcutRegistry,
     pub(crate) shortcut_manager: ShortcutRegistry,
 
 
+    pub(crate) asset_handlers: AssetHandlerRegistry,
+
     #[cfg(target_os = "ios")]
     #[cfg(target_os = "ios")]
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
 }
 }
@@ -91,6 +93,7 @@ impl DesktopService {
         webviews: WebviewQueue,
         webviews: WebviewQueue,
         event_handlers: WindowEventHandlers,
         event_handlers: WindowEventHandlers,
         shortcut_manager: ShortcutRegistry,
         shortcut_manager: ShortcutRegistry,
+        asset_handlers: AssetHandlerRegistry,
     ) -> Self {
     ) -> Self {
         Self {
         Self {
             webview: Rc::new(webview),
             webview: Rc::new(webview),
@@ -100,6 +103,7 @@ impl DesktopService {
             pending_windows: webviews,
             pending_windows: webviews,
             event_handlers,
             event_handlers,
             shortcut_manager,
             shortcut_manager,
+            asset_handlers,
             #[cfg(target_os = "ios")]
             #[cfg(target_os = "ios")]
             views: Default::default(),
             views: Default::default(),
         }
         }
@@ -233,11 +237,11 @@ impl DesktopService {
     /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
     /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
     pub fn create_shortcut(
     pub fn create_shortcut(
         &self,
         &self,
-        accelerator: Accelerator,
+        hotkey: HotKey,
         callback: impl FnMut() + 'static,
         callback: impl FnMut() + 'static,
     ) -> Result<ShortcutId, ShortcutRegistryError> {
     ) -> Result<ShortcutId, ShortcutRegistryError> {
         self.shortcut_manager
         self.shortcut_manager
-            .add_shortcut(accelerator, Box::new(callback))
+            .add_shortcut(hotkey, Box::new(callback))
     }
     }
 
 
     /// Remove a global shortcut
     /// Remove a global shortcut
@@ -250,6 +254,20 @@ impl DesktopService {
         self.shortcut_manager.remove_all()
         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
     /// Push an objc view to the window
     #[cfg(target_os = "ios")]
     #[cfg(target_os = "ios")]
     pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
     pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
@@ -369,17 +387,10 @@ impl WryWindowEventHandlerInner {
         target: &EventLoopWindowTarget<UserWindowEvent>,
         target: &EventLoopWindowTarget<UserWindowEvent>,
     ) {
     ) {
         // if this event does not apply to the window this listener cares about, return
         // if this event does not apply to the window this listener cares about, return
-        match event {
-            Event::WindowEvent { window_id, .. }
-            | Event::MenuEvent {
-                window_id: Some(window_id),
-                ..
-            } => {
-                if *window_id != self.window_id {
-                    return;
-                }
+        if let Event::WindowEvent { window_id, .. } = event {
+            if *window_id != self.window_id {
+                return;
             }
             }
-            _ => (),
         }
         }
         (self.handler)(event, target)
         (self.handler)(event, target)
     }
     }

+ 18 - 9
packages/desktop/src/lib.rs

@@ -10,16 +10,16 @@ mod escape;
 mod eval;
 mod eval;
 mod events;
 mod events;
 mod file_upload;
 mod file_upload;
+#[cfg(any(target_os = "ios", target_os = "android"))]
+mod mobile_shortcut;
 mod protocol;
 mod protocol;
 mod query;
 mod query;
 mod shortcut;
 mod shortcut;
 mod waker;
 mod waker;
 mod webview;
 mod webview;
 
 
-#[cfg(any(target_os = "ios", target_os = "android"))]
-mod mobile_shortcut;
-
 use crate::query::QueryResult;
 use crate::query::QueryResult;
+use crate::shortcut::GlobalHotKeyEvent;
 pub use cfg::{Config, WindowCloseBehaviour};
 pub use cfg::{Config, WindowCloseBehaviour};
 pub use desktop_context::DesktopContext;
 pub use desktop_context::DesktopContext;
 pub use desktop_context::{
 pub use desktop_context::{
@@ -32,6 +32,7 @@ use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
 use element::DesktopElement;
 use element::DesktopElement;
 use eval::init_eval;
 use eval::init_eval;
 use futures_util::{pin_mut, FutureExt};
 use futures_util::{pin_mut, FutureExt};
+pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
 use shortcut::ShortcutRegistry;
 use shortcut::ShortcutRegistry;
 pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
 pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
 use std::cell::Cell;
 use std::cell::Cell;
@@ -43,10 +44,12 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
 pub use tao::window::WindowBuilder;
 pub use tao::window::WindowBuilder;
 use tao::{
 use tao::{
     event::{Event, StartCause, WindowEvent},
     event::{Event, StartCause, WindowEvent},
-    event_loop::{ControlFlow, EventLoop},
+    event_loop::ControlFlow,
 };
 };
+pub use webview::build_default_menu_bar;
 pub use wry;
 pub use wry;
 pub use wry::application as tao;
 pub use wry::application as tao;
+use wry::application::event_loop::EventLoopBuilder;
 use wry::webview::WebView;
 use wry::webview::WebView;
 use wry::{application::window::WindowId, webview::WebContext};
 use wry::{application::window::WindowId, webview::WebContext};
 
 
@@ -120,7 +123,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
 /// }
 /// }
 /// ```
 /// ```
 pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
 pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
-    let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
+    let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
 
 
     let proxy = event_loop.create_proxy();
     let proxy = event_loop.create_proxy();
 
 
@@ -157,7 +160,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
 
 
     let queue = WebviewQueue::default();
     let queue = WebviewQueue::default();
 
 
-    let shortcut_manager = ShortcutRegistry::new(&event_loop);
+    let shortcut_manager = ShortcutRegistry::new();
+    let global_hotkey_channel = GlobalHotKeyEvent::receiver();
 
 
     // move the props into a cell so we can pop it out later to create the first window
     // move the props into a cell so we can pop it out later to create the first window
     // iOS panics if we create a window before the event loop is started
     // iOS panics if we create a window before the event loop is started
@@ -166,10 +170,14 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
     let mut is_visible_before_start = true;
     let mut is_visible_before_start = true;
 
 
     event_loop.run(move |window_event, event_loop, control_flow| {
     event_loop.run(move |window_event, event_loop, control_flow| {
-        *control_flow = ControlFlow::Wait;
+        *control_flow = ControlFlow::Poll;
 
 
         event_handlers.apply_event(&window_event, event_loop);
         event_handlers.apply_event(&window_event, event_loop);
 
 
+        if let Ok(event) = global_hotkey_channel.try_recv() {
+            shortcut_manager.call_handlers(event);
+        }
+
         match window_event {
         match window_event {
             Event::WindowEvent {
             Event::WindowEvent {
                 event, window_id, ..
                 event, window_id, ..
@@ -375,7 +383,6 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
 
 
                 _ => {}
                 _ => {}
             },
             },
-            Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
             _ => {}
             _ => {}
         }
         }
     })
     })
@@ -390,7 +397,8 @@ fn create_new_window(
     event_handlers: &WindowEventHandlers,
     event_handlers: &WindowEventHandlers,
     shortcut_manager: ShortcutRegistry,
     shortcut_manager: ShortcutRegistry,
 ) -> WebviewHandler {
 ) -> WebviewHandler {
-    let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
+    let (webview, web_context, asset_handlers) =
+        webview::build(&mut cfg, event_loop, proxy.clone());
     let desktop_context = Rc::from(DesktopService::new(
     let desktop_context = Rc::from(DesktopService::new(
         webview,
         webview,
         proxy.clone(),
         proxy.clone(),
@@ -398,6 +406,7 @@ fn create_new_window(
         queue.clone(),
         queue.clone(),
         event_handlers.clone(),
         event_handlers.clone(),
         shortcut_manager,
         shortcut_manager,
+        asset_handlers,
     ));
     ));
 
 
     let cx = dom.base_scope();
     let cx = dom.base_scope();

+ 54 - 20
packages/desktop/src/mobile_shortcut.rs

@@ -1,29 +1,51 @@
 #![allow(unused)]
 #![allow(unused)]
 
 
 use super::*;
 use super::*;
-use wry::application::accelerator::Accelerator;
+use std::str::FromStr;
 use wry::application::event_loop::EventLoopWindowTarget;
 use wry::application::event_loop::EventLoopWindowTarget;
 
 
-pub struct GlobalShortcut();
-pub struct ShortcutManager();
+use dioxus_html::input_data::keyboard_types::Modifiers;
 
 
-impl ShortcutManager {
-    pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
-        Self()
+#[derive(Clone, Debug)]
+pub struct Accelerator;
+
+#[derive(Clone, Copy)]
+pub struct HotKey;
+
+impl HotKey {
+    pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
+        Self
+    }
+
+    pub fn id(&self) -> u32 {
+        0
+    }
+}
+
+impl FromStr for HotKey {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(HotKey)
+    }
+}
+
+pub struct GlobalHotKeyManager();
+
+impl GlobalHotKeyManager {
+    pub fn new() -> Result<Self, HotkeyError> {
+        Ok(Self())
     }
     }
 
 
-    pub fn register(
-        &mut self,
-        accelerator: Accelerator,
-    ) -> Result<GlobalShortcut, ShortcutManagerError> {
-        Ok(GlobalShortcut())
+    pub fn register(&mut self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
+        Ok(HotKey)
     }
     }
 
 
-    pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
+    pub fn unregister(&mut self, id: HotKey) -> Result<(), HotkeyError> {
         Ok(())
         Ok(())
     }
     }
 
 
-    pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
+    pub fn unregister_all(&mut self, _: &[HotKey]) -> Result<(), HotkeyError> {
         Ok(())
         Ok(())
     }
     }
 }
 }
@@ -33,23 +55,35 @@ use std::{error, fmt};
 /// An error whose cause the `ShortcutManager` to fail.
 /// An error whose cause the `ShortcutManager` to fail.
 #[non_exhaustive]
 #[non_exhaustive]
 #[derive(Debug)]
 #[derive(Debug)]
-pub enum ShortcutManagerError {
+pub enum HotkeyError {
     AcceleratorAlreadyRegistered(Accelerator),
     AcceleratorAlreadyRegistered(Accelerator),
     AcceleratorNotRegistered(Accelerator),
     AcceleratorNotRegistered(Accelerator),
-    InvalidAccelerator(String),
+    HotKeyParseError(String),
 }
 }
 
 
-impl error::Error for ShortcutManagerError {}
-impl fmt::Display for ShortcutManagerError {
+impl error::Error for HotkeyError {}
+impl fmt::Display for HotkeyError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
         match self {
         match self {
-            ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
+            HotkeyError::AcceleratorAlreadyRegistered(e) => {
                 f.pad(&format!("hotkey already registered: {:?}", e))
                 f.pad(&format!("hotkey already registered: {:?}", e))
             }
             }
-            ShortcutManagerError::AcceleratorNotRegistered(e) => {
+            HotkeyError::AcceleratorNotRegistered(e) => {
                 f.pad(&format!("hotkey not registered: {:?}", e))
                 f.pad(&format!("hotkey not registered: {:?}", e))
             }
             }
-            ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
+            HotkeyError::HotKeyParseError(e) => e.fmt(f),
         }
         }
     }
     }
 }
 }
+
+pub struct GlobalHotKeyEvent {
+    pub id: u32,
+}
+
+impl GlobalHotKeyEvent {
+    pub fn receiver() -> crossbeam_channel::Receiver<GlobalHotKeyEvent> {
+        crossbeam_channel::unbounded().1
+    }
+}
+
+pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code;

+ 168 - 8
packages/desktop/src/protocol.rs

@@ -1,13 +1,26 @@
+use dioxus_core::ScopeState;
 use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
 use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
+use slab::Slab;
 use std::{
 use std::{
     borrow::Cow,
     borrow::Cow,
+    future::Future,
+    ops::Deref,
     path::{Path, PathBuf},
     path::{Path, PathBuf},
+    pin::Pin,
+    rc::Rc,
+    sync::Arc,
+};
+use tokio::{
+    runtime::Handle,
+    sync::{OnceCell, RwLock},
 };
 };
 use wry::{
 use wry::{
     http::{status::StatusCode, Request, Response},
     http::{status::StatusCode, Request, Response},
     Result,
     Result,
 };
 };
 
 
+use crate::{use_window, DesktopContext};
+
 fn module_loader(root_name: &str) -> String {
 fn module_loader(root_name: &str) -> String {
     let js = INTERPRETER_JS.replace(
     let js = INTERPRETER_JS.replace(
         "/*POST_HANDLE_EDITS*/",
         "/*POST_HANDLE_EDITS*/",
@@ -51,12 +64,156 @@ fn module_loader(root_name: &str) -> String {
     )
     )
 }
 }
 
 
-pub(super) fn desktop_handler(
-    request: &Request<Vec<u8>>,
+/// 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_head: Option<String>,
     custom_index: Option<String>,
     custom_index: Option<String>,
     root_name: &str,
     root_name: &str,
-) -> Result<Response<Cow<'static, [u8]>>> {
+    asset_handlers: &AssetHandlerRegistry,
+) -> Result<AssetResponse> {
+    let request = AssetRequest::from(request);
+
     // If the request is for the root, we'll serve the index.html file.
     // If the request is for the root, we'll serve the index.html file.
     if request.uri().path() == "/" {
     if request.uri().path() == "/" {
         // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
         // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
@@ -91,18 +248,21 @@ pub(super) fn desktop_handler(
             .map_err(From::from);
             .map_err(From::from);
     }
     }
 
 
+    // 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.
     // 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.
     // If the path is relative, we'll try to serve it from the assets directory.
     let mut asset = get_asset_root()
     let mut asset = get_asset_root()
         .unwrap_or_else(|| Path::new(".").to_path_buf())
         .unwrap_or_else(|| Path::new(".").to_path_buf())
-        .join(&path);
+        .join(&request.path);
 
 
     if !asset.exists() {
     if !asset.exists() {
-        asset = PathBuf::from("/").join(path);
+        asset = PathBuf::from("/").join(request.path);
     }
     }
 
 
     if asset.exists() {
     if asset.exists() {

+ 149 - 158
packages/desktop/src/shortcut.rs

@@ -3,11 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
 use dioxus_core::ScopeState;
 use dioxus_core::ScopeState;
 use dioxus_html::input_data::keyboard_types::Modifiers;
 use dioxus_html::input_data::keyboard_types::Modifiers;
 use slab::Slab;
 use slab::Slab;
-use wry::application::{
-    accelerator::{Accelerator, AcceleratorId},
-    event_loop::EventLoopWindowTarget,
-    keyboard::{KeyCode, ModifiersState},
-};
+use wry::application::keyboard::ModifiersState;
 
 
 use crate::{desktop_context::DesktopContext, use_window};
 use crate::{desktop_context::DesktopContext, use_window};
 
 
@@ -20,22 +16,25 @@ use crate::{desktop_context::DesktopContext, use_window};
     target_os = "netbsd",
     target_os = "netbsd",
     target_os = "openbsd"
     target_os = "openbsd"
 ))]
 ))]
-use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
+pub use global_hotkey::{
+    hotkey::{Code, HotKey},
+    Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager,
+};
 
 
 #[cfg(any(target_os = "ios", target_os = "android"))]
 #[cfg(any(target_os = "ios", target_os = "android"))]
 pub use crate::mobile_shortcut::*;
 pub use crate::mobile_shortcut::*;
 
 
 #[derive(Clone)]
 #[derive(Clone)]
 pub(crate) struct ShortcutRegistry {
 pub(crate) struct ShortcutRegistry {
-    manager: Rc<RefCell<ShortcutManager>>,
+    manager: Rc<RefCell<GlobalHotKeyManager>>,
     shortcuts: ShortcutMap,
     shortcuts: ShortcutMap,
 }
 }
 
 
-type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
+type ShortcutMap = Rc<RefCell<HashMap<u32, Shortcut>>>;
 
 
 struct Shortcut {
 struct Shortcut {
     #[allow(unused)]
     #[allow(unused)]
-    shortcut: GlobalShortcut,
+    shortcut: HotKey,
     callbacks: Slab<Box<dyn FnMut()>>,
     callbacks: Slab<Box<dyn FnMut()>>,
 }
 }
 
 
@@ -54,15 +53,15 @@ impl Shortcut {
 }
 }
 
 
 impl ShortcutRegistry {
 impl ShortcutRegistry {
-    pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
+    pub fn new() -> Self {
         Self {
         Self {
-            manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
+            manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())),
             shortcuts: Rc::new(RefCell::new(HashMap::new())),
             shortcuts: Rc::new(RefCell::new(HashMap::new())),
         }
         }
     }
     }
 
 
-    pub(crate) fn call_handlers(&self, id: AcceleratorId) {
-        if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
+    pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) {
+        if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) {
             for (_, callback) in callbacks.iter_mut() {
             for (_, callback) in callbacks.iter_mut() {
                 (callback)();
                 (callback)();
             }
             }
@@ -71,10 +70,10 @@ impl ShortcutRegistry {
 
 
     pub(crate) fn add_shortcut(
     pub(crate) fn add_shortcut(
         &self,
         &self,
-        accelerator: Accelerator,
+        hotkey: HotKey,
         callback: Box<dyn FnMut()>,
         callback: Box<dyn FnMut()>,
     ) -> Result<ShortcutId, ShortcutRegistryError> {
     ) -> Result<ShortcutId, ShortcutRegistryError> {
-        let accelerator_id = accelerator.clone().id();
+        let accelerator_id = hotkey.clone().id();
         let mut shortcuts = self.shortcuts.borrow_mut();
         let mut shortcuts = self.shortcuts.borrow_mut();
         Ok(
         Ok(
             if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
             if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
@@ -84,12 +83,12 @@ impl ShortcutRegistry {
                     number: id,
                     number: id,
                 }
                 }
             } else {
             } else {
-                match self.manager.borrow_mut().register(accelerator) {
-                    Ok(global_shortcut) => {
+                match self.manager.borrow_mut().register(hotkey) {
+                    Ok(_) => {
                         let mut slab = Slab::new();
                         let mut slab = Slab::new();
                         let id = slab.insert(callback);
                         let id = slab.insert(callback);
                         let shortcut = Shortcut {
                         let shortcut = Shortcut {
-                            shortcut: global_shortcut,
+                            shortcut: hotkey,
                             callbacks: slab,
                             callbacks: slab,
                         };
                         };
                         shortcuts.insert(accelerator_id, shortcut);
                         shortcuts.insert(accelerator_id, shortcut);
@@ -98,7 +97,7 @@ impl ShortcutRegistry {
                             number: id,
                             number: id,
                         }
                         }
                     }
                     }
-                    Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
+                    Err(HotkeyError::HotKeyParseError(shortcut)) => {
                         return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
                         return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
                     }
                     }
                     Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
                     Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
@@ -113,15 +112,6 @@ impl ShortcutRegistry {
             callbacks.remove(id.number);
             callbacks.remove(id.number);
             if callbacks.is_empty() {
             if callbacks.is_empty() {
                 if let Some(_shortcut) = shortcuts.remove(&id.id) {
                 if let Some(_shortcut) = shortcuts.remove(&id.id) {
-                    #[cfg(any(
-                        target_os = "windows",
-                        target_os = "macos",
-                        target_os = "linux",
-                        target_os = "dragonfly",
-                        target_os = "freebsd",
-                        target_os = "netbsd",
-                        target_os = "openbsd"
-                    ))]
                     let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
                     let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
                 }
                 }
             }
             }
@@ -130,8 +120,8 @@ impl ShortcutRegistry {
 
 
     pub(crate) fn remove_all(&self) {
     pub(crate) fn remove_all(&self) {
         let mut shortcuts = self.shortcuts.borrow_mut();
         let mut shortcuts = self.shortcuts.borrow_mut();
-        shortcuts.clear();
-        let _ = self.manager.borrow_mut().unregister_all();
+        let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect();
+        let _ = self.manager.borrow_mut().unregister_all(&hotkeys);
     }
     }
 }
 }
 
 
@@ -148,7 +138,7 @@ pub enum ShortcutRegistryError {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 /// An global id for a shortcut.
 /// An global id for a shortcut.
 pub struct ShortcutId {
 pub struct ShortcutId {
-    id: AcceleratorId,
+    id: u32,
     number: usize,
     number: usize,
 }
 }
 
 
@@ -160,30 +150,30 @@ pub struct ShortcutHandle {
 }
 }
 
 
 pub trait IntoAccelerator {
 pub trait IntoAccelerator {
-    fn accelerator(&self) -> Accelerator;
+    fn accelerator(&self) -> HotKey;
 }
 }
 
 
 impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
 impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
-    fn accelerator(&self) -> Accelerator {
-        Accelerator::new(Some(self.1), self.0.into_key_code())
+    fn accelerator(&self) -> HotKey {
+        HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code())
     }
     }
 }
 }
 
 
 impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
 impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
-    fn accelerator(&self) -> Accelerator {
-        Accelerator::new(Some(self.0), self.1.into_key_code())
+    fn accelerator(&self) -> HotKey {
+        HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code())
     }
     }
 }
 }
 
 
 impl IntoAccelerator for dioxus_html::KeyCode {
 impl IntoAccelerator for dioxus_html::KeyCode {
-    fn accelerator(&self) -> Accelerator {
-        Accelerator::new(None, self.into_key_code())
+    fn accelerator(&self) -> HotKey {
+        HotKey::new(None, self.into_key_code())
     }
     }
 }
 }
 
 
 impl IntoAccelerator for &str {
 impl IntoAccelerator for &str {
-    fn accelerator(&self) -> Accelerator {
-        Accelerator::from_str(self).unwrap()
+    fn accelerator(&self) -> HotKey {
+        HotKey::from_str(self).unwrap()
     }
     }
 }
 }
 
 
@@ -220,143 +210,144 @@ impl Drop for ShortcutHandle {
 }
 }
 
 
 pub trait IntoModifersState {
 pub trait IntoModifersState {
-    fn into_modifiers_state(self) -> ModifiersState;
+    fn into_modifiers_state(self) -> Modifiers;
 }
 }
 
 
 impl IntoModifersState for ModifiersState {
 impl IntoModifersState for ModifiersState {
-    fn into_modifiers_state(self) -> ModifiersState {
-        self
-    }
-}
-
-impl IntoModifersState for Modifiers {
-    fn into_modifiers_state(self) -> ModifiersState {
-        let mut state = ModifiersState::empty();
-        if self.contains(Modifiers::SHIFT) {
-            state |= ModifiersState::SHIFT
+    fn into_modifiers_state(self) -> Modifiers {
+        let mut modifiers = Modifiers::default();
+        if self.shift_key() {
+            modifiers |= Modifiers::SHIFT;
         }
         }
-        if self.contains(Modifiers::CONTROL) {
-            state |= ModifiersState::CONTROL
+        if self.control_key() {
+            modifiers |= Modifiers::CONTROL;
         }
         }
-        if self.contains(Modifiers::ALT) {
-            state |= ModifiersState::ALT
+        if self.alt_key() {
+            modifiers |= Modifiers::ALT;
         }
         }
-        if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
-            state |= ModifiersState::SUPER
+        if self.super_key() {
+            modifiers |= Modifiers::META;
         }
         }
-        state
+
+        modifiers
+    }
+}
+
+impl IntoModifersState for Modifiers {
+    fn into_modifiers_state(self) -> Modifiers {
+        self
     }
     }
 }
 }
 
 
 pub trait IntoKeyCode {
 pub trait IntoKeyCode {
-    fn into_key_code(self) -> KeyCode;
+    fn into_key_code(self) -> Code;
 }
 }
 
 
-impl IntoKeyCode for KeyCode {
-    fn into_key_code(self) -> KeyCode {
+impl IntoKeyCode for Code {
+    fn into_key_code(self) -> Code {
         self
         self
     }
     }
 }
 }
 
 
 impl IntoKeyCode for dioxus_html::KeyCode {
 impl IntoKeyCode for dioxus_html::KeyCode {
-    fn into_key_code(self) -> KeyCode {
+    fn into_key_code(self) -> Code {
         match self {
         match self {
-            dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
-            dioxus_html::KeyCode::Tab => KeyCode::Tab,
-            dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
-            dioxus_html::KeyCode::Enter => KeyCode::Enter,
-            dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
-            dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
-            dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
-            dioxus_html::KeyCode::Pause => KeyCode::Pause,
-            dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
-            dioxus_html::KeyCode::Escape => KeyCode::Escape,
-            dioxus_html::KeyCode::Space => KeyCode::Space,
-            dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
-            dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
-            dioxus_html::KeyCode::End => KeyCode::End,
-            dioxus_html::KeyCode::Home => KeyCode::Home,
-            dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
-            dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
-            dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
-            dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
-            dioxus_html::KeyCode::Insert => KeyCode::Insert,
-            dioxus_html::KeyCode::Delete => KeyCode::Delete,
-            dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
-            dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
-            dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
-            dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
-            dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
-            dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
-            dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
-            dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
-            dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
-            dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
-            dioxus_html::KeyCode::A => KeyCode::KeyA,
-            dioxus_html::KeyCode::B => KeyCode::KeyB,
-            dioxus_html::KeyCode::C => KeyCode::KeyC,
-            dioxus_html::KeyCode::D => KeyCode::KeyD,
-            dioxus_html::KeyCode::E => KeyCode::KeyE,
-            dioxus_html::KeyCode::F => KeyCode::KeyF,
-            dioxus_html::KeyCode::G => KeyCode::KeyG,
-            dioxus_html::KeyCode::H => KeyCode::KeyH,
-            dioxus_html::KeyCode::I => KeyCode::KeyI,
-            dioxus_html::KeyCode::J => KeyCode::KeyJ,
-            dioxus_html::KeyCode::K => KeyCode::KeyK,
-            dioxus_html::KeyCode::L => KeyCode::KeyL,
-            dioxus_html::KeyCode::M => KeyCode::KeyM,
-            dioxus_html::KeyCode::N => KeyCode::KeyN,
-            dioxus_html::KeyCode::O => KeyCode::KeyO,
-            dioxus_html::KeyCode::P => KeyCode::KeyP,
-            dioxus_html::KeyCode::Q => KeyCode::KeyQ,
-            dioxus_html::KeyCode::R => KeyCode::KeyR,
-            dioxus_html::KeyCode::S => KeyCode::KeyS,
-            dioxus_html::KeyCode::T => KeyCode::KeyT,
-            dioxus_html::KeyCode::U => KeyCode::KeyU,
-            dioxus_html::KeyCode::V => KeyCode::KeyV,
-            dioxus_html::KeyCode::W => KeyCode::KeyW,
-            dioxus_html::KeyCode::X => KeyCode::KeyX,
-            dioxus_html::KeyCode::Y => KeyCode::KeyY,
-            dioxus_html::KeyCode::Z => KeyCode::KeyZ,
-            dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
-            dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
-            dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
-            dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
-            dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
-            dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
-            dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
-            dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
-            dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
-            dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
-            dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
-            dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
-            dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
-            dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
-            dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
-            dioxus_html::KeyCode::F1 => KeyCode::F1,
-            dioxus_html::KeyCode::F2 => KeyCode::F2,
-            dioxus_html::KeyCode::F3 => KeyCode::F3,
-            dioxus_html::KeyCode::F4 => KeyCode::F4,
-            dioxus_html::KeyCode::F5 => KeyCode::F5,
-            dioxus_html::KeyCode::F6 => KeyCode::F6,
-            dioxus_html::KeyCode::F7 => KeyCode::F7,
-            dioxus_html::KeyCode::F8 => KeyCode::F8,
-            dioxus_html::KeyCode::F9 => KeyCode::F9,
-            dioxus_html::KeyCode::F10 => KeyCode::F10,
-            dioxus_html::KeyCode::F11 => KeyCode::F11,
-            dioxus_html::KeyCode::F12 => KeyCode::F12,
-            dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
-            dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
-            dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
-            dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
-            dioxus_html::KeyCode::Comma => KeyCode::Comma,
-            dioxus_html::KeyCode::Period => KeyCode::Period,
-            dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
-            dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
-            dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
-            dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
-            dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
-            dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
+            dioxus_html::KeyCode::Backspace => Code::Backspace,
+            dioxus_html::KeyCode::Tab => Code::Tab,
+            dioxus_html::KeyCode::Clear => Code::NumpadClear,
+            dioxus_html::KeyCode::Enter => Code::Enter,
+            dioxus_html::KeyCode::Shift => Code::ShiftLeft,
+            dioxus_html::KeyCode::Ctrl => Code::ControlLeft,
+            dioxus_html::KeyCode::Alt => Code::AltLeft,
+            dioxus_html::KeyCode::Pause => Code::Pause,
+            dioxus_html::KeyCode::CapsLock => Code::CapsLock,
+            dioxus_html::KeyCode::Escape => Code::Escape,
+            dioxus_html::KeyCode::Space => Code::Space,
+            dioxus_html::KeyCode::PageUp => Code::PageUp,
+            dioxus_html::KeyCode::PageDown => Code::PageDown,
+            dioxus_html::KeyCode::End => Code::End,
+            dioxus_html::KeyCode::Home => Code::Home,
+            dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft,
+            dioxus_html::KeyCode::UpArrow => Code::ArrowUp,
+            dioxus_html::KeyCode::RightArrow => Code::ArrowRight,
+            dioxus_html::KeyCode::DownArrow => Code::ArrowDown,
+            dioxus_html::KeyCode::Insert => Code::Insert,
+            dioxus_html::KeyCode::Delete => Code::Delete,
+            dioxus_html::KeyCode::Num0 => Code::Numpad0,
+            dioxus_html::KeyCode::Num1 => Code::Numpad1,
+            dioxus_html::KeyCode::Num2 => Code::Numpad2,
+            dioxus_html::KeyCode::Num3 => Code::Numpad3,
+            dioxus_html::KeyCode::Num4 => Code::Numpad4,
+            dioxus_html::KeyCode::Num5 => Code::Numpad5,
+            dioxus_html::KeyCode::Num6 => Code::Numpad6,
+            dioxus_html::KeyCode::Num7 => Code::Numpad7,
+            dioxus_html::KeyCode::Num8 => Code::Numpad8,
+            dioxus_html::KeyCode::Num9 => Code::Numpad9,
+            dioxus_html::KeyCode::A => Code::KeyA,
+            dioxus_html::KeyCode::B => Code::KeyB,
+            dioxus_html::KeyCode::C => Code::KeyC,
+            dioxus_html::KeyCode::D => Code::KeyD,
+            dioxus_html::KeyCode::E => Code::KeyE,
+            dioxus_html::KeyCode::F => Code::KeyF,
+            dioxus_html::KeyCode::G => Code::KeyG,
+            dioxus_html::KeyCode::H => Code::KeyH,
+            dioxus_html::KeyCode::I => Code::KeyI,
+            dioxus_html::KeyCode::J => Code::KeyJ,
+            dioxus_html::KeyCode::K => Code::KeyK,
+            dioxus_html::KeyCode::L => Code::KeyL,
+            dioxus_html::KeyCode::M => Code::KeyM,
+            dioxus_html::KeyCode::N => Code::KeyN,
+            dioxus_html::KeyCode::O => Code::KeyO,
+            dioxus_html::KeyCode::P => Code::KeyP,
+            dioxus_html::KeyCode::Q => Code::KeyQ,
+            dioxus_html::KeyCode::R => Code::KeyR,
+            dioxus_html::KeyCode::S => Code::KeyS,
+            dioxus_html::KeyCode::T => Code::KeyT,
+            dioxus_html::KeyCode::U => Code::KeyU,
+            dioxus_html::KeyCode::V => Code::KeyV,
+            dioxus_html::KeyCode::W => Code::KeyW,
+            dioxus_html::KeyCode::X => Code::KeyX,
+            dioxus_html::KeyCode::Y => Code::KeyY,
+            dioxus_html::KeyCode::Z => Code::KeyZ,
+            dioxus_html::KeyCode::Numpad0 => Code::Numpad0,
+            dioxus_html::KeyCode::Numpad1 => Code::Numpad1,
+            dioxus_html::KeyCode::Numpad2 => Code::Numpad2,
+            dioxus_html::KeyCode::Numpad3 => Code::Numpad3,
+            dioxus_html::KeyCode::Numpad4 => Code::Numpad4,
+            dioxus_html::KeyCode::Numpad5 => Code::Numpad5,
+            dioxus_html::KeyCode::Numpad6 => Code::Numpad6,
+            dioxus_html::KeyCode::Numpad7 => Code::Numpad7,
+            dioxus_html::KeyCode::Numpad8 => Code::Numpad8,
+            dioxus_html::KeyCode::Numpad9 => Code::Numpad9,
+            dioxus_html::KeyCode::Multiply => Code::NumpadMultiply,
+            dioxus_html::KeyCode::Add => Code::NumpadAdd,
+            dioxus_html::KeyCode::Subtract => Code::NumpadSubtract,
+            dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal,
+            dioxus_html::KeyCode::Divide => Code::NumpadDivide,
+            dioxus_html::KeyCode::F1 => Code::F1,
+            dioxus_html::KeyCode::F2 => Code::F2,
+            dioxus_html::KeyCode::F3 => Code::F3,
+            dioxus_html::KeyCode::F4 => Code::F4,
+            dioxus_html::KeyCode::F5 => Code::F5,
+            dioxus_html::KeyCode::F6 => Code::F6,
+            dioxus_html::KeyCode::F7 => Code::F7,
+            dioxus_html::KeyCode::F8 => Code::F8,
+            dioxus_html::KeyCode::F9 => Code::F9,
+            dioxus_html::KeyCode::F10 => Code::F10,
+            dioxus_html::KeyCode::F11 => Code::F11,
+            dioxus_html::KeyCode::F12 => Code::F12,
+            dioxus_html::KeyCode::NumLock => Code::NumLock,
+            dioxus_html::KeyCode::ScrollLock => Code::ScrollLock,
+            dioxus_html::KeyCode::Semicolon => Code::Semicolon,
+            dioxus_html::KeyCode::EqualSign => Code::Equal,
+            dioxus_html::KeyCode::Comma => Code::Comma,
+            dioxus_html::KeyCode::Period => Code::Period,
+            dioxus_html::KeyCode::ForwardSlash => Code::Slash,
+            dioxus_html::KeyCode::GraveAccent => Code::Backquote,
+            dioxus_html::KeyCode::OpenBracket => Code::BracketLeft,
+            dioxus_html::KeyCode::BackSlash => Code::Backslash,
+            dioxus_html::KeyCode::CloseBraket => Code::BracketRight,
+            dioxus_html::KeyCode::SingleQuote => Code::Quote,
             key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
             key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
         }
         }
     }
     }

+ 97 - 7
packages/desktop/src/webview.rs

@@ -1,24 +1,31 @@
 use crate::desktop_context::EventData;
 use crate::desktop_context::EventData;
-use crate::protocol;
+use crate::protocol::{self, AssetHandlerRegistry};
 use crate::{desktop_context::UserWindowEvent, Config};
 use crate::{desktop_context::UserWindowEvent, Config};
 use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
 use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
 pub use wry;
 pub use wry;
 pub use wry::application as tao;
 pub use wry::application as tao;
+use wry::application::menu::{MenuBar, MenuItem};
 use wry::application::window::Window;
 use wry::application::window::Window;
+use wry::http::Response;
 use wry::webview::{WebContext, WebView, WebViewBuilder};
 use wry::webview::{WebContext, WebView, WebViewBuilder};
 
 
 pub fn build(
 pub fn build(
     cfg: &mut Config,
     cfg: &mut Config,
     event_loop: &EventLoopWindowTarget<UserWindowEvent>,
     event_loop: &EventLoopWindowTarget<UserWindowEvent>,
     proxy: EventLoopProxy<UserWindowEvent>,
     proxy: EventLoopProxy<UserWindowEvent>,
-) -> (WebView, WebContext) {
-    let builder = cfg.window.clone();
+) -> (WebView, WebContext, AssetHandlerRegistry) {
     let window = builder.with_visible(false).build(event_loop).unwrap();
     let window = builder.with_visible(false).build(event_loop).unwrap();
     let file_handler = cfg.file_drop_handler.take();
     let file_handler = cfg.file_drop_handler.take();
     let custom_head = cfg.custom_head.clone();
     let custom_head = cfg.custom_head.clone();
     let index_file = cfg.custom_index.clone();
     let index_file = cfg.custom_index.clone();
     let root_name = cfg.root_name.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
     // 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() {
     if cfg.window.window.window_icon.is_none() {
         window.set_window_icon(Some(
         window.set_window_icon(Some(
@@ -32,6 +39,8 @@ pub fn build(
     }
     }
 
 
     let mut web_context = WebContext::new(cfg.data_dir.clone());
     let mut web_context = WebContext::new(cfg.data_dir.clone());
+    let asset_handlers = AssetHandlerRegistry::new();
+    let asset_handlers_ref = asset_handlers.clone();
 
 
     let mut webview = WebViewBuilder::new(window)
     let mut webview = WebViewBuilder::new(window)
         .unwrap()
         .unwrap()
@@ -44,8 +53,22 @@ pub fn build(
                 _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
                 _ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
             }
             }
         })
         })
-        .with_custom_protocol(String::from("dioxus"), move |r| {
-            protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name)
+        .with_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,
+                    &asset_handlers_ref,
+                )
+                .await;
+                responder.respond(response);
+            });
         })
         })
         .with_file_drop_handler(move |window, evet| {
         .with_file_drop_handler(move |window, evet| {
             file_handler
             file_handler
@@ -71,7 +94,16 @@ pub fn build(
     // .with_web_context(&mut web_context);
     // .with_web_context(&mut web_context);
 
 
     for (name, handler) in cfg.protocols.drain(..) {
     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 {
     if cfg.disable_context_menu {
@@ -94,5 +126,63 @@ pub fn build(
         webview = webview.with_devtools(true);
         webview = webview.with_devtools(true);
     }
     }
 
 
-    (webview.build().unwrap(), web_context)
+    (webview.build().unwrap(), web_context, asset_handlers)
+}
+
+/// 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
         // todo: remove
         let mut trimmed = format!("{event:?}");
         let mut trimmed = format!("{event:?}");
         trimmed.truncate(200);
         trimmed.truncate(200);
-        rsx!(p { "{trimmed}" })
+        rsx!( p { "{trimmed}" } )
     });
     });
 
 
     let log_event = move |event: Event| {
     let log_event = move |event: Event| {
@@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element {
     };
     };
 
 
     cx.render(rsx! {
     cx.render(rsx! {
-        div {
-            width: "100%",
-            height: "100%",
-            flex_direction: "column",
+        div { width: "100%", height: "100%", flex_direction: "column",
             div {
             div {
                 width: "80%",
                 width: "80%",
                 height: "50%",
                 height: "50%",
@@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element {
 
 
                 onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
                 onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
                 onclick: move |event| log_event(Event::MouseClick(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())),
                 onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
                 onmouseup: move |event| log_event(Event::MouseUp(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())),
                 onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
 
 
                 "Hover, click, type or scroll to see the info down below"
                 "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"
 axum-macros = "0.3.7"
 
 
 # salvo
 # 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"
 serde = "1.0.159"
 
 
 # Dioxus + SSR
 # Dioxus + SSR

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

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

+ 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.
     /// 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> {
     pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
         let location = self.store.claim();
         let location = self.store.claim();
-        GenerationalBox {
+        let key = GenerationalBox {
             raw: location,
             raw: location,
             #[cfg(any(debug_assertions, feature = "check_generation"))]
             #[cfg(any(debug_assertions, feature = "check_generation"))]
             generation: location.0.generation.get(),
             generation: location.0.generation.get(),
             #[cfg(any(debug_assertions, feature = "debug_ownership"))]
             #[cfg(any(debug_assertions, feature = "debug_ownership"))]
             created_at: std::panic::Location::caller(),
             created_at: std::panic::Location::caller(),
             _marker: PhantomData,
             _marker: PhantomData,
-        }
+        };
+        self.owned.borrow_mut().push(location);
+        key
     }
     }
 }
 }
 
 

+ 2 - 1
packages/html/Cargo.toml

@@ -21,7 +21,7 @@ keyboard-types = "0.7"
 async-trait = "0.1.58"
 async-trait = "0.1.58"
 serde-value = "0.7.0"
 serde-value = "0.7.0"
 tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
 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"
 async-channel = "1.8.0"
 serde_json = { version = "1", optional = true }
 serde_json = { version = "1", optional = true }
 
 
@@ -68,3 +68,4 @@ mounted = [
 wasm-bind = ["web-sys", "wasm-bindgen"]
 wasm-bind = ["web-sys", "wasm-bindgen"]
 native-bind = ["tokio"]
 native-bind = ["tokio"]
 hot-reload-context = ["dioxus-rsx"]
 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 {
 macro_rules! impl_element {
     (
     (
         $(#[$attr:meta])*
         $(#[$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!(
             impl_element!(
                 $(#[$attr])*
                 $(#[$attr])*
@@ -998,9 +1049,8 @@ builder_constructors! {
         src: Uri DEFAULT,
         src: Uri DEFAULT,
         text: String 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",
         r#script: String "script",
     };
     };
 
 

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

@@ -119,10 +119,7 @@ impl_event! {
     /// oncontextmenu
     /// oncontextmenu
     oncontextmenu
     oncontextmenu
 
 
-    /// ondoubleclick
-    ondoubleclick
-
-    /// ondoubleclick
+    #[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
     ondblclick
     ondblclick
 
 
     /// onmousedown
     /// onmousedown
@@ -149,6 +146,22 @@ impl_event! {
     onmouseup
     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 {
 impl MouseData {
     /// Construct MouseData with the specified properties
     /// 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 {
 macro_rules! trait_methods {
     (
     (
         @base
         @base
         $(#[$trait_attr:meta])*
         $(#[$trait_attr:meta])*
         $trait:ident;
         $trait:ident;
         $fn:ident;
         $fn:ident;
+        $fn_html_to_rsx:ident;
         $(
         $(
             $(#[$attr:meta])*
             $(#[$attr:meta])*
             $name:ident $(: $($arg:literal),*)*;
             $name:ident $(: $($arg:literal),*)*;
@@ -62,6 +94,18 @@ macro_rules! trait_methods {
             )*
             )*
             None
             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
     // Rename the incoming ident and apply a custom namespace
@@ -79,6 +123,7 @@ trait_methods! {
 
 
     GlobalAttributes;
     GlobalAttributes;
     map_global_attributes;
     map_global_attributes;
+    map_html_global_attributes_to_rsx;
 
 
     /// Prevent the default action for this element.
     /// Prevent the default action for this element.
     ///
     ///
@@ -1593,6 +1638,7 @@ trait_methods! {
     @base
     @base
     SvgAttributes;
     SvgAttributes;
     map_svg_attributes;
     map_svg_attributes;
+    map_html_svg_attributes_to_rsx;
 
 
     /// Prevent the default action for this element.
     /// Prevent the default action for this element.
     ///
     ///

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

@@ -19,6 +19,8 @@
 mod elements;
 mod elements;
 #[cfg(feature = "hot-reload-context")]
 #[cfg(feature = "hot-reload-context")]
 pub use elements::HtmlCtx;
 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 events;
 pub mod geometry;
 pub mod geometry;
 mod global_attributes;
 mod global_attributes;

+ 13 - 2
packages/liveview/Cargo.toml

@@ -34,10 +34,14 @@ warp = { version = "0.3.3", optional = true }
 axum = { version = "0.6.1", optional = true, features = ["ws"] }
 axum = { version = "0.6.1", optional = true, features = ["ws"] }
 
 
 # salvo
 # salvo
-salvo = { version = "0.44.1", optional = true, features = ["ws"] }
+salvo = { version = "0.63.0", optional = true, features = ["websocket"] }
 once_cell = "1.17.1"
 once_cell = "1.17.1"
 async-trait = "0.1.71"
 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 is ... complicated?
 # actix-files = { version = "0.6.2", optional = true }
 # actix-files = { version = "0.6.2", optional = true }
 # actix-web = { version = "4.2.1", optional = true }
 # actix-web = { version = "4.2.1", optional = true }
@@ -49,13 +53,16 @@ tokio = { workspace = true, features = ["full"] }
 dioxus = { workspace = true }
 dioxus = { workspace = true }
 warp = "0.3.3"
 warp = "0.3.3"
 axum = { version = "0.6.1", features = ["ws"] }
 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"
 tower = "0.4.13"
 
 
 [features]
 [features]
 default = ["hot-reload"]
 default = ["hot-reload"]
 # actix = ["actix-files", "actix-web", "actix-ws"]
 # actix = ["actix-files", "actix-web", "actix-ws"]
 hot-reload = ["dioxus-hot-reload"]
 hot-reload = ["dioxus-hot-reload"]
+rocket = ["dep:rocket", "dep:rocket_ws"]
 
 
 [[example]]
 [[example]]
 name = "axum"
 name = "axum"
@@ -68,3 +75,7 @@ required-features = ["salvo"]
 [[example]]
 [[example]]
 name = "warp"
 name = "warp"
 required-features = ["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
 - Axum
 - Warp
 - Warp
 - Salvo
 - Salvo
+- Rocket
 
 
 Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
 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")]
     #[cfg(feature = "salvo")]
     pub use salvo_adapter::*;
     pub use salvo_adapter::*;
+
+    #[cfg(feature = "rocket")]
+    pub mod rocket_adapter;
+    #[cfg(feature = "rocket")]
+    pub use rocket_adapter::*;
 }
 }
 
 
 pub use adapters::*;
 pub use adapters::*;

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

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

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

@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
 [dependencies]
 [dependencies]
 dioxus-autofmt = { workspace = true }
 dioxus-autofmt = { workspace = true }
 dioxus-rsx = { workspace = true }
 dioxus-rsx = { workspace = true }
+dioxus-html = { workspace = true, features = ["html-to-rsx"]}
 html_parser.workspace = true
 html_parser.workspace = true
 proc-macro2 = "1.0.49"
 proc-macro2 = "1.0.49"
 quote = "1.0.23"
 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")]
 #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
 
 
 use convert_case::{Case, Casing};
 use convert_case::{Case, Casing};
+use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 use dioxus_rsx::{
 use dioxus_rsx::{
     BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
     BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
 };
 };
@@ -24,26 +25,41 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
     match node {
     match node {
         Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
         Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
         Node::Element(el) => {
         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
             let mut attributes: Vec<_> = el
                 .attributes
                 .attributes
                 .iter()
                 .iter()
                 .map(|(name, value)| {
                 .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 {
                     } 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 {
                     ElementAttrNamed {
                         el_name: el_name.clone(),
                         el_name: el_name.clone(),
-                        attr: ElementAttr::AttrText {
-                            value: ifmt_from_text(value.as_deref().unwrap_or("false")),
-                            name: ident,
-                        },
+                        attr,
                     }
                     }
                 })
                 })
                 .collect();
                 .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 id="asd">hello world!</div>
         <div for="asd">hello world!</div>
         <div for="asd">hello world!</div>
         <div async="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>
     </div>
     "#
     "#
     .trim();
     .trim();
@@ -28,8 +26,6 @@ fn simple_elements() {
         div { id: "asd", "hello world!" }
         div { id: "asd", "hello world!" }
         div { r#for: "asd", "hello world!" }
         div { r#for: "asd", "hello world!" }
         div { r#async: "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);
     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);
+}

+ 11 - 6
packages/rsx/src/lib.rs

@@ -204,12 +204,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
             None => quote! { None },
             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)| {
         let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
             context.current_path.push(idx as u8);
             context.current_path.push(idx as u8);
             let out = context.render_static_node(root);
             let out = context.render_static_node(root);

+ 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.
 /// are enabled), it will instead make a network request to the server.
 ///
 ///
 /// You can specify one, two, or three arguments to the server function:
 /// 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`).
 ///   (e.g., `MyServerFn`).
 /// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
 /// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
 ///   (e.g., `"/api"`). Defaults to `"/"`.
 ///   (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
         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,
     memory_cache_limit: usize,
     invalidate_after: Option<Duration>,
     invalidate_after: Option<Duration>,
     map_path: Option<PathMapFn>,
     map_path: Option<PathMapFn>,
+    clear_cache: bool,
 }
 }
 
 
 impl Default for IncrementalRendererConfig {
 impl Default for IncrementalRendererConfig {
@@ -83,9 +84,16 @@ impl IncrementalRendererConfig {
             memory_cache_limit: 10000,
             memory_cache_limit: 10000,
             invalidate_after: None,
             invalidate_after: None,
             map_path: 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`.
     /// 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.
     /// 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 {
     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.
     /// Build the incremental renderer.
     pub fn build(self) -> IncrementalRenderer {
     pub fn build(self) -> IncrementalRenderer {
         let static_dir = self.static_dir.clone();
         let static_dir = self.static_dir.clone();
-        IncrementalRenderer {
+        let mut renderer = IncrementalRenderer {
             static_dir: self.static_dir.clone(),
             static_dir: self.static_dir.clone(),
             memory_cache: NonZeroUsize::new(self.memory_cache_limit)
             memory_cache: NonZeroUsize::new(self.memory_cache_limit)
                 .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
                 .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
@@ -129,6 +137,12 @@ impl IncrementalRendererConfig {
                     path
                     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>");
     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] = &[
 pub(crate) const BOOL_ATTRS: &[&str] = &[
     "allowfullscreen",
     "allowfullscreen",
     "allowpaymentrequest",
     "allowpaymentrequest",

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

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

+ 15 - 11
packages/web/src/dom.rs

@@ -256,17 +256,21 @@ impl WebsysDom {
         i.flush();
         i.flush();
 
 
         for id in to_mount {
         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, ..
                 children, attrs, ..
             } => {
             } => {
                 let mut mounted_id = None;
                 let mut mounted_id = None;
+                let mut should_send_mount_event = true;
                 for attr in *attrs {
                 for attr in *attrs {
                     if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
                     if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
                         let attribute = &vnode.dynamic_attrs[*id];
                         let attribute = &vnode.dynamic_attrs[*id];
@@ -134,16 +135,24 @@ impl WebsysDom {
                         let name = attribute.name;
                         let name = attribute.name;
                         if let AttributeValue::Listener(_) = value {
                         if let AttributeValue::Listener(_) = value {
                             let event_name = &name[2..];
                             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 {
                 if let Some(id) = mounted_id {
                     set_node(hydrated, id, current_child.clone()?);
                     set_node(hydrated, id, current_child.clone()?);
+                    if should_send_mount_event {
+                        self.send_mount_event(id);
+                    }
                 }
                 }
                 if !children.is_empty() {
                 if !children.is_empty() {
                     let mut children_current_child = current_child
                     let mut children_current_child = current_child