浏览代码

Merge branch 'upstream-master' into fix-hot-reloading

= 2 年之前
父节点
当前提交
14dd568987
共有 92 个文件被更改,包括 3483 次插入2482 次删除
  1. 1 1
      .github/workflows/docs.yml
  2. 1 1
      .github/workflows/macos.yml
  3. 4 4
      .github/workflows/main.yml
  4. 3 2
      .github/workflows/windows.yml
  5. 1 0
      Cargo.toml
  6. 8 0
      examples/custom_element.rs
  7. 14 1
      examples/eval.rs
  8. 1 2
      examples/rsx_usage.rs
  9. 60 0
      examples/simple_desktop.rs
  10. 6 0
      examples/svg_basic.rs
  11. 1 1
      packages/core-macro/Cargo.toml
  12. 0 10
      packages/core-macro/src/lib.rs
  13. 4 1
      packages/core/Cargo.toml
  14. 18 11
      packages/core/src/arena.rs
  15. 267 193
      packages/core/src/create.rs
  16. 189 123
      packages/core/src/diff.rs
  17. 9 7
      packages/core/src/dirty_scope.rs
  18. 9 199
      packages/core/src/lazynodes.rs
  19. 3 3
      packages/core/src/lib.rs
  20. 1 4
      packages/core/src/mutations.rs
  21. 29 4
      packages/core/src/nodes.rs
  22. 13 6
      packages/core/src/scope_arena.rs
  23. 7 5
      packages/core/src/scopes.rs
  24. 5 9
      packages/core/src/virtual_dom.rs
  25. 1 1
      packages/core/tests/kitchen_sink.rs
  26. 2 2
      packages/core/tests/miri_simple.rs
  27. 1 3
      packages/core/tests/miri_stress.rs
  28. 1 1
      packages/desktop/Cargo.toml
  29. 11 12
      packages/desktop/src/controller.rs
  30. 59 19
      packages/desktop/src/desktop_context.rs
  31. 1 59
      packages/desktop/src/events.rs
  32. 22 13
      packages/desktop/src/lib.rs
  33. 7 0
      packages/desktop/src/main.js
  34. 12 5
      packages/desktop/src/protocol.rs
  35. 0 2
      packages/dioxus/src/lib.rs
  36. 5 2
      packages/html/Cargo.toml
  37. 2 3
      packages/html/src/events/drag.rs
  38. 2 3
      packages/html/src/events/form.rs
  39. 6 0
      packages/html/src/lib.rs
  40. 152 0
      packages/html/src/transit.rs
  41. 3 1
      packages/interpreter/Cargo.toml
  42. 3 3
      packages/interpreter/src/bindings.rs
  43. 13 16
      packages/interpreter/src/interpreter.js
  44. 5 0
      packages/interpreter/src/lib.rs
  45. 231 0
      packages/interpreter/src/sledgehammer_bindings.rs
  46. 17 3
      packages/liveview/Cargo.toml
  47. 35 14
      packages/liveview/examples/axum.rs
  48. 58 42
      packages/liveview/examples/salvo.rs
  49. 49 28
      packages/liveview/examples/warp.rs
  50. 17 88
      packages/liveview/src/adapters/axum_adapter.rs
  51. 17 102
      packages/liveview/src/adapters/salvo_adapter.rs
  52. 20 102
      packages/liveview/src/adapters/warp_adapter.rs
  53. 0 207
      packages/liveview/src/events.rs
  54. 0 15
      packages/liveview/src/index.html
  55. 0 973
      packages/liveview/src/interpreter.js
  56. 38 39
      packages/liveview/src/lib.rs
  57. 36 0
      packages/liveview/src/main.js
  58. 123 0
      packages/liveview/src/pool.rs
  59. 1 2
      packages/native-core-macro/src/lib.rs
  60. 11 24
      packages/native-core/src/real_dom.rs
  61. 484 0
      packages/native-core/src/utils/cursor.rs
  62. 3 0
      packages/native-core/src/utils/mod.rs
  63. 0 0
      packages/native-core/src/utils/persistant_iterator.rs
  64. 4 0
      packages/router/Cargo.toml
  65. 7 1
      packages/router/examples/simple.rs
  66. 8 0
      packages/router/src/components/link.rs
  67. 3 3
      packages/router/src/components/route.rs
  68. 1 0
      packages/router/src/service.rs
  69. 6 2
      packages/rsx/Cargo.toml
  70. 1 2
      packages/rsx/src/element.rs
  71. 5 4
      packages/rsx/src/lib.rs
  72. 1 0
      packages/tui/Cargo.toml
  73. 8 6
      packages/tui/examples/tui_colorpicker.rs
  74. 93 0
      packages/tui/examples/tui_widgets.rs
  75. 4 1
      packages/tui/src/hooks.rs
  76. 54 1
      packages/tui/src/layout.rs
  77. 18 3
      packages/tui/src/lib.rs
  78. 1 0
      packages/tui/src/prelude/mod.rs
  79. 1 1
      packages/tui/src/query.rs
  80. 1 1
      packages/tui/src/render.rs
  81. 58 0
      packages/tui/src/widgets/button.rs
  82. 81 0
      packages/tui/src/widgets/checkbox.rs
  83. 102 0
      packages/tui/src/widgets/input.rs
  84. 18 0
      packages/tui/src/widgets/mod.rs
  85. 208 0
      packages/tui/src/widgets/number.rs
  86. 185 0
      packages/tui/src/widgets/password.rs
  87. 107 0
      packages/tui/src/widgets/slider.rs
  88. 181 0
      packages/tui/src/widgets/textbox.rs
  89. 2 1
      packages/web/Cargo.toml
  90. 166 46
      packages/web/src/dom.rs
  91. 7 35
      packages/web/src/lib.rs
  92. 50 4
      packages/web/src/util.rs

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

@@ -13,7 +13,6 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     environment: docs
     environment: docs
     steps:
     steps:
-      - uses: actions/checkout@v3
 
 
       # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
       # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released
       # - name: Setup mdBook
       # - name: Setup mdBook
@@ -25,6 +24,7 @@ jobs:
       - name: Setup mdBook
       - name: Setup mdBook
         run: |
         run: |
           cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1
           cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev e74fdb1
+      - uses: actions/checkout@v3
 
 
       - name: Build
       - name: Build
         run: cd docs &&
         run: cd docs &&

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

@@ -30,13 +30,13 @@ jobs:
     name: Test Suite
     name: Test Suite
     runs-on: macos-latest
     runs-on: macos-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
       - uses: actions-rs/toolchain@v1
       - uses: actions-rs/toolchain@v1
         with:
         with:
           profile: minimal
           profile: minimal
           toolchain: stable
           toolchain: stable
           override: true
           override: true
       - uses: Swatinem/rust-cache@v2
       - uses: Swatinem/rust-cache@v2
+      - uses: actions/checkout@v3
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: test
           command: test

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

@@ -32,7 +32,6 @@ jobs:
     name: Check
     name: Check
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
       - uses: actions-rs/toolchain@v1
       - uses: actions-rs/toolchain@v1
         with:
         with:
           profile: minimal
           profile: minimal
@@ -41,6 +40,7 @@ jobs:
       - uses: Swatinem/rust-cache@v2
       - uses: Swatinem/rust-cache@v2
       - run: sudo apt-get update
       - run: sudo apt-get update
       - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
       - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
+      - uses: actions/checkout@v3
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: check
           command: check
@@ -51,7 +51,6 @@ jobs:
     name: Test Suite
     name: Test Suite
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
       - uses: actions-rs/toolchain@v1
       - uses: actions-rs/toolchain@v1
         with:
         with:
           profile: minimal
           profile: minimal
@@ -63,6 +62,7 @@ jobs:
       - uses: davidB/rust-cargo-make@v1
       - uses: davidB/rust-cargo-make@v1
       - uses: browser-actions/setup-firefox@latest
       - uses: browser-actions/setup-firefox@latest
       - uses: jetli/wasm-pack-action@v0.4.0
       - uses: jetli/wasm-pack-action@v0.4.0
+      - uses: actions/checkout@v3
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: make
           command: make
@@ -73,7 +73,6 @@ jobs:
     name: Rustfmt
     name: Rustfmt
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
       - uses: actions-rs/toolchain@v1
       - uses: actions-rs/toolchain@v1
         with:
         with:
           profile: minimal
           profile: minimal
@@ -81,6 +80,7 @@ jobs:
           override: true
           override: true
       - uses: Swatinem/rust-cache@v2
       - uses: Swatinem/rust-cache@v2
       - run: rustup component add rustfmt
       - run: rustup component add rustfmt
+      - uses: actions/checkout@v3
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: fmt
           command: fmt
@@ -91,7 +91,6 @@ jobs:
     name: Clippy
     name: Clippy
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v3
       - uses: actions-rs/toolchain@v1
       - uses: actions-rs/toolchain@v1
         with:
         with:
           profile: minimal
           profile: minimal
@@ -101,6 +100,7 @@ jobs:
       - run: sudo apt-get update
       - run: sudo apt-get update
       - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
       - run: sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
       - run: rustup component add clippy
       - run: rustup component add clippy
+      - uses: actions/checkout@v3
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: clippy
           command: clippy

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

@@ -47,8 +47,6 @@ jobs:
       # which causes failures for some of rustfmt's line-ending sensitive tests
       # which causes failures for some of rustfmt's line-ending sensitive tests
       - name: disable git eol translation
       - name: disable git eol translation
         run: git config --global core.autocrlf false
         run: git config --global core.autocrlf false
-      - name: checkout
-        uses: actions/checkout@v3
 
 
         # Run build
         # Run build
       - name: Install Rustup using win.rustup.rs
       - name: Install Rustup using win.rustup.rs
@@ -66,6 +64,9 @@ jobs:
         if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
         if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
         shell: bash
         shell: bash
 
 
+      - name: checkout
+        uses: actions/checkout@v3
+
       - name: test
       - name: test
         run: |
         run: |
           rustc -Vv
           rustc -Vv

+ 1 - 0
Cargo.toml

@@ -57,6 +57,7 @@ reqwest = { version = "0.11.9", features = ["json"] }
 fern = { version = "0.6.0", features = ["colored"] }
 fern = { version = "0.6.0", features = ["colored"] }
 thiserror = "1.0.30"
 thiserror = "1.0.30"
 env_logger = "0.9.0"
 env_logger = "0.9.0"
+simple_logger = "4.0.0"
 
 
 [profile.release]
 [profile.release]
 opt-level = 3
 opt-level = 3

+ 8 - 0
examples/custom_element.rs

@@ -16,6 +16,12 @@ fn main() {
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
+    let g = cx.component(component, (), "component");
+    let c = cx.make_node(g);
+    cx.render(rsx! {
+        div { c }
+    })
+
     // let nf = NodeFactory::new(cx);
     // let nf = NodeFactory::new(cx);
 
 
     // let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
     // let mut attrs = dioxus::core::exports::bumpalo::collections::Vec::new_in(nf.bump());
@@ -27,6 +33,8 @@ fn app(cx: Scope) -> Element {
     // attrs.push(nf.attr("age", format_args!("47"), None, false));
     // attrs.push(nf.attr("age", format_args!("47"), None, false));
 
 
     // Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
     // Some(nf.raw_element("my-element", None, &[], attrs.into_bump_slice(), &[], None))
+}
 
 
+fn component(cx: Scope) -> Element {
     todo!()
     todo!()
 }
 }

+ 14 - 1
examples/eval.rs

@@ -1,4 +1,5 @@
 use dioxus::prelude::*;
 use dioxus::prelude::*;
+use dioxus_desktop::EvalResult;
 
 
 fn main() {
 fn main() {
     dioxus_desktop::launch(app);
     dioxus_desktop::launch(app);
@@ -7,6 +8,15 @@ fn main() {
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {
     let script = use_state(cx, String::new);
     let script = use_state(cx, String::new);
     let eval = dioxus_desktop::use_eval(cx);
     let eval = dioxus_desktop::use_eval(cx);
+    let future: &UseRef<Option<EvalResult>> = use_ref(cx, || None);
+    if future.read().is_some() {
+        let future_clone = future.clone();
+        cx.spawn(async move {
+            if let Some(fut) = future_clone.with_mut(|o| o.take()) {
+                println!("{:?}", fut.await)
+            }
+        });
+    }
 
 
     cx.render(rsx! {
     cx.render(rsx! {
         div {
         div {
@@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element {
                 oninput: move |e| script.set(e.value.clone()),
                 oninput: move |e| script.set(e.value.clone()),
             }
             }
             button {
             button {
-                onclick: move |_| eval(script.to_string()),
+                onclick: move |_| {
+                    let fut = eval(script);
+                    future.set(Some(fut));
+                },
                 "Execute"
                 "Execute"
             }
             }
         }
         }

+ 1 - 2
examples/rsx_usage.rs

@@ -1,5 +1,4 @@
 #![allow(non_snake_case)]
 #![allow(non_snake_case)]
-
 //! A tour of the rsx! macro
 //! A tour of the rsx! macro
 //! ------------------------
 //! ------------------------
 //!
 //!
@@ -39,7 +38,7 @@
 //! - Accept a list of vnodes as children for a Fragment component
 //! - Accept a list of vnodes as children for a Fragment component
 //! - Allow keyed fragments in iterators
 //! - Allow keyed fragments in iterators
 //! - Allow top-level fragments
 //! - Allow top-level fragments
-//!
+
 fn main() {
 fn main() {
     dioxus_desktop::launch(app);
     dioxus_desktop::launch(app);
 }
 }

+ 60 - 0
examples/simple_desktop.rs

@@ -0,0 +1,60 @@
+use dioxus::prelude::*;
+use dioxus_router::*;
+
+fn main() {
+    simple_logger::SimpleLogger::new()
+        .with_level(log::LevelFilter::Debug)
+        .with_module_level("dioxus_router", log::LevelFilter::Trace)
+        .with_module_level("dioxus", log::LevelFilter::Trace)
+        .init()
+        .unwrap();
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        Router {
+            h1 { "Your app here" }
+            ul {
+                Link { to: "/", li { "home" } }
+                Link { to: "/blog", li { "blog" } }
+                Link { to: "/blog/tim", li { "tims' blog" } }
+                Link { to: "/blog/bill", li { "bills' blog" } }
+                Link { to: "/blog/james",
+                        li { "james amazing' blog" }
+                }
+                Link { to: "/apples", li { "go to apples" } }
+            }
+            Route { to: "/", Home {} }
+            Route { to: "/blog/", BlogList {} }
+            Route { to: "/blog/:id/", BlogPost {} }
+            Route { to: "/oranges", "Oranges are not apples!" }
+            Redirect { from: "/apples", to: "/oranges" }
+        }
+    })
+}
+
+fn Home(cx: Scope) -> Element {
+    log::debug!("rendering home {:?}", cx.scope_id());
+    cx.render(rsx! { h1 { "Home" } })
+}
+
+fn BlogList(cx: Scope) -> Element {
+    log::debug!("rendering blog list {:?}", cx.scope_id());
+    cx.render(rsx! { div { "Blog List" } })
+}
+
+fn BlogPost(cx: Scope) -> Element {
+    let Some(id) = use_route(cx).segment("id") else {
+        return cx.render(rsx! { div { "No blog post id" } })
+    };
+
+    log::debug!("rendering blog post {}", id);
+
+    cx.render(rsx! {
+        div {
+            h3 { "blog post: {id:?}"  }
+            Link { to: "/blog/", "back to blog list" }
+        }
+    })
+}

+ 6 - 0
examples/svg_basic.rs

@@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element {
             stroke: "blue",
             stroke: "blue",
             stroke_width: "5",
             stroke_width: "5",
         }
         }
+        path {
+            d: "M9.00001 9C9 62 103.5 124 103.5 178",
+            stroke: "#3CC4DC",
+            "stroke-linecap": "square",
+            "stroke-width": "square",
+        }
     }))
     }))
 }
 }
 
 

+ 1 - 1
packages/core-macro/Cargo.toml

@@ -18,7 +18,7 @@ proc-macro = true
 proc-macro2 = { version = "1.0" }
 proc-macro2 = { version = "1.0" }
 quote = "1.0"
 quote = "1.0"
 syn = { version = "1.0", features = ["full", "extra-traits"] }
 syn = { version = "1.0", features = ["full", "extra-traits"] }
-dioxus-rsx = {  path = "../rsx" }
+dioxus-rsx = {  path = "../rsx", version = "0.0.1" }
 
 
 # testing
 # testing
 [dev-dependencies]
 [dev-dependencies]

+ 0 - 10
packages/core-macro/src/lib.rs

@@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
 }
 }
 
 
 /// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
 /// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
-///
-/// ## Complete Reference Guide:
-/// ```ignore
-#[doc = include_str!("../../../examples/rsx_usage.rs")]
-/// ```
 #[proc_macro]
 #[proc_macro]
 pub fn rsx(s: TokenStream) -> TokenStream {
 pub fn rsx(s: TokenStream) -> TokenStream {
     match syn::parse::<rsx::CallBody>(s) {
     match syn::parse::<rsx::CallBody>(s) {
@@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
 /// The render! macro makes it easy for developers to write jsx-style markup in their components.
 /// The render! macro makes it easy for developers to write jsx-style markup in their components.
 ///
 ///
 /// The render macro automatically renders rsx - making it unhygenic.
 /// The render macro automatically renders rsx - making it unhygenic.
-///
-/// ## Complete Reference Guide:
-/// ```ignore
-#[doc = include_str!("../../../examples/rsx_usage.rs")]
-/// ```
 #[proc_macro]
 #[proc_macro]
 pub fn render(s: TokenStream) -> TokenStream {
 pub fn render(s: TokenStream) -> TokenStream {
     match syn::parse::<rsx::CallBody>(s) {
     match syn::parse::<rsx::CallBody>(s) {

+ 4 - 1
packages/core/Cargo.toml

@@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 bumpalo = { version = "3.6", features = ["collections", "boxed"] }
 bumpalo = { version = "3.6", features = ["collections", "boxed"] }
 
 
 # faster hashmaps
 # faster hashmaps
-fxhash = "0.2"
+rustc-hash = "1.1.0"
 
 
 # Used in diffing
 # Used in diffing
 longest-increasing-subsequence = "0.1.0"
 longest-increasing-subsequence = "0.1.0"
@@ -35,6 +35,9 @@ indexmap = "1.7"
 serde = { version = "1", features = ["derive"], optional = true }
 serde = { version = "1", features = ["derive"], optional = true }
 anyhow = "1.0.66"
 anyhow = "1.0.66"
 
 
+smallbox = "0.8.1"
+log = "0.4.17"
+
 [dev-dependencies]
 [dev-dependencies]
 tokio = { version = "*", features = ["full"] }
 tokio = { version = "*", features = ["full"] }
 dioxus = { path = "../dioxus" }
 dioxus = { path = "../dioxus" }

+ 18 - 11
packages/core/src/arena.rs

@@ -34,14 +34,14 @@ impl ElementRef {
 
 
 impl VirtualDom {
 impl VirtualDom {
     pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
     pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
-        self.next(template, ElementPath::Deep(path))
+        self.next_reference(template, ElementPath::Deep(path))
     }
     }
 
 
     pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
     pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
-        self.next(template, ElementPath::Root(path))
+        self.next_reference(template, ElementPath::Root(path))
     }
     }
 
 
-    fn next(&mut self, template: &VNode, path: ElementPath) -> ElementId {
+    fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
         let entry = self.elements.vacant_entry();
         let entry = self.elements.vacant_entry();
         let id = entry.key();
         let id = entry.key();
 
 
@@ -95,22 +95,27 @@ impl VirtualDom {
     fn drop_scope_inner(&mut self, node: &VNode) {
     fn drop_scope_inner(&mut self, node: &VNode) {
         node.clear_listeners();
         node.clear_listeners();
         node.dynamic_nodes.iter().for_each(|node| match node {
         node.dynamic_nodes.iter().for_each(|node| match node {
-            DynamicNode::Component(c) => self.drop_scope(c.scope.get().unwrap()),
+            DynamicNode::Component(c) => {
+                if let Some(f) = c.scope.get() {
+                    self.drop_scope(f)
+                }
+            }
             DynamicNode::Fragment(nodes) => {
             DynamicNode::Fragment(nodes) => {
                 nodes.iter().for_each(|node| self.drop_scope_inner(node))
                 nodes.iter().for_each(|node| self.drop_scope_inner(node))
             }
             }
             DynamicNode::Placeholder(t) => {
             DynamicNode::Placeholder(t) => {
-                self.try_reclaim(t.get());
+                self.try_reclaim(t.id.get().unwrap());
             }
             }
             DynamicNode::Text(t) => {
             DynamicNode::Text(t) => {
-                self.try_reclaim(t.id.get());
+                self.try_reclaim(t.id.get().unwrap());
             }
             }
         });
         });
 
 
         for root in node.root_ids {
         for root in node.root_ids {
-            let id = root.get();
-            if id.0 != 0 {
-                self.try_reclaim(id);
+            if let Some(id) = root.get() {
+                if id.0 != 0 {
+                    self.try_reclaim(id);
+                }
             }
             }
         }
         }
     }
     }
@@ -131,8 +136,10 @@ impl VirtualDom {
         node.dynamic_nodes.iter().for_each(|child| match child {
         node.dynamic_nodes.iter().for_each(|child| match child {
             // Only descend if the props are borrowed
             // Only descend if the props are borrowed
             DynamicNode::Component(c) if !c.static_props => {
             DynamicNode::Component(c) if !c.static_props => {
-                self.ensure_drop_safety(c.scope.get().unwrap());
-                c.props.set(None);
+                if let Some(scope) = c.scope.get() {
+                    self.ensure_drop_safety(scope);
+                }
+                c.props.take();
             }
             }
 
 
             DynamicNode::Fragment(f) => f
             DynamicNode::Fragment(f) => f

+ 267 - 193
packages/core/src/create.rs

@@ -1,13 +1,15 @@
-use std::cell::Cell;
-use std::rc::Rc;
-
-use crate::innerlude::{VComponent, VText};
+use crate::innerlude::{VComponent, VPlaceholder, VText};
 use crate::mutations::Mutation;
 use crate::mutations::Mutation;
 use crate::mutations::Mutation::*;
 use crate::mutations::Mutation::*;
 use crate::nodes::VNode;
 use crate::nodes::VNode;
 use crate::nodes::{DynamicNode, TemplateNode};
 use crate::nodes::{DynamicNode, TemplateNode};
 use crate::virtual_dom::VirtualDom;
 use crate::virtual_dom::VirtualDom;
 use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
 use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext};
+use std::cell::Cell;
+use std::iter::{Enumerate, Peekable};
+use std::rc::Rc;
+use std::slice;
+use TemplateNode::*;
 
 
 impl<'b> VirtualDom {
 impl<'b> VirtualDom {
     /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
     /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
@@ -17,187 +19,232 @@ impl<'b> VirtualDom {
         self.scope_stack.push(scope);
         self.scope_stack.push(scope);
         let out = self.create(template);
         let out = self.create(template);
         self.scope_stack.pop();
         self.scope_stack.pop();
-
         out
         out
     }
     }
 
 
     /// Create this template and write its mutations
     /// Create this template and write its mutations
-    pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize {
+    pub(crate) fn create(&mut self, node: &'b VNode<'b>) -> usize {
         // The best renderers will have templates prehydrated and registered
         // The best renderers will have templates prehydrated and registered
         // Just in case, let's create the template using instructions anyways
         // Just in case, let's create the template using instructions anyways
-        if !self.templates.contains_key(&template.template.name) {
-            self.register_template(template);
+        if !self.templates.contains_key(&node.template.name) {
+            self.register_template(node);
         }
         }
 
 
+        // we know that this will generate at least one mutation per node
+        self.mutations.edits.reserve(node.template.roots.len());
+
         // Walk the roots, creating nodes and assigning IDs
         // Walk the roots, creating nodes and assigning IDs
         // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
         // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS)
-        let mut dynamic_attrs = template.template.attr_paths.iter().enumerate().peekable();
-        let mut dynamic_nodes = template.template.node_paths.iter().enumerate().peekable();
-
-        let cur_scope = self.scope_stack.last().copied().unwrap();
-
-        let mut on_stack = 0;
-        for (root_idx, root) in template.template.roots.iter().enumerate() {
-            // We might need to generate an ID for the root node
-            on_stack += match root {
-                TemplateNode::DynamicText { id } | TemplateNode::Dynamic { id } => {
-                    match &template.dynamic_nodes[*id] {
-                        // a dynamic text node doesn't replace a template node, instead we create it on the fly
-                        DynamicNode::Text(VText { id: slot, value }) => {
-                            let id = self.next_element(template, template.template.node_paths[*id]);
-                            slot.set(id);
-
-                            // Safety: we promise not to re-alias this text later on after committing it to the mutation
-                            let unbounded_text = unsafe { std::mem::transmute(*value) };
-                            self.mutations.push(CreateTextNode {
-                                value: unbounded_text,
-                                id,
-                            });
-
-                            1
-                        }
-
-                        DynamicNode::Placeholder(slot) => {
-                            let id = self.next_element(template, template.template.node_paths[*id]);
-                            slot.set(id);
-                            self.mutations.push(CreatePlaceholder { id });
-                            1
-                        }
-
-                        DynamicNode::Fragment(_) | DynamicNode::Component { .. } => {
-                            self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id)
-                        }
-                    }
-                }
+        let mut attrs = node.template.attr_paths.iter().enumerate().peekable();
+        let mut nodes = node.template.node_paths.iter().enumerate().peekable();
+
+        node.template
+            .roots
+            .iter()
+            .enumerate()
+            .map(|(idx, root)| match root {
+                DynamicText { id } | Dynamic { id } => self.write_dynamic_root(node, *id),
+                Element { .. } => self.write_element_root(node, idx, &mut attrs, &mut nodes),
+                Text { .. } => self.write_static_text_root(node, idx),
+            })
+            .sum()
+    }
+
+    fn write_static_text_root(&mut self, node: &VNode, idx: usize) -> usize {
+        // Simply just load the template root, no modifications needed
+        self.load_template_root(node, idx);
+
+        // Text producs just one node on the stack
+        1
+    }
+
+    fn write_dynamic_root(&mut self, template: &'b VNode<'b>, idx: usize) -> usize {
+        use DynamicNode::*;
+        match &template.dynamic_nodes[idx] {
+            node @ Fragment(_) => self.create_dynamic_node(template, node, idx),
+            node @ Component { .. } => self.create_dynamic_node(template, node, idx),
+            Placeholder(VPlaceholder { id }) => {
+                let id = self.set_slot(template, id, idx);
+                self.mutations.push(CreatePlaceholder { id });
+                1
+            }
+            Text(VText { id, value }) => {
+                let id = self.set_slot(template, id, idx);
+                self.create_static_text(value, id);
+                1
+            }
+        }
+    }
+
+    fn create_static_text(&mut self, value: &str, id: ElementId) {
+        // Safety: we promise not to re-alias this text later on after committing it to the mutation
+        let unbounded_text: &str = unsafe { std::mem::transmute(value) };
+        self.mutations.push(CreateTextNode {
+            value: unbounded_text,
+            id,
+        });
+    }
+
+    /// We write all the descndent data for this element
+    ///
+    /// Elements can contain other nodes - and those nodes can be dynamic or static
+    ///
+    /// We want to make sure we write these nodes while on top of the root
+    fn write_element_root(
+        &mut self,
+        template: &'b VNode<'b>,
+        root_idx: usize,
+        dynamic_attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
+        dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
+    ) -> usize {
+        // Load the template root and get the ID for the node on the stack
+        let root_on_stack = self.load_template_root(template, root_idx);
+
+        // Write all the attributes below this root
+        self.write_attrs_on_root(dynamic_attrs, root_idx, root_on_stack, template);
+
+        // Load in all of the placeholder or dynamic content under this root too
+        self.load_placeholders(dynamic_nodes, root_idx, template);
+
+        1
+    }
+
+    /// Load all of the placeholder nodes for descendents of this root node
+    ///
+    /// ```rust, ignore
+    /// rsx! {
+    ///     div {
+    ///         // This is a placeholder
+    ///         some_value,
+    ///
+    ///         // Load this too
+    ///         "{some_text}"
+    ///     }
+    /// }
+    /// ```
+    fn load_placeholders(
+        &mut self,
+        dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
+        root_idx: usize,
+        template: &'b VNode<'b>,
+    ) {
+        let (start, end) = match collect_dyn_node_range(dynamic_nodes, root_idx) {
+            Some((a, b)) => (a, b),
+            None => return,
+        };
+
+        for idx in (start..=end).rev() {
+            let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
+            if m > 0 {
+                // The path is one shorter because the top node is the root
+                let path = &template.template.node_paths[idx][1..];
+                self.mutations.push(ReplacePlaceholder { m, path });
+            }
+        }
+    }
 
 
-                TemplateNode::Element { .. } | TemplateNode::Text { .. } => {
-                    let this_id = self.next_root(template, root_idx);
-
-                    template.root_ids[root_idx].set(this_id);
-                    self.mutations.push(LoadTemplate {
-                        name: template.template.name,
-                        index: root_idx,
-                        id: this_id,
-                    });
-
-                    // we're on top of a node that has a dynamic attribute for a descendant
-                    // Set that attribute now before the stack gets in a weird state
-                    while let Some((mut attr_id, path)) =
-                        dynamic_attrs.next_if(|(_, p)| p[0] == root_idx as u8)
-                    {
-                        // if attribute is on a root node, then we've already created the element
-                        // Else, it's deep in the template and we should create a new id for it
-                        let id = match path.len() {
-                            1 => this_id,
-                            _ => {
-                                let id = self
-                                    .next_element(template, template.template.attr_paths[attr_id]);
-                                self.mutations.push(Mutation::AssignId {
-                                    path: &path[1..],
-                                    id,
-                                });
-                                id
-                            }
-                        };
-
-                        loop {
-                            let attribute = template.dynamic_attrs.get(attr_id).unwrap();
-                            attribute.mounted_element.set(id);
-
-                            // Safety: we promise not to re-alias this text later on after committing it to the mutation
-                            let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
-
-                            match &attribute.value {
-                                AttributeValue::Text(value) => {
-                                    // Safety: we promise not to re-alias this text later on after committing it to the mutation
-                                    let unbounded_value = unsafe { std::mem::transmute(*value) };
-
-                                    self.mutations.push(SetAttribute {
-                                        name: unbounded_name,
-                                        value: unbounded_value,
-                                        ns: attribute.namespace,
-                                        id,
-                                    })
-                                }
-                                AttributeValue::Bool(value) => {
-                                    self.mutations.push(SetBoolAttribute {
-                                        name: unbounded_name,
-                                        value: *value,
-                                        id,
-                                    })
-                                }
-                                AttributeValue::Listener(_) => {
-                                    self.mutations.push(NewEventListener {
-                                        // all listeners start with "on"
-                                        name: &unbounded_name[2..],
-                                        scope: cur_scope,
-                                        id,
-                                    })
-                                }
-                                AttributeValue::Float(_) => todo!(),
-                                AttributeValue::Int(_) => todo!(),
-                                AttributeValue::Any(_) => todo!(),
-                                AttributeValue::None => todo!(),
-                            }
-
-                            // Only push the dynamic attributes forward if they match the current path (same element)
-                            match dynamic_attrs.next_if(|(_, p)| *p == path) {
-                                Some((next_attr_id, _)) => attr_id = next_attr_id,
-                                None => break,
-                            }
-                        }
-                    }
-
-                    // We're on top of a node that has a dynamic child for a descendant
-                    // Skip any node that's a root
-                    let mut start = None;
-                    let mut end = None;
-
-                    // Collect all the dynamic nodes below this root
-                    // We assign the start and end of the range of dynamic nodes since they area ordered in terms of tree path
-                    //
-                    // [0]
-                    // [1, 1]     <---|
-                    // [1, 1, 1]  <---| these are the range of dynamic nodes below root 1
-                    // [1, 1, 2]  <---|
-                    // [2]
-                    //
-                    // We collect each range and then create them and replace the placeholder in the template
-                    while let Some((idx, p)) =
-                        dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8)
-                    {
-                        if p.len() == 1 {
-                            continue;
-                        }
-
-                        if start.is_none() {
-                            start = Some(idx);
-                        }
-
-                        end = Some(idx);
-                    }
-
-                    //
-                    if let (Some(start), Some(end)) = (start, end) {
-                        for idx in start..=end {
-                            let node = &template.dynamic_nodes[idx];
-                            let m = self.create_dynamic_node(template, node, idx);
-                            if m > 0 {
-                                self.mutations.push(ReplacePlaceholder {
-                                    m,
-                                    path: &template.template.node_paths[idx][1..],
-                                });
-                            }
-                        }
-                    }
-
-                    // elements create only one node :-)
-                    1
+    fn write_attrs_on_root(
+        &mut self,
+        attrs: &mut Peekable<Enumerate<slice::Iter<&'static [u8]>>>,
+        root_idx: usize,
+        root: ElementId,
+        node: &VNode,
+    ) {
+        while let Some((mut attr_id, path)) = attrs.next_if(|(_, p)| p[0] == root_idx as u8) {
+            let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
+
+            loop {
+                self.write_attribute(&node.dynamic_attrs[attr_id], id);
+
+                // Only push the dynamic attributes forward if they match the current path (same element)
+                match attrs.next_if(|(_, p)| *p == path) {
+                    Some((next_attr_id, _)) => attr_id = next_attr_id,
+                    None => break,
                 }
                 }
-            };
+            }
+        }
+    }
+
+    fn write_attribute(&mut self, attribute: &crate::Attribute, id: ElementId) {
+        // Make sure we set the attribute's associated id
+        attribute.mounted_element.set(id);
+
+        // Safety: we promise not to re-alias this text later on after committing it to the mutation
+        let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
+
+        match &attribute.value {
+            AttributeValue::Text(value) => {
+                // Safety: we promise not to re-alias this text later on after committing it to the mutation
+                let unbounded_value = unsafe { std::mem::transmute(*value) };
+
+                self.mutations.push(SetAttribute {
+                    name: unbounded_name,
+                    value: unbounded_value,
+                    ns: attribute.namespace,
+                    id,
+                })
+            }
+            AttributeValue::Bool(value) => self.mutations.push(SetBoolAttribute {
+                name: unbounded_name,
+                value: *value,
+                id,
+            }),
+            AttributeValue::Listener(_) => {
+                self.mutations.push(NewEventListener {
+                    // all listeners start with "on"
+                    name: &unbounded_name[2..],
+                    id,
+                })
+            }
+            AttributeValue::Float(_) => todo!(),
+            AttributeValue::Int(_) => todo!(),
+            AttributeValue::Any(_) => todo!(),
+            AttributeValue::None => todo!(),
         }
         }
+    }
+
+    fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
+        // Get an ID for this root since it's a real root
+        let this_id = self.next_root(template, root_idx);
+        template.root_ids[root_idx].set(Some(this_id));
+
+        self.mutations.push(LoadTemplate {
+            name: template.template.name,
+            index: root_idx,
+            id: this_id,
+        });
 
 
-        on_stack
+        this_id
+    }
+
+    /// We have some dynamic attributes attached to a some node
+    ///
+    /// That node needs to be loaded at runtime, so we need to give it an ID
+    ///
+    /// If the node in question is on the stack, we just return that ID
+    ///
+    /// If the node is not on the stack, we create a new ID for it and assign it
+    fn assign_static_node_as_dynamic(
+        &mut self,
+        path: &'static [u8],
+        this_id: ElementId,
+        template: &VNode,
+        attr_id: usize,
+    ) -> ElementId {
+        if path.len() == 1 {
+            return this_id;
+        }
+
+        // if attribute is on a root node, then we've already created the element
+        // Else, it's deep in the template and we should create a new id for it
+        let id = self.next_element(template, template.template.attr_paths[attr_id]);
+
+        self.mutations.push(Mutation::AssignId {
+            path: &path[1..],
+            id,
+        });
+
+        id
     }
     }
 
 
     /// Insert a new template into the VirtualDom's template registry
     /// Insert a new template into the VirtualDom's template registry
@@ -207,17 +254,9 @@ impl<'b> VirtualDom {
             .insert(template.template.name, template.template);
             .insert(template.template.name, template.template);
 
 
         // If it's all dynamic nodes, then we don't need to register it
         // If it's all dynamic nodes, then we don't need to register it
-        // Quickly run through and see if it's all just dynamic nodes
-        if template.template.roots.iter().all(|root| {
-            matches!(
-                root,
-                TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
-            )
-        }) {
-            return;
+        if !template.template.is_completely_dynamic() {
+            self.mutations.templates.push(template.template);
         }
         }
-
-        self.mutations.templates.push(template.template);
     }
     }
 
 
     pub(crate) fn create_dynamic_node(
     pub(crate) fn create_dynamic_node(
@@ -245,7 +284,7 @@ impl<'b> VirtualDom {
         let new_id = self.next_element(template, template.template.node_paths[idx]);
         let new_id = self.next_element(template, template.template.node_paths[idx]);
 
 
         // Make sure the text node is assigned to the correct element
         // Make sure the text node is assigned to the correct element
-        text.id.set(new_id);
+        text.id.set(Some(new_id));
 
 
         // Safety: we promise not to re-alias this text later on after committing it to the mutation
         // Safety: we promise not to re-alias this text later on after committing it to the mutation
         let value = unsafe { std::mem::transmute(text.value) };
         let value = unsafe { std::mem::transmute(text.value) };
@@ -263,7 +302,7 @@ impl<'b> VirtualDom {
 
 
     pub(crate) fn create_placeholder(
     pub(crate) fn create_placeholder(
         &mut self,
         &mut self,
-        slot: &Cell<ElementId>,
+        placeholder: &VPlaceholder,
         template: &'b VNode<'b>,
         template: &'b VNode<'b>,
         idx: usize,
         idx: usize,
     ) -> usize {
     ) -> usize {
@@ -271,7 +310,7 @@ impl<'b> VirtualDom {
         let id = self.next_element(template, template.template.node_paths[idx]);
         let id = self.next_element(template, template.template.node_paths[idx]);
 
 
         // Make sure the text node is assigned to the correct element
         // Make sure the text node is assigned to the correct element
-        slot.set(id);
+        placeholder.id.set(Some(id));
 
 
         // Assign the ID to the existing node in the template
         // Assign the ID to the existing node in the template
         self.mutations.push(AssignId {
         self.mutations.push(AssignId {
@@ -284,7 +323,7 @@ impl<'b> VirtualDom {
     }
     }
 
 
     pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
     pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
-        nodes.iter().fold(0, |acc, child| acc + self.create(child))
+        nodes.iter().map(|child| self.create(child)).sum()
     }
     }
 
 
     pub(super) fn create_component_node(
     pub(super) fn create_component_node(
@@ -293,15 +332,17 @@ impl<'b> VirtualDom {
         component: &'b VComponent<'b>,
         component: &'b VComponent<'b>,
         idx: usize,
         idx: usize,
     ) -> usize {
     ) -> usize {
-        let props = component
-            .props
-            .replace(None)
-            .expect("Props to always exist when a component is being created");
-
-        let unbounded_props = unsafe { std::mem::transmute(props) };
+        let scope = match component.props.take() {
+            Some(props) => {
+                let unbounded_props = unsafe { std::mem::transmute(props) };
+                let scope = self.new_scope(unbounded_props, component.name);
+                scope.id
+            }
+
+            // Component is coming back, it probably still exists, right?
+            None => component.scope.get().unwrap(),
+        };
 
 
-        let scope = self.new_scope(unbounded_props, component.name);
-        let scope = scope.id;
         component.scope.set(Some(scope));
         component.scope.set(Some(scope));
 
 
         let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
         let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
@@ -391,4 +432,37 @@ impl<'b> VirtualDom {
 
 
         0
         0
     }
     }
+
+    fn set_slot(
+        &mut self,
+        template: &'b VNode<'b>,
+        slot: &'b Cell<Option<ElementId>>,
+        id: usize,
+    ) -> ElementId {
+        let id = self.next_element(template, template.template.node_paths[id]);
+        slot.set(Some(id));
+        id
+    }
+}
+
+fn collect_dyn_node_range(
+    dynamic_nodes: &mut Peekable<Enumerate<slice::Iter<&[u8]>>>,
+    root_idx: usize,
+) -> Option<(usize, usize)> {
+    let start = match dynamic_nodes.peek() {
+        Some((idx, p)) if p[0] == root_idx as u8 => *idx,
+        _ => return None,
+    };
+
+    let mut end = start;
+
+    while let Some((idx, p)) = dynamic_nodes.next_if(|(_, p)| p[0] == root_idx as u8) {
+        if p.len() == 1 {
+            continue;
+        }
+
+        end = idx;
+    }
+
+    Some((start, end))
 }
 }

+ 189 - 123
packages/core/src/diff.rs

@@ -1,17 +1,15 @@
-use std::cell::Cell;
-
 use crate::{
 use crate::{
     arena::ElementId,
     arena::ElementId,
-    innerlude::{DirtyScope, VComponent, VText},
+    innerlude::{DirtyScope, VComponent, VPlaceholder, VText},
     mutations::Mutation,
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::RenderReturn,
     nodes::{DynamicNode, VNode},
     nodes::{DynamicNode, VNode},
     scopes::ScopeId,
     scopes::ScopeId,
     virtual_dom::VirtualDom,
     virtual_dom::VirtualDom,
-    AttributeValue, TemplateNode,
+    Attribute, AttributeValue, TemplateNode,
 };
 };
 
 
-use fxhash::{FxHashMap, FxHashSet};
+use rustc_hash::{FxHashMap, FxHashSet};
 use DynamicNode::*;
 use DynamicNode::*;
 
 
 impl<'b> VirtualDom {
 impl<'b> VirtualDom {
@@ -56,87 +54,101 @@ impl<'b> VirtualDom {
     fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
     fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
 
 
     fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
     fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
-        if left_template.template.name != right_template.template.name {
+        // If the templates are the same, we don't need to do anything, nor do we want to
+        if templates_are_the_same(left_template, right_template) {
+            return;
+        }
+
+        // If the templates are different by name, we need to replace the entire template
+        if templates_are_different(left_template, right_template) {
             return self.light_diff_templates(left_template, right_template);
             return self.light_diff_templates(left_template, right_template);
         }
         }
 
 
-        for (left_attr, right_attr) in left_template
+        // If the templates are the same, we can diff the attributes and children
+        // Start with the attributes
+        left_template
             .dynamic_attrs
             .dynamic_attrs
             .iter()
             .iter()
             .zip(right_template.dynamic_attrs.iter())
             .zip(right_template.dynamic_attrs.iter())
-        {
-            // Move over the ID from the old to the new
-            right_attr
-                .mounted_element
-                .set(left_attr.mounted_element.get());
-
-            // We want to make sure anything listener that gets pulled is valid
-            if let AttributeValue::Listener(_) = right_attr.value {
-                self.update_template(left_attr.mounted_element.get(), right_template);
-            }
+            .for_each(|(left_attr, right_attr)| {
+                // Move over the ID from the old to the new
+                right_attr
+                    .mounted_element
+                    .set(left_attr.mounted_element.get());
+
+                // We want to make sure anything listener that gets pulled is valid
+                if let AttributeValue::Listener(_) = right_attr.value {
+                    self.update_template(left_attr.mounted_element.get(), right_template);
+                }
 
 
-            if left_attr.value != right_attr.value || left_attr.volatile {
-                // todo: add more types of attribute values
-                match right_attr.value {
-                    AttributeValue::Text(text) => {
-                        let name = unsafe { std::mem::transmute(left_attr.name) };
-                        let value = unsafe { std::mem::transmute(text) };
-                        self.mutations.push(Mutation::SetAttribute {
-                            id: left_attr.mounted_element.get(),
-                            ns: right_attr.namespace,
-                            name,
-                            value,
-                        });
-                    }
-                    // todo: more types of attribute values
-                    _ => todo!("other attribute types"),
+                // If the attributes are different (or volatile), we need to update them
+                if left_attr.value != right_attr.value || left_attr.volatile {
+                    self.update_attribute(right_attr, left_attr);
                 }
                 }
-            }
-        }
+            });
 
 
-        for (idx, (left_node, right_node)) in left_template
+        // Now diff the dynamic nodes
+        left_template
             .dynamic_nodes
             .dynamic_nodes
             .iter()
             .iter()
             .zip(right_template.dynamic_nodes.iter())
             .zip(right_template.dynamic_nodes.iter())
             .enumerate()
             .enumerate()
-        {
-            match (left_node, right_node) {
-                (Text(left), Text(right)) => self.diff_vtext(left, right),
-                (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
-                (Placeholder(left), Placeholder(right)) => {
-                    right.set(left.get());
-                }
-                (Component(left), Component(right)) => {
-                    self.diff_vcomponent(left, right, right_template, idx)
-                }
-                (Placeholder(left), Fragment(right)) => {
-                    self.replace_placeholder_with_nodes(left, right)
-                }
-                (Fragment(left), Placeholder(right)) => {
-                    self.replace_nodes_with_placeholder(left, right)
-                }
-                _ => todo!(),
-            };
-        }
+            .for_each(|(idx, (left_node, right_node))| {
+                self.diff_dynamic_node(left_node, right_node, right_template, idx);
+            });
 
 
-        // Make sure the roots get transferred over
-        for (left, right) in left_template
+        // Make sure the roots get transferred over while we're here
+        left_template
             .root_ids
             .root_ids
             .iter()
             .iter()
             .zip(right_template.root_ids.iter())
             .zip(right_template.root_ids.iter())
-        {
-            right.set(left.get());
+            .for_each(|(left, right)| right.set(left.get()));
+    }
+
+    fn diff_dynamic_node(
+        &mut self,
+        left_node: &'b DynamicNode<'b>,
+        right_node: &'b DynamicNode<'b>,
+        node: &'b VNode<'b>,
+        idx: usize,
+    ) {
+        match (left_node, right_node) {
+            (Text(left), Text(right)) => self.diff_vtext(left, right, node),
+            (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
+            (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
+            (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx),
+            (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right),
+            (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
+            _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
+        };
+    }
+
+    fn update_attribute(&mut self, right_attr: &Attribute, left_attr: &Attribute) {
+        // todo: add more types of attribute values
+        match right_attr.value {
+            AttributeValue::Text(text) => {
+                let name = unsafe { std::mem::transmute(left_attr.name) };
+                let value = unsafe { std::mem::transmute(text) };
+                self.mutations.push(Mutation::SetAttribute {
+                    id: left_attr.mounted_element.get(),
+                    ns: right_attr.namespace,
+                    name,
+                    value,
+                });
+            }
+            // todo: more types of attribute values
+            _ => todo!("other attribute types"),
         }
         }
     }
     }
 
 
-    fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
+    fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) {
         let m = self.create_children(r);
         let m = self.create_children(r);
-        let id = l.get();
+        let id = l.id.get().unwrap();
         self.mutations.push(Mutation::ReplaceWith { id, m });
         self.mutations.push(Mutation::ReplaceWith { id, m });
         self.reclaim(id);
         self.reclaim(id);
     }
     }
 
 
-    fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
+    fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
         // Remove the old nodes, except for one
         // Remove the old nodes, except for one
         self.remove_nodes(&l[1..]);
         self.remove_nodes(&l[1..]);
 
 
@@ -145,7 +157,7 @@ impl<'b> VirtualDom {
 
 
         // Create the placeholder first, ensuring we get a dedicated ID for the placeholder
         // Create the placeholder first, ensuring we get a dedicated ID for the placeholder
         let placeholder = self.next_element(&l[0], &[]);
         let placeholder = self.next_element(&l[0], &[]);
-        r.set(placeholder);
+        r.id.set(Some(placeholder));
         self.mutations
         self.mutations
             .push(Mutation::CreatePlaceholder { id: placeholder });
             .push(Mutation::CreatePlaceholder { id: placeholder });
 
 
@@ -162,6 +174,10 @@ impl<'b> VirtualDom {
         right_template: &'b VNode<'b>,
         right_template: &'b VNode<'b>,
         idx: usize,
         idx: usize,
     ) {
     ) {
+        if std::ptr::eq(left, right) {
+            return;
+        }
+
         // Replace components that have different render fns
         // Replace components that have different render fns
         if left.render_fn != right.render_fn {
         if left.render_fn != right.render_fn {
             let created = self.create_component_node(right_template, right, idx);
             let created = self.create_component_node(right_template, right, idx);
@@ -181,12 +197,22 @@ impl<'b> VirtualDom {
         }
         }
 
 
         // Make sure the new vcomponent has the right scopeid associated to it
         // Make sure the new vcomponent has the right scopeid associated to it
-        let scope_id = left.scope.get().unwrap();
+        let Some(scope_id) = left.scope.get() else {
+            return;
+        };
+        // let scope_id = left.scope.get().unwrap_or_else(|| {
+        //     panic!(
+        //         "A component should always have a scope associated to it. {:?}\n {:#?}",
+        //         right.name,
+        //         std::backtrace::Backtrace::force_capture()
+        //     )
+        // });
+
         right.scope.set(Some(scope_id));
         right.scope.set(Some(scope_id));
 
 
         // copy out the box for both
         // copy out the box for both
         let old = self.scopes[scope_id.0].props.as_ref();
         let old = self.scopes[scope_id.0].props.as_ref();
-        let new = right.props.replace(None).unwrap();
+        let new = right.props.take().unwrap();
 
 
         // If the props are static, then we try to memoize by setting the new with the old
         // If the props are static, then we try to memoize by setting the new with the old
         // The target scopestate still has the reference to the old props, so there's no need to update anything
         // The target scopestate still has the reference to the old props, so there's no need to update anything
@@ -201,6 +227,11 @@ impl<'b> VirtualDom {
         // Now run the component and diff it
         // Now run the component and diff it
         self.run_scope(scope_id);
         self.run_scope(scope_id);
         self.diff_scope(scope_id);
         self.diff_scope(scope_id);
+
+        self.dirty_scopes.remove(&DirtyScope {
+            height: self.scopes[scope_id.0].height,
+            id: scope_id,
+        });
     }
     }
 
 
     /// Lightly diff the two templates, checking only their roots.
     /// Lightly diff the two templates, checking only their roots.
@@ -254,10 +285,13 @@ impl<'b> VirtualDom {
     ///
     ///
     /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
     /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
     /// different.
     /// different.
-    fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
-        let id = left.id.get();
+    fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
+        let id = left
+            .id
+            .get()
+            .unwrap_or_else(|| self.next_element(node, &[0]));
 
 
-        right.id.set(id);
+        right.id.set(Some(id));
         if left.value != right.value {
         if left.value != right.value {
             let value = unsafe { std::mem::transmute(right.value) };
             let value = unsafe { std::mem::transmute(right.value) };
             self.mutations.push(Mutation::SetText { id, value });
             self.mutations.push(Mutation::SetText { id, value });
@@ -269,9 +303,9 @@ impl<'b> VirtualDom {
     /// All IDs will be garbage collected
     /// All IDs will be garbage collected
     fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
     fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
         let id = match node.dynamic_root(0) {
         let id = match node.dynamic_root(0) {
-            None => node.root_ids[0].get(),
-            Some(Text(t)) => t.id.get(),
-            Some(Placeholder(e)) => e.get(),
+            None => node.root_ids[0].get().unwrap(),
+            Some(Text(t)) => t.id.get().unwrap(),
+            Some(Placeholder(e)) => e.id.get().unwrap(),
             Some(Fragment(nodes)) => {
             Some(Fragment(nodes)) => {
                 let id = self.replace_inner(&nodes[0]);
                 let id = self.replace_inner(&nodes[0]);
                 self.remove_nodes(&nodes[1..]);
                 self.remove_nodes(&nodes[1..]);
@@ -309,14 +343,23 @@ impl<'b> VirtualDom {
 
 
             match dyn_node {
             match dyn_node {
                 Component(comp) => {
                 Component(comp) => {
-                    let scope = comp.scope.get().unwrap();
-                    match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-                        RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
-                        _ => todo!("cannot handle nonstandard nodes"),
-                    };
+                    if let Some(scope) = comp.scope.take() {
+                        match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+                            RenderReturn::Sync(Ok(t)) => self.clean_up_node(t),
+                            _ => todo!("cannot handle nonstandard nodes"),
+                        };
+                    }
+                }
+                Text(t) => {
+                    if let Some(id) = t.id.take() {
+                        self.reclaim(id)
+                    }
+                }
+                Placeholder(t) => {
+                    if let Some(id) = t.id.take() {
+                        self.reclaim(id)
+                    }
                 }
                 }
-                Text(t) => self.reclaim(t.id.get()),
-                Placeholder(t) => self.reclaim(t.get()),
                 Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)),
                 Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)),
             };
             };
         }
         }
@@ -344,25 +387,25 @@ impl<'b> VirtualDom {
     fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
     fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) {
         match node.dynamic_root(idx) {
         match node.dynamic_root(idx) {
             Some(Text(i)) => {
             Some(Text(i)) => {
-                let id = i.id.get();
+                let id = i.id.take().unwrap();
                 self.mutations.push(Mutation::Remove { id });
                 self.mutations.push(Mutation::Remove { id });
                 self.reclaim(id);
                 self.reclaim(id);
             }
             }
             Some(Placeholder(e)) => {
             Some(Placeholder(e)) => {
-                let id = e.get();
+                let id = e.id.take().unwrap();
                 self.mutations.push(Mutation::Remove { id });
                 self.mutations.push(Mutation::Remove { id });
                 self.reclaim(id);
                 self.reclaim(id);
             }
             }
             Some(Fragment(nodes)) => self.remove_nodes(nodes),
             Some(Fragment(nodes)) => self.remove_nodes(nodes),
             Some(Component(comp)) => {
             Some(Component(comp)) => {
-                let scope = comp.scope.get().unwrap();
+                let scope = comp.scope.take().unwrap();
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                     RenderReturn::Sync(Ok(t)) => self.remove_node(t),
                     RenderReturn::Sync(Ok(t)) => self.remove_node(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                     _ => todo!("cannot handle nonstandard nodes"),
                 };
                 };
             }
             }
             None => {
             None => {
-                let id = node.root_ids[idx].get();
+                let id = node.root_ids[idx].get().unwrap();
                 self.mutations.push(Mutation::Remove { id });
                 self.mutations.push(Mutation::Remove { id });
                 self.reclaim(id);
                 self.reclaim(id);
             }
             }
@@ -432,7 +475,7 @@ impl<'b> VirtualDom {
     // The stack is empty upon entry.
     // The stack is empty upon entry.
     fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
     fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
         if cfg!(debug_assertions) {
         if cfg!(debug_assertions) {
-            let mut keys = fxhash::FxHashSet::default();
+            let mut keys = rustc_hash::FxHashSet::default();
             let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
             let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
                 keys.clear();
                 keys.clear();
                 for child in children {
                 for child in children {
@@ -748,12 +791,16 @@ impl<'b> VirtualDom {
     fn remove_node(&mut self, node: &'b VNode<'b>) {
     fn remove_node(&mut self, node: &'b VNode<'b>) {
         for (idx, _) in node.template.roots.iter().enumerate() {
         for (idx, _) in node.template.roots.iter().enumerate() {
             let id = match node.dynamic_root(idx) {
             let id = match node.dynamic_root(idx) {
-                Some(Text(t)) => t.id.get(),
-                Some(Placeholder(t)) => t.get(),
+                Some(Text(t)) => t.id.take(),
+                Some(Placeholder(t)) => t.id.take(),
                 Some(Fragment(t)) => return self.remove_nodes(t),
                 Some(Fragment(t)) => return self.remove_nodes(t),
-                Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()),
+                Some(Component(comp)) => {
+                    comp.scope.set(None);
+                    return self.remove_component(comp.scope.get().unwrap());
+                }
                 None => node.root_ids[idx].get(),
                 None => node.root_ids[idx].get(),
-            };
+            }
+            .unwrap();
 
 
             self.mutations.push(Mutation::Remove { id })
             self.mutations.push(Mutation::Remove { id })
         }
         }
@@ -761,7 +808,7 @@ impl<'b> VirtualDom {
         self.clean_up_node(node);
         self.clean_up_node(node);
 
 
         for root in node.root_ids {
         for root in node.root_ids {
-            let id = root.get();
+            let id = root.get().unwrap();
             if id.0 != 0 {
             if id.0 != 0 {
                 self.reclaim(id);
                 self.reclaim(id);
             }
             }
@@ -784,41 +831,45 @@ impl<'b> VirtualDom {
 
 
     /// Push all the real nodes on the stack
     /// Push all the real nodes on the stack
     fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
     fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
-        let mut onstack = 0;
-
-        for (idx, _) in node.template.roots.iter().enumerate() {
-            match node.dynamic_root(idx) {
-                Some(Text(t)) => {
-                    self.mutations.push(Mutation::PushRoot { id: t.id.get() });
-                    onstack += 1;
-                }
-                Some(Placeholder(t)) => {
-                    self.mutations.push(Mutation::PushRoot { id: t.get() });
-                    onstack += 1;
-                }
-                Some(Fragment(nodes)) => {
-                    for node in *nodes {
-                        onstack += self.push_all_real_nodes(node);
+        node.template
+            .roots
+            .iter()
+            .enumerate()
+            .map(|(idx, _)| {
+                match node.dynamic_root(idx) {
+                    Some(Text(t)) => {
+                        self.mutations.push(Mutation::PushRoot {
+                            id: t.id.get().unwrap(),
+                        });
+                        1
                     }
                     }
-                }
-                Some(Component(comp)) => {
-                    let scope = comp.scope.get().unwrap();
-                    onstack +=
+                    Some(Placeholder(t)) => {
+                        self.mutations.push(Mutation::PushRoot {
+                            id: t.id.get().unwrap(),
+                        });
+                        1
+                    }
+                    Some(Fragment(nodes)) => nodes
+                        .iter()
+                        .map(|node| self.push_all_real_nodes(node))
+                        .count(),
+
+                    Some(Component(comp)) => {
+                        let scope = comp.scope.get().unwrap();
                         match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                         match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                             RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
                             RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
                             _ => todo!(),
                             _ => todo!(),
                         }
                         }
-                }
-                None => {
-                    self.mutations.push(Mutation::PushRoot {
-                        id: node.root_ids[idx].get(),
-                    });
-                    onstack += 1;
-                }
-            };
-        }
-
-        onstack
+                    }
+                    None => {
+                        self.mutations.push(Mutation::PushRoot {
+                            id: node.root_ids[idx].get().unwrap(),
+                        });
+                        1
+                    }
+                };
+            })
+            .count()
     }
     }
 
 
     fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
     fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize {
@@ -839,10 +890,10 @@ impl<'b> VirtualDom {
 
 
     fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
     fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
         match node.dynamic_root(0) {
         match node.dynamic_root(0) {
-            None => node.root_ids[0].get(),
-            Some(Text(t)) => t.id.get(),
+            None => node.root_ids[0].get().unwrap(),
+            Some(Text(t)) => t.id.get().unwrap(),
             Some(Fragment(t)) => self.find_first_element(&t[0]),
             Some(Fragment(t)) => self.find_first_element(&t[0]),
-            Some(Placeholder(t)) => t.get(),
+            Some(Placeholder(t)) => t.id.get().unwrap(),
             Some(Component(comp)) => {
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
                 let scope = comp.scope.get().unwrap();
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
@@ -855,10 +906,10 @@ impl<'b> VirtualDom {
 
 
     fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
     fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
         match node.dynamic_root(node.template.roots.len() - 1) {
         match node.dynamic_root(node.template.roots.len() - 1) {
-            None => node.root_ids.last().unwrap().get(),
-            Some(Text(t)) => t.id.get(),
+            None => node.root_ids.last().unwrap().get().unwrap(),
+            Some(Text(t)) => t.id.get().unwrap(),
             Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
             Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
-            Some(Placeholder(t)) => t.get(),
+            Some(Placeholder(t)) => t.id.get().unwrap(),
             Some(Component(comp)) => {
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
                 let scope = comp.scope.get().unwrap();
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
@@ -892,6 +943,21 @@ impl<'b> VirtualDom {
     }
     }
 }
 }
 
 
+/// Are the templates the same?
+///
+/// We need to check for the obvious case, and the non-obvious case where the template as cloned
+///
+/// We use the pointer of the dynamic_node list in this case
+fn templates_are_the_same<'b>(left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) -> bool {
+    std::ptr::eq(left_template, right_template)
+        || std::ptr::eq(left_template.dynamic_nodes, right_template.dynamic_nodes)
+}
+
+fn templates_are_different(left_template: &VNode, right_template: &VNode) -> bool {
+    !std::ptr::eq(left_template.template.name, right_template.template.name)
+        && left_template.template.name != right_template.template.name
+}
+
 fn matching_components<'a>(
 fn matching_components<'a>(
     left: &'a VNode<'a>,
     left: &'a VNode<'a>,
     right: &'a VNode<'a>,
     right: &'a VNode<'a>,

+ 9 - 7
packages/core/src/dirty_scope.rs

@@ -1,19 +1,21 @@
+use std::hash::Hash;
+
 use crate::ScopeId;
 use crate::ScopeId;
 
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
 pub struct DirtyScope {
 pub struct DirtyScope {
     pub height: u32,
     pub height: u32,
     pub id: ScopeId,
     pub id: ScopeId,
 }
 }
 
 
-impl PartialOrd for DirtyScope {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.height.cmp(&other.height))
+impl PartialEq for DirtyScope {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id
     }
     }
 }
 }
 
 
-impl Ord for DirtyScope {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        self.height.cmp(&other.height)
+impl Hash for DirtyScope {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.id.hash(state);
     }
     }
 }
 }

+ 9 - 199
packages/core/src/lazynodes.rs

@@ -11,8 +11,9 @@
 //! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
 //! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
 //! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
 //! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
 
 
+use smallbox::{smallbox, space::S16, SmallBox};
+
 use crate::{innerlude::VNode, ScopeState};
 use crate::{innerlude::VNode, ScopeState};
-use std::mem;
 
 
 /// A concrete type provider for closures that build [`VNode`] structures.
 /// A concrete type provider for closures that build [`VNode`] structures.
 ///
 ///
@@ -24,14 +25,7 @@ use std::mem;
 /// LazyNodes::new(|f| f.element("div", [], [], [] None))
 /// LazyNodes::new(|f| f.element("div", [], [], [] None))
 /// ```
 /// ```
 pub struct LazyNodes<'a, 'b> {
 pub struct LazyNodes<'a, 'b> {
-    inner: StackNodeStorage<'a, 'b>,
-}
-
-type StackHeapSize = [usize; 16];
-
-enum StackNodeStorage<'a, 'b> {
-    Stack(LazyStack),
-    Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
+    inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
 }
 }
 
 
 impl<'a, 'b> LazyNodes<'a, 'b> {
 impl<'a, 'b> LazyNodes<'a, 'b> {
@@ -44,114 +38,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
         // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
         // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
         let mut slot = Some(val);
         let mut slot = Some(val);
 
 
-        let val = move |fac: Option<&'a ScopeState>| {
-            fac.map(
-                slot.take()
-                    .expect("LazyNodes closure to be called only once"),
-            )
-        };
-
-        // miri does not know how to work with mucking directly into bytes
-        // just use a heap allocated type when miri is running
-        if cfg!(miri) {
-            Self {
-                inner: StackNodeStorage::Heap(Box::new(val)),
-            }
-        } else {
-            unsafe { LazyNodes::new_inner(val) }
-        }
-    }
-
-    /// Create a new [`LazyNodes`] closure, but force it onto the heap.
-    pub fn new_boxed<F>(inner: F) -> Self
-    where
-        F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b,
-    {
-        // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
-        let mut slot = Some(inner);
-
         Self {
         Self {
-            inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
-                fac.map(
-                    slot.take()
-                        .expect("LazyNodes closure to be called only once"),
-                )
-            })),
-        }
-    }
-
-    unsafe fn new_inner<F>(val: F) -> Self
-    where
-        F: FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b,
-    {
-        let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>;
-
-        assert_eq!(
-            ptr as *const u8, &val as *const _ as *const u8,
-            "MISUSE: Closure returned different pointer"
-        );
-        assert_eq!(
-            std::mem::size_of_val(&*ptr),
-            std::mem::size_of::<F>(),
-            "MISUSE: Closure returned a subset pointer"
-        );
-
-        let words = ptr_as_slice(&mut ptr);
-        assert!(
-            words[0] == &val as *const _ as usize,
-            "BUG: Pointer layout is not (data_ptr, info...)"
-        );
-
-        // - Ensure that Self is aligned same as data requires
-        assert!(
-            std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
-            "TODO: Enforce alignment >{} (requires {})",
-            std::mem::align_of::<Self>(),
-            std::mem::align_of::<F>()
-        );
-
-        let info = &words[1..];
-        let data = words[0] as *mut ();
-        let size = mem::size_of::<F>();
-
-        let stored_size = info.len() * mem::size_of::<usize>() + size;
-        let max_size = mem::size_of::<StackHeapSize>();
-
-        if stored_size > max_size {
-            Self {
-                inner: StackNodeStorage::Heap(Box::new(val)),
-            }
-        } else {
-            let mut buf: StackHeapSize = StackHeapSize::default();
-
-            assert!(info.len() + round_to_words(size) <= buf.as_ref().len());
-
-            // Place pointer information at the end of the region
-            // - Allows the data to be at the start for alignment purposes
-            {
-                let info_ofs = buf.as_ref().len() - info.len();
-                let info_dst = &mut buf.as_mut()[info_ofs..];
-                for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
-                    *d = *v;
-                }
-            }
-
-            let src_ptr = data as *const u8;
-            let dataptr = buf.as_mut_ptr().cast::<u8>();
-
-            for i in 0..size {
-                *dataptr.add(i) = *src_ptr.add(i);
-            }
-
-            std::mem::forget(val);
-
-            Self {
-                inner: StackNodeStorage::Stack(LazyStack {
-                    _align: [],
-                    buf,
-                    dropped: false,
-                }),
-            }
+            inner: smallbox!(move |f| {
+                let val = slot.take().expect("cannot call LazyNodes twice");
+                val(f)
+            }),
         }
         }
     }
     }
 
 
@@ -163,88 +54,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
     /// let node = f.call(cac);
     /// let node = f.call(cac);
     /// ```
     /// ```
     #[must_use]
     #[must_use]
-    pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
-        match self.inner {
-            StackNodeStorage::Heap(mut lazy) => {
-                lazy(Some(f)).expect("Closure should not be called twice")
-            }
-            StackNodeStorage::Stack(mut stack) => stack.call(f),
-        }
+    pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
+        (self.inner)(f)
     }
     }
 }
 }
-
-struct LazyStack {
-    _align: [u64; 0],
-    buf: StackHeapSize,
-    dropped: bool,
-}
-
-impl LazyStack {
-    fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> {
-        let LazyStack { buf, .. } = self;
-        let data = buf.as_ref();
-
-        let info_size =
-            mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>>()
-                / mem::size_of::<usize>()
-                - 1;
-
-        let info_ofs = data.len() - info_size;
-
-        let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> =
-            unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
-
-        self.dropped = true;
-
-        let clos = unsafe { &mut *g };
-        clos(Some(f)).unwrap()
-    }
-}
-impl Drop for LazyStack {
-    fn drop(&mut self) {
-        if !self.dropped {
-            let LazyStack { buf, .. } = self;
-            let data = buf.as_ref();
-
-            let info_size =
-                mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
-                    / mem::size_of::<usize>()
-                    - 1;
-
-            let info_ofs = data.len() - info_size;
-
-            let g: *mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>> =
-                unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
-
-            self.dropped = true;
-
-            let clos = unsafe { &mut *g };
-            clos(None);
-        }
-    }
-}
-
-/// Obtain mutable access to a pointer's words
-fn ptr_as_slice<T>(ptr: &mut T) -> &mut [usize] {
-    assert!(mem::size_of::<T>() % mem::size_of::<usize>() == 0);
-    let words = mem::size_of::<T>() / mem::size_of::<usize>();
-    // SAFE: Points to valid memory (a raw pointer)
-    unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) }
-}
-
-/// Re-construct a fat pointer
-unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut T {
-    let mut rv = mem::MaybeUninit::<*mut T>::uninit();
-    {
-        let s = ptr_as_slice(&mut rv);
-        s[0] = data_ptr;
-        s[1..].copy_from_slice(meta_vals);
-    }
-    let rv = rv.assume_init();
-    assert_eq!(rv as *const (), data_ptr as *const ());
-    rv
-}
-
-fn round_to_words(len: usize) -> usize {
-    (len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
-}

+ 3 - 3
packages/core/src/lib.rs

@@ -81,9 +81,9 @@ pub use crate::innerlude::{
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 pub mod prelude {
 pub mod prelude {
     pub use crate::innerlude::{
     pub use crate::innerlude::{
-        fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
-        ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
-        VirtualDom,
+        fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
+        Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
+        VNode, VirtualDom,
     };
     };
 }
 }
 
 

+ 1 - 4
packages/core/src/mutations.rs

@@ -1,4 +1,4 @@
-use fxhash::FxHashSet;
+use rustc_hash::FxHashSet;
 
 
 use crate::{arena::ElementId, ScopeId, Template};
 use crate::{arena::ElementId, ScopeId, Template};
 
 
@@ -230,9 +230,6 @@ pub enum Mutation<'a> {
         /// The name of the event to listen for.
         /// The name of the event to listen for.
         name: &'a str,
         name: &'a str,
 
 
-        /// The ID of the node to attach the listener to.
-        scope: ScopeId,
-
         /// The ID of the node to attach the listener to.
         /// The ID of the node to attach the listener to.
         id: ElementId,
         id: ElementId,
     },
     },

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

@@ -46,7 +46,7 @@ pub struct VNode<'a> {
 
 
     /// The IDs for the roots of this template - to be used when moving the template around and removing it from
     /// The IDs for the roots of this template - to be used when moving the template around and removing it from
     /// the actual Dom
     /// the actual Dom
-    pub root_ids: &'a [Cell<ElementId>],
+    pub root_ids: &'a [Cell<Option<ElementId>>],
 
 
     /// The dynamic parts of the template
     /// The dynamic parts of the template
     pub dynamic_nodes: &'a [DynamicNode<'a>],
     pub dynamic_nodes: &'a [DynamicNode<'a>],
@@ -145,6 +145,18 @@ where
     Ok(&*Box::leak(deserialized))
     Ok(&*Box::leak(deserialized))
 }
 }
 
 
+impl<'a> Template<'a> {
+    /// Is this template worth caching at all, since it's completely runtime?
+    ///
+    /// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway.
+    pub fn is_completely_dynamic(&self) -> bool {
+        use TemplateNode::*;
+        self.roots
+            .iter()
+            .all(|root| matches!(root, Dynamic { .. } | DynamicText { .. }))
+    }
+}
+
 /// A statically known node in a layout.
 /// A statically known node in a layout.
 ///
 ///
 /// This can be created at compile time, saving the VirtualDom time when diffing the tree
 /// This can be created at compile time, saving the VirtualDom time when diffing the tree
@@ -224,7 +236,7 @@ pub enum DynamicNode<'a> {
     /// Used by suspense when a node isn't ready and by fragments that don't render anything
     /// Used by suspense when a node isn't ready and by fragments that don't render anything
     ///
     ///
     /// In code, this is just an ElementId whose initial value is set to 0 upon creation
     /// In code, this is just an ElementId whose initial value is set to 0 upon creation
-    Placeholder(Cell<ElementId>),
+    Placeholder(VPlaceholder),
 
 
     /// A list of VNodes.
     /// A list of VNodes.
     ///
     ///
@@ -259,7 +271,7 @@ pub struct VComponent<'a> {
     /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
     /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
     pub render_fn: *const (),
     pub render_fn: *const (),
 
 
-    pub(crate) props: Cell<Option<Box<dyn AnyProps<'a> + 'a>>>,
+    pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
 }
 }
 
 
 impl<'a> std::fmt::Debug for VComponent<'a> {
 impl<'a> std::fmt::Debug for VComponent<'a> {
@@ -279,7 +291,14 @@ pub struct VText<'a> {
     pub value: &'a str,
     pub value: &'a str,
 
 
     /// The ID of this node in the real DOM
     /// The ID of this node in the real DOM
-    pub id: Cell<ElementId>,
+    pub id: Cell<Option<ElementId>>,
+}
+
+/// A placeholder node, used by suspense and fragments
+#[derive(Debug, Default)]
+pub struct VPlaceholder {
+    /// The ID of this node in the real DOM
+    pub id: Cell<Option<ElementId>>,
 }
 }
 
 
 /// An attribute of the TemplateNode, created at compile time
 /// An attribute of the TemplateNode, created at compile time
@@ -464,6 +483,12 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
     }
     }
 }
 }
 
 
+impl<'a> IntoDynNode<'a> for DynamicNode<'a> {
+    fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
+        self
+    }
+}
+
 // An element that's an error is currently lost into the ether
 // An element that's an error is currently lost into the ether
 impl<'a> IntoDynNode<'a> for Element<'a> {
 impl<'a> IntoDynNode<'a> for Element<'a> {
     fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
     fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {

+ 13 - 6
packages/core/src/scope_arena.rs

@@ -8,6 +8,7 @@ use crate::{
     scopes::{ScopeId, ScopeState},
     scopes::{ScopeId, ScopeState},
     virtual_dom::VirtualDom,
     virtual_dom::VirtualDom,
 };
 };
+use bumpalo::Bump;
 use futures_util::FutureExt;
 use futures_util::FutureExt;
 use std::{
 use std::{
     mem,
     mem,
@@ -34,8 +35,8 @@ impl VirtualDom {
             props: Some(props),
             props: Some(props),
             name,
             name,
             placeholder: Default::default(),
             placeholder: Default::default(),
-            node_arena_1: BumpFrame::new(50),
-            node_arena_2: BumpFrame::new(50),
+            node_arena_1: BumpFrame::new(0),
+            node_arena_2: BumpFrame::new(0),
             spawned_tasks: Default::default(),
             spawned_tasks: Default::default(),
             render_cnt: Default::default(),
             render_cnt: Default::default(),
             hook_arena: Default::default(),
             hook_arena: Default::default(),
@@ -62,7 +63,13 @@ impl VirtualDom {
         let mut new_nodes = unsafe {
         let mut new_nodes = unsafe {
             let scope = self.scopes[scope_id.0].as_mut();
             let scope = self.scopes[scope_id.0].as_mut();
 
 
-            scope.previous_frame_mut().bump.reset();
+            // if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient
+            if scope.previous_frame().bump.allocated_bytes() == 0 {
+                scope.previous_frame_mut().bump =
+                    Bump::with_capacity(scope.current_frame().bump.allocated_bytes());
+            } else {
+                scope.previous_frame_mut().bump.reset();
+            }
 
 
             // Make sure to reset the hook counter so we give out hooks in the right order
             // Make sure to reset the hook counter so we give out hooks in the right order
             scope.hook_idx.set(0);
             scope.hook_idx.set(0);
@@ -128,8 +135,8 @@ impl VirtualDom {
         let frame = scope.previous_frame();
         let frame = scope.previous_frame();
 
 
         // set the new head of the bump frame
         // set the new head of the bump frame
-        let alloced = &*frame.bump.alloc(new_nodes);
-        frame.node.set(alloced);
+        let allocated = &*frame.bump.alloc(new_nodes);
+        frame.node.set(allocated);
 
 
         // And move the render generation forward by one
         // And move the render generation forward by one
         scope.render_cnt.set(scope.render_cnt.get() + 1);
         scope.render_cnt.set(scope.render_cnt.get() + 1);
@@ -141,6 +148,6 @@ impl VirtualDom {
         });
         });
 
 
         // rebind the lifetime now that its stored internally
         // rebind the lifetime now that its stored internally
-        unsafe { mem::transmute(alloced) }
+        unsafe { mem::transmute(allocated) }
     }
     }
 }
 }

+ 7 - 5
packages/core/src/scopes.rs

@@ -10,10 +10,10 @@ use crate::{
     Attribute, AttributeValue, Element, Event, Properties, TaskId,
     Attribute, AttributeValue, Element, Event, Properties, TaskId,
 };
 };
 use bumpalo::{boxed::Box as BumpBox, Bump};
 use bumpalo::{boxed::Box as BumpBox, Bump};
+use rustc_hash::{FxHashMap, FxHashSet};
 use std::{
 use std::{
     any::{Any, TypeId},
     any::{Any, TypeId},
     cell::{Cell, RefCell},
     cell::{Cell, RefCell},
-    collections::{HashMap, HashSet},
     fmt::Arguments,
     fmt::Arguments,
     future::Future,
     future::Future,
     rc::Rc,
     rc::Rc,
@@ -82,10 +82,10 @@ pub struct ScopeState {
     pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
     pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
     pub(crate) hook_idx: Cell<usize>,
     pub(crate) hook_idx: Cell<usize>,
 
 
-    pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
+    pub(crate) shared_contexts: RefCell<FxHashMap<TypeId, Box<dyn Any>>>,
 
 
     pub(crate) tasks: Rc<Scheduler>,
     pub(crate) tasks: Rc<Scheduler>,
-    pub(crate) spawned_tasks: HashSet<TaskId>,
+    pub(crate) spawned_tasks: FxHashSet<TaskId>,
 
 
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
     pub(crate) placeholder: Cell<Option<ElementId>>,
     pub(crate) placeholder: Cell<Option<ElementId>>,
@@ -237,7 +237,9 @@ impl<'src> ScopeState {
     /// This method should be used when you want to schedule an update for a component
     /// This method should be used when you want to schedule an update for a component
     pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
     pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
         let chan = self.tasks.sender.clone();
         let chan = self.tasks.sender.clone();
-        Arc::new(move |id| drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
+        Arc::new(move |id| {
+            chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
+        })
     }
     }
 
 
     /// Mark this scope as dirty, and schedule a render for it.
     /// Mark this scope as dirty, and schedule a render for it.
@@ -448,7 +450,7 @@ impl<'src> ScopeState {
             name: fn_name,
             name: fn_name,
             render_fn: component as *const (),
             render_fn: component as *const (),
             static_props: P::IS_STATIC,
             static_props: P::IS_STATIC,
-            props: Cell::new(Some(extended)),
+            props: RefCell::new(Some(extended)),
             scope: Cell::new(None),
             scope: Cell::new(None),
         })
         })
     }
     }

+ 5 - 9
packages/core/src/virtual_dom.rs

@@ -14,15 +14,9 @@ use crate::{
     AttributeValue, Element, Event, Scope, SuspenseContext,
     AttributeValue, Element, Event, Scope, SuspenseContext,
 };
 };
 use futures_util::{pin_mut, StreamExt};
 use futures_util::{pin_mut, StreamExt};
+use rustc_hash::FxHashMap;
 use slab::Slab;
 use slab::Slab;
-use std::{
-    any::Any,
-    borrow::BorrowMut,
-    cell::Cell,
-    collections::{BTreeSet, HashMap},
-    future::Future,
-    rc::Rc,
-};
+use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
 
 
 /// A virtual node system that progresses user events and diffs UI trees.
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
 ///
@@ -148,7 +142,7 @@ use std::{
 /// }
 /// }
 /// ```
 /// ```
 pub struct VirtualDom {
 pub struct VirtualDom {
-    pub(crate) templates: HashMap<TemplateId, Template<'static>>,
+    pub(crate) templates: FxHashMap<TemplateId, Template<'static>>,
     pub(crate) scopes: Slab<Box<ScopeState>>,
     pub(crate) scopes: Slab<Box<ScopeState>>,
     pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
     pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
     pub(crate) scheduler: Rc<Scheduler>,
     pub(crate) scheduler: Rc<Scheduler>,
@@ -542,6 +536,8 @@ impl VirtualDom {
     pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
     pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
         pin_mut!(deadline);
         pin_mut!(deadline);
 
 
+        self.process_events();
+
         loop {
         loop {
             // first, unload any complete suspense trees
             // first, unload any complete suspense trees
             for finished_fiber in self.finished_fibers.drain(..) {
             for finished_fiber in self.finished_fibers.drain(..) {

+ 1 - 1
packages/core/tests/kitchen_sink.rs

@@ -34,7 +34,7 @@ fn dual_stream() {
         [
         [
             LoadTemplate { name: "template", index: 0, id: ElementId(1) },
             LoadTemplate { name: "template", index: 0, id: ElementId(1) },
             SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
             SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
-            NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) },
+            NewEventListener { name: "click", id: ElementId(1) },
             HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
             HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
             AppendChildren { id: ElementId(0), m: 1 }
             AppendChildren { id: ElementId(0), m: 1 }
         ],
         ],

+ 2 - 2
packages/core/tests/miri_simple.rs

@@ -62,8 +62,8 @@ fn contexts_drop() {
     _ = dom.render_immediate();
     _ = dom.render_immediate();
 }
 }
 
 
-#[test]
-fn tasks_drop() {
+#[tokio::test]
+async fn tasks_drop() {
     fn app(cx: Scope) -> Element {
     fn app(cx: Scope) -> Element {
         cx.spawn(async {
         cx.spawn(async {
             tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
             tokio::time::sleep(std::time::Duration::from_millis(100000)).await;

+ 1 - 3
packages/core/tests/miri_stress.rs

@@ -12,9 +12,7 @@ fn test_memory_leak() {
     fn app(cx: Scope) -> Element {
     fn app(cx: Scope) -> Element {
         let val = cx.generation();
         let val = cx.generation();
 
 
-        cx.spawn(async {
-            tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
-        });
+        cx.spawn(async {});
 
 
         if val == 2 || val == 4 {
         if val == 2 || val == 4 {
             return cx.render(rsx!(()));
             return cx.render(rsx!(()));

+ 1 - 1
packages/desktop/Cargo.toml

@@ -20,7 +20,7 @@ serde = "1.0.136"
 serde_json = "1.0.79"
 serde_json = "1.0.79"
 thiserror = "1.0.30"
 thiserror = "1.0.30"
 log = "0.4.14"
 log = "0.4.14"
-wry = { version = "0.22.0" }
+wry = { version = "0.23.4" }
 futures-channel = "0.3.21"
 futures-channel = "0.3.21"
 tokio = { version = "1.16.1", features = [
 tokio = { version = "1.16.1", features = [
     "sync",
     "sync",

+ 11 - 12
packages/desktop/src/controller.rs

@@ -1,10 +1,12 @@
 use crate::desktop_context::{DesktopContext, UserWindowEvent};
 use crate::desktop_context::{DesktopContext, UserWindowEvent};
-use crate::events::{decode_event, EventMessage};
+use crate::events::IpcMessage;
 use dioxus_core::*;
 use dioxus_core::*;
+use dioxus_html::{Html, HtmlEvent};
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use futures_util::StreamExt;
 use futures_util::StreamExt;
 #[cfg(target_os = "ios")]
 #[cfg(target_os = "ios")]
 use objc::runtime::Object;
 use objc::runtime::Object;
+use serde_json::Value;
 use std::{
 use std::{
     collections::HashMap,
     collections::HashMap,
     sync::Arc,
     sync::Arc,
@@ -19,11 +21,12 @@ use wry::{
 
 
 pub(super) struct DesktopController {
 pub(super) struct DesktopController {
     pub(super) webviews: HashMap<WindowId, WebView>,
     pub(super) webviews: HashMap<WindowId, WebView>,
+    pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
     pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
     pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
     pub(super) quit_app_on_close: bool,
     pub(super) quit_app_on_close: bool,
     pub(super) is_ready: Arc<AtomicBool>,
     pub(super) is_ready: Arc<AtomicBool>,
     pub(super) proxy: EventLoopProxy<UserWindowEvent>,
     pub(super) proxy: EventLoopProxy<UserWindowEvent>,
-    pub(super) event_tx: UnboundedSender<serde_json::Value>,
+    pub(super) event_tx: UnboundedSender<Html>,
     #[cfg(debug_assertions)]
     #[cfg(debug_assertions)]
     pub(super) templates_tx: UnboundedSender<Template<'static>>,
     pub(super) templates_tx: UnboundedSender<Template<'static>>,
 
 
@@ -40,12 +43,13 @@ impl DesktopController {
         proxy: EventLoopProxy<UserWindowEvent>,
         proxy: EventLoopProxy<UserWindowEvent>,
     ) -> Self {
     ) -> Self {
         let edit_queue = Arc::new(Mutex::new(Vec::new()));
         let edit_queue = Arc::new(Mutex::new(Vec::new()));
-        let (event_tx, mut event_rx) = unbounded();
+        let (event_tx, mut event_rx) = unbounded::<HtmlEvent>();
         let (templates_tx, mut templates_rx) = unbounded();
         let (templates_tx, mut templates_rx) = unbounded();
         let proxy2 = proxy.clone();
         let proxy2 = proxy.clone();
 
 
         let pending_edits = edit_queue.clone();
         let pending_edits = edit_queue.clone();
         let desktop_context_proxy = proxy.clone();
         let desktop_context_proxy = proxy.clone();
+        let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
 
 
         std::thread::spawn(move || {
         std::thread::spawn(move || {
             // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
             // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
@@ -57,7 +61,7 @@ impl DesktopController {
 
 
             runtime.block_on(async move {
             runtime.block_on(async move {
                 let mut dom = VirtualDom::new_with_props(root, props)
                 let mut dom = VirtualDom::new_with_props(root, props)
-                    .with_root_context(DesktopContext::new(desktop_context_proxy));
+                    .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
                 {
                 {
                     let edits = dom.rebuild();
                     let edits = dom.rebuild();
                     let mut queue = edit_queue.lock().unwrap();
                     let mut queue = edit_queue.lock().unwrap();
@@ -80,14 +84,8 @@ impl DesktopController {
                             dom.replace_template(template);
                             dom.replace_template(template);
                         }
                         }
                         _ = dom.wait_for_work() => {}
                         _ = dom.wait_for_work() => {}
-                        Some(json_value) = event_rx.next() => {
-                            if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) {
-                                let name = value.event.clone();
-                                let el_id = ElementId(value.mounted_dom_id);
-                                if let Some(evt) = decode_event(value) {
-                                    dom.handle_event(&name,  evt, el_id,  dioxus_html::events::event_bubbles(&name));
-                                }
-                            }
+                        Some(value) = event_rx.next() => {
+                            dom.handle_event(&value.name,  value.data.into_any(), value.element,  dioxus_html::events::event_bubbles(&value.name));
                         }
                         }
                     }
                     }
 
 
@@ -103,6 +101,7 @@ impl DesktopController {
 
 
         Self {
         Self {
             pending_edits,
             pending_edits,
+            eval_sender,
             webviews: HashMap::new(),
             webviews: HashMap::new(),
             is_ready: Arc::new(AtomicBool::new(false)),
             is_ready: Arc::new(AtomicBool::new(false)),
             quit_app_on_close: true,
             quit_app_on_close: true,

+ 59 - 19
packages/desktop/src/desktop_context.rs

@@ -2,6 +2,11 @@ use std::rc::Rc;
 
 
 use crate::controller::DesktopController;
 use crate::controller::DesktopController;
 use dioxus_core::ScopeState;
 use dioxus_core::ScopeState;
+use serde::de::Error;
+use serde_json::Value;
+use std::future::Future;
+use std::future::IntoFuture;
+use std::pin::Pin;
 use wry::application::event_loop::ControlFlow;
 use wry::application::event_loop::ControlFlow;
 use wry::application::event_loop::EventLoopProxy;
 use wry::application::event_loop::EventLoopProxy;
 #[cfg(target_os = "ios")]
 #[cfg(target_os = "ios")]
@@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
         .unwrap()
         .unwrap()
 }
 }
 
 
-/// Get a closure that executes any JavaScript in the WebView context.
-pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
-    let desktop = use_window(cx);
-
-    &*cx.use_hook(|| {
-        let desktop = desktop.clone();
-        Rc::new(move |script| desktop.eval(script))
-    } as Rc<dyn Fn(String)>)
-}
-
 /// An imperative interface to the current window.
 /// An imperative interface to the current window.
 ///
 ///
 /// To get a handle to the current window, use the [`use_window`] hook.
 /// To get a handle to the current window, use the [`use_window`] hook.
@@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
 pub struct DesktopContext {
 pub struct DesktopContext {
     /// The wry/tao proxy to the current window
     /// The wry/tao proxy to the current window
     pub proxy: ProxyType,
     pub proxy: ProxyType,
+    pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
 }
 }
 
 
 impl DesktopContext {
 impl DesktopContext {
-    pub(crate) fn new(proxy: ProxyType) -> Self {
-        Self { proxy }
+    pub(crate) fn new(
+        proxy: ProxyType,
+        eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
+    ) -> Self {
+        Self {
+            proxy,
+            eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
+        }
     }
     }
 
 
     /// trigger the drag-window event
     /// trigger the drag-window event
@@ -236,6 +238,18 @@ impl DesktopController {
             Resizable(state) => window.set_resizable(state),
             Resizable(state) => window.set_resizable(state),
             AlwaysOnTop(state) => window.set_always_on_top(state),
             AlwaysOnTop(state) => window.set_always_on_top(state),
 
 
+            Eval(code) => {
+                let script = format!(
+                    r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
+                        {}
+                    }})()}}));"#,
+                    code
+                );
+                if let Err(e) = webview.evaluate_script(&script) {
+                    // we can't panic this error.
+                    log::warn!("Eval script error: {e}");
+                }
+            }
             CursorVisible(state) => window.set_cursor_visible(state),
             CursorVisible(state) => window.set_cursor_visible(state),
             CursorGrab(state) => {
             CursorGrab(state) => {
                 let _ = window.set_cursor_grab(state);
                 let _ = window.set_cursor_grab(state);
@@ -259,13 +273,6 @@ impl DesktopController {
                 log::warn!("Devtools are disabled in release builds");
                 log::warn!("Devtools are disabled in release builds");
             }
             }
 
 
-            Eval(code) => {
-                if let Err(e) = webview.evaluate_script(code.as_str()) {
-                    // we can't panic this error.
-                    log::warn!("Eval script error: {e}");
-                }
-            }
-
             #[cfg(target_os = "ios")]
             #[cfg(target_os = "ios")]
             PushView(view) => unsafe {
             PushView(view) => unsafe {
                 use objc::runtime::Object;
                 use objc::runtime::Object;
@@ -295,6 +302,39 @@ impl DesktopController {
     }
     }
 }
 }
 
 
+/// Get a closure that executes any JavaScript in the WebView context.
+pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
+    let desktop = use_window(cx).clone();
+    cx.use_hook(|| {
+        move |script| {
+            desktop.eval(script);
+            let recv = desktop.eval_reciever.clone();
+            EvalResult { reciever: recv }
+        }
+    })
+}
+
+/// A future that resolves to the result of a JavaScript evaluation.
+pub struct EvalResult {
+    reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
+}
+
+impl IntoFuture for EvalResult {
+    type Output = Result<serde_json::Value, serde_json::Error>;
+
+    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        Box::pin(async move {
+            let mut reciever = self.reciever.lock().await;
+            match reciever.recv().await {
+                Some(result) => Ok(result),
+                None => Err(serde_json::Error::custom("No result returned")),
+            }
+        }) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
+    }
+}
+
 #[cfg(target_os = "ios")]
 #[cfg(target_os = "ios")]
 fn is_main_thread() -> bool {
 fn is_main_thread() -> bool {
     use objc::runtime::{Class, BOOL, NO};
     use objc::runtime::{Class, BOOL, NO};

+ 1 - 59
packages/desktop/src/events.rs

@@ -6,7 +6,7 @@ use serde_json::from_value;
 use std::any::Any;
 use std::any::Any;
 use std::rc::Rc;
 use std::rc::Rc;
 
 
-#[derive(Deserialize, Serialize)]
+#[derive(Deserialize, Serialize, Debug)]
 pub(crate) struct IpcMessage {
 pub(crate) struct IpcMessage {
     method: String,
     method: String,
     params: serde_json::Value,
     params: serde_json::Value,
@@ -31,61 +31,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
         }
         }
     }
     }
 }
 }
-
-macro_rules! match_data {
-    (
-        $m:ident;
-        $name:ident;
-        $(
-            $tip:ty => $($mname:literal)|* ;
-        )*
-    ) => {
-        match $name {
-            $( $($mname)|* => {
-                let val: $tip = from_value::<$tip>($m).ok()?;
-                Rc::new(val) as Rc<dyn Any>
-            })*
-            _ => return None,
-        }
-    };
-}
-
-#[derive(Deserialize)]
-pub struct EventMessage {
-    pub contents: serde_json::Value,
-    pub event: String,
-    pub mounted_dom_id: usize,
-}
-
-pub fn decode_event(value: EventMessage) -> Option<Rc<dyn Any>> {
-    let val = value.contents;
-    let name = value.event.as_str();
-    type DragData = MouseData;
-
-    let evt = match_data! { val; name;
-        MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
-        ClipboardData => "copy" | "cut" | "paste";
-        CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
-        KeyboardData => "keydown" | "keypress" | "keyup";
-        FocusData => "blur" | "focus" | "focusin" | "focusout";
-        FormData => "change" | "input" | "invalid" | "reset" | "submit";
-        DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
-        PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
-        SelectionData => "selectstart" | "selectionchange" | "select";
-        TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
-        ScrollData => "scroll";
-        WheelData => "wheel";
-        MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
-            | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
-            | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
-            | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
-            | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
-        AnimationData => "animationstart" | "animationend" | "animationiteration";
-        TransitionData => "transitionend";
-        ToggleData => "toggle";
-        // ImageData => "load" | "error";
-        // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
-    };
-
-    Some(evt)
-}

+ 22 - 13
packages/desktop/src/lib.rs

@@ -17,7 +17,8 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use desktop_context::UserWindowEvent;
 use desktop_context::UserWindowEvent;
-pub use desktop_context::{use_eval, use_window, DesktopContext};
+pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
+use dioxus_html::HtmlEvent;
 use futures_channel::mpsc::UnboundedSender;
 use futures_channel::mpsc::UnboundedSender;
 pub use wry;
 pub use wry;
 pub use wry::application as tao;
 pub use wry::application as tao;
@@ -145,6 +146,7 @@ impl DesktopController {
             event_loop,
             event_loop,
             self.is_ready.clone(),
             self.is_ready.clone(),
             self.proxy.clone(),
             self.proxy.clone(),
+            self.eval_sender.clone(),
             self.event_tx.clone(),
             self.event_tx.clone(),
         );
         );
 
 
@@ -157,7 +159,8 @@ fn build_webview(
     event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
     event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
     is_ready: Arc<AtomicBool>,
     is_ready: Arc<AtomicBool>,
     proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
     proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
-    event_tx: UnboundedSender<serde_json::Value>,
+    eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
+    event_tx: UnboundedSender<HtmlEvent>,
 ) -> wry::webview::WebView {
 ) -> wry::webview::WebView {
     let builder = cfg.window.clone();
     let builder = cfg.window.clone();
     let window = builder.build(event_loop).unwrap();
     let window = builder.build(event_loop).unwrap();
@@ -186,8 +189,14 @@ fn build_webview(
         .with_ipc_handler(move |_window: &Window, payload: String| {
         .with_ipc_handler(move |_window: &Window, payload: String| {
             parse_ipc_message(&payload)
             parse_ipc_message(&payload)
                 .map(|message| match message.method() {
                 .map(|message| match message.method() {
+                    "eval_result" => {
+                        let result = message.params();
+                        eval_sender.send(result).unwrap();
+                    }
                     "user_event" => {
                     "user_event" => {
-                        _ = event_tx.unbounded_send(message.params());
+                        if let Ok(evt) = serde_json::from_value(message.params()) {
+                            _ = event_tx.unbounded_send(evt);
+                        }
                     }
                     }
                     "initialize" => {
                     "initialize" => {
                         is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
                         is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -234,16 +243,16 @@ fn build_webview(
         // in release mode, we don't want to show the dev tool or reload menus
         // in release mode, we don't want to show the dev tool or reload menus
         webview = webview.with_initialization_script(
         webview = webview.with_initialization_script(
             r#"
             r#"
-                        if (document.addEventListener) {
-                        document.addEventListener('contextmenu', function(e) {
-                            e.preventDefault();
-                        }, false);
-                        } else {
-                        document.attachEvent('oncontextmenu', function() {
-                            window.event.returnValue = false;
-                        });
-                        }
-                    "#,
+        if (document.addEventListener) {
+            document.addEventListener('contextmenu', function(e) {
+                e.preventDefault();
+            }, false);
+        } else {
+            document.attachEvent('oncontextmenu', function() {
+                window.event.returnValue = false;
+            });
+        }
+    "#,
         )
         )
     } else {
     } else {
         // in debug, we are okay with the reload menu showing and dev tool
         // in debug, we are okay with the reload menu showing and dev tool

+ 7 - 0
packages/desktop/src/main.js

@@ -0,0 +1,7 @@
+export function main() {
+  let root = window.document.getElementById("main");
+  if (root != null) {
+    window.interpreter = new Interpreter(root);
+    window.ipc.postMessage(serializeIpcMessage("initialize"));
+  }
+}

+ 12 - 5
packages/desktop/src/protocol.rs

@@ -52,7 +52,14 @@ pub(super) fn desktop_handler(
     } else if trimmed == "index.js" {
     } else if trimmed == "index.js" {
         Response::builder()
         Response::builder()
             .header("Content-Type", "text/javascript")
             .header("Content-Type", "text/javascript")
-            .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
+            .body(
+                format!(
+                    "{} {}",
+                    dioxus_interpreter_js::INTERPRETER_JS,
+                    include_str!("./main.js")
+                )
+                .into_bytes(),
+            )
             .map_err(From::from)
             .map_err(From::from)
     } else {
     } else {
         let asset_root = asset_root
         let asset_root = asset_root
@@ -104,10 +111,10 @@ fn get_asset_root() -> Option<PathBuf> {
     #[cfg(target_os = "macos")]
     #[cfg(target_os = "macos")]
     {
     {
         let bundle = core_foundation::bundle::CFBundle::main_bundle();
         let bundle = core_foundation::bundle::CFBundle::main_bundle();
-        let bundle_path = dbg!(bundle.path()?);
-        let resources_path = dbg!(bundle.resources_path()?);
-        let absolute_resources_root = dbg!(bundle_path.join(resources_path));
-        let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
+        let bundle_path = bundle.path()?;
+        let resources_path = bundle.resources_path()?;
+        let absolute_resources_root = bundle_path.join(resources_path);
+        let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
 
 
         return Some(canonical_resources_root);
         return Some(canonical_resources_root);
     }
     }

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

@@ -1,5 +1,3 @@
-#![doc = include_str!("../../../notes/README.md")]
-
 pub use dioxus_core as core;
 pub use dioxus_core as core;
 
 
 #[cfg(feature = "hooks")]
 #[cfg(feature = "hooks")]

+ 5 - 2
packages/html/Cargo.toml

@@ -40,7 +40,10 @@ features = [
     "ClipboardEvent",
     "ClipboardEvent",
 ]
 ]
 
 
+[dev-dependencies]
+serde_json = "*"
+
 [features]
 [features]
-default = []
-serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"]
+default = ["serialize"]
+serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
 wasm-bind = ["web-sys", "wasm-bindgen"]
 wasm-bind = ["web-sys", "wasm-bindgen"]

+ 2 - 3
packages/html/src/events/drag.rs

@@ -10,12 +10,11 @@ pub type DragEvent = Event<DragData>;
 /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location
 /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location
 /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
 /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
 /// application-specific way.
 /// application-specific way.
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[derive(Debug, Clone)]
 pub struct DragData {
 pub struct DragData {
     /// Inherit mouse data
     /// Inherit mouse data
     pub mouse: MouseData,
     pub mouse: MouseData,
-
-    /// And then add the rest of the drag data
-    pub data: Box<dyn Any>,
 }
 }
 
 
 impl_event! {
 impl_event! {

+ 2 - 3
packages/html/src/events/form.rs

@@ -11,9 +11,8 @@ pub struct FormData {
     pub value: String,
     pub value: String,
 
 
     pub values: HashMap<String, String>,
     pub values: HashMap<String, String>,
-
-    #[cfg_attr(feature = "serialize", serde(skip))]
-    pub files: Option<Arc<dyn FileEngine>>,
+    // #[cfg_attr(feature = "serialize", serde(skip))]
+    // pub files: Option<Arc<dyn FileEngine>>,
 }
 }
 
 
 impl Debug for FormData {
 impl Debug for FormData {

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

@@ -22,6 +22,12 @@ mod render_template;
 #[cfg(feature = "wasm-bind")]
 #[cfg(feature = "wasm-bind")]
 mod web_sys_bind;
 mod web_sys_bind;
 
 
+#[cfg(feature = "serialize")]
+mod transit;
+
+#[cfg(feature = "serialize")]
+pub use transit::*;
+
 pub use elements::*;
 pub use elements::*;
 pub use events::*;
 pub use events::*;
 pub use global_attributes::*;
 pub use global_attributes::*;

+ 152 - 0
packages/html/src/transit.rs

@@ -0,0 +1,152 @@
+use std::{any::Any, rc::Rc};
+
+use crate::events::*;
+use dioxus_core::ElementId;
+use serde::{Deserialize, Serialize};
+
+// macro_rules! match_data {
+//     (
+//         $m:ident;
+//         $name:ident;
+//         $(
+//             $tip:ty => $($mname:literal)|* ;
+//         )*
+//     ) => {
+//         match $name {
+//             $( $($mname)|* => {
+//                 let val: $tip = from_value::<$tip>($m).ok()?;
+//                 Rc::new(val) as Rc<dyn Any>
+//             })*
+//             _ => return None,
+//         }
+//     };
+// }
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct HtmlEvent {
+    pub element: ElementId,
+    pub name: String,
+    pub data: EventData,
+    pub bubbles: bool,
+}
+
+impl HtmlEvent {
+    pub fn bubbles(&self) -> bool {
+        event_bubbles(&self.name)
+    }
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+#[serde(untagged)]
+pub enum EventData {
+    Mouse(MouseData),
+    Clipboard(ClipboardData),
+    Composition(CompositionData),
+    Keyboard(KeyboardData),
+    Focus(FocusData),
+    Form(FormData),
+    Drag(DragData),
+    Pointer(PointerData),
+    Selection(SelectionData),
+    Touch(TouchData),
+    Scroll(ScrollData),
+    Wheel(WheelData),
+    Media(MediaData),
+    Animation(AnimationData),
+    Transition(TransitionData),
+    Toggle(ToggleData),
+}
+
+impl EventData {
+    pub fn into_any(self) -> Rc<dyn Any> {
+        match self {
+            EventData::Mouse(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Clipboard(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Composition(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Keyboard(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Focus(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Form(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Drag(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Pointer(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Selection(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Touch(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Scroll(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Wheel(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Media(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
+            EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
+        }
+    }
+}
+
+#[test]
+fn test_back_and_forth() {
+    let data = HtmlEvent {
+        element: ElementId(0),
+        data: EventData::Mouse(MouseData::default()),
+        name: "click".to_string(),
+        bubbles: true,
+    };
+
+    println!("{}", serde_json::to_string_pretty(&data).unwrap());
+
+    let o = r#"
+{
+  "element": 0,
+  "name": "click",
+  "bubbles": true,
+  "data": {
+    "alt_key": false,
+    "button": 0,
+    "buttons": 0,
+    "client_x": 0,
+    "client_y": 0,
+    "ctrl_key": false,
+    "meta_key": false,
+    "offset_x": 0,
+    "offset_y": 0,
+    "page_x": 0,
+    "page_y": 0,
+    "screen_x": 0,
+    "screen_y": 0,
+    "shift_key": false
+  }
+}
+    "#;
+
+    let p: HtmlEvent = serde_json::from_str(o).unwrap();
+}
+
+// pub fn decode_event(value: ) -> Option<Rc<dyn Any>> {
+//     let val = value.data;
+//     let name = value.event.as_str();
+//     type DragData = MouseData;
+
+//     let evt = match_data! { val; name;
+//         MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup";
+//         ClipboardData => "copy" | "cut" | "paste";
+//         CompositionData => "compositionend" | "compositionstart" | "compositionupdate";
+//         KeyboardData => "keydown" | "keypress" | "keyup";
+//         FocusData => "blur" | "focus" | "focusin" | "focusout";
+//         FormData => "change" | "input" | "invalid" | "reset" | "submit";
+//         DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop";
+//         PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture";
+//         SelectionData => "selectstart" | "selectionchange" | "select";
+//         TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart";
+//         ScrollData => "scroll";
+//         WheelData => "wheel";
+//         MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
+//             | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata"
+//             | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress"
+//             | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate"
+//             | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout";
+//         AnimationData => "animationstart" | "animationend" | "animationiteration";
+//         TransitionData => "transitionend";
+//         ToggleData => "toggle";
+//         // ImageData => "load" | "error";
+//         // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
+//     };
+
+//     Some(evt)
+// }

+ 3 - 1
packages/interpreter/Cargo.toml

@@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 wasm-bindgen = { version = "0.2.79", optional = true }
 wasm-bindgen = { version = "0.2.79", optional = true }
 js-sys = { version = "0.3.56", optional = true }
 js-sys = { version = "0.3.56", optional = true }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
-
+sledgehammer_bindgen = { version = "0.1.3", optional = true }
+sledgehammer_utils = { version = "0.1.0", optional = true }
 
 
 [features]
 [features]
 default = []
 default = []
 web = ["wasm-bindgen", "js-sys", "web-sys"]
 web = ["wasm-bindgen", "js-sys", "web-sys"]
+sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]

+ 3 - 3
packages/interpreter/src/bindings.rs

@@ -17,9 +17,6 @@ extern "C" {
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
     pub fn MountToRoot(this: &Interpreter);
     pub fn MountToRoot(this: &Interpreter);
 
 
-    #[wasm_bindgen(method)]
-    pub fn AppendChildren(this: &Interpreter, m: u32, id: u32);
-
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
     pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
     pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
 
 
@@ -76,4 +73,7 @@ extern "C" {
 
 
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
     pub fn PushRoot(this: &Interpreter, id: u32);
     pub fn PushRoot(this: &Interpreter, id: u32);
+
+    #[wasm_bindgen(method)]
+    pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
 }
 }

+ 13 - 16
packages/interpreter/src/interpreter.js

@@ -1,11 +1,3 @@
-export function main() {
-  let root = window.document.getElementById("main");
-  if (root != null) {
-    window.interpreter = new Interpreter(root);
-    window.ipc.postMessage(serializeIpcMessage("initialize"));
-  }
-}
-
 class ListenerMap {
 class ListenerMap {
   constructor(root) {
   constructor(root) {
     // bubbling events can listen at the root element
     // bubbling events can listen at the root element
@@ -60,7 +52,7 @@ class ListenerMap {
   }
   }
 }
 }
 
 
-export class Interpreter {
+class Interpreter {
   constructor(root) {
   constructor(root) {
     this.root = root;
     this.root = root;
     this.listeners = new ListenerMap(root);
     this.listeners = new ListenerMap(root);
@@ -90,7 +82,8 @@ export class Interpreter {
     this.stack.pop();
     this.stack.pop();
   }
   }
   AppendChildren(many) {
   AppendChildren(many) {
-    let root = this.stack[this.stack.length - (1 + many)];
+    // let root = this.nodes[id];
+    let root = this.stack[this.stack.length - 1 - many];
     let to_add = this.stack.splice(this.stack.length - many);
     let to_add = this.stack.splice(this.stack.length - many);
     for (let i = 0; i < many; i++) {
     for (let i = 0; i < many; i++) {
       root.appendChild(to_add[i]);
       root.appendChild(to_add[i]);
@@ -352,7 +345,10 @@ export class Interpreter {
         break;
         break;
       case "NewEventListener":
       case "NewEventListener":
         // this handler is only provided on desktop implementations since this
         // this handler is only provided on desktop implementations since this
-        // method is not used by the web implementation
+        // method is not used by the web implementationa
+
+        let bubbles = event_bubbles(edit.name);
+
         let handler = (event) => {
         let handler = (event) => {
           let target = event.target;
           let target = event.target;
           if (target != null) {
           if (target != null) {
@@ -434,20 +430,21 @@ export class Interpreter {
             }
             }
             window.ipc.postMessage(
             window.ipc.postMessage(
               serializeIpcMessage("user_event", {
               serializeIpcMessage("user_event", {
-                event: edit.name,
-                mounted_dom_id: parseInt(realId),
-                contents: contents,
+                name: edit.name,
+                element: parseInt(realId),
+                data: contents,
+                bubbles: bubbles,
               })
               })
             );
             );
           }
           }
         };
         };
-        this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
+        this.NewEventListener(edit.name, edit.id, bubbles, handler);
         break;
         break;
     }
     }
   }
   }
 }
 }
 
 
-export function serialize_event(event) {
+function serialize_event(event) {
   switch (event.type) {
   switch (event.type) {
     case "copy":
     case "copy":
     case "cut":
     case "cut":

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

@@ -1,5 +1,10 @@
 pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
 pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
 
 
+#[cfg(feature = "sledgehammer")]
+mod sledgehammer_bindings;
+#[cfg(feature = "sledgehammer")]
+pub use sledgehammer_bindings::*;
+
 #[cfg(feature = "web")]
 #[cfg(feature = "web")]
 mod bindings;
 mod bindings;
 
 

+ 231 - 0
packages/interpreter/src/sledgehammer_bindings.rs

@@ -0,0 +1,231 @@
+use js_sys::Function;
+use sledgehammer_bindgen::bindgen;
+use web_sys::Node;
+
+#[bindgen]
+mod js {
+    const JS: &str = r#"
+    class ListenerMap {
+        constructor(root) {
+            // bubbling events can listen at the root element
+            this.global = {};
+            // non bubbling events listen at the element the listener was created at
+            this.local = {};
+            this.root = null;
+            this.handler = null;
+        }
+
+        create(event_name, element, bubbles) {
+            if (bubbles) {
+                if (this.global[event_name] === undefined) {
+                    this.global[event_name] = {};
+                    this.global[event_name].active = 1;
+                    this.root.addEventListener(event_name, this.handler);
+                } else {
+                    this.global[event_name].active++;
+                }
+            }
+            else {
+                const id = element.getAttribute("data-dioxus-id");
+                if (!this.local[id]) {
+                    this.local[id] = {};
+                }
+                element.addEventListener(event_name, this.handler);
+            }
+        }
+
+        remove(element, event_name, bubbles) {
+            if (bubbles) {
+                this.global[event_name].active--;
+                if (this.global[event_name].active === 0) {
+                    this.root.removeEventListener(event_name, this.global[event_name].callback);
+                    delete this.global[event_name];
+                }
+            }
+            else {
+                const id = element.getAttribute("data-dioxus-id");
+                delete this.local[id][event_name];
+                if (this.local[id].length === 0) {
+                    delete this.local[id];
+                }
+                element.removeEventListener(event_name, this.handler);
+            }
+        }
+
+        removeAllNonBubbling(element) {
+            const id = element.getAttribute("data-dioxus-id");
+            delete this.local[id];
+        }
+    }
+    function SetAttributeInner(node, field, value, ns) {
+        const name = field;
+        if (ns === "style") {
+            // ????? why do we need to do this
+            if (node.style === undefined) {
+                node.style = {};
+            }
+            node.style[name] = value;
+        } else if (ns !== null && ns !== undefined && ns !== "") {
+            node.setAttributeNS(ns, name, value);
+        } else {
+            switch (name) {
+                case "value":
+                    if (value !== node.value) {
+                        node.value = value;
+                    }
+                    break;
+                case "checked":
+                    node.checked = value === "true";
+                    break;
+                case "selected":
+                    node.selected = value === "true";
+                    break;
+                case "dangerous_inner_html":
+                    node.innerHTML = value;
+                    break;
+                default:
+                    // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+                    if (value === "false" && bool_attrs.hasOwnProperty(name)) {
+                        node.removeAttribute(name);
+                    } else {
+                        node.setAttribute(name, value);
+                    }
+            }
+        }
+    }
+    function LoadChild(ptr, len) {
+        // iterate through each number and get that child
+        node = stack[stack.length - 1];
+        ptr_end = ptr + len;
+        for (; ptr < ptr_end; ptr++) {
+            end = m.getUint8(ptr);
+            for (node = node.firstChild; end > 0; end--) {
+                node = node.nextSibling;
+            }
+        }
+        return node;
+    }
+    const listeners = new ListenerMap();
+    let nodes = [];
+    let stack = [];
+    const templates = {};
+    let node, els, end, ptr_end, k;
+    export function save_template(nodes, tmpl_id) {
+        templates[tmpl_id] = nodes;
+    }
+    export function set_node(id, node) {
+        nodes[id] = node;
+    }
+    export function initilize(root, handler) {
+        listeners.handler = handler;
+        nodes = [root];
+        stack = [root];
+        listeners.root = root;
+    }
+    function AppendChildren(id, many){
+        root = nodes[id];
+        els = stack.splice(stack.length-many);
+        for (k = 0; k < many; k++) {
+            root.appendChild(els[k]);
+        }
+    }
+    "#;
+
+    extern "C" {
+        #[wasm_bindgen]
+        pub fn save_template(nodes: Vec<Node>, tmpl_id: u32);
+
+        #[wasm_bindgen]
+        pub fn set_node(id: u32, node: Node);
+
+        #[wasm_bindgen]
+        pub fn initilize(root: Node, handler: &Function);
+    }
+
+    fn mount_to_root() {
+        "{AppendChildren(root, stack.length-1);}"
+    }
+    fn push_root(root: u32) {
+        "{stack.push(nodes[$root$]);}"
+    }
+    fn append_children(id: u32, many: u32) {
+        "{AppendChildren($id$, $many$);}"
+    }
+    fn pop_root() {
+        "{stack.pop();}"
+    }
+    fn replace_with(id: u32, n: u32) {
+        "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
+    }
+    fn insert_after(id: u32, n: u32) {
+        "{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
+    }
+    fn insert_before(id: u32, n: u32) {
+        "{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
+    }
+    fn remove(id: u32) {
+        "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
+    }
+    fn create_raw_text(text: &str) {
+        "{stack.push(document.createTextNode($text$));}"
+    }
+    fn create_text_node(text: &str, id: u32) {
+        "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
+    }
+    fn create_placeholder(id: u32) {
+        "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
+    }
+    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"#
+    }
+    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
+    }
+    fn set_text(id: u32, text: &str) {
+        "{nodes[$id$].textContent = $text$;}"
+    }
+    fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}"
+    }
+    fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
+        r#"{name = $field$;
+        node = this.nodes[$id$];
+        if (ns == "style") {
+            node.style.removeProperty(name);
+        } else if (ns !== null && ns !== undefined && ns !== "") {
+            node.removeAttributeNS(ns, name);
+        } else if (name === "value") {
+            node.value = "";
+        } else if (name === "checked") {
+            node.checked = false;
+        } else if (name === "selected") {
+            node.selected = false;
+        } else if (name === "dangerous_inner_html") {
+            node.innerHTML = "";
+        } else {
+            node.removeAttribute(name);
+        }}"#
+    }
+    fn assign_id(ptr: u32, len: u8, id: u32) {
+        "{nodes[$id$] = LoadChild($ptr$, $len$);}"
+    }
+    fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
+        r#"{
+            node = LoadChild($ptr$, $len$);
+            if (node.nodeType == Node.TEXT_NODE) {
+                node.textContent = value;
+            } else {
+                let text = document.createTextNode(value);
+                node.replaceWith(text);
+                node = text;
+            }
+            nodes[$id$] = node;
+        }"#
+    }
+    fn replace_placeholder(ptr: u32, len: u8, n: u32) {
+        "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
+    }
+    fn load_template(tmpl_id: u32, index: u32, id: u32) {
+        "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
+    }
+}

+ 17 - 3
packages/liveview/Cargo.toml

@@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus"
 license = "MIT/Apache-2.0"
 license = "MIT/Apache-2.0"
 
 
 
 
-
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 
 [dependencies]
 [dependencies]
@@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] }
 
 
 dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
 dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
 dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
 dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
-
+dioxus-interpreter-js = { path = "../interpreter" }
 
 
 # warp
 # warp
 warp = { version = "0.3", optional = true }
 warp = { version = "0.3", optional = true }
@@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true }
 
 
 # salvo
 # salvo
 salvo = { version = "0.32.0", optional = true, features = ["ws"] }
 salvo = { version = "0.32.0", optional = true, features = ["ws"] }
+thiserror = "1.0.37"
+uuid = { version = "1.2.2", features = ["v4"] }
+anyhow = "1.0.66"
 
 
 [dev-dependencies]
 [dev-dependencies]
 tokio = { version = "1", features = ["full"] }
 tokio = { version = "1", features = ["full"] }
@@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] }
 tower = "0.4.12"
 tower = "0.4.12"
 
 
 [features]
 [features]
-default = []
+default = ["salvo"]
+
+[[example]]
+name = "axum"
+required-features = ["axum"]
+
+[[example]]
+name = "salvo"
+required-features = ["salvo"]
+
+[[example]]
+name = "warp"
+required-features = ["warp"]

+ 35 - 14
packages/liveview/examples/axum.rs

@@ -1,32 +1,53 @@
-#[cfg(not(feature = "axum"))]
-fn main() {}
+use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+use dioxus::prelude::*;
+
+fn app(cx: Scope) -> Element {
+    let mut num = use_state(cx, || 0);
+
+    cx.render(rsx! {
+        div {
+            "hello axum! {num}"
+            button { onclick: move |_| num += 1, "Increment" }
+        }
+    })
+}
 
 
-#[cfg(feature = "axum")]
 #[tokio::main]
 #[tokio::main]
 async fn main() {
 async fn main() {
-    use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
-    use dioxus_core::{Element, LazyNodes, Scope};
     pretty_env_logger::init();
     pretty_env_logger::init();
 
 
-    fn app(cx: Scope) -> Element {
-        cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
-    }
-
     let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
     let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
 
 
-    let view = dioxus_liveview::new(addr);
-    let body = view.body("<title>Dioxus Liveview</title>");
+    let view = dioxus_liveview::LiveViewPool::new();
 
 
     let app = Router::new()
     let app = Router::new()
-        .route("/", get(move || async { Html(body) }))
         .route(
         .route(
-            "/app",
+            "/",
+            get(move || async move {
+                Html(format!(
+                    r#"
+            <!DOCTYPE html>
+            <html>
+                <head> <title>Dioxus LiveView with Warp</title>  </head>
+                <body> <div id="main"></div> </body>
+                {glue}
+            </html>
+            "#,
+                    glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
+                ))
+            }),
+        )
+        .route(
+            "/ws",
             get(move |ws: WebSocketUpgrade| async move {
             get(move |ws: WebSocketUpgrade| async move {
                 ws.on_upgrade(move |socket| async move {
                 ws.on_upgrade(move |socket| async move {
-                    view.upgrade_axum(socket, app).await;
+                    _ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
                 })
                 })
             }),
             }),
         );
         );
+
+    println!("Listening on http://{}", addr);
+
     axum::Server::bind(&addr.to_string().parse().unwrap())
     axum::Server::bind(&addr.to_string().parse().unwrap())
         .serve(app.into_make_service())
         .serve(app.into_make_service())
         .await
         .await

+ 58 - 42
packages/liveview/examples/salvo.rs

@@ -1,55 +1,71 @@
-#[cfg(not(feature = "salvo"))]
-fn main() {}
+use dioxus::prelude::*;
 
 
-#[cfg(feature = "salvo")]
-#[tokio::main]
-async fn main() {
-    use std::sync::Arc;
+use dioxus_liveview::LiveViewPool;
+use salvo::extra::affix;
+use salvo::extra::ws::WsHandler;
+use salvo::prelude::*;
+use std::net::SocketAddr;
+use std::sync::Arc;
 
 
-    use dioxus_core::{Element, LazyNodes, Scope};
-    use dioxus_liveview as liveview;
-    use dioxus_liveview::Liveview;
-    use salvo::extra::affix;
-    use salvo::extra::ws::WsHandler;
-    use salvo::prelude::*;
+fn app(cx: Scope) -> Element {
+    let mut num = use_state(cx, || 0);
 
 
-    fn app(cx: Scope) -> Element {
-        cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
-    }
+    cx.render(rsx! {
+        div {
+            "hello salvo! {num}"
+            button { onclick: move |_| num += 1, "Increment" }
+        }
+    })
+}
 
 
+#[tokio::main]
+async fn main() {
     pretty_env_logger::init();
     pretty_env_logger::init();
 
 
-    let addr = ([127, 0, 0, 1], 3030);
+    let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
+
+    let view = LiveViewPool::new();
 
 
-    // todo: compactify this routing under one liveview::app method
-    let view = liveview::new(addr);
     let router = Router::new()
     let router = Router::new()
         .hoop(affix::inject(Arc::new(view)))
         .hoop(affix::inject(Arc::new(view)))
         .get(index)
         .get(index)
-        .push(Router::with_path("app").get(connect));
+        .push(Router::with_path("ws").get(connect));
+
+    println!("Listening on http://{}", addr);
+
     Server::new(TcpListener::bind(addr)).serve(router).await;
     Server::new(TcpListener::bind(addr)).serve(router).await;
+}
+
+#[handler]
+fn index(_depot: &mut Depot, res: &mut Response) {
+    let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
+    res.render(Text::Html(format!(
+        r#"
+            <!DOCTYPE html>
+            <html>
+                <head> <title>Dioxus LiveView with Warp</title>  </head>
+                <body> <div id="main"></div> </body>
+                {glue}
+            </html>
+            "#,
+        glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
+    )));
+}
+
+#[handler]
+async fn connect(
+    req: &mut Request,
+    depot: &mut Depot,
+    res: &mut Response,
+) -> Result<(), StatusError> {
+    let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
+    let fut = WsHandler::new().handle(req, res)?;
+
+    tokio::spawn(async move {
+        if let Some(ws) = fut.await {
+            _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
+        }
+    });
 
 
-    #[handler]
-    fn index(depot: &mut Depot, res: &mut Response) {
-        let view = depot.obtain::<Arc<Liveview>>().unwrap();
-        let body = view.body("<title>Dioxus LiveView</title>");
-        res.render(Text::Html(body));
-    }
-
-    #[handler]
-    async fn connect(
-        req: &mut Request,
-        depot: &mut Depot,
-        res: &mut Response,
-    ) -> Result<(), StatusError> {
-        let view = depot.obtain::<Arc<Liveview>>().unwrap().clone();
-        let fut = WsHandler::new().handle(req, res)?;
-        let fut = async move {
-            if let Some(ws) = fut.await {
-                view.upgrade_salvo(ws, app).await;
-            }
-        };
-        tokio::task::spawn(fut);
-        Ok(())
-    }
+    Ok(())
 }
 }

+ 49 - 28
packages/liveview/examples/warp.rs

@@ -1,35 +1,56 @@
-#[cfg(not(feature = "warp"))]
-fn main() {}
+use dioxus::prelude::*;
+use dioxus_liveview::adapters::warp_adapter::warp_socket;
+use dioxus_liveview::LiveViewPool;
+use std::net::SocketAddr;
+use warp::ws::Ws;
+use warp::Filter;
+
+fn app(cx: Scope) -> Element {
+    let mut num = use_state(cx, || 0);
+
+    cx.render(rsx! {
+        div {
+            "hello warp! {num}"
+            button {
+                onclick: move |_| num += 1,
+                "Increment"
+            }
+        }
+    })
+}
 
 
-#[cfg(feature = "warp")]
 #[tokio::main]
 #[tokio::main]
 async fn main() {
 async fn main() {
-    use dioxus_core::{Element, LazyNodes, Scope};
-    use dioxus_liveview as liveview;
-    use warp::ws::Ws;
-    use warp::Filter;
+    pretty_env_logger::init();
 
 
-    fn app(cx: Scope) -> Element {
-        cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!"))))
-    }
+    let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
 
 
-    pretty_env_logger::init();
+    let index = warp::path::end().map(move || {
+        warp::reply::html(format!(
+            r#"
+            <!DOCTYPE html>
+            <html>
+                <head> <title>Dioxus LiveView with Warp</title>  </head>
+                <body> <div id="main"></div> </body>
+                {glue}
+            </html>
+            "#,
+            glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
+        ))
+    });
+
+    let pool = LiveViewPool::new();
+
+    let ws = warp::path("ws")
+        .and(warp::ws())
+        .and(warp::any().map(move || pool.clone()))
+        .map(move |ws: Ws, pool: LiveViewPool| {
+            ws.on_upgrade(|ws| async move {
+                let _ = pool.launch(warp_socket(ws), app).await;
+            })
+        });
+
+    println!("Listening on http://{}", addr);
 
 
-    let addr = ([127, 0, 0, 1], 3030);
-
-    // todo: compactify this routing under one liveview::app method
-    let view = liveview::new(addr);
-    let body = view.body("<title>Dioxus LiveView</title>");
-
-    let routes = warp::path::end()
-        .map(move || warp::reply::html(body.clone()))
-        .or(warp::path("app")
-            .and(warp::ws())
-            .and(warp::any().map(move || view.clone()))
-            .map(|ws: Ws, view: liveview::Liveview| {
-                ws.on_upgrade(|socket| async move {
-                    view.upgrade_warp(socket, app).await;
-                })
-            }));
-    warp::serve(routes).run(addr).await;
+    warp::serve(index.or(ws)).run(addr).await;
 }
 }

+ 17 - 88
packages/liveview/src/adapters/axum_adapter.rs

@@ -1,94 +1,23 @@
-use crate::events;
+use crate::{LiveViewError, LiveViewSocket};
 use axum::extract::ws::{Message, WebSocket};
 use axum::extract::ws::{Message, WebSocket};
-use dioxus_core::prelude::*;
-use futures_util::{
-    future::{select, Either},
-    pin_mut, SinkExt, StreamExt,
-};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
+use futures_util::{SinkExt, StreamExt};
 
 
-impl crate::Liveview {
-    pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
-        connect(ws, self.pool.clone(), app, ()).await;
-    }
+/// Convert a warp websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket {
+    ws.map(transform_rx)
+        .with(transform_tx)
+        .sink_map_err(|_| LiveViewError::SendingFailed)
+}
 
 
-    pub async fn upgrade_axum_with_props<T>(
-        &self,
-        ws: WebSocket,
-        app: fn(Scope<T>) -> Element,
-        props: T,
-    ) where
-        T: Send + Sync + 'static,
-    {
-        connect(ws, self.pool.clone(), app, props).await;
-    }
+fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
+    message
+        .map_err(|_| LiveViewError::SendingFailed)?
+        .into_text()
+        .map_err(|_| LiveViewError::SendingFailed)
 }
 }
 
 
-pub async fn connect<T>(
-    socket: WebSocket,
-    pool: LocalPoolHandle,
-    app: fn(Scope<T>) -> Element,
-    props: T,
-) where
-    T: Send + Sync + 'static,
-{
-    let (mut user_ws_tx, mut user_ws_rx) = socket.split();
-    let (event_tx, event_rx) = mpsc::unbounded_channel();
-    let (edits_tx, edits_rx) = mpsc::unbounded_channel();
-    let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
-    let mut event_rx = UnboundedReceiverStream::new(event_rx);
-    let vdom_fut = pool.clone().spawn_pinned(move || async move {
-        let mut vdom = VirtualDom::new_with_props(app, props);
-        let edits = vdom.rebuild();
-        let serialized = serde_json::to_string(&edits.edits).unwrap();
-        edits_tx.send(serialized).unwrap();
-        loop {
-            let new_event = {
-                let vdom_fut = vdom.wait_for_work();
-                pin_mut!(vdom_fut);
-                match select(event_rx.next(), vdom_fut).await {
-                    Either::Left((l, _)) => l,
-                    Either::Right((_, _)) => None,
-                }
-            };
-            if let Some(new_event) = new_event {
-                vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
-            } else {
-                let mutations = vdom.work_with_deadline(|| false);
-                for mutation in mutations {
-                    let edits = serde_json::to_string(&mutation.edits).unwrap();
-                    edits_tx.send(edits).unwrap();
-                }
-            }
-        }
-    });
-    loop {
-        match select(user_ws_rx.next(), edits_rx.next()).await {
-            Either::Left((l, _)) => {
-                if let Some(Ok(msg)) = l {
-                    if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) {
-                        let user_event = events::trigger_from_serialized(msg.params);
-                        event_tx.send(user_event).unwrap();
-                    } else {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-            Either::Right((edits, _)) => {
-                if let Some(edits) = edits {
-                    // send the edits to the client
-                    if user_ws_tx.send(Message::Text(edits)).await.is_err() {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-        }
-    }
-    vdom_fut.abort();
+async fn transform_tx(message: String) -> Result<Message, axum::Error> {
+    Ok(Message::Text(message))
 }
 }

+ 17 - 102
packages/liveview/src/adapters/salvo_adapter.rs

@@ -1,110 +1,25 @@
-use crate::events;
-use dioxus_core::prelude::*;
-use futures_util::{pin_mut, SinkExt, StreamExt};
+use futures_util::{SinkExt, StreamExt};
 use salvo::extra::ws::{Message, WebSocket};
 use salvo::extra::ws::{Message, WebSocket};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
 
 
-impl crate::Liveview {
-    pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) {
-        connect(ws, self.pool.clone(), app, ()).await;
-    }
-    pub async fn upgrade_salvo_with_props<T>(
-        &self,
-        ws: salvo::extra::ws::WebSocket,
-        app: fn(Scope<T>) -> Element,
-        props: T,
-    ) where
-        T: Send + Sync + 'static,
-    {
-        connect(ws, self.pool.clone(), app, props).await;
-    }
-}
-
-pub async fn connect<T>(
-    ws: WebSocket,
-    pool: LocalPoolHandle,
-    app: fn(Scope<T>) -> Element,
-    props: T,
-) where
-    T: Send + Sync + 'static,
-{
-    // Use a counter to assign a new unique ID for this user.
-
-    // Split the socket into a sender and receive of messages.
-    let (mut user_ws_tx, mut user_ws_rx) = ws.split();
-
-    let (event_tx, event_rx) = mpsc::unbounded_channel();
-    let (edits_tx, edits_rx) = mpsc::unbounded_channel();
-
-    let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
-    let mut event_rx = UnboundedReceiverStream::new(event_rx);
-
-    let vdom_fut = pool.spawn_pinned(move || async move {
-        let mut vdom = VirtualDom::new_with_props(app, props);
-
-        let edits = vdom.rebuild();
+use crate::{LiveViewError, LiveViewSocket};
 
 
-        let serialized = serde_json::to_string(&edits.edits).unwrap();
-        edits_tx.send(serialized).unwrap();
-
-        loop {
-            use futures_util::future::{select, Either};
-
-            let new_event = {
-                let vdom_fut = vdom.wait_for_work();
-
-                pin_mut!(vdom_fut);
-
-                match select(event_rx.next(), vdom_fut).await {
-                    Either::Left((l, _)) => l,
-                    Either::Right((_, _)) => None,
-                }
-            };
+/// Convert a salvo websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
+    ws.map(transform_rx)
+        .with(transform_tx)
+        .sink_map_err(|_| LiveViewError::SendingFailed)
+}
 
 
-            if let Some(new_event) = new_event {
-                vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
-            } else {
-                let mutations = vdom.work_with_deadline(|| false);
-                for mutation in mutations {
-                    let edits = serde_json::to_string(&mutation.edits).unwrap();
-                    edits_tx.send(edits).unwrap();
-                }
-            }
-        }
-    });
+fn transform_rx(message: Result<Message, salvo::Error>) -> Result<String, LiveViewError> {
+    let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
 
 
-    loop {
-        use futures_util::future::{select, Either};
+    let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
 
 
-        match select(user_ws_rx.next(), edits_rx.next()).await {
-            Either::Left((l, _)) => {
-                if let Some(Ok(msg)) = l {
-                    if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
-                        if msg.method == "user_event" {
-                            let user_event = events::trigger_from_serialized(msg.params);
-                            event_tx.send(user_event).unwrap();
-                        }
-                    } else {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-            Either::Right((edits, _)) => {
-                if let Some(edits) = edits {
-                    // send the edits to the client
-                    if user_ws_tx.send(Message::text(edits)).await.is_err() {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-        }
-    }
+    Ok(msg)
+}
 
 
-    vdom_fut.abort();
+async fn transform_tx(message: String) -> Result<Message, salvo::Error> {
+    Ok(Message::text(message))
 }
 }

+ 20 - 102
packages/liveview/src/adapters/warp_adapter.rs

@@ -1,110 +1,28 @@
-use crate::events;
-use dioxus_core::prelude::*;
-use futures_util::{pin_mut, SinkExt, StreamExt};
-use tokio::sync::mpsc;
-use tokio_stream::wrappers::UnboundedReceiverStream;
-use tokio_util::task::LocalPoolHandle;
+use crate::{LiveViewError, LiveViewSocket};
+use futures_util::{SinkExt, StreamExt};
 use warp::ws::{Message, WebSocket};
 use warp::ws::{Message, WebSocket};
 
 
-impl crate::Liveview {
-    pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
-        connect(ws, self.pool.clone(), app, ()).await;
-    }
-    pub async fn upgrade_warp_with_props<T>(
-        &self,
-        ws: warp::ws::WebSocket,
-        app: fn(Scope<T>) -> Element,
-        props: T,
-    ) where
-        T: Send + Sync + 'static,
-    {
-        connect(ws, self.pool.clone(), app, props).await;
-    }
+/// Convert a warp websocket into a LiveViewSocket
+///
+/// This is required to launch a LiveView app using the warp web framework
+pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
+    ws.map(transform_rx)
+        .with(transform_tx)
+        .sink_map_err(|_| LiveViewError::SendingFailed)
 }
 }
 
 
-pub async fn connect<T>(
-    ws: WebSocket,
-    pool: LocalPoolHandle,
-    app: fn(Scope<T>) -> Element,
-    props: T,
-) where
-    T: Send + Sync + 'static,
-{
-    // Use a counter to assign a new unique ID for this user.
+fn transform_rx(message: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
+    // destructure the message into the buffer we got from warp
+    let msg = message
+        .map_err(|_| LiveViewError::SendingFailed)?
+        .into_bytes();
 
 
-    // Split the socket into a sender and receive of messages.
-    let (mut user_ws_tx, mut user_ws_rx) = ws.split();
+    // transform it back into a string, saving us the allocation
+    let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
 
 
-    let (event_tx, event_rx) = mpsc::unbounded_channel();
-    let (edits_tx, edits_rx) = mpsc::unbounded_channel();
-
-    let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
-    let mut event_rx = UnboundedReceiverStream::new(event_rx);
-
-    let vdom_fut = pool.spawn_pinned(move || async move {
-        let mut vdom = VirtualDom::new_with_props(app, props);
-
-        let edits = vdom.rebuild();
-
-        let serialized = serde_json::to_string(&edits.edits).unwrap();
-        edits_tx.send(serialized).unwrap();
-
-        loop {
-            use futures_util::future::{select, Either};
-
-            let new_event = {
-                let vdom_fut = vdom.wait_for_work();
-
-                pin_mut!(vdom_fut);
-
-                match select(event_rx.next(), vdom_fut).await {
-                    Either::Left((l, _)) => l,
-                    Either::Right((_, _)) => None,
-                }
-            };
-
-            if let Some(new_event) = new_event {
-                vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event));
-            } else {
-                let mutations = vdom.work_with_deadline(|| false);
-                for mutation in mutations {
-                    let edits = serde_json::to_string(&mutation.edits).unwrap();
-                    edits_tx.send(edits).unwrap();
-                }
-            }
-        }
-    });
-
-    loop {
-        use futures_util::future::{select, Either};
-
-        match select(user_ws_rx.next(), edits_rx.next()).await {
-            Either::Left((l, _)) => {
-                if let Some(Ok(msg)) = l {
-                    if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
-                        if msg.method == "user_event" {
-                            let user_event = events::trigger_from_serialized(msg.params);
-                            event_tx.send(user_event).unwrap();
-                        }
-                    } else {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-            Either::Right((edits, _)) => {
-                if let Some(edits) = edits {
-                    // send the edits to the client
-                    if user_ws_tx.send(Message::text(edits)).await.is_err() {
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            }
-        }
-    }
+    Ok(msg)
+}
 
 
-    vdom_fut.abort();
+async fn transform_tx(message: String) -> Result<Message, warp::Error> {
+    Ok(Message::text(message))
 }
 }

+ 0 - 207
packages/liveview/src/events.rs

@@ -1,207 +0,0 @@
-#![allow(dead_code)]
-
-//! Convert a serialized event to an event trigger
-
-use std::any::Any;
-use std::sync::Arc;
-
-use dioxus_core::ElementId;
-// use dioxus_html::event_bubbles;
-use dioxus_html::events::*;
-
-#[derive(serde::Serialize, serde::Deserialize)]
-pub(crate) struct IpcMessage {
-    pub method: String,
-    pub params: serde_json::Value,
-}
-
-pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
-    match serde_json::from_str(payload) {
-        Ok(message) => Some(message),
-        Err(_) => None,
-    }
-}
-
-#[derive(serde::Serialize, serde::Deserialize)]
-struct ImEvent {
-    event: String,
-    mounted_dom_id: ElementId,
-    contents: serde_json::Value,
-}
-
-pub fn trigger_from_serialized(_val: serde_json::Value) {
-    todo!()
-    // let ImEvent {
-    //     event,
-    //     mounted_dom_id,
-    //     contents,
-    // } = serde_json::from_value(val).unwrap();
-
-    // let mounted_dom_id = Some(mounted_dom_id);
-
-    // let name = event_name_from_type(&event);
-    // let event = make_synthetic_event(&event, contents);
-
-    // UserEvent {
-    //     name,
-    //     scope_id: None,
-    //     element: mounted_dom_id,
-    //     data: event,
-    //     bubbles: event_bubbles(name),
-    // }
-}
-
-fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any> {
-    match name {
-        "copy" | "cut" | "paste" => {
-            //
-            Arc::new(ClipboardData {})
-        }
-        "compositionend" | "compositionstart" | "compositionupdate" => {
-            Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
-        }
-        "keydown" | "keypress" | "keyup" => {
-            let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
-            Arc::new(evt)
-        }
-        "focus" | "blur" | "focusout" | "focusin" => {
-            //
-            Arc::new(FocusData {})
-        }
-
-        // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
-        // don't have a good solution with the serialized event problem
-        "change" | "input" | "invalid" | "reset" | "submit" => {
-            Arc::new(serde_json::from_value::<FormData>(val).unwrap())
-        }
-
-        "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
-        | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
-        | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
-            Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
-        }
-        "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
-        | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
-            Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
-        }
-        "select" => {
-            //
-            Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
-        }
-
-        "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
-            Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
-        }
-
-        "scroll" => Arc::new(()),
-
-        "wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
-
-        "animationstart" | "animationend" | "animationiteration" => {
-            Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
-        }
-
-        "transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
-
-        "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
-        | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
-        | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
-        | "timeupdate" | "volumechange" | "waiting" => {
-            //
-            Arc::new(MediaData {})
-        }
-
-        "toggle" => Arc::new(ToggleData {}),
-
-        _ => Arc::new(()),
-    }
-}
-
-fn event_name_from_type(typ: &str) -> &'static str {
-    match typ {
-        "copy" => "copy",
-        "cut" => "cut",
-        "paste" => "paste",
-        "compositionend" => "compositionend",
-        "compositionstart" => "compositionstart",
-        "compositionupdate" => "compositionupdate",
-        "keydown" => "keydown",
-        "keypress" => "keypress",
-        "keyup" => "keyup",
-        "focus" => "focus",
-        "focusout" => "focusout",
-        "focusin" => "focusin",
-        "blur" => "blur",
-        "change" => "change",
-        "input" => "input",
-        "invalid" => "invalid",
-        "reset" => "reset",
-        "submit" => "submit",
-        "click" => "click",
-        "contextmenu" => "contextmenu",
-        "doubleclick" => "doubleclick",
-        "drag" => "drag",
-        "dragend" => "dragend",
-        "dragenter" => "dragenter",
-        "dragexit" => "dragexit",
-        "dragleave" => "dragleave",
-        "dragover" => "dragover",
-        "dragstart" => "dragstart",
-        "drop" => "drop",
-        "mousedown" => "mousedown",
-        "mouseenter" => "mouseenter",
-        "mouseleave" => "mouseleave",
-        "mousemove" => "mousemove",
-        "mouseout" => "mouseout",
-        "mouseover" => "mouseover",
-        "mouseup" => "mouseup",
-        "pointerdown" => "pointerdown",
-        "pointermove" => "pointermove",
-        "pointerup" => "pointerup",
-        "pointercancel" => "pointercancel",
-        "gotpointercapture" => "gotpointercapture",
-        "lostpointercapture" => "lostpointercapture",
-        "pointerenter" => "pointerenter",
-        "pointerleave" => "pointerleave",
-        "pointerover" => "pointerover",
-        "pointerout" => "pointerout",
-        "select" => "select",
-        "touchcancel" => "touchcancel",
-        "touchend" => "touchend",
-        "touchmove" => "touchmove",
-        "touchstart" => "touchstart",
-        "scroll" => "scroll",
-        "wheel" => "wheel",
-        "animationstart" => "animationstart",
-        "animationend" => "animationend",
-        "animationiteration" => "animationiteration",
-        "transitionend" => "transitionend",
-        "abort" => "abort",
-        "canplay" => "canplay",
-        "canplaythrough" => "canplaythrough",
-        "durationchange" => "durationchange",
-        "emptied" => "emptied",
-        "encrypted" => "encrypted",
-        "ended" => "ended",
-        "error" => "error",
-        "loadeddata" => "loadeddata",
-        "loadedmetadata" => "loadedmetadata",
-        "loadstart" => "loadstart",
-        "pause" => "pause",
-        "play" => "play",
-        "playing" => "playing",
-        "progress" => "progress",
-        "ratechange" => "ratechange",
-        "seeked" => "seeked",
-        "seeking" => "seeking",
-        "stalled" => "stalled",
-        "suspend" => "suspend",
-        "timeupdate" => "timeupdate",
-        "volumechange" => "volumechange",
-        "waiting" => "waiting",
-        "toggle" => "toggle",
-        _ => {
-            panic!("unsupported event type")
-        }
-    }
-}

+ 0 - 15
packages/liveview/src/index.html

@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>Dioxus app</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-  </head>
-  <body>
-    <div id="main"></div>
-    <script>
-      import("./index.js").then(function (module) {
-        module.main();
-      });
-    </script>
-  </body>
-</html>

+ 0 - 973
packages/liveview/src/interpreter.js

@@ -1,973 +0,0 @@
-function main() {
-  let root = window.document.getElementById("main");
-
-  if (root != null) {
-    // create a new ipc
-    window.ipc = new IPC(root);
-
-    window.ipc.send(serializeIpcMessage("initialize"));
-  }
-}
-
-class IPC {
-  constructor(root) {
-    // connect to the websocket
-    window.interpreter = new Interpreter(root);
-
-    this.ws = new WebSocket(WS_ADDR);
-
-    this.ws.onopen = () => {
-      console.log("Connected to the websocket");
-    };
-
-    this.ws.onerror = (err) => {
-      console.error("Error: ", err);
-    };
-
-    this.ws.onmessage = (event) => {
-      let edits = JSON.parse(event.data);
-      window.interpreter.handleEdits(edits);
-    };
-  }
-
-  send(msg) {
-    this.ws.send(msg);
-  }
-}
-
-class ListenerMap {
-  constructor(root) {
-    // bubbling events can listen at the root element
-    this.global = {};
-    // non bubbling events listen at the element the listener was created at
-    this.local = {};
-    this.root = root;
-  }
-
-  create(event_name, element, handler, bubbles) {
-    if (bubbles) {
-      if (this.global[event_name] === undefined) {
-        this.global[event_name] = {};
-        this.global[event_name].active = 1;
-        this.global[event_name].callback = handler;
-        this.root.addEventListener(event_name, handler);
-      } else {
-        this.global[event_name].active++;
-      }
-    }
-    else {
-      const id = element.getAttribute("data-dioxus-id");
-      if (!this.local[id]) {
-        this.local[id] = {};
-      }
-      this.local[id][event_name] = handler;
-      element.addEventListener(event_name, handler);
-    }
-  }
-
-  remove(element, event_name, bubbles) {
-    if (bubbles) {
-      this.global[event_name].active--;
-      if (this.global[event_name].active === 0) {
-        this.root.removeEventListener(event_name, this.global[event_name].callback);
-        delete this.global[event_name];
-      }
-    }
-    else {
-      const id = element.getAttribute("data-dioxus-id");
-      delete this.local[id][event_name];
-      if (this.local[id].length === 0) {
-        delete this.local[id];
-      }
-      element.removeEventListener(event_name, handler);
-    }
-  }
-}
-
-class Interpreter {
-  constructor(root) {
-    this.root = root;
-    this.lastNode = root;
-    this.listeners = new ListenerMap(root);
-    this.handlers = {};
-    this.nodes = [root];
-    this.parents = [];
-  }
-  checkAppendParent() {
-    if (this.parents.length > 0) {
-      const lastParent = this.parents[this.parents.length - 1];
-      lastParent[1]--;
-      if (lastParent[1] === 0) {
-        this.parents.pop();
-      }
-      lastParent[0].appendChild(this.lastNode);
-    }
-  }
-  AppendChildren(root, children) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    for (let i = 0; i < children.length; i++) {
-      node.appendChild(this.nodes[children[i]]);
-    }
-  }
-  ReplaceWith(root, nodes) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    let els = [];
-    for (let i = 0; i < nodes.length; i++) {
-      els.push(this.nodes[nodes[i]])
-    }
-    node.replaceWith(...els);
-  }
-  InsertAfter(root, nodes) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    let els = [];
-    for (let i = 0; i < nodes.length; i++) {
-      els.push(this.nodes[nodes[i]])
-    }
-    node.after(...els);
-  }
-  InsertBefore(root, nodes) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    let els = [];
-    for (let i = 0; i < nodes.length; i++) {
-      els.push(this.nodes[nodes[i]])
-    }
-    node.before(...els);
-  }
-  Remove(root) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    if (node !== undefined) {
-      node.remove();
-    }
-  }
-  CreateTextNode(text, root) {
-    this.lastNode = document.createTextNode(text);
-    this.checkAppendParent();
-    if (root != null) {
-      this.nodes[root] = this.lastNode;
-    }
-  }
-  CreateElement(tag, root, children) {
-    this.lastNode = document.createElement(tag);
-    this.checkAppendParent();
-    if (root != null) {
-      this.nodes[root] = this.lastNode;
-    }
-    if (children > 0) {
-      this.parents.push([this.lastNode, children]);
-    }
-  }
-  CreateElementNs(tag, root, ns, children) {
-    this.lastNode = document.createElementNS(ns, tag);
-    this.checkAppendParent();
-    if (root != null) {
-      this.nodes[root] = this.lastNode;
-    }
-    if (children > 0) {
-      this.parents.push([this.lastNode, children]);
-    }
-  }
-  CreatePlaceholder(root) {
-    this.lastNode = document.createElement("pre");
-    this.lastNode.hidden = true;
-    this.checkAppendParent();
-    if (root != null) {
-      this.nodes[root] = this.lastNode;
-    }
-  }
-  NewEventListener(event_name, root, handler, bubbles) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    node.setAttribute("data-dioxus-id", `${root}`);
-    this.listeners.create(event_name, node, handler, bubbles);
-  }
-  RemoveEventListener(root, event_name, bubbles) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    node.removeAttribute(`data-dioxus-id`);
-    this.listeners.remove(node, event_name, bubbles);
-  }
-  SetText(root, text) {
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    node.data = text;
-  }
-  SetAttribute(root, field, value, ns) {
-    const name = field;
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    if (ns === "style") {
-      // @ts-ignore
-      node.style[name] = value;
-    } else if (ns != null || ns != undefined) {
-      node.setAttributeNS(ns, name, value);
-    } else {
-      switch (name) {
-        case "value":
-          if (value !== node.value) {
-            node.value = value;
-          }
-          break;
-        case "checked":
-          node.checked = value === "true";
-          break;
-        case "selected":
-          node.selected = value === "true";
-          break;
-        case "dangerous_inner_html":
-          node.innerHTML = value;
-          break;
-        default:
-          // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
-          if (value === "false" && bool_attrs.hasOwnProperty(name)) {
-            node.removeAttribute(name);
-          } else {
-            node.setAttribute(name, value);
-          }
-      }
-    }
-  }
-  RemoveAttribute(root, field, ns) {
-    const name = field;
-    let node;
-    if (root == null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[root];
-    }
-    if (ns == "style") {
-      node.style.removeProperty(name);
-    } else if (ns !== null || ns !== undefined) {
-      node.removeAttributeNS(ns, name);
-    } else if (name === "value") {
-      node.value = "";
-    } else if (name === "checked") {
-      node.checked = false;
-    } else if (name === "selected") {
-      node.selected = false;
-    } else if (name === "dangerous_inner_html") {
-      node.innerHTML = "";
-    } else {
-      node.removeAttribute(name);
-    }
-  }
-  CloneNode(old, new_id) {
-    let node;
-    if (old === null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[old];
-    }
-    this.nodes[new_id] = node.cloneNode(true);
-  }
-  CloneNodeChildren(old, new_ids) {
-    let node;
-    if (old === null) {
-      node = this.lastNode;
-    } else {
-      node = this.nodes[old];
-    }
-    const old_node = node.cloneNode(true);
-    let i = 0;
-    for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) {
-      this.nodes[new_ids[i++]] = node;
-    }
-  }
-  FirstChild() {
-    this.lastNode = this.lastNode.firstChild;
-  }
-  NextSibling() {
-    this.lastNode = this.lastNode.nextSibling;
-  }
-  ParentNode() {
-    this.lastNode = this.lastNode.parentNode;
-  }
-  StoreWithId(id) {
-    this.nodes[id] = this.lastNode;
-  }
-  SetLastNode(root) {
-    this.lastNode = this.nodes[root];
-  }
-  handleEdits(edits) {
-    for (let edit of edits) {
-      this.handleEdit(edit);
-    }
-  }
-  handleEdit(edit) {
-    switch (edit.type) {
-      case "PushRoot":
-        this.PushRoot(edit.root);
-        break;
-      case "AppendChildren":
-        this.AppendChildren(edit.root, edit.children);
-        break;
-      case "ReplaceWith":
-        this.ReplaceWith(edit.root, edit.nodes);
-        break;
-      case "InsertAfter":
-        this.InsertAfter(edit.root, edit.nodes);
-        break;
-      case "InsertBefore":
-        this.InsertBefore(edit.root, edit.nodes);
-        break;
-      case "Remove":
-        this.Remove(edit.root);
-        break;
-      case "CreateTextNode":
-        this.CreateTextNode(edit.text, edit.root);
-        break;
-      case "CreateElement":
-        this.CreateElement(edit.tag, edit.root, edit.children);
-        break;
-      case "CreateElementNs":
-        this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children);
-        break;
-      case "CreatePlaceholder":
-        this.CreatePlaceholder(edit.root);
-        break;
-      case "RemoveEventListener":
-        this.RemoveEventListener(edit.root, edit.event_name);
-        break;
-      case "NewEventListener":
-        // this handler is only provided on desktop implementations since this
-        // method is not used by the web implementation
-        let handler = (event) => {
-          let target = event.target;
-          if (target != null) {
-            let realId = target.getAttribute(`data-dioxus-id`);
-            let shouldPreventDefault = target.getAttribute(
-              `dioxus-prevent-default`
-            );
-
-            if (event.type === "click") {
-              // todo call prevent default if it's the right type of event
-              if (shouldPreventDefault !== `onclick`) {
-                if (target.tagName === "A") {
-                  event.preventDefault();
-                  const href = target.getAttribute("href");
-                  if (href !== "" && href !== null && href !== undefined) {
-                    window.ipc.postMessage(
-                      serializeIpcMessage("browser_open", { href })
-                    );
-                  }
-                }
-              }
-
-              // also prevent buttons from submitting
-              if (target.tagName === "BUTTON" && event.type == "submit") {
-                event.preventDefault();
-              }
-            }
-            // walk the tree to find the real element
-            while (realId == null) {
-              // we've reached the root we don't want to send an event
-              if (target.parentElement === null) {
-                return;
-              }
-
-              target = target.parentElement;
-              realId = target.getAttribute(`data-dioxus-id`);
-            }
-
-            shouldPreventDefault = target.getAttribute(
-              `dioxus-prevent-default`
-            );
-
-            let contents = serialize_event(event);
-
-            if (shouldPreventDefault === `on${event.type}`) {
-              event.preventDefault();
-            }
-
-            if (event.type === "submit") {
-              event.preventDefault();
-            }
-
-            if (
-              target.tagName === "FORM" &&
-              (event.type === "submit" || event.type === "input")
-            ) {
-              for (let x = 0; x < target.elements.length; x++) {
-                let element = target.elements[x];
-                let name = element.getAttribute("name");
-                if (name != null) {
-                  if (element.getAttribute("type") === "checkbox") {
-                    // @ts-ignore
-                    contents.values[name] = element.checked ? "true" : "false";
-                  } else if (element.getAttribute("type") === "radio") {
-                    if (element.checked) {
-                      contents.values[name] = element.value;
-                    }
-                  } else {
-                    // @ts-ignore
-                    contents.values[name] =
-                      element.value ?? element.textContent;
-                  }
-                }
-              }
-            }
-
-            if (realId === null) {
-              return;
-            }
-            realId = parseInt(realId);
-            window.ipc.send(
-              serializeIpcMessage("user_event", {
-                event: edit.event_name,
-                mounted_dom_id: realId,
-                contents: contents,
-              })
-            );
-          }
-        };
-        this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
-
-        break;
-      case "SetText":
-        this.SetText(edit.root, edit.text);
-        break;
-      case "SetAttribute":
-        this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
-        break;
-      case "RemoveAttribute":
-        this.RemoveAttribute(edit.root, edit.name, edit.ns);
-        break;
-      case "CloneNode":
-        this.CloneNode(edit.id, edit.new_id);
-        break;
-      case "CloneNodeChildren":
-        this.CloneNodeChildren(edit.id, edit.new_ids);
-        break;
-      case "FirstChild":
-        this.FirstChild();
-        break;
-      case "NextSibling":
-        this.NextSibling();
-        break;
-      case "ParentNode":
-        this.ParentNode();
-        break;
-      case "StoreWithId":
-        this.StoreWithId(BigInt(edit.id));
-        break;
-      case "SetLastNode":
-        this.SetLastNode(BigInt(edit.id));
-        break;
-    }
-  }
-}
-
-function serialize_event(event) {
-  switch (event.type) {
-    case "copy":
-    case "cut":
-    case "past": {
-      return {};
-    }
-    case "compositionend":
-    case "compositionstart":
-    case "compositionupdate": {
-      let { data } = event;
-      return {
-        data,
-      };
-    }
-    case "keydown":
-    case "keypress":
-    case "keyup": {
-      let {
-        charCode,
-        key,
-        altKey,
-        ctrlKey,
-        metaKey,
-        keyCode,
-        shiftKey,
-        location,
-        repeat,
-        which,
-        code,
-      } = event;
-      return {
-        char_code: charCode,
-        key: key,
-        alt_key: altKey,
-        ctrl_key: ctrlKey,
-        meta_key: metaKey,
-        key_code: keyCode,
-        shift_key: shiftKey,
-        location: location,
-        repeat: repeat,
-        which: which,
-        code,
-      };
-    }
-    case "focus":
-    case "blur": {
-      return {};
-    }
-    case "change": {
-      let target = event.target;
-      let value;
-      if (target.type === "checkbox" || target.type === "radio") {
-        value = target.checked ? "true" : "false";
-      } else {
-        value = target.value ?? target.textContent;
-      }
-      return {
-        value: value,
-        values: {},
-      };
-    }
-    case "input":
-    case "invalid":
-    case "reset":
-    case "submit": {
-      let target = event.target;
-      let value = target.value ?? target.textContent;
-
-      if (target.type === "checkbox") {
-        value = target.checked ? "true" : "false";
-      }
-
-      return {
-        value: value,
-        values: {},
-      };
-    }
-    case "click":
-    case "contextmenu":
-    case "doubleclick":
-    case "dblclick":
-    case "drag":
-    case "dragend":
-    case "dragenter":
-    case "dragexit":
-    case "dragleave":
-    case "dragover":
-    case "dragstart":
-    case "drop":
-    case "mousedown":
-    case "mouseenter":
-    case "mouseleave":
-    case "mousemove":
-    case "mouseout":
-    case "mouseover":
-    case "mouseup": {
-      const {
-        altKey,
-        button,
-        buttons,
-        clientX,
-        clientY,
-        ctrlKey,
-        metaKey,
-        offsetX,
-        offsetY,
-        pageX,
-        pageY,
-        screenX,
-        screenY,
-        shiftKey,
-      } = event;
-      return {
-        alt_key: altKey,
-        button: button,
-        buttons: buttons,
-        client_x: clientX,
-        client_y: clientY,
-        ctrl_key: ctrlKey,
-        meta_key: metaKey,
-        offset_x: offsetX,
-        offset_y: offsetY,
-        page_x: pageX,
-        page_y: pageY,
-        screen_x: screenX,
-        screen_y: screenY,
-        shift_key: shiftKey,
-      };
-    }
-    case "pointerdown":
-    case "pointermove":
-    case "pointerup":
-    case "pointercancel":
-    case "gotpointercapture":
-    case "lostpointercapture":
-    case "pointerenter":
-    case "pointerleave":
-    case "pointerover":
-    case "pointerout": {
-      const {
-        altKey,
-        button,
-        buttons,
-        clientX,
-        clientY,
-        ctrlKey,
-        metaKey,
-        pageX,
-        pageY,
-        screenX,
-        screenY,
-        shiftKey,
-        pointerId,
-        width,
-        height,
-        pressure,
-        tangentialPressure,
-        tiltX,
-        tiltY,
-        twist,
-        pointerType,
-        isPrimary,
-      } = event;
-      return {
-        alt_key: altKey,
-        button: button,
-        buttons: buttons,
-        client_x: clientX,
-        client_y: clientY,
-        ctrl_key: ctrlKey,
-        meta_key: metaKey,
-        page_x: pageX,
-        page_y: pageY,
-        screen_x: screenX,
-        screen_y: screenY,
-        shift_key: shiftKey,
-        pointer_id: pointerId,
-        width: width,
-        height: height,
-        pressure: pressure,
-        tangential_pressure: tangentialPressure,
-        tilt_x: tiltX,
-        tilt_y: tiltY,
-        twist: twist,
-        pointer_type: pointerType,
-        is_primary: isPrimary,
-      };
-    }
-    case "select": {
-      return {};
-    }
-    case "touchcancel":
-    case "touchend":
-    case "touchmove":
-    case "touchstart": {
-      const { altKey, ctrlKey, metaKey, shiftKey } = event;
-      return {
-        // changed_touches: event.changedTouches,
-        // target_touches: event.targetTouches,
-        // touches: event.touches,
-        alt_key: altKey,
-        ctrl_key: ctrlKey,
-        meta_key: metaKey,
-        shift_key: shiftKey,
-      };
-    }
-    case "scroll": {
-      return {};
-    }
-    case "wheel": {
-      const { deltaX, deltaY, deltaZ, deltaMode } = event;
-      return {
-        delta_x: deltaX,
-        delta_y: deltaY,
-        delta_z: deltaZ,
-        delta_mode: deltaMode,
-      };
-    }
-    case "animationstart":
-    case "animationend":
-    case "animationiteration": {
-      const { animationName, elapsedTime, pseudoElement } = event;
-      return {
-        animation_name: animationName,
-        elapsed_time: elapsedTime,
-        pseudo_element: pseudoElement,
-      };
-    }
-    case "transitionend": {
-      const { propertyName, elapsedTime, pseudoElement } = event;
-      return {
-        property_name: propertyName,
-        elapsed_time: elapsedTime,
-        pseudo_element: pseudoElement,
-      };
-    }
-    case "abort":
-    case "canplay":
-    case "canplaythrough":
-    case "durationchange":
-    case "emptied":
-    case "encrypted":
-    case "ended":
-    case "error":
-    case "loadeddata":
-    case "loadedmetadata":
-    case "loadstart":
-    case "pause":
-    case "play":
-    case "playing":
-    case "progress":
-    case "ratechange":
-    case "seeked":
-    case "seeking":
-    case "stalled":
-    case "suspend":
-    case "timeupdate":
-    case "volumechange":
-    case "waiting": {
-      return {};
-    }
-    case "toggle": {
-      return {};
-    }
-    default: {
-      return {};
-    }
-  }
-}
-function serializeIpcMessage(method, params = {}) {
-  return JSON.stringify({ method, params });
-}
-const bool_attrs = {
-  allowfullscreen: true,
-  allowpaymentrequest: true,
-  async: true,
-  autofocus: true,
-  autoplay: true,
-  checked: true,
-  controls: true,
-  default: true,
-  defer: true,
-  disabled: true,
-  formnovalidate: true,
-  hidden: true,
-  ismap: true,
-  itemscope: true,
-  loop: true,
-  multiple: true,
-  muted: true,
-  nomodule: true,
-  novalidate: true,
-  open: true,
-  playsinline: true,
-  readonly: true,
-  required: true,
-  reversed: true,
-  selected: true,
-  truespeed: true,
-};
-
-function is_element_node(node) {
-  return node.nodeType == 1;
-}
-
-function event_bubbles(event) {
-  switch (event) {
-    case "copy":
-      return true;
-    case "cut":
-      return true;
-    case "paste":
-      return true;
-    case "compositionend":
-      return true;
-    case "compositionstart":
-      return true;
-    case "compositionupdate":
-      return true;
-    case "keydown":
-      return true;
-    case "keypress":
-      return true;
-    case "keyup":
-      return true;
-    case "focus":
-      return false;
-    case "focusout":
-      return true;
-    case "focusin":
-      return true;
-    case "blur":
-      return false;
-    case "change":
-      return true;
-    case "input":
-      return true;
-    case "invalid":
-      return true;
-    case "reset":
-      return true;
-    case "submit":
-      return true;
-    case "click":
-      return true;
-    case "contextmenu":
-      return true;
-    case "doubleclick":
-      return true;
-    case "dblclick":
-      return true;
-    case "drag":
-      return true;
-    case "dragend":
-      return true;
-    case "dragenter":
-      return false;
-    case "dragexit":
-      return false;
-    case "dragleave":
-      return true;
-    case "dragover":
-      return true;
-    case "dragstart":
-      return true;
-    case "drop":
-      return true;
-    case "mousedown":
-      return true;
-    case "mouseenter":
-      return false;
-    case "mouseleave":
-      return false;
-    case "mousemove":
-      return true;
-    case "mouseout":
-      return true;
-    case "scroll":
-      return false;
-    case "mouseover":
-      return true;
-    case "mouseup":
-      return true;
-    case "pointerdown":
-      return true;
-    case "pointermove":
-      return true;
-    case "pointerup":
-      return true;
-    case "pointercancel":
-      return true;
-    case "gotpointercapture":
-      return true;
-    case "lostpointercapture":
-      return true;
-    case "pointerenter":
-      return false;
-    case "pointerleave":
-      return false;
-    case "pointerover":
-      return true;
-    case "pointerout":
-      return true;
-    case "select":
-      return true;
-    case "touchcancel":
-      return true;
-    case "touchend":
-      return true;
-    case "touchmove":
-      return true;
-    case "touchstart":
-      return true;
-    case "wheel":
-      return true;
-    case "abort":
-      return false;
-    case "canplay":
-      return false;
-    case "canplaythrough":
-      return false;
-    case "durationchange":
-      return false;
-    case "emptied":
-      return false;
-    case "encrypted":
-      return true;
-    case "ended":
-      return false;
-    case "error":
-      return false;
-    case "loadeddata":
-      return false;
-    case "loadedmetadata":
-      return false;
-    case "loadstart":
-      return false;
-    case "pause":
-      return false;
-    case "play":
-      return false;
-    case "playing":
-      return false;
-    case "progress":
-      return false;
-    case "ratechange":
-      return false;
-    case "seeked":
-      return false;
-    case "seeking":
-      return false;
-    case "stalled":
-      return false;
-    case "suspend":
-      return false;
-    case "timeupdate":
-      return false;
-    case "volumechange":
-      return false;
-    case "waiting":
-      return false;
-    case "animationstart":
-      return true;
-    case "animationend":
-      return true;
-    case "animationiteration":
-      return true;
-    case "transitionend":
-      return true;
-    case "toggle":
-      return true;
-  }
-}

+ 38 - 39
packages/liveview/src/lib.rs

@@ -1,56 +1,55 @@
-#![allow(dead_code)]
-
-pub(crate) mod events;
 pub mod adapters {
 pub mod adapters {
     #[cfg(feature = "warp")]
     #[cfg(feature = "warp")]
     pub mod warp_adapter;
     pub mod warp_adapter;
+    #[cfg(feature = "warp")]
+    pub use warp_adapter::*;
 
 
     #[cfg(feature = "axum")]
     #[cfg(feature = "axum")]
     pub mod axum_adapter;
     pub mod axum_adapter;
+    #[cfg(feature = "axum")]
+    pub use axum_adapter::*;
 
 
     #[cfg(feature = "salvo")]
     #[cfg(feature = "salvo")]
     pub mod salvo_adapter;
     pub mod salvo_adapter;
+
+    #[cfg(feature = "salvo")]
+    pub use salvo_adapter::*;
 }
 }
 
 
-use std::net::SocketAddr;
+pub use adapters::*;
 
 
-use tokio_util::task::LocalPoolHandle;
+pub mod pool;
+use futures_util::{SinkExt, StreamExt};
+pub use pool::*;
 
 
-#[derive(Clone)]
-pub struct Liveview {
-    pool: LocalPoolHandle,
-    addr: String,
-}
+pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
+impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
 
 
-impl Liveview {
-    pub fn body(&self, header: &str) -> String {
-        format!(
-            r#"
-<!DOCTYPE html>
-<html>
-  <head>
-    {header}
-  </head>
-  <body>
-    <div id="main"></div>
-    <script>
-      var WS_ADDR = "ws://{addr}/app";
-      {interpreter}
-      main();
-    </script>
-  </body>
-</html>"#,
-            addr = self.addr,
-            interpreter = include_str!("../src/interpreter.js")
-        )
-    }
-}
+pub trait WebsocketRx: StreamExt<Item = Result<String, LiveViewError>> {}
+impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError>> {}
 
 
-pub fn new(addr: impl Into<SocketAddr>) -> Liveview {
-    let addr: SocketAddr = addr.into();
+#[derive(Debug, thiserror::Error)]
+pub enum LiveViewError {
+    #[error("warp error")]
+    SendingFailed,
+}
 
 
-    Liveview {
-        pool: LocalPoolHandle::new(16),
-        addr: addr.to_string(),
-    }
+use dioxus_interpreter_js::INTERPRETER_JS;
+static MAIN_JS: &str = include_str!("./main.js");
+
+/// This script that gets injected into your app connects this page to the websocket endpoint
+///
+/// Once the endpoint is connected, it will send the initial state of the app, and then start
+/// processing user events and returning edits to the liveview instance
+pub fn interpreter_glue(url: &str) -> String {
+    format!(
+        r#"
+<script>
+    var WS_ADDR = "{url}";
+    {INTERPRETER_JS}
+    {MAIN_JS}
+    main();
+</script>
+    "#
+    )
 }
 }

+ 36 - 0
packages/liveview/src/main.js

@@ -0,0 +1,36 @@
+function main() {
+  let root = window.document.getElementById("main");
+
+  if (root != null) {
+    // create a new ipc
+    window.ipc = new IPC(root);
+    window.ipc.postMessage(serializeIpcMessage("initialize"));
+  }
+}
+
+class IPC {
+  constructor(root) {
+    // connect to the websocket
+    window.interpreter = new Interpreter(root);
+
+    this.ws = new WebSocket(WS_ADDR);
+
+    this.ws.onopen = () => {
+      console.log("Connected to the websocket");
+    };
+
+    this.ws.onerror = (err) => {
+      console.error("Error: ", err);
+    };
+
+    this.ws.onmessage = (event) => {
+      console.log("Received message: ", event.data);
+      let edits = JSON.parse(event.data);
+      window.interpreter.handleEdits(edits);
+    };
+  }
+
+  postMessage(msg) {
+    this.ws.send(msg);
+  }
+}

+ 123 - 0
packages/liveview/src/pool.rs

@@ -0,0 +1,123 @@
+use crate::LiveViewError;
+use dioxus_core::prelude::*;
+use dioxus_html::HtmlEvent;
+use futures_util::{pin_mut, SinkExt, StreamExt};
+use std::time::Duration;
+use tokio_util::task::LocalPoolHandle;
+
+#[derive(Clone)]
+pub struct LiveViewPool {
+    pub(crate) pool: LocalPoolHandle,
+}
+
+impl Default for LiveViewPool {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl LiveViewPool {
+    pub fn new() -> Self {
+        LiveViewPool {
+            pool: LocalPoolHandle::new(16),
+        }
+    }
+
+    pub async fn launch(
+        &self,
+        ws: impl LiveViewSocket,
+        app: fn(Scope<()>) -> Element,
+    ) -> Result<(), LiveViewError> {
+        self.launch_with_props(ws, app, ()).await
+    }
+
+    pub async fn launch_with_props<T: Send + 'static>(
+        &self,
+        ws: impl LiveViewSocket,
+        app: fn(Scope<T>) -> Element,
+        props: T,
+    ) -> Result<(), LiveViewError> {
+        match self.pool.spawn_pinned(move || run(app, props, ws)).await {
+            Ok(Ok(_)) => Ok(()),
+            Ok(Err(e)) => Err(e),
+            Err(_) => Err(LiveViewError::SendingFailed),
+        }
+    }
+}
+
+/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client
+pub trait LiveViewSocket:
+    SinkExt<String, Error = LiveViewError>
+    + StreamExt<Item = Result<String, LiveViewError>>
+    + Send
+    + 'static
+{
+}
+
+impl<S> LiveViewSocket for S where
+    S: SinkExt<String, Error = LiveViewError>
+        + StreamExt<Item = Result<String, LiveViewError>>
+        + Send
+        + 'static
+{
+}
+
+/// The primary event loop for the VirtualDom waiting for user input
+///
+/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework.
+///
+/// As long as your framework can provide a Sink and Stream of Strings, you can use this function.
+///
+/// You might need to transform the error types of the web backend into the LiveView error type.
+pub async fn run<T>(
+    app: Component<T>,
+    props: T,
+    ws: impl LiveViewSocket,
+) -> Result<(), LiveViewError>
+where
+    T: Send + 'static,
+{
+    let mut vdom = VirtualDom::new_with_props(app, props);
+
+    // todo: use an efficient binary packed format for this
+    let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
+
+    // pin the futures so we can use select!
+    pin_mut!(ws);
+
+    // send the initial render to the client
+    ws.send(edits).await?;
+
+    // desktop uses this wrapper struct thing around the actual event itself
+    // this is sorta driven by tao/wry
+    #[derive(serde::Deserialize)]
+    struct IpcMessage {
+        params: HtmlEvent,
+    }
+
+    loop {
+        tokio::select! {
+            // poll any futures or suspense
+            _ = vdom.wait_for_work() => {}
+
+            evt = ws.next() => {
+                match evt {
+                    Some(Ok(evt)) => {
+                        if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(&evt) {
+                            vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
+                        }
+                    }
+                    // log this I guess? when would we get an error here?
+                    Some(Err(_e)) => {},
+                    None => return Ok(()),
+                }
+            }
+        }
+
+        let edits = vdom
+            .render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
+            .await;
+
+        ws.send(serde_json::to_string(&edits).unwrap()).await?;
+    }
+}

+ 1 - 2
packages/native-core-macro/src/lib.rs

@@ -306,8 +306,7 @@ impl Member {
                     + field.ty.to_token_stream().to_string().as_str())
                     + field.ty.to_token_stream().to_string().as_str())
                 .as_str(),
                 .as_str(),
                 Span::call_site(),
                 Span::call_site(),
-            )
-            .into(),
+            ),
             ident: field.ident.as_ref()?.clone(),
             ident: field.ident.as_ref()?.clone(),
         })
         })
     }
     }

+ 11 - 24
packages/native-core/src/real_dom.rs

@@ -102,11 +102,7 @@ impl<S: State> RealDom<S> {
         self.tree.add_child(node_id, child_id);
         self.tree.add_child(node_id, child_id);
     }
     }
 
 
-    fn create_template_node(
-        &mut self,
-        node: &TemplateNode,
-        mutations_vec: &mut FxHashMap<RealNodeId, NodeMask>,
-    ) -> RealNodeId {
+    fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId {
         match node {
         match node {
             TemplateNode::Element {
             TemplateNode::Element {
                 tag,
                 tag,
@@ -139,27 +135,18 @@ impl<S: State> RealDom<S> {
                 });
                 });
                 let node_id = self.create_node(node);
                 let node_id = self.create_node(node);
                 for child in *children {
                 for child in *children {
-                    let child_id = self.create_template_node(child, mutations_vec);
+                    let child_id = self.create_template_node(child);
                     self.add_child(node_id, child_id);
                     self.add_child(node_id, child_id);
                 }
                 }
                 node_id
                 node_id
             }
             }
-            TemplateNode::Text { text } => {
-                let node_id = self.create_node(Node::new(NodeType::Text {
-                    text: text.to_string(),
-                }));
-                node_id
-            }
-            TemplateNode::Dynamic { .. } => {
-                let node_id = self.create_node(Node::new(NodeType::Placeholder));
-                node_id
-            }
-            TemplateNode::DynamicText { .. } => {
-                let node_id = self.create_node(Node::new(NodeType::Text {
-                    text: String::new(),
-                }));
-                node_id
-            }
+            TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text {
+                text: text.to_string(),
+            })),
+            TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)),
+            TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text {
+                text: String::new(),
+            })),
         }
         }
     }
     }
 
 
@@ -172,7 +159,7 @@ impl<S: State> RealDom<S> {
         for template in mutations.templates {
         for template in mutations.templates {
             let mut template_root_ids = Vec::new();
             let mut template_root_ids = Vec::new();
             for root in template.roots {
             for root in template.roots {
-                let id = self.create_template_node(root, &mut nodes_updated);
+                let id = self.create_template_node(root);
                 template_root_ids.push(id);
                 template_root_ids.push(id);
             }
             }
             self.templates
             self.templates
@@ -319,7 +306,7 @@ impl<S: State> RealDom<S> {
                     }
                     }
                     mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
                     mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated);
                 }
                 }
-                NewEventListener { name, scope: _, id } => {
+                NewEventListener { name, id } => {
                     let node_id = self.element_to_node_id(id);
                     let node_id = self.element_to_node_id(id);
                     let node = self.tree.get_mut(node_id).unwrap();
                     let node = self.tree.get_mut(node_id).unwrap();
                     if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type {
                     if let NodeType::Element { listeners, .. } = &mut node.node_data.node_type {

+ 484 - 0
packages/native-core/src/utils/cursor.rs

@@ -0,0 +1,484 @@
+use std::cmp::Ordering;
+
+use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Pos {
+    pub col: usize,
+    pub row: usize,
+}
+
+impl Pos {
+    pub fn new(col: usize, row: usize) -> Self {
+        Self { row, col }
+    }
+
+    pub fn up(&mut self, rope: &str) {
+        self.move_row(-1, rope);
+    }
+
+    pub fn down(&mut self, rope: &str) {
+        self.move_row(1, rope);
+    }
+
+    pub fn right(&mut self, rope: &str) {
+        self.move_col(1, rope);
+    }
+
+    pub fn left(&mut self, rope: &str) {
+        self.move_col(-1, rope);
+    }
+
+    pub fn move_row(&mut self, change: i32, rope: &str) {
+        let new = self.row as i32 + change;
+        if new >= 0 && new < rope.lines().count() as i32 {
+            self.row = new as usize;
+        }
+    }
+
+    pub fn move_col(&mut self, change: i32, rope: &str) {
+        self.realize_col(rope);
+        let idx = self.idx(rope) as i32;
+        if idx + change >= 0 && idx + change <= rope.len() as i32 {
+            let len_line = self.len_line(rope) as i32;
+            let new_col = self.col as i32 + change;
+            let diff = new_col - len_line;
+            if diff > 0 {
+                self.down(rope);
+                self.col = 0;
+                self.move_col(diff - 1, rope);
+            } else if new_col < 0 {
+                self.up(rope);
+                self.col = self.len_line(rope);
+                self.move_col(new_col + 1, rope);
+            } else {
+                self.col = new_col as usize;
+            }
+        }
+    }
+
+    pub fn col(&self, rope: &str) -> usize {
+        self.col.min(self.len_line(rope))
+    }
+
+    pub fn row(&self) -> usize {
+        self.row
+    }
+
+    fn len_line(&self, rope: &str) -> usize {
+        let line = rope.lines().nth(self.row).unwrap_or_default();
+        let len = line.len();
+        if len > 0 && line.chars().nth(len - 1) == Some('\n') {
+            len - 1
+        } else {
+            len
+        }
+    }
+
+    pub fn idx(&self, rope: &str) -> usize {
+        rope.lines().take(self.row).map(|l| l.len()).sum::<usize>() + self.col(rope)
+    }
+
+    // the column can be more than the line length, cap it
+    pub fn realize_col(&mut self, rope: &str) {
+        self.col = self.col(rope);
+    }
+}
+
+impl Ord for Pos {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.row.cmp(&other.row).then(self.col.cmp(&other.col))
+    }
+}
+
+impl PartialOrd for Pos {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Cursor {
+    pub start: Pos,
+    pub end: Option<Pos>,
+}
+
+impl Cursor {
+    pub fn from_start(pos: Pos) -> Self {
+        Self {
+            start: pos,
+            end: None,
+        }
+    }
+
+    pub fn new(start: Pos, end: Pos) -> Self {
+        Self {
+            start,
+            end: Some(end),
+        }
+    }
+
+    fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
+        if shift {
+            self.with_end(f);
+        } else {
+            f(&mut self.start);
+            self.end = None;
+        }
+    }
+
+    fn delete_selection(&mut self, text: &mut String) -> [i32; 2] {
+        let first = self.first();
+        let last = self.last();
+        let dr = first.row as i32 - last.row as i32;
+        let dc = if dr != 0 {
+            -(last.col as i32)
+        } else {
+            first.col as i32 - last.col as i32
+        };
+        text.replace_range(first.idx(text)..last.idx(text), "");
+        if let Some(end) = self.end.take() {
+            if self.start > end {
+                self.start = end;
+            }
+        }
+        [dc, dr]
+    }
+
+    pub fn handle_input(
+        &mut self,
+        data: &dioxus_html::KeyboardData,
+        text: &mut String,
+        max_width: usize,
+    ) {
+        use Code::*;
+        match data.code() {
+            ArrowUp => {
+                self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT));
+            }
+            ArrowDown => {
+                self.move_cursor(
+                    |c| c.down(text),
+                    data.modifiers().contains(Modifiers::SHIFT),
+                );
+            }
+            ArrowRight => {
+                if data.modifiers().contains(Modifiers::CONTROL) {
+                    self.move_cursor(
+                        |c| {
+                            let mut change = 1;
+                            let idx = c.idx(text);
+                            let length = text.len();
+                            while idx + change < length {
+                                let chr = text.chars().nth(idx + change).unwrap();
+                                if chr.is_whitespace() {
+                                    break;
+                                }
+                                change += 1;
+                            }
+                            c.move_col(change as i32, text);
+                        },
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                } else {
+                    self.move_cursor(
+                        |c| c.right(text),
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                }
+            }
+            ArrowLeft => {
+                if data.modifiers().contains(Modifiers::CONTROL) {
+                    self.move_cursor(
+                        |c| {
+                            let mut change = -1;
+                            let idx = c.idx(text) as i32;
+                            while idx + change > 0 {
+                                let chr = text.chars().nth((idx + change) as usize).unwrap();
+                                if chr == ' ' {
+                                    break;
+                                }
+                                change -= 1;
+                            }
+                            c.move_col(change as i32, text);
+                        },
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                } else {
+                    self.move_cursor(
+                        |c| c.left(text),
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                }
+            }
+            End => {
+                self.move_cursor(
+                    |c| c.col = c.len_line(text),
+                    data.modifiers().contains(Modifiers::SHIFT),
+                );
+            }
+            Home => {
+                self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
+            }
+            Backspace => {
+                self.start.realize_col(text);
+                let mut start_idx = self.start.idx(text);
+                if self.end.is_some() {
+                    self.delete_selection(text);
+                } else if start_idx > 0 {
+                    self.start.left(text);
+                    text.replace_range(start_idx - 1..start_idx, "");
+                    if data.modifiers().contains(Modifiers::CONTROL) {
+                        start_idx = self.start.idx(text);
+                        while start_idx > 0
+                            && text
+                                .chars()
+                                .nth(start_idx - 1)
+                                .filter(|c| *c != ' ')
+                                .is_some()
+                        {
+                            self.start.left(text);
+                            text.replace_range(start_idx - 1..start_idx, "");
+                            start_idx = self.start.idx(text);
+                        }
+                    }
+                }
+            }
+            Enter => {
+                if text.len() + 1 - self.selection_len(text) <= max_width {
+                    text.insert(self.start.idx(text), '\n');
+                    self.start.col = 0;
+                    self.start.down(text);
+                }
+            }
+            Tab => {
+                if text.len() + 1 - self.selection_len(text) <= max_width {
+                    self.start.realize_col(text);
+                    self.delete_selection(text);
+                    text.insert(self.start.idx(text), '\t');
+                    self.start.right(text);
+                }
+            }
+            _ => {
+                self.start.realize_col(text);
+                if let Key::Character(character) = data.key() {
+                    if text.len() + 1 - self.selection_len(text) <= max_width {
+                        self.delete_selection(text);
+                        let character = character.chars().next().unwrap();
+                        text.insert(self.start.idx(text), character);
+                        self.start.right(text);
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
+        let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
+        f(&mut new);
+        self.end.replace(new);
+    }
+
+    pub fn first(&self) -> &Pos {
+        if let Some(e) = &self.end {
+            e.min(&self.start)
+        } else {
+            &self.start
+        }
+    }
+
+    pub fn last(&self) -> &Pos {
+        if let Some(e) = &self.end {
+            e.max(&self.start)
+        } else {
+            &self.start
+        }
+    }
+
+    pub fn selection_len(&self, text: &str) -> usize {
+        self.last().idx(text) - self.first().idx(text)
+    }
+}
+
+impl Default for Cursor {
+    fn default() -> Self {
+        Self {
+            start: Pos::new(0, 0),
+            end: None,
+        }
+    }
+}
+
+#[test]
+fn pos_direction_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+    pos.down(text);
+    assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len());
+    pos.up(text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+    pos.left(text);
+    assert_eq!(
+        pos.col(text),
+        text.lines().next().unwrap_or_default().len() - 1
+    );
+    pos.right(text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+}
+
+#[test]
+fn pos_col_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    // move inside a row
+    pos.move_col(-5, text);
+    assert_eq!(
+        pos.col(text),
+        text.lines().next().unwrap_or_default().len() - 5
+    );
+    pos.move_col(5, text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+
+    // move between rows
+    pos.move_col(3, text);
+    assert_eq!(pos.col(text), 2);
+    pos.move_col(-3, text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+
+    // don't panic if moving out of range
+    pos.move_col(-100, text);
+    pos.move_col(1000, text);
+}
+
+#[test]
+fn cursor_row_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    pos.move_row(1, text);
+    assert_eq!(pos.row(), 1);
+    pos.move_row(-1, text);
+    assert_eq!(pos.row(), 0);
+
+    // don't panic if moving out of range
+    pos.move_row(-100, text);
+    pos.move_row(1000, text);
+}
+
+#[test]
+fn cursor_input() {
+    let mut cursor = Cursor::from_start(Pos::new(0, 0));
+    let mut text = "hello world\nhi".to_string();
+
+    for _ in 0..5 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::ArrowRight,
+                dioxus_html::input_data::keyboard_types::Code::ArrowRight,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            10,
+        );
+    }
+
+    for _ in 0..5 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::Backspace,
+                dioxus_html::input_data::keyboard_types::Code::Backspace,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            10,
+        );
+    }
+
+    assert_eq!(text, " world\nhi");
+
+    let goal_text = "hello world\nhi";
+    let max_width = goal_text.len();
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyH,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyE,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyL,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyL,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyO,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    // these should be ignored
+    for _ in 0..10 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
+                dioxus_html::input_data::keyboard_types::Code::KeyO,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            max_width,
+        );
+    }
+
+    assert_eq!(text.to_string(), goal_text);
+}

+ 3 - 0
packages/native-core/src/utils/mod.rs

@@ -0,0 +1,3 @@
+mod persistant_iterator;
+pub use persistant_iterator::*;
+pub mod cursor;

+ 0 - 0
packages/native-core/src/utils.rs → packages/native-core/src/utils/persistant_iterator.rs


+ 4 - 0
packages/router/Cargo.toml

@@ -36,6 +36,7 @@ thiserror = "1.0.30"
 futures-util = "0.3.21"
 futures-util = "0.3.21"
 serde = { version = "1", optional = true }
 serde = { version = "1", optional = true }
 serde_urlencoded = { version = "0.7.1", optional = true }
 serde_urlencoded = { version = "0.7.1", optional = true }
+simple_logger = "4.0.0"
 
 
 [features]
 [features]
 default = ["query"]
 default = ["query"]
@@ -50,6 +51,9 @@ wasm-logger = "0.2.0"
 wasm-bindgen-test = "0.3"
 wasm-bindgen-test = "0.3"
 gloo-utils = "0.1.2"
 gloo-utils = "0.1.2"
 dioxus-web = { path = "../web" }
 dioxus-web = { path = "../web" }
+# dioxus-desktop = { path = "../desktop", optional = true }
+
+# not wasm
 
 
 [target.wasm32-unknown-unknown.dev-dependencies]
 [target.wasm32-unknown-unknown.dev-dependencies]
 dioxus-router = { path = ".", features = ["web"] }
 dioxus-router = { path = ".", features = ["web"] }

+ 7 - 1
packages/router/examples/simple.rs

@@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element {
                 Link { to: "/blog", li { "blog" } }
                 Link { to: "/blog", li { "blog" } }
                 Link { to: "/blog/tim", li { "tims' blog" } }
                 Link { to: "/blog/tim", li { "tims' blog" } }
                 Link { to: "/blog/bill", li { "bills' blog" } }
                 Link { to: "/blog/bill", li { "bills' blog" } }
+                Link { to: "/blog/james", li { "james amazing' blog" } }
                 Link { to: "/apples", li { "go to apples" } }
                 Link { to: "/apples", li { "go to apples" } }
             }
             }
             Route { to: "/", Home {} }
             Route { to: "/", Home {} }
@@ -42,5 +43,10 @@ fn BlogPost(cx: Scope) -> Element {
 
 
     log::trace!("rendering blog post {}", id);
     log::trace!("rendering blog post {}", id);
 
 
-    cx.render(rsx! { div { "{id:?}" } })
+    cx.render(rsx! {
+        div {
+            h3 { "blog post: {id:?}"  }
+            Link { to: "/blog/", "back to blog list" }
+        }
+    })
 }
 }

+ 8 - 0
packages/router/src/components/link.rs

@@ -120,9 +120,17 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
             prevent_default: "{prevent_default}",
             prevent_default: "{prevent_default}",
             target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
             target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
             onclick: move |_| {
             onclick: move |_| {
+                log::trace!("Clicked link to {}", to);
+
                 if !outerlink {
                 if !outerlink {
                     if let Some(service) = svc {
                     if let Some(service) = svc {
+                        log::trace!("Pushing route to {}", to);
                         service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
                         service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
+
+                        #[cfg(feature = "web")]
+                        {
+                            web_sys::window().unwrap().scroll_to_with_x_and_y(0.0, 0.0);
+                        }
                     } else {
                     } else {
                         log::error!(
                         log::error!(
                             "Attempted to create a Link to {} outside of a Router context", cx.props
                             "Attempted to create a Link to {} outside of a Router context", cx.props

+ 3 - 3
packages/router/src/components/route.rs

@@ -45,13 +45,13 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
         router_root.register_total_route(route_context.total_route, cx.scope_id());
         router_root.register_total_route(route_context.total_route, cx.scope_id());
     });
     });
 
 
-    log::trace!("Checking Route: {:?}", cx.props.to);
+    log::debug!("Checking Route: {:?}", cx.props.to);
 
 
     if router_root.should_render(cx.scope_id()) {
     if router_root.should_render(cx.scope_id()) {
-        log::trace!("Route should render: {:?}", cx.scope_id());
+        log::debug!("Route should render: {:?}", cx.scope_id());
         cx.render(rsx!(&cx.props.children))
         cx.render(rsx!(&cx.props.children))
     } else {
     } else {
-        log::trace!("Route should *not* render: {:?}", cx.scope_id());
+        log::debug!("Route should *not* render: {:?}", cx.scope_id());
         cx.render(rsx!(()))
         cx.render(rsx!(()))
     }
     }
 }
 }

+ 1 - 0
packages/router/src/service.rs

@@ -180,6 +180,7 @@ impl RouterService {
         (self.regen_any_route)(self.router_id);
         (self.regen_any_route)(self.router_id);
 
 
         for listener in self.onchange_listeners.borrow().iter() {
         for listener in self.onchange_listeners.borrow().iter() {
+            log::trace!("Regenerating scope {:?}", listener);
             (self.regen_any_route)(*listener);
             (self.regen_any_route)(*listener);
         }
         }
 
 

+ 6 - 2
packages/rsx/Cargo.toml

@@ -1,8 +1,13 @@
 [package]
 [package]
 name = "dioxus-rsx"
 name = "dioxus-rsx"
-version = "0.0.0"
+version = "0.0.1"
 edition = "2018"
 edition = "2018"
 license = "MIT/Apache-2.0"
 license = "MIT/Apache-2.0"
+description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+documentation = "https://docs.rs/dioxus-rsx"
+keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 
@@ -10,6 +15,5 @@ license = "MIT/Apache-2.0"
 proc-macro2 = { version = "1.0", features = ["span-locations"] }
 proc-macro2 = { version = "1.0", features = ["span-locations"] }
 syn = { version = "1.0", features = ["full", "extra-traits"] }
 syn = { version = "1.0", features = ["full", "extra-traits"] }
 quote = { version = "1.0" }
 quote = { version = "1.0" }
-dioxus-core = { path = "../core", features = ["serialize"] }
 serde = { version = "1.0", features = ["derive"] }
 serde = { version = "1.0", features = ["derive"] }
 internment = "0.7.0"
 internment = "0.7.0"

+ 1 - 2
packages/rsx/src/element.rs

@@ -45,7 +45,7 @@ impl Parse for Element {
 
 
                 content.parse::<Token![:]>()?;
                 content.parse::<Token![:]>()?;
 
 
-                if content.peek(LitStr) && content.peek2(Token![,]) {
+                if content.peek(LitStr) {
                     let value = content.parse()?;
                     let value = content.parse()?;
                     attributes.push(ElementAttrNamed {
                     attributes.push(ElementAttrNamed {
                         el_name: el_name.clone(),
                         el_name: el_name.clone(),
@@ -53,7 +53,6 @@ impl Parse for Element {
                     });
                     });
                 } else {
                 } else {
                     let value = content.parse::<Expr>()?;
                     let value = content.parse::<Expr>()?;
-
                     attributes.push(ElementAttrNamed {
                     attributes.push(ElementAttrNamed {
                         el_name: el_name.clone(),
                         el_name: el_name.clone(),
                         attr: ElementAttr::CustomAttrExpression { name, value },
                         attr: ElementAttr::CustomAttrExpression { name, value },

+ 5 - 4
packages/rsx/src/lib.rs

@@ -217,7 +217,8 @@ impl<'a, Ctx: HotReloadingContext> ToTokens for TemplateRenderer<'a, Ctx> {
                 parent: None,
                 parent: None,
                 key: #key_tokens,
                 key: #key_tokens,
                 template: TEMPLATE,
                 template: TEMPLATE,
-                root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([::dioxus::core::ElementId(0); #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(),
+                root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut _).as_slice_of_cells(),
+                // root_ids: std::cell::Cell::from_mut( __cx.bump().alloc([None; #num_roots]) as &mut [::dioxus::core::ElementId]).as_slice_of_cells(),
                 dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
                 dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
                 dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
                 dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
             }
             }
@@ -441,7 +442,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
                 let el_name = &el.name;
                 let el_name = &el.name;
                 let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
                 let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
                     ElementAttr::AttrText { name, value } if value.is_static() => {
                     ElementAttr::AttrText { name, value } if value.is_static() => {
-                        let value = value.source.as_ref().unwrap();
+                        let value = value.to_static().unwrap();
                         quote! {
                         quote! {
                             ::dioxus::core::TemplateAttribute::Static {
                             ::dioxus::core::TemplateAttribute::Static {
                                 name: dioxus_elements::#el_name::#name.0,
                                 name: dioxus_elements::#el_name::#name.0,
@@ -455,7 +456,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
                     }
                     }
 
 
                     ElementAttr::CustomAttrText { name, value } if value.is_static() => {
                     ElementAttr::CustomAttrText { name, value } if value.is_static() => {
-                        let value = value.source.as_ref().unwrap();
+                        let value = value.to_static().unwrap();
                         quote! {
                         quote! {
                             ::dioxus::core::TemplateAttribute::Static {
                             ::dioxus::core::TemplateAttribute::Static {
                                 name: #name,
                                 name: #name,
@@ -503,7 +504,7 @@ impl<'a, Ctx: HotReloadingContext> DynamicContext<'a, Ctx> {
             }
             }
 
 
             BodyNode::Text(text) if text.is_static() => {
             BodyNode::Text(text) if text.is_static() => {
-                let text = text.source.as_ref().unwrap();
+                let text = text.to_static().unwrap();
                 quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
                 quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
             }
             }
 
 

+ 1 - 0
packages/tui/Cargo.toml

@@ -13,6 +13,7 @@ license = "MIT/Apache-2.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 
 [dependencies]
 [dependencies]
+dioxus = { path = "../dioxus", version = "^0.2.1" }
 dioxus-core = { path = "../core", version = "^0.2.1" }
 dioxus-core = { path = "../core", version = "^0.2.1" }
 dioxus-html = { path = "../html", version = "^0.2.1" }
 dioxus-html = { path = "../html", version = "^0.2.1" }
 dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
 dioxus-native-core = { path = "../native-core", version = "^0.2.0" }

+ 8 - 6
packages/tui/examples/tui_colorpicker.rs

@@ -17,12 +17,14 @@ fn app(cx: Scope) -> Element {
             width: "100%",
             width: "100%",
             background_color: "hsl({hue}, 70%, {brightness}%)",
             background_color: "hsl({hue}, 70%, {brightness}%)",
             onmousemove: move |evt| {
             onmousemove: move |evt| {
-                if let RenderReturn::Sync(Ok(node))=cx.root_node(){
-                    let node = tui_query.get(node.root_ids[0].get());
-                    let Size{width, height} = node.size().unwrap();
-                    let pos = evt.inner().element_coordinates();
-                    hue.set((pos.x as f32/width as f32)*255.0);
-                    brightness.set((pos.y as f32/height as f32)*100.0);
+                if let RenderReturn::Sync(Ok(node)) = cx.root_node() {
+                    if let Some(id) = node.root_ids[0].get() {
+                        let node = tui_query.get(id);
+                        let Size{width, height} = node.size().unwrap();
+                        let pos = evt.inner().element_coordinates();
+                        hue.set((pos.x as f32/width as f32)*255.0);
+                        brightness.set((pos.y as f32/height as f32)*100.0);
+                    }
                 }
                 }
             },
             },
             "hsl({hue}, 70%, {brightness}%)",
             "hsl({hue}, 70%, {brightness}%)",

+ 93 - 0
packages/tui/examples/tui_widgets.rs

@@ -0,0 +1,93 @@
+use dioxus::prelude::*;
+use dioxus_html::FormData;
+use dioxus_tui::prelude::*;
+use dioxus_tui::Config;
+
+fn main() {
+    dioxus_tui::launch_cfg(app, Config::new());
+}
+
+fn app(cx: Scope) -> Element {
+    let bg_green = use_state(cx, || false);
+
+    let color = if *bg_green.get() { "green" } else { "red" };
+    cx.render(rsx! {
+        div{
+            width: "100%",
+            background_color: "{color}",
+            flex_direction: "column",
+            align_items: "center",
+            justify_content: "center",
+
+            Input{
+                oninput: |data: FormData| if &data.value == "good"{
+                    bg_green.set(true);
+                } else{
+                    bg_green.set(false);
+                },
+                r#type: "checkbox",
+                value: "good",
+                width: "50%",
+                height: "10%",
+                checked: "true",
+            }
+            Input{
+                oninput: |data: FormData| if &data.value == "hello world"{
+                    bg_green.set(true);
+                } else{
+                    bg_green.set(false);
+                },
+                width: "50%",
+                height: "10%",
+                maxlength: "11",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "range",
+                width: "50%",
+                height: "10%",
+                min: "20",
+                max: "80",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if data.value == "10"{
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "number",
+                width: "50%",
+                height: "10%",
+                maxlength: "4",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if data.value == "hello world"{
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "password",
+                width: "50%",
+                height: "10%",
+                maxlength: "11",
+            }
+            Input{
+                onclick: |_: FormData| bg_green.set(true),
+                r#type: "button",
+                value: "green",
+                width: "50%",
+                height: "10%",
+            }
+        }
+    })
+}

+ 4 - 1
packages/tui/src/hooks.rs

@@ -287,7 +287,10 @@ impl InnerInputState {
 
 
         fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
         fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
             let Point { x, y } = layout.location;
             let Point { x, y } = layout.location;
-            let node_origin = ClientPoint::new(x.into(), y.into());
+            let node_origin = ClientPoint::new(
+                layout_to_screen_space(x).into(),
+                layout_to_screen_space(y).into(),
+            );
 
 
             let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
             let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
                 .to_point()
                 .to_point()

+ 54 - 1
packages/tui/src/layout.rs

@@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState;
 use dioxus_native_core_macro::sorted_str_slice;
 use dioxus_native_core_macro::sorted_str_slice;
 use taffy::prelude::*;
 use taffy::prelude::*;
 
 
-use crate::screen_to_layout_space;
+use crate::{screen_to_layout_space, unit_to_layout_space};
 
 
 #[derive(Debug, Clone, Copy, PartialEq)]
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub(crate) enum PossiblyUninitalized<T> {
 pub(crate) enum PossiblyUninitalized<T> {
@@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout {
                 child_layout.push(l.node.unwrap());
                 child_layout.push(l.node.unwrap());
             }
             }
 
 
+            fn scale_dimention(d: Dimension) -> Dimension {
+                match d {
+                    Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
+                    Dimension::Percent(p) => Dimension::Percent(p),
+                    Dimension::Auto => Dimension::Auto,
+                    Dimension::Undefined => Dimension::Undefined,
+                }
+            }
+            let style = Style {
+                position: Rect {
+                    left: scale_dimention(style.position.left),
+                    right: scale_dimention(style.position.right),
+                    top: scale_dimention(style.position.top),
+                    bottom: scale_dimention(style.position.bottom),
+                },
+                margin: Rect {
+                    left: scale_dimention(style.margin.left),
+                    right: scale_dimention(style.margin.right),
+                    top: scale_dimention(style.margin.top),
+                    bottom: scale_dimention(style.margin.bottom),
+                },
+                padding: Rect {
+                    left: scale_dimention(style.padding.left),
+                    right: scale_dimention(style.padding.right),
+                    top: scale_dimention(style.padding.top),
+                    bottom: scale_dimention(style.padding.bottom),
+                },
+                border: Rect {
+                    left: scale_dimention(style.border.left),
+                    right: scale_dimention(style.border.right),
+                    top: scale_dimention(style.border.top),
+                    bottom: scale_dimention(style.border.bottom),
+                },
+                gap: Size {
+                    width: scale_dimention(style.gap.width),
+                    height: scale_dimention(style.gap.height),
+                },
+                flex_basis: scale_dimention(style.flex_basis),
+                size: Size {
+                    width: scale_dimention(style.size.width),
+                    height: scale_dimention(style.size.height),
+                },
+                min_size: Size {
+                    width: scale_dimention(style.min_size.width),
+                    height: scale_dimention(style.min_size.height),
+                },
+                max_size: Size {
+                    width: scale_dimention(style.max_size.width),
+                    height: scale_dimention(style.max_size.height),
+                },
+                ..style
+            };
+
             if let PossiblyUninitalized::Initialized(n) = self.node {
             if let PossiblyUninitalized::Initialized(n) = self.node {
                 if self.style != style {
                 if self.style != style {
                     taffy.set_style(n, style).unwrap();
                     taffy.set_style(n, style).unwrap();

+ 18 - 3
packages/tui/src/lib.rs

@@ -1,5 +1,6 @@
 use anyhow::Result;
 use anyhow::Result;
 use crossterm::{
 use crossterm::{
+    cursor::{MoveTo, RestorePosition, SavePosition, Show},
     event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
     event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
     execute,
     execute,
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@@ -28,11 +29,13 @@ mod focus;
 mod hooks;
 mod hooks;
 mod layout;
 mod layout;
 mod node;
 mod node;
+pub mod prelude;
 pub mod query;
 pub mod query;
 mod render;
 mod render;
 mod style;
 mod style;
 mod style_attributes;
 mod style_attributes;
 mod widget;
 mod widget;
+mod widgets;
 
 
 pub use config::*;
 pub use config::*;
 pub use hooks::*;
 pub use hooks::*;
@@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
     screen as f32 * 10.0
     screen as f32 * 10.0
 }
 }
 
 
+pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
+    screen * 10.0
+}
+
 pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
 pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
     layout / 10.0
     layout / 10.0
 }
 }
@@ -136,7 +143,13 @@ fn render_vdom(
             let mut terminal = (!cfg.headless).then(|| {
             let mut terminal = (!cfg.headless).then(|| {
                 enable_raw_mode().unwrap();
                 enable_raw_mode().unwrap();
                 let mut stdout = std::io::stdout();
                 let mut stdout = std::io::stdout();
-                execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
+                execute!(
+                    stdout,
+                    EnterAlternateScreen,
+                    EnableMouseCapture,
+                    MoveTo(0, 1000)
+                )
+                .unwrap();
                 let backend = CrosstermBackend::new(io::stdout());
                 let backend = CrosstermBackend::new(io::stdout());
                 Terminal::new(backend).unwrap()
                 Terminal::new(backend).unwrap()
             });
             });
@@ -181,14 +194,16 @@ fn render_vdom(
                         taffy.compute_layout(root_node, size).unwrap();
                         taffy.compute_layout(root_node, size).unwrap();
                     }
                     }
                     if let Some(terminal) = &mut terminal {
                     if let Some(terminal) = &mut terminal {
+                        execute!(terminal.backend_mut(), SavePosition).unwrap();
                         terminal.draw(|frame| {
                         terminal.draw(|frame| {
                             let rdom = rdom.borrow();
                             let rdom = rdom.borrow();
                             let mut taffy = taffy.lock().expect("taffy lock poisoned");
                             let mut taffy = taffy.lock().expect("taffy lock poisoned");
                             // size is guaranteed to not change when rendering
                             // size is guaranteed to not change when rendering
-                            resize(frame.size(), &mut *taffy, &rdom);
+                            resize(frame.size(), &mut taffy, &rdom);
                             let root = &rdom[NodeId(0)];
                             let root = &rdom[NodeId(0)];
-                            render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO);
+                            render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO);
                         })?;
                         })?;
+                        execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
                     } else {
                     } else {
                         let rdom = rdom.borrow();
                         let rdom = rdom.borrow();
                         resize(
                         resize(

+ 1 - 0
packages/tui/src/prelude/mod.rs

@@ -0,0 +1 @@
+pub use crate::widgets::*;

+ 1 - 1
packages/tui/src/query.rs

@@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> {
             .ok();
             .ok();
         layout.map(|layout| Layout {
         layout.map(|layout| Layout {
             order: layout.order,
             order: layout.order,
-            size: layout.size.map(|v| layout_to_screen_space(v)),
+            size: layout.size.map(layout_to_screen_space),
             location: Point {
             location: Point {
                 x: layout_to_screen_space(layout.location.x),
                 x: layout_to_screen_space(layout.location.x),
                 y: layout_to_screen_space(layout.location.y),
                 y: layout_to_screen_space(layout.location.y),

+ 1 - 1
packages/tui/src/render.rs

@@ -41,7 +41,7 @@ pub(crate) fn render_vnode(
     let x = layout_to_screen_space(fx).round() as u16;
     let x = layout_to_screen_space(fx).round() as u16;
     let y = layout_to_screen_space(fy).round() as u16;
     let y = layout_to_screen_space(fy).round() as u16;
     let Size { width, height } = *size;
     let Size { width, height } = *size;
-    let width = layout_to_screen_space(fx + width).round() as u16 + x;
+    let width = layout_to_screen_space(fx + width).round() as u16 - x;
     let height = layout_to_screen_space(fy + height).round() as u16 - y;
     let height = layout_to_screen_space(fy + height).round() as u16 - y;
 
 
     match &node.node_data.node_type {
     match &node.node_data.node_type {

+ 58 - 0
packages/tui/src/widgets/button.rs

@@ -0,0 +1,58 @@
+use std::collections::HashMap;
+
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+
+#[derive(Props)]
+pub(crate) struct ButtonProps<'a> {
+    #[props(!optional)]
+    raw_onclick: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+
+#[allow(non_snake_case)]
+pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
+    let state = use_state(cx, || false);
+    let width = cx.props.width.unwrap_or("1px");
+    let height = cx.props.height.unwrap_or("1px");
+
+    let single_char = width == "1px" || height == "1px";
+    let text = if let Some(v) = cx.props.value { v } else { "" };
+    let border_style = if single_char { "none" } else { "solid" };
+    let update = || {
+        let new_state = !state.get();
+        if let Some(callback) = cx.props.raw_onclick {
+            callback.call(FormData {
+                value: text.to_string(),
+                values: HashMap::new(),
+            });
+        }
+        state.set(new_state);
+    };
+    cx.render(rsx! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border_style}",
+            flex_direction: "row",
+            align_items: "center",
+            justify_content: "center",
+            onclick: move |_| {
+                update();
+            },
+            onkeydown: move |evt|{
+                if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false }  {
+                    update();
+                }
+            },
+            "{text}"
+        }
+    })
+}

+ 81 - 0
packages/tui/src/widgets/checkbox.rs

@@ -0,0 +1,81 @@
+use std::collections::HashMap;
+
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+
+#[derive(Props)]
+pub(crate) struct CheckBoxProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+    #[props(!optional)]
+    checked: Option<&'a str>,
+}
+
+#[allow(non_snake_case)]
+pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
+    let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
+    let width = cx.props.width.unwrap_or("1px");
+    let height = cx.props.height.unwrap_or("1px");
+
+    let single_char = width == "1px" && height == "1px";
+    let text = if single_char {
+        if *state.get() {
+            "☑"
+        } else {
+            "☐"
+        }
+    } else if *state.get() {
+        "✓"
+    } else {
+        " "
+    };
+    let border_style = if width == "1px" || height == "1px" {
+        "none"
+    } else {
+        "solid"
+    };
+    let update = move || {
+        let new_state = !state.get();
+        if let Some(callback) = cx.props.raw_oninput {
+            callback.call(FormData {
+                value: if let Some(value) = &cx.props.value {
+                    if new_state {
+                        value.to_string()
+                    } else {
+                        String::new()
+                    }
+                } else {
+                    "on".to_string()
+                },
+                values: HashMap::new(),
+            });
+        }
+        state.set(new_state);
+    };
+    cx.render(rsx! {
+        div {
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border_style}",
+            align_items: "center",
+            justify_content: "center",
+            onclick: move |_| {
+                update();
+            },
+            onkeydown: move |evt| {
+                if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false }  {
+                    update();
+                }
+            },
+            "{text}"
+        }
+    })
+}

+ 102 - 0
packages/tui/src/widgets/input.rs

@@ -0,0 +1,102 @@
+use dioxus::prelude::*;
+use dioxus_core::prelude::fc_to_builder;
+use dioxus_html::FormData;
+
+use crate::widgets::button::Button;
+use crate::widgets::checkbox::CheckBox;
+use crate::widgets::number::NumbericInput;
+use crate::widgets::password::Password;
+use crate::widgets::slider::Slider;
+use crate::widgets::textbox::TextBox;
+
+#[derive(Props)]
+pub struct InputProps<'a> {
+    r#type: Option<&'static str>,
+    oninput: Option<EventHandler<'a, FormData>>,
+    onclick: Option<EventHandler<'a, FormData>>,
+    value: Option<&'a str>,
+    size: Option<&'a str>,
+    maxlength: Option<&'a str>,
+    width: Option<&'a str>,
+    height: Option<&'a str>,
+    min: Option<&'a str>,
+    max: Option<&'a str>,
+    step: Option<&'a str>,
+    checked: Option<&'a str>,
+}
+
+#[allow(non_snake_case)]
+pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> {
+    cx.render(match cx.props.r#type {
+        Some("checkbox") => {
+            rsx! {
+                CheckBox{
+                    raw_oninput: cx.props.oninput.as_ref(),
+                    value: cx.props.value,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                    checked: cx.props.checked,
+                }
+            }
+        }
+        Some("range") => {
+            rsx! {
+                Slider{
+                    raw_oninput: cx.props.oninput.as_ref(),
+                    value: cx.props.value,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                    max: cx.props.max,
+                    min: cx.props.min,
+                    step: cx.props.step,
+                }
+            }
+        }
+        Some("button") => {
+            rsx! {
+                Button{
+                    raw_onclick: cx.props.onclick.as_ref(),
+                    value: cx.props.value,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                }
+            }
+        }
+        Some("number") => {
+            rsx! {
+                NumbericInput{
+                    raw_oninput: cx.props.oninput.as_ref(),
+                    value: cx.props.value,
+                    size: cx.props.size,
+                    max_length: cx.props.maxlength,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                }
+            }
+        }
+        Some("password") => {
+            rsx! {
+                Password{
+                    raw_oninput: cx.props.oninput.as_ref(),
+                    value: cx.props.value,
+                    size: cx.props.size,
+                    max_length: cx.props.maxlength,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                }
+            }
+        }
+        _ => {
+            rsx! {
+                TextBox{
+                    raw_oninput: cx.props.oninput.as_ref(),
+                    value: cx.props.value,
+                    size: cx.props.size,
+                    max_length: cx.props.maxlength,
+                    width: cx.props.width,
+                    height: cx.props.height,
+                }
+            }
+        }
+    })
+}

+ 18 - 0
packages/tui/src/widgets/mod.rs

@@ -0,0 +1,18 @@
+mod button;
+mod checkbox;
+mod input;
+mod number;
+mod password;
+mod slider;
+mod textbox;
+
+use dioxus_core::{ElementId, RenderReturn, Scope};
+pub use input::*;
+
+pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
+    if let RenderReturn::Sync(Ok(sync)) = cx.root_node() {
+        sync.root_ids.get(0).and_then(|id| id.get())
+    } else {
+        None
+    }
+}

+ 208 - 0
packages/tui/src/widgets/number.rs

@@ -0,0 +1,208 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+use crossterm::{cursor::MoveTo, execute};
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+use dioxus_native_core::utils::cursor::{Cursor, Pos};
+use std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct NumbericInputProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    let update = |text: String| {
+        if let Some(input_handler) = &cx.props.raw_oninput {
+            input_handler.call(FormData {
+                value: text,
+                values: HashMap::new(),
+            });
+        }
+    };
+    let increase = move || {
+        let mut text = text_ref.write();
+        *text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
+        update(text.clone());
+    };
+    let decrease = move || {
+        let mut text = text_ref.write();
+        *text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
+        update(text.clone());
+    };
+
+    render! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border}",
+
+            onkeydown: move |k| {
+                let is_text = match k.key(){
+                    Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
+                    Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true,
+                    _  => false,
+                };
+                if is_text{
+                    let mut text = text_ref.write();
+                    cursor.write().handle_input(&k, &mut text, max_len);
+                    update(text.clone());
+
+                    let node = tui_query.get(get_root_id(cx).unwrap());
+                    let Point{ x, y } = node.pos().unwrap();
+
+                    let Pos { col, row } = cursor.read().start;
+                    let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                    if let Ok(pos) = crossterm::cursor::position() {
+                        if pos != (x, y){
+                            execute!(stdout(), MoveTo(x, y)).unwrap();
+                        }
+                    }
+                    else{
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    match k.key() {
+                        Key::ArrowUp =>{
+                            increase();
+                        }
+                        Key::ArrowDown =>{
+                            decrease();
+                        }
+                        _ => ()
+                    }
+                }
+            },
+            onmousemove: move |evt| {
+                if *dragging.get() {
+                    let offset = evt.data.element_coordinates();
+                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
+                    if border != "none" {
+                        new.col = new.col.saturating_sub(1);
+                    }
+                    // textboxs are only one line tall
+                    new.row = 0;
+
+                    if new != cursor.read().start {
+                        cursor.write().end = Some(new);
+                    }
+                }
+            },
+            onmousedown: move |evt| {
+                let offset = evt.data.element_coordinates();
+                let mut new = Pos::new(offset.x as usize,  offset.y as usize);
+                if border != "none" {
+                    new.col = new.col.saturating_sub(1);
+                }
+                new.row = 0;
+
+                new.realize_col(&text_ref.read());
+                cursor.set(Cursor::from_start(new));
+                dragging.set(true);
+                let node = tui_query_clone.get(get_root_id(cx).unwrap());
+                let Point{ x, y } = node.pos().unwrap();
+
+                let Pos { col, row } = cursor.read().start;
+                let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                if let Ok(pos) = crossterm::cursor::position() {
+                    if pos != (x, y){
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    execute!(stdout(), MoveTo(x, y)).unwrap();
+                }
+            },
+            onmouseup: move |_| {
+                dragging.set(false);
+            },
+            onmouseleave: move |_| {
+                dragging.set(false);
+            },
+            onmouseenter: move |_| {
+                dragging.set(false);
+            },
+            onfocusout: |_| {
+                execute!(stdout(), MoveTo(0, 1000)).unwrap();
+            },
+
+            "{text_before_first_cursor}"
+
+            span{
+                background_color: "rgba(255, 255, 255, 50%)",
+
+                "{text_highlighted}"
+            }
+
+            "{text_after_second_cursor}"
+        }
+    }
+}

+ 185 - 0
packages/tui/src/widgets/password.rs

@@ -0,0 +1,185 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+use crossterm::{cursor::*, execute};
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+use dioxus_native_core::utils::cursor::{Cursor, Pos};
+use std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct PasswordProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
+    let text_highlighted = ".".repeat(text_highlighted.len());
+    let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    render! {
+        div {
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border}",
+
+            onkeydown: move |k| {
+                if k.key()== Key::Enter {
+                    return;
+                }
+                let mut text = text_ref.write();
+                cursor.write().handle_input(&k, &mut text, max_len);
+                if let Some(input_handler) = &cx.props.raw_oninput{
+                    input_handler.call(FormData{
+                        value: text.clone(),
+                        values: HashMap::new(),
+                    });
+                }
+
+                let node = tui_query.get(get_root_id(cx).unwrap());
+                let Point{ x, y } = node.pos().unwrap();
+
+                let Pos { col, row } = cursor.read().start;
+                let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                if let Ok(pos) = crossterm::cursor::position() {
+                    if pos != (x, y){
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    execute!(stdout(), MoveTo(x, y)).unwrap();
+                }
+            },
+
+            onmousemove: move |evt| {
+                if *dragging.get() {
+                    let offset = evt.data.element_coordinates();
+                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
+                    if border != "none" {
+                        new.col = new.col.saturating_sub(1);
+                    }
+                    // textboxs are only one line tall
+                    new.row = 0;
+
+                    if new != cursor.read().start {
+                        cursor.write().end = Some(new);
+                    }
+                }
+            },
+            onmousedown: move |evt| {
+                let offset = evt.data.element_coordinates();
+                let mut new = Pos::new(offset.x as usize, offset.y as usize);
+                if border != "none" {
+                    new.col = new.col.saturating_sub(1);
+                }
+                // textboxs are only one line tall
+                new.row = 0;
+
+                new.realize_col(&text_ref.read());
+                cursor.set(Cursor::from_start(new));
+                dragging.set(true);
+                let node = tui_query_clone.get(get_root_id(cx).unwrap());
+                let Point{ x, y } = node.pos().unwrap();
+
+                let Pos { col, row } = cursor.read().start;
+                let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                if let Ok(pos) = crossterm::cursor::position() {
+                    if pos != (x, y){
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    execute!(stdout(), MoveTo(x, y)).unwrap();
+                }
+            },
+            onmouseup: move |_| {
+                dragging.set(false);
+            },
+            onmouseleave: move |_| {
+                dragging.set(false);
+            },
+            onmouseenter: move |_| {
+                dragging.set(false);
+            },
+            onfocusout: |_| {
+                execute!(stdout(), MoveTo(0, 1000)).unwrap();
+            },
+
+            "{text_before_first_cursor}"
+
+            span{
+                background_color: "rgba(255, 255, 255, 50%)",
+
+                "{text_highlighted}"
+            }
+
+            "{text_after_second_cursor}"
+        }
+    }
+}

+ 107 - 0
packages/tui/src/widgets/slider.rs

@@ -0,0 +1,107 @@
+use std::collections::HashMap;
+
+use crate::widgets::get_root_id;
+use crate::Query;
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+
+#[derive(Props)]
+pub(crate) struct SliderProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+    #[props(!optional)]
+    min: Option<&'a str>,
+    #[props(!optional)]
+    max: Option<&'a str>,
+    #[props(!optional)]
+    step: Option<&'a str>,
+}
+
+#[allow(non_snake_case)]
+pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+
+    let value_state = use_state(cx, || 0.0);
+    let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
+    let width = cx.props.width.unwrap_or("20px");
+    let height = cx.props.height.unwrap_or("1px");
+    let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
+    let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
+    let size = max - min;
+    let step = cx
+        .props
+        .step
+        .and_then(|v| v.parse().ok())
+        .unwrap_or(size / 10.0);
+
+    let current_value = if let Some(value) = value {
+        value
+    } else {
+        *value_state.get()
+    }
+    .max(min)
+    .min(max);
+    let fst_width = 100.0 * (current_value - min) / size;
+    let snd_width = 100.0 * (max - current_value) / size;
+    assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0);
+
+    let update = |value: String| {
+        if let Some(oninput) = cx.props.raw_oninput {
+            oninput.call(FormData {
+                value,
+                values: HashMap::new(),
+            });
+        }
+    };
+
+    render! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            display: "flex",
+            flex_direction: "row",
+            onkeydown: move |event| {
+                match event.key() {
+                    Key::ArrowLeft => {
+                        value_state.set((current_value - step).max(min).min(max));
+                        update(value_state.current().to_string());
+                    }
+                    Key::ArrowRight => {
+                        value_state.set((current_value + step).max(min).min(max));
+                        update(value_state.current().to_string());
+                    }
+                    _ => ()
+                }
+            },
+            onmousemove: move |evt| {
+                let mouse = evt.data;
+                if !mouse.held_buttons().is_empty(){
+                    let node = tui_query.get(get_root_id(cx).unwrap());
+                    let width = node.size().unwrap().width;
+                    let offset = mouse.element_coordinates();
+                    value_state.set(min + size*(offset.x as f32) / width as f32);
+                    update(value_state.current().to_string());
+                }
+            },
+            div{
+                width: "{fst_width}%",
+                background_color: "rgba(10,10,10,0.5)",
+            }
+            div{
+                "|"
+            }
+            div{
+                width: "{snd_width}%",
+                background_color: "rgba(10,10,10,0.5)",
+            }
+        }
+    }
+}

+ 181 - 0
packages/tui/src/widgets/textbox.rs

@@ -0,0 +1,181 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+use crossterm::{cursor::*, execute};
+use dioxus::prelude::*;
+use dioxus_elements::input_data::keyboard_types::Key;
+use dioxus_html as dioxus_elements;
+use dioxus_html::FormData;
+use dioxus_native_core::utils::cursor::{Cursor, Pos};
+use std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct TextBoxProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    render! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border}",
+
+            onkeydown: move |k| {
+                if k.key() == Key::Enter {
+                    return;
+                }
+                let mut text = text_ref.write();
+                cursor.write().handle_input(&k, &mut text, max_len);
+                if let Some(input_handler) = &cx.props.raw_oninput{
+                    input_handler.call(FormData{
+                        value: text.clone(),
+                        values: HashMap::new(),
+                    });
+                }
+
+                let node = tui_query.get(get_root_id(cx).unwrap());
+                let Point{ x, y } = node.pos().unwrap();
+
+                let Pos { col, row } = cursor.read().start;
+                let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                if let Ok(pos) = crossterm::cursor::position() {
+                    if pos != (x, y){
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    execute!(stdout(), MoveTo(x, y)).unwrap();
+                }
+            },
+
+            onmousemove: move |evt| {
+                if *dragging.get() {
+                    let offset = evt.data.element_coordinates();
+                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
+                    if border != "none" {
+                        new.col = new.col.saturating_sub(1);
+                    }
+                    // textboxs are only one line tall
+                    new.row = 0;
+
+                    if new != cursor.read().start {
+                        cursor.write().end = Some(new);
+                    }
+                }
+            },
+            onmousedown: move |evt| {
+                let offset = evt.data.element_coordinates();
+                let mut new = Pos::new(offset.x as usize, offset.y as usize);
+                if border != "none" {
+                    new.col = new.col.saturating_sub(1);
+                }
+                // textboxs are only one line tall
+                new.row = 0;
+
+                new.realize_col(&text_ref.read());
+                cursor.set(Cursor::from_start(new));
+                dragging.set(true);
+                let node = tui_query_clone.get(get_root_id(cx).unwrap());
+                let Point{ x, y } = node.pos().unwrap();
+
+                let Pos { col, row } = cursor.read().start;
+                let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                if let Ok(pos) = crossterm::cursor::position() {
+                    if pos != (x, y){
+                        execute!(stdout(), MoveTo(x, y)).unwrap();
+                    }
+                }
+                else{
+                    execute!(stdout(), MoveTo(x, y)).unwrap();
+                }
+            },
+            onmouseup: move |_| {
+                dragging.set(false);
+            },
+            onmouseleave: move |_| {
+                dragging.set(false);
+            },
+            onmouseenter: move |_| {
+                dragging.set(false);
+            },
+            onfocusout: |_| {
+                execute!(stdout(), MoveTo(0, 1000)).unwrap();
+            },
+
+            "{text_before_first_cursor}"
+
+            span{
+                background_color: "rgba(255, 255, 255, 50%)",
+
+                "{text_highlighted}"
+            }
+
+            "{text_after_second_cursor}"
+        }
+    }
+}

+ 2 - 1
packages/web/Cargo.toml

@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
 dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
 dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
 dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
 dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
 dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
-    "web"
+    "sledgehammer"
 ] }
 ] }
 
 
 js-sys = "0.3.56"
 js-sys = "0.3.56"
@@ -30,6 +30,7 @@ futures-util = "0.3.19"
 smallstr = "0.2.0"
 smallstr = "0.2.0"
 futures-channel = "0.3.21"
 futures-channel = "0.3.21"
 serde_json = { version = "1.0" }
 serde_json = { version = "1.0" }
+serde = { version = "1.0" }
 serde-wasm-bindgen = "0.4.5"
 serde-wasm-bindgen = "0.4.5"
 
 
 [dependencies.web-sys]
 [dependencies.web-sys]

+ 166 - 46
packages/web/src/dom.rs

@@ -7,24 +7,34 @@
 //! - tests to ensure dyn_into works for various event types.
 //! - tests to ensure dyn_into works for various event types.
 //! - Partial delegation?>
 //! - Partial delegation?>
 
 
-use dioxus_core::{Mutation, Template};
+use dioxus_core::{ElementId, Mutation, Template, TemplateAttribute, TemplateNode};
 use dioxus_html::{event_bubbles, CompositionData, FormData};
 use dioxus_html::{event_bubbles, CompositionData, FormData};
-use dioxus_interpreter_js::Interpreter;
+use dioxus_interpreter_js::{save_template, Channel};
 use futures_channel::mpsc;
 use futures_channel::mpsc;
+use rustc_hash::FxHashMap;
 use std::{any::Any, rc::Rc};
 use std::{any::Any, rc::Rc};
 use wasm_bindgen::{closure::Closure, JsCast};
 use wasm_bindgen::{closure::Closure, JsCast};
-use web_sys::{Document, Element, Event};
+use web_sys::{Document, Element, Event, HtmlElement};
 
 
 use crate::Config;
 use crate::Config;
 
 
 pub struct WebsysDom {
 pub struct WebsysDom {
-    interpreter: Interpreter,
-    handler: Closure<dyn FnMut(&Event)>,
-    _root: Element,
+    document: Document,
+    templates: FxHashMap<String, u32>,
+    max_template_id: u32,
+    interpreter: Channel,
+}
+
+pub struct UiEvent {
+    pub name: String,
+    pub bubbles: bool,
+    pub element: ElementId,
+    pub data: Rc<dyn Any>,
+    pub event: Event,
 }
 }
 
 
 impl WebsysDom {
 impl WebsysDom {
-    pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<Event>) -> Self {
+    pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<UiEvent>) -> Self {
         // eventually, we just want to let the interpreter do all the work of decoding events into our event type
         // eventually, we just want to let the interpreter do all the work of decoding events into our event type
         // a match here in order to avoid some error during runtime browser test
         // a match here in order to avoid some error during runtime browser test
         let document = load_document();
         let document = load_document();
@@ -32,67 +42,158 @@ impl WebsysDom {
             Some(root) => root,
             Some(root) => root,
             None => document.create_element("body").ok().unwrap(),
             None => document.create_element("body").ok().unwrap(),
         };
         };
+        let interpreter = Channel::default();
 
 
+        let handler: Closure<dyn FnMut(&Event)> =
+            Closure::wrap(Box::new(move |event: &web_sys::Event| {
+                let name = event.type_();
+                let element = walk_event_for_id(event);
+                let bubbles = dioxus_html::event_bubbles(name.as_str());
+                if let Some((element, target)) = element {
+                    if target
+                        .get_attribute("dioxus-prevent-default")
+                        .as_deref()
+                        .map(|f| f.trim_start_matches("on"))
+                        == Some(&name)
+                    {
+                        event.prevent_default();
+                    }
+
+                    let data = virtual_event_from_websys_event(event.clone(), target);
+                    let _ = event_channel.unbounded_send(UiEvent {
+                        name,
+                        bubbles,
+                        element,
+                        data,
+                        event: event.clone(),
+                    });
+                }
+            }));
+
+        dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref());
+        handler.forget();
         Self {
         Self {
-            interpreter: Interpreter::new(root.clone()),
-            _root: root,
-            handler: Closure::wrap(Box::new(move |event: &web_sys::Event| {
-                let _ = event_channel.unbounded_send(event.clone());
-            })),
+            document,
+            interpreter,
+            templates: FxHashMap::default(),
+            max_template_id: 0,
         }
         }
     }
     }
 
 
     pub fn mount(&mut self) {
     pub fn mount(&mut self) {
-        self.interpreter.MountToRoot();
+        self.interpreter.mount_to_root();
     }
     }
 
 
     pub fn load_templates(&mut self, templates: &[Template]) {
     pub fn load_templates(&mut self, templates: &[Template]) {
-        log::debug!("Loading templates {:?}", templates);
-
         for template in templates {
         for template in templates {
-            self.interpreter
-                .SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap());
+            let mut roots = vec![];
+
+            for root in template.roots {
+                roots.push(self.create_template_node(root))
+            }
+
+            self.templates
+                .insert(template.name.to_owned(), self.max_template_id);
+            save_template(roots, self.max_template_id);
+            self.max_template_id += 1
+        }
+    }
+
+    fn create_template_node(&self, v: &TemplateNode) -> web_sys::Node {
+        use TemplateNode::*;
+        match v {
+            Element {
+                tag,
+                namespace,
+                attrs,
+                children,
+                ..
+            } => {
+                let el = match namespace {
+                    Some(ns) => self.document.create_element_ns(Some(ns), tag).unwrap(),
+                    None => self.document.create_element(tag).unwrap(),
+                };
+                for attr in *attrs {
+                    if let TemplateAttribute::Static {
+                        name,
+                        value,
+                        namespace,
+                    } = attr
+                    {
+                        match namespace {
+                            Some(ns) if *ns == "style" => el
+                                .dyn_ref::<HtmlElement>()
+                                .unwrap()
+                                .style()
+                                .set_property(name, value)
+                                .unwrap(),
+                            Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(),
+                            None => el.set_attribute(name, value).unwrap(),
+                        }
+                    }
+                }
+                for child in *children {
+                    let _ = el.append_child(&self.create_template_node(child));
+                }
+                el.dyn_into().unwrap()
+            }
+            Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
+            DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
+            Dynamic { .. } => {
+                let el = self.document.create_element("pre").unwrap();
+                let _ = el.toggle_attribute("hidden");
+                el.dyn_into().unwrap()
+            }
         }
         }
     }
     }
 
 
     pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
     pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
         use Mutation::*;
         use Mutation::*;
-        let i = &self.interpreter;
-        for edit in edits.drain(..) {
+        let i = &mut self.interpreter;
+        for edit in &edits {
             match edit {
             match edit {
-                AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32),
-                AssignId { path, id } => i.AssignId(path, id.0 as u32),
-                CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32),
-                CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32),
-                HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32),
-                LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32),
-                ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32),
-                ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32),
-                InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32),
-                InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32),
+                AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
+                AssignId { path, id } => {
+                    i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
+                }
+                CreatePlaceholder { id } => i.create_placeholder(id.0 as u32),
+                CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32),
+                HydrateText { path, value, id } => {
+                    i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32)
+                }
+                LoadTemplate { name, index, id } => {
+                    if let Some(tmpl_id) = self.templates.get(*name) {
+                        i.load_template(*tmpl_id, *index as u32, id.0 as u32)
+                    }
+                }
+                ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32),
+                ReplacePlaceholder { path, m } => {
+                    i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32)
+                }
+                InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32),
+                InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32),
                 SetAttribute {
                 SetAttribute {
                     name,
                     name,
                     value,
                     value,
                     id,
                     id,
                     ns,
                     ns,
-                } => i.SetAttribute(id.0 as u32, name, value.into(), ns),
+                } => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()),
                 SetBoolAttribute { name, value, id } => {
                 SetBoolAttribute { name, value, id } => {
-                    i.SetBoolAttribute(id.0 as u32, name, value)
+                    i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "")
                 }
                 }
-                SetText { value, id } => i.SetText(id.0 as u32, value.into()),
+                SetText { value, id } => i.set_text(id.0 as u32, value),
                 NewEventListener { name, id, .. } => {
                 NewEventListener { name, id, .. } => {
-                    self.interpreter.NewEventListener(
-                        name,
-                        id.0 as u32,
-                        event_bubbles(&name[2..]),
-                        self.handler.as_ref().unchecked_ref(),
-                    );
+                    i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8);
                 }
                 }
-                RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32),
-                Remove { id } => i.Remove(id.0 as u32),
-                PushRoot { id } => i.PushRoot(id.0 as u32),
+                RemoveEventListener { name, id } => {
+                    i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8)
+                }
+                Remove { id } => i.remove(id.0 as u32),
+                PushRoot { id } => i.push_root(id.0 as u32),
             }
             }
         }
         }
+        edits.clear();
+        i.flush();
     }
     }
 }
 }
 
 
@@ -230,9 +331,28 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
         }
         }
     }
     }
 
 
-    Rc::new(FormData {
-        value,
-        values,
-        files: None,
-    })
+    Rc::new(FormData { value, values })
+}
+
+fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
+    use wasm_bindgen::JsCast;
+
+    let mut target = event
+        .target()
+        .expect("missing target")
+        .dyn_into::<web_sys::Element>()
+        .expect("not a valid element");
+
+    loop {
+        match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
+            Some(Ok(id)) => return Some((ElementId(id), target)),
+            Some(Err(_)) => return None,
+
+            // walk the tree upwards until we actually find an event target
+            None => match target.parent_element() {
+                Some(parent) => target = parent,
+                None => return None,
+            },
+        }
+    }
 }
 }

+ 7 - 35
packages/web/src/lib.rs

@@ -55,7 +55,7 @@
 
 
 pub use crate::cfg::Config;
 pub use crate::cfg::Config;
 use crate::dom::virtual_event_from_websys_event;
 use crate::dom::virtual_event_from_websys_event;
-pub use crate::util::use_eval;
+pub use crate::util::{use_eval, EvalResult};
 use dioxus_core::{Element, ElementId, Scope, VirtualDom};
 use dioxus_core::{Element, ElementId, Scope, VirtualDom};
 use futures_util::{pin_mut, FutureExt, StreamExt};
 use futures_util::{pin_mut, FutureExt, StreamExt};
 
 
@@ -195,7 +195,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
     // the mutations come back with nothing - we need to actually mount them
     // the mutations come back with nothing - we need to actually mount them
     websys_dom.mount();
     websys_dom.mount();
 
 
-    let mut work_loop = ric_raf::RafLoop::new();
+    let _work_loop = ric_raf::RafLoop::new();
 
 
     loop {
     loop {
         log::debug!("waiting for work");
         log::debug!("waiting for work");
@@ -222,16 +222,11 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
         // Dequeue all of the events from the channel in send order
         // Dequeue all of the events from the channel in send order
         // todo: we should re-order these if possible
         // todo: we should re-order these if possible
         while let Some(evt) = res {
         while let Some(evt) = res {
-            let name = evt.type_();
-            let element = walk_event_for_id(&evt);
-            let bubbles = dioxus_html::event_bubbles(name.as_str());
-            if let Some((element, target)) = element {
-                let data = virtual_event_from_websys_event(evt, target);
-                dom.handle_event(name.as_str(), data, element, bubbles);
-            }
+            dom.handle_event(evt.name.as_str(), evt.data, evt.element, evt.bubbles);
             res = rx.try_next().transpose().unwrap().ok();
             res = rx.try_next().transpose().unwrap().ok();
         }
         }
 
 
+        // Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
         // Jank free rendering
         // Jank free rendering
         //
         //
         // 1. wait for the browser to give us "idle" time
         // 1. wait for the browser to give us "idle" time
@@ -240,42 +235,19 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
         // 4. Wait for the animation frame to patch the dom
         // 4. Wait for the animation frame to patch the dom
 
 
         // wait for the mainthread to schedule us in
         // wait for the mainthread to schedule us in
-        let deadline = work_loop.wait_for_idle_time().await;
+        // let deadline = work_loop.wait_for_idle_time().await;
 
 
         // run the virtualdom work phase until the frame deadline is reached
         // run the virtualdom work phase until the frame deadline is reached
-        let edits = dom.render_with_deadline(deadline).await;
+        let edits = dom.render_immediate();
 
 
         // wait for the animation frame to fire so we can apply our changes
         // wait for the animation frame to fire so we can apply our changes
-        work_loop.wait_for_raf().await;
+        // work_loop.wait_for_raf().await;
 
 
         websys_dom.load_templates(&edits.templates);
         websys_dom.load_templates(&edits.templates);
         websys_dom.apply_edits(edits.edits);
         websys_dom.apply_edits(edits.edits);
     }
     }
 }
 }
 
 
-fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
-    use wasm_bindgen::JsCast;
-
-    let mut target = event
-        .target()
-        .expect("missing target")
-        .dyn_into::<web_sys::Element>()
-        .expect("not a valid element");
-
-    loop {
-        match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
-            Some(Ok(id)) => return Some((ElementId(id), target)),
-            Some(Err(_)) => return None,
-
-            // walk the tree upwards until we actually find an event target
-            None => match target.parent_element() {
-                Some(parent) => target = parent,
-                None => return None,
-            },
-        }
-    }
-}
-
 // if should_hydrate {
 // if should_hydrate {
 //     // todo: we need to split rebuild and initialize into two phases
 //     // todo: we need to split rebuild and initialize into two phases
 //     // it's a waste to produce edits just to get the vdom loaded
 //     // it's a waste to produce edits just to get the vdom loaded

+ 50 - 4
packages/web/src/util.rs

@@ -1,6 +1,13 @@
 //! Utilities specific to websys
 //! Utilities specific to websys
 
 
+use std::{
+    future::{IntoFuture, Ready},
+    str::FromStr,
+};
+
 use dioxus_core::*;
 use dioxus_core::*;
+use serde::de::Error;
+use serde_json::Value;
 
 
 /// Get a closure that executes any JavaScript in the webpage.
 /// Get a closure that executes any JavaScript in the webpage.
 ///
 ///
@@ -15,12 +22,51 @@ use dioxus_core::*;
 ///
 ///
 /// The closure will panic if the provided script is not valid JavaScript code
 /// The closure will panic if the provided script is not valid JavaScript code
 /// or if it returns an uncaught error.
 /// or if it returns an uncaught error.
-pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
+pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
     cx.use_hook(|| {
     cx.use_hook(|| {
         |script: S| {
         |script: S| {
-            js_sys::Function::new_no_args(&script.to_string())
-                .call0(&wasm_bindgen::JsValue::NULL)
-                .expect("failed to eval script");
+            let body = script.to_string();
+            EvalResult {
+                value: if let Ok(value) =
+                    js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL)
+                {
+                    if let Ok(stringified) = js_sys::JSON::stringify(&value) {
+                        if !stringified.is_undefined() && stringified.is_valid_utf16() {
+                            let string: String = stringified.into();
+                            Value::from_str(&string)
+                        } else {
+                            Err(serde_json::Error::custom("Failed to stringify result"))
+                        }
+                    } else {
+                        Err(serde_json::Error::custom("Failed to stringify result"))
+                    }
+                } else {
+                    Err(serde_json::Error::custom("Failed to execute script"))
+                },
+            }
         }
         }
     })
     })
 }
 }
+
+/// A wrapper around the result of a JavaScript evaluation.
+/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult.
+pub struct EvalResult {
+    value: Result<Value, serde_json::Error>,
+}
+
+impl EvalResult {
+    /// Get the result of the Javascript execution.
+    pub fn get(self) -> Result<Value, serde_json::Error> {
+        self.value
+    }
+}
+
+impl IntoFuture for EvalResult {
+    type Output = Result<Value, serde_json::Error>;
+
+    type IntoFuture = Ready<Result<Value, serde_json::Error>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        std::future::ready(self.value)
+    }
+}