Browse Source

Merge branch 'upstream' into fix-non-str-attributes

Evan Almloff 2 years ago
parent
commit
afd024bcb6
100 changed files with 1359 additions and 2062 deletions
  1. 1 1
      .github/workflows/main.yml
  2. 1 1
      Cargo.toml
  3. 1 1
      docs/guide/examples/component_children_inspect.rs
  4. 0 15
      docs/guide/src/en/best_practices/index.md
  5. 3 9
      docs/guide/src/en/getting_started/desktop.md
  6. 1 1
      docs/guide/src/en/getting_started/hot_reload.md
  7. 3 2
      docs/guide/src/en/getting_started/mobile.md
  8. 5 3
      docs/guide/src/en/getting_started/ssr.md
  9. 4 2
      docs/guide/src/en/getting_started/tui.md
  10. 3 2
      docs/guide/src/en/getting_started/web.md
  11. 2 1
      docs/guide/src/en/interactivity/router.md
  12. 0 16
      docs/guide/src/pt-br/best_practices/index.md
  13. 2 8
      docs/guide/src/pt-br/getting_started/desktop.md
  14. 1 1
      docs/guide/src/pt-br/getting_started/hot_reload.md
  15. 3 2
      docs/guide/src/pt-br/getting_started/mobile.md
  16. 6 4
      docs/guide/src/pt-br/getting_started/ssr.md
  17. 2 1
      docs/guide/src/pt-br/getting_started/tui.md
  18. 2 1
      docs/guide/src/pt-br/getting_started/web.md
  19. 2 1
      docs/guide/src/pt-br/interactivity/router.md
  20. 71 0
      packages/autofmt/README.md
  21. 18 8
      packages/autofmt/src/buffer.rs
  22. 58 21
      packages/autofmt/src/element.rs
  23. 78 5
      packages/autofmt/src/expr.rs
  24. 1 1
      packages/autofmt/src/lib.rs
  25. 7 0
      packages/autofmt/tests/samples.rs
  26. 3 3
      packages/autofmt/tests/samples/complex.rsx
  27. 9 0
      packages/autofmt/tests/samples/key.rsx
  28. 38 0
      packages/autofmt/tests/samples/long.rsx
  29. 25 0
      packages/autofmt/tests/samples/multirsx.rsx
  30. 14 6
      packages/autofmt/tests/samples/simple.rsx
  31. 2 0
      packages/autofmt/tests/wrong.rs
  32. 3 0
      packages/autofmt/tests/wrong/multiexpr.rsx
  33. 3 0
      packages/autofmt/tests/wrong/multiexpr.wrong.rsx
  34. 1 1
      packages/core-macro/src/props/mod.rs
  35. 4 4
      packages/core/Cargo.toml
  36. 16 9
      packages/core/src/arena.rs
  37. 2 2
      packages/core/src/create.rs
  38. 114 66
      packages/core/src/diff.rs
  39. 1 1
      packages/core/src/error_boundary.rs
  40. 3 3
      packages/core/src/fragment.rs
  41. 2 2
      packages/core/src/lib.rs
  42. 3 13
      packages/core/src/nodes.rs
  43. 1 1
      packages/core/src/scheduler/wait.rs
  44. 1 1
      packages/core/src/scopes.rs
  45. 2 2
      packages/core/src/virtual_dom.rs
  46. 5 10
      packages/core/tests/attr_cleanup.rs
  47. 1 1
      packages/core/tests/bubble_error.rs
  48. 10 9
      packages/core/tests/diff_unkeyed_list.rs
  49. 10 0
      packages/desktop/src/cfg.rs
  50. 14 8
      packages/desktop/src/controller.rs
  51. 8 0
      packages/desktop/src/desktop_context.rs
  52. 0 62
      packages/desktop/src/events.rs
  53. 33 27
      packages/desktop/src/lib.rs
  54. 19 7
      packages/desktop/src/protocol.rs
  55. 6 2
      packages/html/Cargo.toml
  56. 1 1
      packages/html/src/events/animation.rs
  57. 1 1
      packages/html/src/events/clipboard.rs
  58. 1 1
      packages/html/src/events/composition.rs
  59. 2 5
      packages/html/src/events/drag.rs
  60. 1 1
      packages/html/src/events/focus.rs
  61. 6 0
      packages/html/src/events/form.rs
  62. 1 1
      packages/html/src/events/image.rs
  63. 1 1
      packages/html/src/events/keyboard.rs
  64. 1 1
      packages/html/src/events/media.rs
  65. 1 1
      packages/html/src/events/mouse.rs
  66. 1 1
      packages/html/src/events/pointer.rs
  67. 1 1
      packages/html/src/events/scroll.rs
  68. 1 1
      packages/html/src/events/selection.rs
  69. 1 1
      packages/html/src/events/toggle.rs
  70. 1 1
      packages/html/src/events/touch.rs
  71. 1 1
      packages/html/src/events/transition.rs
  72. 1 1
      packages/html/src/events/wheel.rs
  73. 6 0
      packages/html/src/lib.rs
  74. 217 0
      packages/html/src/transit.rs
  75. 10 14
      packages/interpreter/src/interpreter.js
  76. 40 20
      packages/liveview/Cargo.toml
  77. 1 47
      packages/liveview/README.md
  78. 35 14
      packages/liveview/examples/axum.rs
  79. 54 43
      packages/liveview/examples/salvo.rs
  80. 49 28
      packages/liveview/examples/warp.rs
  81. 17 88
      packages/liveview/src/adapters/axum_adapter.rs
  82. 19 104
      packages/liveview/src/adapters/salvo_adapter.rs
  83. 20 102
      packages/liveview/src/adapters/warp_adapter.rs
  84. 0 207
      packages/liveview/src/events.rs
  85. 0 973
      packages/liveview/src/interpreter.js
  86. 38 39
      packages/liveview/src/lib.rs
  87. 34 0
      packages/liveview/src/main.js
  88. 149 0
      packages/liveview/src/pool.rs
  89. 1 1
      packages/native-core/src/utils/cursor.rs
  90. 1 1
      packages/router/src/components/redirect.rs
  91. 1 1
      packages/router/src/components/route.rs
  92. 1 0
      packages/rsx/README.md
  93. 1 1
      packages/rsx/src/component.rs
  94. 2 2
      packages/rsx/src/lib.rs
  95. 3 1
      packages/rsx/src/node.rs
  96. 1 1
      packages/ssr/src/lib.rs
  97. 2 2
      packages/ssr/src/renderer.rs
  98. 1 1
      packages/tui/examples/tui_colorpicker.rs
  99. 1 1
      packages/tui/src/widgets/mod.rs
  100. 4 4
      packages/tui/src/widgets/number.rs

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

@@ -104,7 +104,7 @@ jobs:
       - uses: actions-rs/cargo@v1
       - uses: actions-rs/cargo@v1
         with:
         with:
           command: clippy
           command: clippy
-          args: -- -D warnings
+          args: --workspace -- -D warnings
 
 
   # Coverage is disabled until we can fix it
   # Coverage is disabled until we can fix it
   # coverage:
   # coverage:

+ 1 - 1
Cargo.toml

@@ -15,10 +15,10 @@ members = [
     "packages/liveview",
     "packages/liveview",
     "packages/autofmt",
     "packages/autofmt",
     "packages/rsx",
     "packages/rsx",
-    "docs/guide",
     "packages/tui",
     "packages/tui",
     "packages/native-core",
     "packages/native-core",
     "packages/native-core-macro",
     "packages/native-core-macro",
+    "docs/guide",
 ]
 ]
 
 
 # This is a "virtual package"
 # This is a "virtual package"

+ 1 - 1
docs/guide/examples/component_children_inspect.rs

@@ -26,7 +26,7 @@ struct ClickableProps<'a> {
 // ANCHOR: Clickable
 // ANCHOR: Clickable
 fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
 fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
     match cx.props.children {
     match cx.props.children {
-        Ok(VNode { dynamic_nodes, .. }) => {
+        Some(VNode { dynamic_nodes, .. }) => {
             todo!("render some stuff")
             todo!("render some stuff")
         }
         }
         _ => {
         _ => {

+ 0 - 15
docs/guide/src/en/best_practices/index.md

@@ -12,18 +12,3 @@ While it is possible to share state between components, this should only be done
 
 
 - Keep state local to a component if possible
 - Keep state local to a component if possible
 - When sharing state through props, only pass down the specific data necessary
 - When sharing state through props, only pass down the specific data necessary
-
-## Reusable Libraries
-
-When publishing a library designed to work with Dioxus, we highly advise using only the core feature on the `dioxus` crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that might make it not compile on unsupported platforms.
-
-
-❌ Don't include unnecessary dependencies in libraries:
-```toml
-dioxus = { version = "...", features = ["web", "desktop", "full"]}
-```
-
-✅ Only add the features you need:
-```toml
-dioxus = { version = "...", features = "core"}
-```

+ 3 - 9
docs/guide/src/en/getting_started/desktop.md

@@ -25,19 +25,13 @@ cargo new --bin demo
 cd demo
 cd demo
 ```
 ```
 
 
-Add Dioxus with the `desktop` feature (this will edit `Cargo.toml`):
+Add Dioxus and the `desktop` renderer (this will edit `Cargo.toml`):
 
 
 ```shell
 ```shell
-cargo add dioxus --features desktop
+cargo add dioxus
+cargo add dioxus-desktop
 ```
 ```
 
 
-> If your system does not provide the `libappindicator3` library, like Debian/bullseye, you can enable the replacement `ayatana` with an additional flag:
->
->```shell
-># On Debian/bullseye use:
->cargo add dioxus --features desktop --features ayatana
->```
-
 Edit your `main.rs`:
 Edit your `main.rs`:
 
 
 ```rust
 ```rust

+ 1 - 1
docs/guide/src/en/getting_started/hot_reload.md

@@ -8,7 +8,7 @@
 Install [dioxus-cli](https://github.com/DioxusLabs/cli).
 Install [dioxus-cli](https://github.com/DioxusLabs/cli).
 Enable the hot-reload feature on dioxus:
 Enable the hot-reload feature on dioxus:
 ```toml
 ```toml
-dioxus = { version = "*", features = ["web", "hot-reload"] }
+dioxus = { version = "*", features = ["hot-reload"] }
 ```
 ```
 
 
 # Usage
 # Usage

+ 3 - 2
docs/guide/src/en/getting_started/mobile.md

@@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs"
 # clear all the dependencies
 # clear all the dependencies
 [dependencies]
 [dependencies]
 mobile-entry-point = "0.1.0"
 mobile-entry-point = "0.1.0"
-dioxus = { version = "*", features = ["mobile"] }
+dioxus = { version = "*"}
+dioxus-desktop = { version = "*" }
 simple_logger = "*"
 simple_logger = "*"
 ```
 ```
 
 
@@ -62,7 +63,7 @@ Edit your `lib.rs`:
 use dioxus::prelude::*;
 use dioxus::prelude::*;
 
 
 fn main() {
 fn main() {
-    dioxus_mobile::launch(app);
+    dioxus_desktop::launch(app);
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {

+ 5 - 3
docs/guide/src/en/getting_started/ssr.md

@@ -36,10 +36,11 @@ cargo new --bin demo
 cd app
 cd app
 ```
 ```
 
 
-Add Dioxus with the `ssr` feature:
+Add Dioxus and the `ssr` renderer feature:
 
 
 ```shell
 ```shell
-cargo add dioxus --features ssr
+cargo add dioxus
+cargo add dioxus-ssr
 ```
 ```
 
 
 Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
 Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
@@ -54,7 +55,8 @@ Your dependencies should look roughly like this:
 ```toml
 ```toml
 [dependencies]
 [dependencies]
 axum = "0.4.5"
 axum = "0.4.5"
-dioxus = { version = "*", features = ["ssr"] }
+dioxus = { version = "*" }
+dioxus-ssr = { version = "*" }
 tokio = { version = "1.15.0", features = ["full"] }
 tokio = { version = "1.15.0", features = ["full"] }
 ```
 ```
 
 

+ 4 - 2
docs/guide/src/en/getting_started/tui.md

@@ -13,12 +13,13 @@ TUI support is currently quite experimental. Even the project name will change.
 ## Getting Set up
 ## Getting Set up
 
 
 
 
-Start by making a new package and adding our TUI feature.
+Start by making a new package and adding our TUI renderer.
 
 
 ```shell
 ```shell
 cargo new --bin demo
 cargo new --bin demo
 cd demo
 cd demo
-cargo add dioxus --features tui
+cargo add dioxus
+cargo add dioxus-tui
 ```
 ```
 
 
 Then, edit your `main.rs` with the basic template.
 Then, edit your `main.rs` with the basic template.
@@ -42,5 +43,6 @@ Press "ctrl-c" to close the app. To switch from "ctrl-c" to  just "q" to quit yo
 ## Notes
 ## Notes
 
 
 - Our TUI package uses flexbox for layout
 - Our TUI package uses flexbox for layout
+- Regular widgets will not work in the tui render, but the tui renderer has it's own widget components (see [the widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/tui_widgets.rs)).
 - 1px is one character lineheight. Your regular CSS px does not translate.
 - 1px is one character lineheight. Your regular CSS px does not translate.
 - If your app panics, your terminal is wrecked. This will be fixed eventually.
 - If your app panics, your terminal is wrecked. This will be fixed eventually.

+ 3 - 2
docs/guide/src/en/getting_started/web.md

@@ -38,10 +38,11 @@ cargo new --bin demo
 cd demo
 cd demo
 ```
 ```
 
 
-Add Dioxus as a dependency with the `web` feature:
+Add Dioxus as a dependency and add the web renderer:
 
 
 ```bash
 ```bash
-cargo add dioxus --features web
+cargo add dioxus
+cargo add dioxus-web
 ```
 ```
 
 
 Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main":
 Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main":

+ 2 - 1
docs/guide/src/en/interactivity/router.md

@@ -19,7 +19,8 @@ This is where the router crates come in handy. To make sure we're using the rout
 
 
 ```toml
 ```toml
 [dependencies]
 [dependencies]
-dioxus = { version = "0.2", features = ["desktop", "router"] }
+dioxus = { version = "*" }
+dioxus-router = { version = "*" }
 ```
 ```
 
 
 
 

+ 0 - 16
docs/guide/src/pt-br/best_practices/index.md

@@ -12,19 +12,3 @@ Embora seja possível compartilhar o estado entre os componentes, isso só deve
 
 
 - Mantenha o estado local para um componente, se possível
 - Mantenha o estado local para um componente, se possível
 - Ao compartilhar o estado por meio de adereços, passe apenas os dados específicos necessários
 - Ao compartilhar o estado por meio de adereços, passe apenas os dados específicos necessários
-
-## Bibliotecas Reutilizáveis
-
-Ao publicar uma biblioteca projetada para funcionar com o Dioxus, é altamente recomendável usar apenas o recurso principal na `crate` `dioxus`. Isso faz com que sua `crate` seja compilada mais rapidamente, mais estável e evita a inclusão de bibliotecas incompatíveis que podem fazer com que ela não seja compilada em plataformas não suportadas.
-
-❌ Não inclua dependências desnecessárias nas bibliotecas:
-
-```toml
-dioxus = { version = "...", features = ["web", "desktop", "full"]}
-```
-
-✅ Adicione apenas os recursos que você precisa:
-
-```toml
-dioxus = { version = "...", features = "core"}
-```

+ 2 - 8
docs/guide/src/pt-br/getting_started/desktop.md

@@ -29,16 +29,10 @@ cd demo
 Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`):
 Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`):
 
 
 ```shell
 ```shell
-cargo add dioxus --features desktop
+cargo add dioxus
+cargo add dioxus-desktop
 ```
 ```
 
 
-> Se seu sistema não fornece a biblioteca `libappindicator3`, como Debian/bullseye, você pode habilitar a substituta `ayatana` com um _flag_ adicional:
->
-> ```shell
-> # On Debian/bullseye use:
-> cargo add dioxus --features desktop --features ayatana
-> ```
-
 Edite seu `main.rs`:
 Edite seu `main.rs`:
 
 
 ```rust
 ```rust

+ 1 - 1
docs/guide/src/pt-br/getting_started/hot_reload.md

@@ -10,7 +10,7 @@ Instale o [dioxus-cli](https://github.com/DioxusLabs/cli).
 Habilite o recurso de _hot-reload_ no dioxus:
 Habilite o recurso de _hot-reload_ no dioxus:
 
 
 ```toml
 ```toml
-dioxus = { version = "*", features = ["web", "hot-reload"] }
+dioxus = { version = "*", features = ["hot-reload"] }
 ```
 ```
 
 
 # Usage
 # Usage

+ 3 - 2
docs/guide/src/pt-br/getting_started/mobile.md

@@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs"
 # clear all the dependencies
 # clear all the dependencies
 [dependencies]
 [dependencies]
 mobile-entry-point = "0.1.0"
 mobile-entry-point = "0.1.0"
-dioxus = { version = "*", features = ["mobile"] }
+dioxus = { version = "*" }
+dioxus-desktop = { version = "*" }
 simple_logger = "*"
 simple_logger = "*"
 ```
 ```
 
 
@@ -62,7 +63,7 @@ Edite seu `lib.rs`:
 use dioxus::prelude::*;
 use dioxus::prelude::*;
 
 
 fn main() {
 fn main() {
-    dioxus::mobile::launch(app);
+    dioxus_desktop::launch(app);
 }
 }
 
 
 fn app(cx: Scope) -> Element {
 fn app(cx: Scope) -> Element {

+ 6 - 4
docs/guide/src/pt-br/getting_started/ssr.md

@@ -38,7 +38,8 @@ cd app
 Adicione o Dioxus com o recurso `ssr`:
 Adicione o Dioxus com o recurso `ssr`:
 
 
 ```shell
 ```shell
-cargo add dioxus --features ssr
+cargo add dioxus
+cargo add dioxus-ssr
 ```
 ```
 
 
 Em seguida, adicione todas as dependências do Axum. Isso será diferente se você estiver usando um Web Framework diferente
 Em seguida, adicione todas as dependências do Axum. Isso será diferente se você estiver usando um Web Framework diferente
@@ -53,7 +54,8 @@ Suas dependências devem ficar mais ou menos assim:
 ```toml
 ```toml
 [dependencies]
 [dependencies]
 axum = "0.4.5"
 axum = "0.4.5"
-dioxus = { version = "*", features = ["ssr"] }
+dioxus = { version = "*" }
+dioxus-ssr = { version = "*" }
 tokio = { version = "1.15.0", features = ["full"] }
 tokio = { version = "1.15.0", features = ["full"] }
 ```
 ```
 
 
@@ -83,7 +85,7 @@ E, em seguida, adicione nosso _endpoint_. Podemos renderizar `rsx!` diretamente:
 
 
 ```rust
 ```rust
 async fn app_endpoint() -> Html<String> {
 async fn app_endpoint() -> Html<String> {
-    Html(dioxus::ssr::render_lazy(rsx! {
+    Html(dioxus_ssr::render_lazy(rsx! {
             h1 { "hello world!" }
             h1 { "hello world!" }
     }))
     }))
 }
 }
@@ -99,7 +101,7 @@ async fn app_endpoint() -> Html<String> {
     let mut app = VirtualDom::new(app);
     let mut app = VirtualDom::new(app);
     let _ = app.rebuild();
     let _ = app.rebuild();
 
 
-    Html(dioxus::ssr::render_vdom(&app))
+    Html(dioxus_ssr::render_vdom(&app))
 }
 }
 ```
 ```
 
 

+ 2 - 1
docs/guide/src/pt-br/getting_started/tui.md

@@ -17,7 +17,8 @@ Comece criando um novo pacote e adicionando nosso recurso TUI.
 ```shell
 ```shell
 cargo new --bin demo
 cargo new --bin demo
 cd demo
 cd demo
-cargo add dioxus --features tui
+cargo add dioxus
+cargo add dioxus-tui
 ```
 ```
 
 
 Em seguida, edite seu `main.rs` com o modelo básico.
 Em seguida, edite seu `main.rs` com o modelo básico.

+ 2 - 1
docs/guide/src/pt-br/getting_started/web.md

@@ -43,7 +43,8 @@ cd demo
 Adicione o Dioxus como uma dependência com o recurso `web`:
 Adicione o Dioxus como uma dependência com o recurso `web`:
 
 
 ```bash
 ```bash
-cargo add dioxus --features web
+cargo add dioxus
+cargo add dioxus-web
 ```
 ```
 
 
 Adicione um `index.html` para o `Trunk` usar. Certifique-se de que seu elemento "mount point" tenha um ID de "main":
 Adicione um `index.html` para o `Trunk` usar. Certifique-se de que seu elemento "mount point" tenha um ID de "main":

+ 2 - 1
docs/guide/src/pt-br/interactivity/router.md

@@ -18,7 +18,8 @@ Cada uma dessas cenas é independente – não queremos renderizar a página ini
 
 
 ```toml
 ```toml
 [dependencies]
 [dependencies]
-dioxus = { version = "0.2", features = ["desktop", "router"] }
+dioxus = { version = "*" }
+dioxus-router = { version = "*" }
 ```
 ```
 
 
 ## Usando o Roteador
 ## Usando o Roteador

+ 71 - 0
packages/autofmt/README.md

@@ -15,3 +15,74 @@ Sorted roughly in order of what's possible
 - [ ] Format regular exprs
 - [ ] Format regular exprs
 - [ ] Fix prettyplease around chaining
 - [ ] Fix prettyplease around chaining
 - [ ] Don't eat comments in prettyplease
 - [ ] Don't eat comments in prettyplease
+
+
+# Technique
+
+
+div {
+    div {}
+    div {}
+}
+
+
+div
+
+possible line break
+div
+div
+
+
+
+string of possible items within a nesting
+div {
+    attr_pair
+    expr
+    text
+    comment
+}
+a nesting is either a component or an element
+
+idea:
+collect all items into a queue
+q
+```rust
+section {
+    div {
+        h1 { p { "asdasd" } }
+        h1 { p { "asdasd" } }
+    }
+}
+
+section {}
+```
+
+
+// space
+// space
+// space
+
+
+3 - section
+3 - section div
+3 - section div h1
+3 - section div h1 p
+3 - section div h1 p text
+3 - section
+3 - section div
+3 - section div h1
+3 - section div h1 p
+3 - section div h1 p text
+
+block
+
+- when we hit the end of a trail, we can make a decision what needs to be hard breaked
+- most nestings cannot be merged into a single one, so at some point we need to write the line break
+- this is the scan section. we scan forward until it's obvious where to place a hard break
+- when a line is finished, we can print it out by unloading our queued items
+- never double nested
+
+
+Terms
+- break is a whitespace than can flex, dependent on the situation
+- ‹⁠›

+ 18 - 8
packages/autofmt/src/buffer.rs

@@ -125,16 +125,26 @@ impl Buffer {
     }
     }
 
 
     pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
     pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
-        for child in children {
-            // Exprs handle their own indenting/line breaks
-            if !matches!(child, BodyNode::RawExpr(_)) {
-                if self.current_span_is_primary(child.span()) {
-                    self.write_comments(child.span())?;
+        let last_child = children.len();
+
+        for (idx, child) in children.iter().enumerate() {
+            match child {
+                // check if the expr is a short
+                BodyNode::RawExpr { .. } => {
+                    self.tabbed_line()?;
+                    self.write_ident(child)?;
+                    if idx != last_child - 1 {
+                        write!(self.buf, ",")?;
+                    }
+                }
+                _ => {
+                    if self.current_span_is_primary(child.span()) {
+                        self.write_comments(child.span())?;
+                    }
+                    self.tabbed_line()?;
+                    self.write_ident(child)?;
                 }
                 }
-                self.tabbed_line()?;
             }
             }
-
-            self.write_ident(child)?;
         }
         }
 
 
         Ok(())
         Ok(())

+ 58 - 21
packages/autofmt/src/element.rs

@@ -2,6 +2,7 @@ use crate::Buffer;
 use dioxus_rsx::*;
 use dioxus_rsx::*;
 use proc_macro2::Span;
 use proc_macro2::Span;
 use std::{fmt::Result, fmt::Write};
 use std::{fmt::Result, fmt::Write};
+use syn::{spanned::Spanned, Expr};
 
 
 #[derive(Debug)]
 #[derive(Debug)]
 enum ShortOptimization {
 enum ShortOptimization {
@@ -83,12 +84,15 @@ impl Buffer {
 
 
                 self.write_attributes(attributes, key, true)?;
                 self.write_attributes(attributes, key, true)?;
 
 
-                if !children.is_empty() && !attributes.is_empty() {
+                if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.buf, ", ")?;
                     write!(self.buf, ", ")?;
                 }
                 }
 
 
-                for child in children {
+                for (id, child) in children.iter().enumerate() {
                     self.write_ident(child)?;
                     self.write_ident(child)?;
+                    if id != children.len() - 1 && children.len() > 1 {
+                        write!(self.buf, ", ")?;
+                    }
                 }
                 }
 
 
                 write!(self.buf, " ")?;
                 write!(self.buf, " ")?;
@@ -100,7 +104,7 @@ impl Buffer {
                 }
                 }
                 self.write_attributes(attributes, key, true)?;
                 self.write_attributes(attributes, key, true)?;
 
 
-                if !children.is_empty() && !attributes.is_empty() {
+                if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.buf, ",")?;
                     write!(self.buf, ",")?;
                 }
                 }
 
 
@@ -113,7 +117,7 @@ impl Buffer {
             ShortOptimization::NoOpt => {
             ShortOptimization::NoOpt => {
                 self.write_attributes(attributes, key, false)?;
                 self.write_attributes(attributes, key, false)?;
 
 
-                if !children.is_empty() && !attributes.is_empty() {
+                if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.buf, ",")?;
                     write!(self.buf, ",")?;
                 }
                 }
 
 
@@ -286,33 +290,66 @@ impl Buffer {
 
 
                 if attr_len > 80 {
                 if attr_len > 80 {
                     None
                     None
+                } else if comp.children.is_empty() {
+                    Some(attr_len)
                 } else {
                 } else {
-                    self.is_short_children(&comp.children)
-                        .map(|child_len| child_len + attr_len)
+                    None
                 }
                 }
             }
             }
-            [BodyNode::RawExpr(ref _expr)] => {
+            [BodyNode::RawExpr(ref expr)] => {
                 // TODO: let rawexprs to be inlined
                 // TODO: let rawexprs to be inlined
-                // let span = syn::spanned::Spanned::span(&text);
-                // let (start, end) = (span.start(), span.end());
-                // if start.line == end.line {
-                //     Some(end.column - start.column)
-                // } else {
-                //     None
-                // }
-                None
+                get_expr_length(expr)
             }
             }
             [BodyNode::Element(ref el)] => {
             [BodyNode::Element(ref el)] => {
                 let attr_len = self.is_short_attrs(&el.attributes);
                 let attr_len = self.is_short_attrs(&el.attributes);
 
 
-                if attr_len > 80 {
-                    None
-                } else {
-                    self.is_short_children(&el.children)
-                        .map(|child_len| child_len + attr_len)
+                if el.children.is_empty() && attr_len < 80 {
+                    return Some(el.name.to_string().len());
+                }
+
+                if el.children.len() == 1 {
+                    if let BodyNode::Text(ref text) = el.children[0] {
+                        let value = text.source.as_ref().unwrap().value();
+
+                        if value.len() + el.name.to_string().len() + attr_len < 80 {
+                            return Some(value.len() + el.name.to_string().len() + attr_len);
+                        }
+                    }
                 }
                 }
+
+                None
+            }
+            // todo, allow non-elements to be on the same line
+            items => {
+                let mut total_count = 0;
+
+                for item in items {
+                    match item {
+                        BodyNode::Component(_) | BodyNode::Element(_) => return None,
+                        BodyNode::Text(text) => {
+                            total_count += text.source.as_ref().unwrap().value().len()
+                        }
+                        BodyNode::RawExpr(expr) => match get_expr_length(expr) {
+                            Some(len) => total_count += len,
+                            None => return None,
+                        },
+                        BodyNode::ForLoop(_) => todo!(),
+                        BodyNode::IfChain(_) => todo!(),
+                    }
+                }
+
+                Some(total_count)
             }
             }
-            _ => None,
         }
         }
     }
     }
 }
 }
+
+fn get_expr_length(expr: &Expr) -> Option<usize> {
+    let span = expr.span();
+    let (start, end) = (span.start(), span.end());
+    if start.line == end.line {
+        Some(end.column - start.column)
+    } else {
+        None
+    }
+}

+ 78 - 5
packages/autofmt/src/expr.rs

@@ -16,16 +16,63 @@ impl Buffer {
         let placement = exp.span();
         let placement = exp.span();
         let start = placement.start();
         let start = placement.start();
         let end = placement.end();
         let end = placement.end();
-        let num_spaces_desired = (self.indent * 4) as isize;
+        // let num_spaces_desired = (self.indent * 4) as isize;
 
 
-        let first = &self.src[start.line - 1];
-        let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize;
+        // print comments
+        // let mut queued_comments = vec![];
+        // let mut offset = 2;
+        // loop {
+        //     let line = &self.src[start.line - offset];
+        //     if line.trim_start().starts_with("//") {
+        //         queued_comments.push(line);
+        //     } else {
+        //         break;
+        //     }
 
 
-        let offset = num_spaces_real - num_spaces_desired;
+        //     offset += 1;
+        // }
+        // let had_comments = !queued_comments.is_empty();
+        // for comment in queued_comments.into_iter().rev() {
+        //     writeln!(self.buf, "{}", comment)?;
+        // }
 
 
-        for line in &self.src[start.line - 1..end.line] {
+        // if the expr is on one line, just write it directly
+        if start.line == end.line {
+            write!(
+                self.buf,
+                "{}",
+                &self.src[start.line - 1][start.column - 1..end.column].trim()
+            )?;
+            return Ok(());
+        }
+
+        // If the expr is multiline, we want to collect all of its lines together and write them out properly
+        // This involves unshifting the first line if it's aligned
+        let first_line = &self.src[start.line - 1];
+        write!(
+            self.buf,
+            "{}",
+            &first_line[start.column - 1..first_line.len()].trim()
+        )?;
+
+        let first_prefix = &self.src[start.line - 1][..start.column];
+        let offset = match first_prefix.trim() {
+            "" => 0,
+            _ => first_prefix
+                .chars()
+                .rev()
+                .take_while(|c| c.is_whitespace())
+                .count() as isize,
+        };
+
+        for (id, line) in self.src[start.line..end.line].iter().enumerate() {
             writeln!(self.buf)?;
             writeln!(self.buf)?;
             // trim the leading whitespace
             // trim the leading whitespace
+            let line = match id {
+                x if x == (end.line - start.line) - 1 => &line[..end.column],
+                _ => line,
+            };
+
             if offset < 0 {
             if offset < 0 {
                 for _ in 0..-offset {
                 for _ in 0..-offset {
                     write!(self.buf, " ")?;
                     write!(self.buf, " ")?;
@@ -39,6 +86,32 @@ impl Buffer {
             }
             }
         }
         }
 
 
+        // let first = &self.src[start.line - 1];
+        // let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize;
+        // let offset = num_spaces_real - num_spaces_desired;
+
+        // for (row, line) in self.src[start.line - 1..end.line].iter().enumerate() {
+        //     let line = match row {
+        //         0 => &line[start.column - 1..],
+        //         a if a == (end.line - start.line) => &line[..end.column - 1],
+        //         _ => line,
+        //     };
+
+        //     writeln!(self.buf)?;
+        //     // trim the leading whitespace
+        //     if offset < 0 {
+        //         for _ in 0..-offset {
+        //             write!(self.buf, " ")?;
+        //         }
+
+        //         write!(self.buf, "{}", line)?;
+        //     } else {
+        //         let offset = offset as usize;
+        //         let right = &line[offset..];
+        //         write!(self.buf, "{}", right)?;
+        //     }
+        // }
+
         Ok(())
         Ok(())
     }
     }
 }
 }

+ 1 - 1
packages/autofmt/src/lib.rs

@@ -124,7 +124,7 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String {
     let (left, _) = input.split_at(start);
     let (left, _) = input.split_at(start);
     let (_, right) = input.split_at(end);
     let (_, right) = input.split_at(end);
 
 
-    dbg!(&block.formatted);
+    // dbg!(&block.formatted);
 
 
     format!("{}{}{}", left, block.formatted, right)
     format!("{}{}{}", left, block.formatted, right)
 }
 }

+ 7 - 0
packages/autofmt/tests/samples.rs

@@ -23,3 +23,10 @@ twoway! ("complex" => complex);
 twoway! ("tiny" => tiny);
 twoway! ("tiny" => tiny);
 
 
 twoway! ("tinynoopt" => tinynoopt);
 twoway! ("tinynoopt" => tinynoopt);
+
+twoway! ("long" => long);
+
+twoway! ("key" => key);
+
+// Disabled because we can't handle comments on exprs yet
+twoway! ("multirsx" => multirsx);

+ 3 - 3
packages/autofmt/tests/samples/complex.rsx

@@ -27,9 +27,7 @@ rsx! {
                 }
                 }
             })
             })
         }
         }
-        div { class: "px-4",
-            is_current.then(|| rsx!{ children })
-        }
+        div { class: "px-4", is_current.then(|| rsx!{ children }) }
     }
     }
 
 
     // No nesting
     // No nesting
@@ -47,4 +45,6 @@ rsx! {
             let blah = 120;
             let blah = 120;
         }
         }
     }
     }
+
+    div { asdbascasdbasd, asbdasbdabsd, asbdabsdbasdbas }
 }
 }

+ 9 - 0
packages/autofmt/tests/samples/key.rsx

@@ -0,0 +1,9 @@
+rsx! {
+    li { key: "{link}",
+        Link { class: "py-1 px-2 {hover} {hover_bg}", to: "{link}", "{name}" }
+    }
+
+    li { key: "{link}", asd: "asd",
+        Link { class: "py-1 px-2 {hover} {hover_bg}", to: "{link}", "{name}" }
+    }
+}

+ 38 - 0
packages/autofmt/tests/samples/long.rsx

@@ -0,0 +1,38 @@
+use dioxus::prelude::*;
+
+#[inline_props]
+pub fn Explainer<'a>(
+    cx: Scope<'a>,
+    invert: bool,
+    title: &'static str,
+    content: Element<'a>,
+    flasher: Element<'a>,
+) -> Element {
+    // pt-5 sm:pt-24 lg:pt-24
+
+    let mut right = rsx! {
+        div { class: "relative w-1/2", flasher }
+    };
+
+    let align = match invert {
+        true => "mr-auto ml-16",
+        false => "ml-auto mr-16",
+    };
+
+    let mut left = rsx! {
+        div { class: "relative w-1/2 {align} max-w-md leading-8",
+            h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold",
+                "{title}"
+            }
+            content
+        }
+    };
+
+    if *invert {
+        std::mem::swap(&mut left, &mut right);
+    }
+
+    cx.render(rsx! {
+        div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+    })
+}

+ 25 - 0
packages/autofmt/tests/samples/multirsx.rsx

@@ -0,0 +1,25 @@
+rsx! {
+
+    // hi
+    div {}
+
+    // hi
+    div { abcd, ball, s }
+
+    //
+    //
+    //
+    div { abcd, ball, s }
+
+    //
+    //
+    //
+    div {
+        abcd,
+        ball,
+        s,
+
+        //
+        "asdasd"
+    }
+}

+ 14 - 6
packages/autofmt/tests/samples/simple.rsx

@@ -1,9 +1,6 @@
 rsx! {
 rsx! {
     div { "hello world!" }
     div { "hello world!" }
-    div {
-        "hello world!"
-        "goodbye world!"
-    }
+    div { "hello world!", "goodbye world!" }
 
 
     // Simple div
     // Simple div
     div { "hello world!" }
     div { "hello world!" }
@@ -15,7 +12,16 @@ rsx! {
     div { div { "nested" } }
     div { div { "nested" } }
 
 
     // Nested two level
     // Nested two level
-    div { div { h1 { "highly nested" } } }
+    div {
+        div { h1 { "highly nested" } }
+    }
+
+    // Anti-Nested two level
+    div {
+        div {
+            div { h1 { "highly nested" } }
+        }
+    }
 
 
     // Compression
     // Compression
     h3 { class: "mb-2 text-xl font-bold", "Invite Member" }
     h3 { class: "mb-2 text-xl font-bold", "Invite Member" }
@@ -30,7 +36,9 @@ rsx! {
     img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
     img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
 
 
     // One level compression
     // One level compression
-    div { a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" } }
+    div {
+        a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" }
+    }
 
 
     // Components
     // Components
     Component { ..Props {} }
     Component { ..Props {} }

+ 2 - 0
packages/autofmt/tests/wrong.rs

@@ -14,3 +14,5 @@ macro_rules! twoway {
 twoway!("comments" => comments);
 twoway!("comments" => comments);
 
 
 twoway!("multi" => multi);
 twoway!("multi" => multi);
+
+twoway!("multiexpr" => multiexpr);

+ 3 - 0
packages/autofmt/tests/wrong/multiexpr.rsx

@@ -0,0 +1,3 @@
+cx.render(rsx! {
+    div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+})

+ 3 - 0
packages/autofmt/tests/wrong/multiexpr.wrong.rsx

@@ -0,0 +1,3 @@
+cx.render(rsx! {
+    div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+})

+ 1 - 1
packages/core-macro/src/props/mod.rs

@@ -194,7 +194,7 @@ mod field_info {
                 // children field is automatically defaulted to None
                 // children field is automatically defaulted to None
                 if name == "children" {
                 if name == "children" {
                     builder_attr.default =
                     builder_attr.default =
-                        Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap());
+                        Some(syn::parse(quote!(Default::default()).into()).unwrap());
                 }
                 }
 
 
                 // auto detect optional
                 // auto detect optional

+ 4 - 4
packages/core/Cargo.toml

@@ -31,16 +31,16 @@ futures-channel = "0.3.21"
 
 
 indexmap = "1.7"
 indexmap = "1.7"
 
 
-# Serialize the Edits for use in Webview/Liveview instances
-serde = { version = "1", features = ["derive"], optional = true }
-anyhow = "1.0.66"
-
 smallbox = "0.8.1"
 smallbox = "0.8.1"
 log = "0.4.17"
 log = "0.4.17"
 
 
+# Serialize the Edits for use in Webview/Liveview instances
+serde = { version = "1", features = ["derive"], optional = true }
+
 [dev-dependencies]
 [dev-dependencies]
 tokio = { version = "*", features = ["full"] }
 tokio = { version = "*", features = ["full"] }
 dioxus = { path = "../dioxus" }
 dioxus = { path = "../dioxus" }
+pretty_assertions = "1.3.0"
 
 
 [features]
 [features]
 default = []
 default = []

+ 16 - 9
packages/core/src/arena.rs

@@ -81,12 +81,12 @@ impl VirtualDom {
         self.ensure_drop_safety(id);
         self.ensure_drop_safety(id);
 
 
         if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
         if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
-            if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
+            if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
                 self.drop_scope_inner(node)
                 self.drop_scope_inner(node)
             }
             }
         }
         }
         if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } {
         if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } {
-            if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } {
+            if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
                 self.drop_scope_inner(node)
                 self.drop_scope_inner(node)
             }
             }
         }
         }
@@ -115,10 +115,14 @@ impl VirtualDom {
                 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.id.get().unwrap());
+                if let Some(id) = t.id.get() {
+                    self.try_reclaim(id);
+                }
             }
             }
             DynamicNode::Text(t) => {
             DynamicNode::Text(t) => {
-                self.try_reclaim(t.id.get().unwrap());
+                if let Some(id) = t.id.get() {
+                    self.try_reclaim(id);
+                }
             }
             }
         });
         });
 
 
@@ -132,8 +136,8 @@ impl VirtualDom {
     }
     }
 
 
     /// Descend through the tree, removing any borrowed props and listeners
     /// Descend through the tree, removing any borrowed props and listeners
-    pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) {
-        let scope = &self.scopes[scope.0];
+    pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
+        let scope = &self.scopes[scope_id.0];
 
 
         // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
         // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
         // run the hooks (which hold an &mut Reference)
         // run the hooks (which hold an &mut Reference)
@@ -141,10 +145,13 @@ impl VirtualDom {
         let mut props = scope.borrowed_props.borrow_mut();
         let mut props = scope.borrowed_props.borrow_mut();
         props.drain(..).for_each(|comp| {
         props.drain(..).for_each(|comp| {
             let comp = unsafe { &*comp };
             let comp = unsafe { &*comp };
-            if let Some(scope_id) = comp.scope.get() {
-                self.ensure_drop_safety(scope_id);
+            match comp.scope.get() {
+                Some(child) if child != scope_id => self.ensure_drop_safety(child),
+                _ => (),
+            }
+            if let Ok(mut props) = comp.props.try_borrow_mut() {
+                *props = None;
             }
             }
-            drop(comp.props.take());
         });
         });
 
 
         // Now that all the references are gone, we can safely drop our own references in our listeners.
         // Now that all the references are gone, we can safely drop our own references in our listeners.

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

@@ -380,8 +380,8 @@ impl<'b> VirtualDom {
         use RenderReturn::*;
         use RenderReturn::*;
 
 
         match return_nodes {
         match return_nodes {
-            Sync(Ok(t)) => self.mount_component(scope, template, t, idx),
-            Sync(Err(_e)) => todo!("Propogate error upwards"),
+            Sync(Some(t)) => self.mount_component(scope, template, t, idx),
+            Sync(None) => todo!("Propogate error upwards"),
             Async(_) => self.mount_component_placeholder(template, idx, scope),
             Async(_) => self.mount_component_placeholder(template, idx, scope),
         }
         }
     }
     }

+ 114 - 66
packages/core/src/diff.rs

@@ -33,26 +33,26 @@ impl<'b> VirtualDom {
             use RenderReturn::{Async, Sync};
             use RenderReturn::{Async, Sync};
 
 
             match (old, new) {
             match (old, new) {
-                (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
+                (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r),
 
 
                 // Err cases
                 // Err cases
-                (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e),
-                (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r),
-                (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ }
+                (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l),
+                (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r),
+                (Sync(None), Sync(None)) => { /* nothing */ }
 
 
                 // Async
                 // Async
-                (Sync(Ok(_l)), Async(_)) => todo!(),
-                (Sync(Err(_e)), Async(_)) => todo!(),
-                (Async(_), Sync(Ok(_r))) => todo!(),
-                (Async(_), Sync(Err(_e))) => { /* nothing */ }
+                (Sync(Some(_l)), Async(_)) => todo!(),
+                (Sync(None), Async(_)) => todo!(),
+                (Async(_), Sync(Some(_r))) => todo!(),
+                (Async(_), Sync(None)) => { /* nothing */ }
                 (Async(_), Async(_)) => { /* nothing */ }
                 (Async(_), Async(_)) => { /* nothing */ }
             };
             };
         }
         }
         self.scope_stack.pop();
         self.scope_stack.pop();
     }
     }
 
 
-    fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {}
-    fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
+    fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {}
+    fn diff_err_to_ok(&mut self, _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 the templates are the same, we don't need to do anything, nor do we want to
         // If the templates are the same, we don't need to do anything, nor do we want to
@@ -149,22 +149,7 @@ impl<'b> VirtualDom {
 
 
         // 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 head = unsafe {
-                self.scopes[left.scope.get().unwrap().0]
-                    .root_node()
-                    .extend_lifetime_ref()
-            };
-            let last = match head {
-                RenderReturn::Sync(Ok(node)) => self.find_last_element(node),
-                _ => todo!(),
-            };
-            self.mutations.push(Mutation::InsertAfter {
-                id: last,
-                m: created,
-            });
-            self.remove_component_node(left, true);
-            return;
+            return self.replace_vcomponent(right_template, right, idx, left);
         }
         }
 
 
         // Make sure the new vcomponent has the right scopeid associated to it
         // Make sure the new vcomponent has the right scopeid associated to it
@@ -197,6 +182,26 @@ impl<'b> VirtualDom {
         });
         });
     }
     }
 
 
+    fn replace_vcomponent(
+        &mut self,
+        right_template: &'b VNode<'b>,
+        right: &'b VComponent<'b>,
+        idx: usize,
+        left: &'b VComponent<'b>,
+    ) {
+        let m = self.create_component_node(right_template, right, idx);
+
+        self.remove_component_node(left, true);
+
+        // We want to optimize the replace case to use one less mutation if possible
+        // Since mutations are done in reverse, the last node removed will be the first in the stack
+        // Instead of *just* removing it, we can use the replace mutation
+        match self.mutations.edits.pop().unwrap() {
+            Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m }),
+            at => panic!("Expected remove mutation from remove_node {:#?}", at),
+        };
+    }
+
     /// Lightly diff the two templates, checking only their roots.
     /// Lightly diff the two templates, checking only their roots.
     ///
     ///
     /// The goal here is to preserve any existing component state that might exist. This is to preserve some React-like
     /// The goal here is to preserve any existing component state that might exist. This is to preserve some React-like
@@ -668,7 +673,7 @@ impl<'b> VirtualDom {
                     Component(comp) => {
                     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() } {
-                            RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
+                            RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node),
                             _ => todo!(),
                             _ => todo!(),
                         }
                         }
                     }
                     }
@@ -706,11 +711,21 @@ impl<'b> VirtualDom {
     fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
     fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
         let m = self.create_children(right);
         let m = self.create_children(right);
 
 
-        let id = self.find_last_element(left);
-
-        self.mutations.push(Mutation::InsertAfter { id, m });
+        let pre_edits = self.mutations.edits.len();
 
 
         self.remove_node(left, true);
         self.remove_node(left, true);
+
+        // We should always have a remove mutation
+        // Eventually we don't want to generate placeholders, so this might not be true. But it's true today
+        assert!(self.mutations.edits.len() > pre_edits);
+
+        // We want to optimize the replace case to use one less mutation if possible
+        // Since mutations are done in reverse, the last node removed will be the first in the stack
+        // Instead of *just* removing it, we can use the replace mutation
+        match self.mutations.edits.pop().unwrap() {
+            Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m }),
+            _ => panic!("Expected remove mutation from remove_node"),
+        };
     }
     }
 
 
     fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
     fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
@@ -719,24 +734,45 @@ impl<'b> VirtualDom {
 
 
         r.id.set(Some(placeholder));
         r.id.set(Some(placeholder));
 
 
-        let id = self.find_last_element(&l[0]);
-
         self.mutations
         self.mutations
             .push(Mutation::CreatePlaceholder { id: placeholder });
             .push(Mutation::CreatePlaceholder { id: placeholder });
 
 
-        self.mutations.push(Mutation::InsertAfter { id, m: 1 });
-
         self.remove_nodes(l);
         self.remove_nodes(l);
+
+        // We want to optimize the replace case to use one less mutation if possible
+        // Since mutations are done in reverse, the last node removed will be the first in the stack
+        // Instead of *just* removing it, we can use the replace mutation
+        match self.mutations.edits.pop().unwrap() {
+            Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }),
+            _ => panic!("Expected remove mutation from remove_node"),
+        };
     }
     }
 
 
     /// Remove these nodes from the dom
     /// Remove these nodes from the dom
     /// Wont generate mutations for the inner nodes
     /// Wont generate mutations for the inner nodes
     fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
     fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
-        nodes.iter().for_each(|node| self.remove_node(node, true));
+        nodes
+            .iter()
+            .rev()
+            .for_each(|node| self.remove_node(node, true));
     }
     }
 
 
     fn remove_node(&mut self, node: &'b VNode<'b>, gen_muts: bool) {
     fn remove_node(&mut self, node: &'b VNode<'b>, gen_muts: bool) {
+        // Clean up any attributes that have claimed a static node as dynamic for mount/unmounta
+        // Will not generate mutations!
+        self.reclaim_attributes(node);
+
+        // Remove the nested dynamic nodes
+        // We don't generate mutations for these, as they will be removed by the parent (in the next line)
+        // But we still need to make sure to reclaim them from the arena and drop their hooks, etc
+        self.remove_nested_dyn_nodes(node);
+
         // Clean up the roots, assuming we need to generate mutations for these
         // Clean up the roots, assuming we need to generate mutations for these
+        // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)
+        self.reclaim_roots(node, gen_muts);
+    }
+
+    fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
         for (idx, _) in node.template.roots.iter().enumerate() {
         for (idx, _) in node.template.roots.iter().enumerate() {
             if let Some(dy) = node.dynamic_root(idx) {
             if let Some(dy) = node.dynamic_root(idx) {
                 self.remove_dynamic_node(dy, gen_muts);
                 self.remove_dynamic_node(dy, gen_muts);
@@ -748,17 +784,9 @@ impl<'b> VirtualDom {
                 self.reclaim(id);
                 self.reclaim(id);
             }
             }
         }
         }
+    }
 
 
-        for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() {
-            // Roots are cleaned up automatically above
-            if node.template.node_paths[idx].len() == 1 {
-                continue;
-            }
-
-            self.remove_dynamic_node(dyn_node, false);
-        }
-
-        // we clean up nodes with dynamic attributes, provided the node is unique and not a root node
+    fn reclaim_attributes(&mut self, node: &VNode) {
         let mut id = None;
         let mut id = None;
         for (idx, attr) in node.dynamic_attrs.iter().enumerate() {
         for (idx, attr) in node.dynamic_attrs.iter().enumerate() {
             // We'll clean up the root nodes either way, so don't worry
             // We'll clean up the root nodes either way, so don't worry
@@ -778,49 +806,69 @@ impl<'b> VirtualDom {
         }
         }
     }
     }
 
 
+    fn remove_nested_dyn_nodes(&mut self, node: &VNode) {
+        for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() {
+            // Roots are cleaned up automatically above
+            if node.template.node_paths[idx].len() == 1 {
+                continue;
+            }
+
+            self.remove_dynamic_node(dyn_node, false);
+        }
+    }
+
     fn remove_dynamic_node(&mut self, node: &DynamicNode, gen_muts: bool) {
     fn remove_dynamic_node(&mut self, node: &DynamicNode, gen_muts: bool) {
         match node {
         match node {
             Component(comp) => self.remove_component_node(comp, gen_muts),
             Component(comp) => self.remove_component_node(comp, gen_muts),
-            Text(t) => self.remove_text_node(t),
-            Placeholder(t) => self.remove_placeholder(t),
+            Text(t) => self.remove_text_node(t, gen_muts),
+            Placeholder(t) => self.remove_placeholder(t, gen_muts),
             Fragment(nodes) => nodes
             Fragment(nodes) => nodes
                 .iter()
                 .iter()
                 .for_each(|node| self.remove_node(node, gen_muts)),
                 .for_each(|node| self.remove_node(node, gen_muts)),
         };
         };
     }
     }
 
 
-    fn remove_placeholder(&mut self, t: &VPlaceholder) {
+    fn remove_placeholder(&mut self, t: &VPlaceholder, gen_muts: bool) {
         if let Some(id) = t.id.take() {
         if let Some(id) = t.id.take() {
+            if gen_muts {
+                self.mutations.push(Mutation::Remove { id });
+            }
             self.reclaim(id)
             self.reclaim(id)
         }
         }
     }
     }
 
 
-    fn remove_text_node(&mut self, t: &VText) {
+    fn remove_text_node(&mut self, t: &VText, gen_muts: bool) {
         if let Some(id) = t.id.take() {
         if let Some(id) = t.id.take() {
+            if gen_muts {
+                self.mutations.push(Mutation::Remove { id });
+            }
             self.reclaim(id)
             self.reclaim(id)
         }
         }
     }
     }
 
 
     fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) {
     fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) {
-        if let Some(scope) = comp.scope.take() {
-            match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-                RenderReturn::Sync(Ok(t)) => self.remove_node(t, gen_muts),
-                _ => todo!("cannot handle nonstandard nodes"),
-            };
+        let scope = comp.scope.take().unwrap();
 
 
-            let props = self.scopes[scope.0].props.take();
+        match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+            RenderReturn::Sync(Some(t)) => {
+                println!("Removing component node sync {:?}", gen_muts);
+                self.remove_node(t, gen_muts)
+            }
+            _ => todo!("cannot handle nonstandard nodes"),
+        };
 
 
-            self.dirty_scopes.remove(&DirtyScope {
-                height: self.scopes[scope.0].height,
-                id: scope,
-            });
+        let props = self.scopes[scope.0].props.take();
 
 
-            *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
+        self.dirty_scopes.remove(&DirtyScope {
+            height: self.scopes[scope.0].height,
+            id: scope,
+        });
 
 
-            // make sure to wipe any of its props and listeners
-            self.ensure_drop_safety(scope);
-            self.scopes.remove(scope.0);
-        }
+        *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
+
+        // make sure to wipe any of its props and listeners
+        self.ensure_drop_safety(scope);
+        self.scopes.remove(scope.0);
     }
     }
 
 
     fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
     fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
@@ -832,7 +880,7 @@ impl<'b> VirtualDom {
             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() } {
-                    RenderReturn::Sync(Ok(t)) => self.find_first_element(t),
+                    RenderReturn::Sync(Some(t)) => self.find_first_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
                 }
             }
             }
@@ -848,7 +896,7 @@ impl<'b> VirtualDom {
             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() } {
-                    RenderReturn::Sync(Ok(t)) => self.find_last_element(t),
+                    RenderReturn::Sync(Some(t)) => self.find_last_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
                 }
             }
             }

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

@@ -5,7 +5,7 @@ use crate::ScopeId;
 /// A boundary that will capture any errors from child components
 /// A boundary that will capture any errors from child components
 #[allow(dead_code)]
 #[allow(dead_code)]
 pub struct ErrorBoundary {
 pub struct ErrorBoundary {
-    error: RefCell<Option<(anyhow::Error, ScopeId)>>,
+    error: RefCell<Option<ScopeId>>,
     id: ScopeId,
     id: ScopeId,
 }
 }
 
 

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

@@ -27,8 +27,8 @@ use crate::innerlude::*;
 /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
 /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
 #[allow(non_upper_case_globals, non_snake_case)]
 #[allow(non_upper_case_globals, non_snake_case)]
 pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
 pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
-    let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?;
-    Ok(VNode {
+    let children = cx.props.0.as_ref()?;
+    Some(VNode {
         key: children.key,
         key: children.key,
         parent: children.parent,
         parent: children.parent,
         template: children.template,
         template: children.template,
@@ -95,7 +95,7 @@ impl<'a> Properties for FragmentProps<'a> {
     type Builder = FragmentBuilder<'a, false>;
     type Builder = FragmentBuilder<'a, false>;
     const IS_STATIC: bool = false;
     const IS_STATIC: bool = false;
     fn builder() -> Self::Builder {
     fn builder() -> Self::Builder {
-        FragmentBuilder(VNode::empty())
+        FragmentBuilder(None)
     }
     }
     unsafe fn memoize(&self, _other: &Self) -> bool {
     unsafe fn memoize(&self, _other: &Self) -> bool {
         false
         false

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

@@ -34,10 +34,10 @@ pub(crate) mod innerlude {
     pub use crate::scopes::*;
     pub use crate::scopes::*;
     pub use crate::virtual_dom::*;
     pub use crate::virtual_dom::*;
 
 
-    /// An [`Element`] is a possibly-errored [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
+    /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`].
     ///
     ///
     /// An Errored [`Element`] will propagate the error to the nearest error boundary.
     /// An Errored [`Element`] will propagate the error to the nearest error boundary.
-    pub type Element<'a> = Result<VNode<'a>, anyhow::Error>;
+    pub type Element<'a> = Option<VNode<'a>>;
 
 
     /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
     /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
     ///
     ///

+ 3 - 13
packages/core/src/nodes.rs

@@ -58,7 +58,7 @@ pub struct VNode<'a> {
 impl<'a> VNode<'a> {
 impl<'a> VNode<'a> {
     /// Create a template with no nodes that will be skipped over during diffing
     /// Create a template with no nodes that will be skipped over during diffing
     pub fn empty() -> Element<'a> {
     pub fn empty() -> Element<'a> {
-        Ok(VNode {
+        Some(VNode {
             key: None,
             key: None,
             parent: None,
             parent: None,
             root_ids: &[],
             root_ids: &[],
@@ -602,16 +602,6 @@ impl<'a> IntoDynNode<'a> for DynamicNode<'a> {
     }
     }
 }
 }
 
 
-// An element that's an error is currently lost into the ether
-impl<'a> IntoDynNode<'a> for Element<'a> {
-    fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
-        match self {
-            Ok(val) => val.into_vnode(_cx),
-            _ => DynamicNode::default(),
-        }
-    }
-}
-
 impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
 impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
     fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
     fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
         match self {
         match self {
@@ -624,7 +614,7 @@ impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
 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> {
         match self.as_ref() {
         match self.as_ref() {
-            Ok(val) => val.clone().into_vnode(_cx),
+            Some(val) => val.clone().into_vnode(_cx),
             _ => DynamicNode::default(),
             _ => DynamicNode::default(),
         }
         }
     }
     }
@@ -678,7 +668,7 @@ impl<'a> IntoTemplate<'a> for VNode<'a> {
 impl<'a> IntoTemplate<'a> for Element<'a> {
 impl<'a> IntoTemplate<'a> for Element<'a> {
     fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
     fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
         match self {
         match self {
-            Ok(val) => val.into_template(_cx),
+            Some(val) => val.into_template(_cx),
             _ => VNode::empty().unwrap(),
             _ => VNode::empty().unwrap(),
         }
         }
     }
     }

+ 1 - 1
packages/core/src/scheduler/wait.rs

@@ -78,7 +78,7 @@ impl VirtualDom {
 
 
             fiber.waiting_on.borrow_mut().remove(&id);
             fiber.waiting_on.borrow_mut().remove(&id);
 
 
-            if let RenderReturn::Sync(Ok(template)) = ret {
+            if let RenderReturn::Sync(Some(template)) = ret {
                 let mutations_ref = &mut fiber.mutations.borrow_mut();
                 let mutations_ref = &mut fiber.mutations.borrow_mut();
                 let mutations = &mut **mutations_ref;
                 let mutations = &mut **mutations_ref;
                 let template: &VNode = unsafe { std::mem::transmute(template) };
                 let template: &VNode = unsafe { std::mem::transmute(template) };

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

@@ -390,7 +390,7 @@ impl<'src> ScopeState {
             }
             }
         }
         }
 
 
-        Ok(element)
+        Some(element)
     }
     }
 
 
     /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator
     /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator

+ 2 - 2
packages/core/src/virtual_dom.rs

@@ -477,7 +477,7 @@ impl VirtualDom {
     pub fn rebuild(&mut self) -> Mutations {
     pub fn rebuild(&mut self) -> Mutations {
         match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
         match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
             // Rebuilding implies we append the created elements to the root
             // Rebuilding implies we append the created elements to the root
-            RenderReturn::Sync(Ok(node)) => {
+            RenderReturn::Sync(Some(node)) => {
                 let m = self.create_scope(ScopeId(0), node);
                 let m = self.create_scope(ScopeId(0), node);
                 self.mutations.edits.push(Mutation::AppendChildren {
                 self.mutations.edits.push(Mutation::AppendChildren {
                     id: ElementId(0),
                     id: ElementId(0),
@@ -485,7 +485,7 @@ impl VirtualDom {
                 });
                 });
             }
             }
             // If an error occurs, we should try to render the default error component and context where the error occured
             // If an error occurs, we should try to render the default error component and context where the error occured
-            RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e),
+            RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"),
             RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
             RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
         }
         }
 
 

+ 5 - 10
packages/core/tests/attr_cleanup.rs

@@ -54,7 +54,7 @@ fn attrs_cycle() {
     assert_eq!(
     assert_eq!(
         dom.render_immediate().santize().edits,
         dom.render_immediate().santize().edits,
         [
         [
-            LoadTemplate { name: "template", index: 0, id: ElementId(3) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(1) },
             ReplaceWith { id: ElementId(2), m: 1 }
             ReplaceWith { id: ElementId(2), m: 1 }
         ]
         ]
     );
     );
@@ -64,15 +64,10 @@ fn attrs_cycle() {
         dom.render_immediate().santize().edits,
         dom.render_immediate().santize().edits,
         [
         [
             LoadTemplate { name: "template", index: 0, id: ElementId(2) },
             LoadTemplate { name: "template", index: 0, id: ElementId(2) },
-            AssignId { path: &[0], id: ElementId(1) },
-            SetAttribute {
-                name: "class",
-                value: "3".into_value(&bump),
-                id: ElementId(1),
-                ns: None
-            },
-            SetAttribute { name: "id", value: "3".into_value(&bump), id: ElementId(1), ns: None },
-            ReplaceWith { id: ElementId(3), m: 1 }
+            AssignId { path: &[0], id: ElementId(3) },
+            SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None },
+            SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None },
+            ReplaceWith { id: ElementId(1), m: 1 }
         ]
         ]
     );
     );
 
 

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

@@ -9,7 +9,7 @@ fn app(cx: Scope) -> Element {
         _ => unreachable!(),
         _ => unreachable!(),
     };
     };
 
 
-    let value = raw.parse::<f32>()?;
+    let value = raw.parse::<f32>().unwrap_or(123.123);
 
 
     cx.render(rsx! {
     cx.render(rsx! {
         div { "hello {value}" }
         div { "hello {value}" }

+ 10 - 9
packages/core/tests/diff_unkeyed_list.rs

@@ -1,5 +1,6 @@
 use dioxus::core::{ElementId, Mutation::*};
 use dioxus::core::{ElementId, Mutation::*};
 use dioxus::prelude::*;
 use dioxus::prelude::*;
+use pretty_assertions::assert_eq;
 
 
 #[test]
 #[test]
 fn list_creates_one_by_one() {
 fn list_creates_one_by_one() {
@@ -125,7 +126,7 @@ fn removes_one_by_one() {
     assert_eq!(
     assert_eq!(
         dom.render_immediate().santize().edits,
         dom.render_immediate().santize().edits,
         [
         [
-            CreatePlaceholder { id: ElementId(3) },
+            CreatePlaceholder { id: ElementId(4) },
             ReplaceWith { id: ElementId(2), m: 1 }
             ReplaceWith { id: ElementId(2), m: 1 }
         ]
         ]
     );
     );
@@ -137,12 +138,12 @@ fn removes_one_by_one() {
         dom.render_immediate().santize().edits,
         dom.render_immediate().santize().edits,
         [
         [
             LoadTemplate { name: "template", index: 0, id: ElementId(2) },
             LoadTemplate { name: "template", index: 0, id: ElementId(2) },
-            HydrateText { path: &[0], value: "0", id: ElementId(4) },
+            HydrateText { path: &[0], value: "0", id: ElementId(3) },
             LoadTemplate { name: "template", index: 0, id: ElementId(5) },
             LoadTemplate { name: "template", index: 0, id: ElementId(5) },
             HydrateText { path: &[0], value: "1", id: ElementId(6) },
             HydrateText { path: &[0], value: "1", id: ElementId(6) },
             LoadTemplate { name: "template", index: 0, id: ElementId(7) },
             LoadTemplate { name: "template", index: 0, id: ElementId(7) },
             HydrateText { path: &[0], value: "2", id: ElementId(8) },
             HydrateText { path: &[0], value: "2", id: ElementId(8) },
-            ReplaceWith { id: ElementId(3), m: 3 }
+            ReplaceWith { id: ElementId(4), m: 3 }
         ]
         ]
     );
     );
 }
 }
@@ -264,9 +265,9 @@ fn removes_one_by_one_multiroot() {
     assert_eq!(
     assert_eq!(
         dom.render_immediate().santize().edits,
         dom.render_immediate().santize().edits,
         [
         [
-            Remove { id: ElementId(4) },
-            CreatePlaceholder { id: ElementId(5) },
-            ReplaceWith { id: ElementId(2), m: 1 }
+            CreatePlaceholder { id: ElementId(8) },
+            Remove { id: ElementId(2) },
+            ReplaceWith { id: ElementId(4), m: 1 }
         ]
         ]
     );
     );
 }
 }
@@ -364,11 +365,11 @@ fn remove_many() {
         assert_eq!(
         assert_eq!(
             edits.edits,
             edits.edits,
             [
             [
+                CreatePlaceholder { id: ElementId(11,) },
                 Remove { id: ElementId(9,) },
                 Remove { id: ElementId(9,) },
                 Remove { id: ElementId(7,) },
                 Remove { id: ElementId(7,) },
                 Remove { id: ElementId(5,) },
                 Remove { id: ElementId(5,) },
                 Remove { id: ElementId(1,) },
                 Remove { id: ElementId(1,) },
-                CreatePlaceholder { id: ElementId(3,) },
                 ReplaceWith { id: ElementId(2,), m: 1 },
                 ReplaceWith { id: ElementId(2,), m: 1 },
             ]
             ]
         );
         );
@@ -381,8 +382,8 @@ fn remove_many() {
             edits.edits,
             edits.edits,
             [
             [
                 LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
                 LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
-                HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) },
-                ReplaceWith { id: ElementId(3,), m: 1 },
+                HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
+                ReplaceWith { id: ElementId(11,), m: 1 },
             ]
             ]
         )
         )
     }
     }

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

@@ -21,6 +21,7 @@ pub struct Config {
     pub(crate) resource_dir: Option<PathBuf>,
     pub(crate) resource_dir: Option<PathBuf>,
     pub(crate) custom_head: Option<String>,
     pub(crate) custom_head: Option<String>,
     pub(crate) custom_index: Option<String>,
     pub(crate) custom_index: Option<String>,
+    pub(crate) root_name: String,
 }
 }
 
 
 type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
 type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@@ -46,6 +47,7 @@ impl Config {
             resource_dir: None,
             resource_dir: None,
             custom_head: None,
             custom_head: None,
             custom_index: None,
             custom_index: None,
+            root_name: "main".to_string(),
         }
         }
     }
     }
 
 
@@ -126,6 +128,14 @@ impl Config {
         self.custom_index = Some(index);
         self.custom_index = Some(index);
         self
         self
     }
     }
+
+    /// Set the name of the element that Dioxus will use as the root.
+    ///
+    /// This is akint to calling React.render() on the element with the specified name.
+    pub fn with_root_name(mut self, name: impl Into<String>) -> Self {
+        self.root_name = name.into();
+        self
+    }
 }
 }
 
 
 impl Default for Config {
 impl Default for Config {

+ 14 - 8
packages/desktop/src/controller.rs

@@ -1,6 +1,6 @@
 use crate::desktop_context::{DesktopContext, UserWindowEvent};
 use crate::desktop_context::{DesktopContext, UserWindowEvent};
-use crate::events::{decode_event, EventMessage};
 use dioxus_core::*;
 use dioxus_core::*;
+use dioxus_html::HtmlEvent;
 use futures_channel::mpsc::{unbounded, UnboundedSender};
 use futures_channel::mpsc::{unbounded, UnboundedSender};
 use futures_util::StreamExt;
 use futures_util::StreamExt;
 #[cfg(target_os = "ios")]
 #[cfg(target_os = "ios")]
@@ -50,6 +50,7 @@ impl DesktopController {
         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
             // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
             // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
+
             let runtime = tokio::runtime::Builder::new_multi_thread()
             let runtime = tokio::runtime::Builder::new_multi_thread()
                 .enable_all()
                 .enable_all()
                 .build()
                 .build()
@@ -69,12 +70,14 @@ impl DesktopController {
                     tokio::select! {
                     tokio::select! {
                         _ = dom.wait_for_work() => {}
                         _ = dom.wait_for_work() => {}
                         Some(json_value) = event_rx.next() => {
                         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));
-                                }
+                            if let Ok(value) = serde_json::from_value::<HtmlEvent>(json_value) {
+                                let HtmlEvent {
+                                    name,
+                                    element,
+                                    bubbles,
+                                    data
+                                } = value;
+                                dom.handle_event(&name,  data.into_any(), element,  bubbles);
                             }
                             }
                         }
                         }
                     }
                     }
@@ -83,7 +86,10 @@ impl DesktopController {
                         .render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
                         .render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
                         .await;
                         .await;
 
 
-                    edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap());
+                    edit_queue
+                        .lock()
+                        .unwrap()
+                        .push(serde_json::to_string(&muts).unwrap());
                     let _ = proxy.send_event(UserWindowEvent::EditsReady);
                     let _ = proxy.send_event(UserWindowEvent::EditsReady);
                 }
                 }
             })
             })

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

@@ -7,6 +7,7 @@ use serde_json::Value;
 use std::future::Future;
 use std::future::Future;
 use std::future::IntoFuture;
 use std::future::IntoFuture;
 use std::pin::Pin;
 use std::pin::Pin;
+use wry::application::dpi::LogicalSize;
 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")]
@@ -136,6 +137,11 @@ impl DesktopContext {
         let _ = self.proxy.send_event(SetZoomLevel(scale_factor));
         let _ = self.proxy.send_event(SetZoomLevel(scale_factor));
     }
     }
 
 
+    /// modifies the inner size of the window
+    pub fn set_inner_size(&self, logical_size: LogicalSize<f64>) {
+        let _ = self.proxy.send_event(SetInnerSize(logical_size));
+    }
+
     /// launch print modal
     /// launch print modal
     pub fn print(&self) {
     pub fn print(&self) {
         let _ = self.proxy.send_event(Print);
         let _ = self.proxy.send_event(Print);
@@ -193,6 +199,7 @@ pub enum UserWindowEvent {
     SetDecorations(bool),
     SetDecorations(bool),
 
 
     SetZoomLevel(f64),
     SetZoomLevel(f64),
+    SetInnerSize(LogicalSize<f64>),
 
 
     Print,
     Print,
     DevTool,
     DevTool,
@@ -265,6 +272,7 @@ impl DesktopController {
             SetDecorations(state) => window.set_decorations(state),
             SetDecorations(state) => window.set_decorations(state),
 
 
             SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
             SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
+            SetInnerSize(logical_size) => window.set_inner_size(logical_size),
 
 
             Print => {
             Print => {
                 if let Err(e) = webview.print() {
                 if let Err(e) = webview.print() {

+ 0 - 62
packages/desktop/src/events.rs

@@ -1,10 +1,6 @@
 //! Convert a serialized event to an event trigger
 //! Convert a serialized event to an event trigger
 
 
-use dioxus_html::events::*;
 use serde::{Deserialize, Serialize};
 use serde::{Deserialize, Serialize};
-use serde_json::from_value;
-use std::any::Any;
-use std::rc::Rc;
 
 
 #[derive(Deserialize, Serialize)]
 #[derive(Deserialize, Serialize)]
 pub(crate) struct IpcMessage {
 pub(crate) struct IpcMessage {
@@ -31,61 +27,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)
-}

+ 33 - 27
packages/desktop/src/lib.rs

@@ -164,6 +164,7 @@ fn build_webview(
     let custom_head = cfg.custom_head.clone();
     let custom_head = cfg.custom_head.clone();
     let resource_dir = cfg.resource_dir.clone();
     let resource_dir = cfg.resource_dir.clone();
     let index_file = cfg.custom_index.clone();
     let index_file = cfg.custom_index.clone();
+    let root_name = cfg.root_name.clone();
 
 
     // We assume that if the icon is None in cfg, then the user just didnt set it
     // We assume that if the icon is None in cfg, then the user just didnt set it
     if cfg.window.window.window_icon.is_none() {
     if cfg.window.window.window_icon.is_none() {
@@ -183,36 +184,40 @@ fn build_webview(
         .with_url("dioxus://index.html/")
         .with_url("dioxus://index.html/")
         .unwrap()
         .unwrap()
         .with_ipc_handler(move |_window: &Window, payload: String| {
         .with_ipc_handler(move |_window: &Window, payload: String| {
-            parse_ipc_message(&payload)
-                .map(|message| match message.method() {
-                    "eval_result" => {
-                        let result = message.params();
-                        eval_sender.send(result).unwrap();
-                    }
-                    "user_event" => {
-                        _ = event_tx.unbounded_send(message.params());
-                    }
-                    "initialize" => {
-                        is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
-                        let _ = proxy.send_event(UserWindowEvent::EditsReady);
-                    }
-                    "browser_open" => {
-                        let data = message.params();
-                        log::trace!("Open browser: {:?}", data);
-                        if let Some(temp) = data.as_object() {
-                            if temp.contains_key("href") {
-                                let url = temp.get("href").unwrap().as_str().unwrap();
-                                if let Err(e) = webbrowser::open(url) {
-                                    log::error!("Open Browser error: {:?}", e);
-                                }
+            let message = match parse_ipc_message(&payload) {
+                Some(message) => message,
+                None => {
+                    log::error!("Failed to parse IPC message: {}", payload);
+                    return;
+                }
+            };
+
+            match message.method() {
+                "eval_result" => {
+                    let result = message.params();
+                    eval_sender.send(result).unwrap();
+                }
+                "user_event" => {
+                    _ = event_tx.unbounded_send(message.params());
+                }
+                "initialize" => {
+                    is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
+                    let _ = proxy.send_event(UserWindowEvent::EditsReady);
+                }
+                "browser_open" => {
+                    let data = message.params();
+                    log::trace!("Open browser: {:?}", data);
+                    if let Some(temp) = data.as_object() {
+                        if temp.contains_key("href") {
+                            let url = temp.get("href").unwrap().as_str().unwrap();
+                            if let Err(e) = webbrowser::open(url) {
+                                log::error!("Open Browser error: {:?}", e);
                             }
                             }
                         }
                         }
                     }
                     }
-                    _ => (),
-                })
-                .unwrap_or_else(|| {
-                    log::warn!("invalid IPC message received");
-                });
+                }
+                _ => (),
+            }
         })
         })
         .with_custom_protocol(String::from("dioxus"), move |r| {
         .with_custom_protocol(String::from("dioxus"), move |r| {
             protocol::desktop_handler(
             protocol::desktop_handler(
@@ -220,6 +225,7 @@ fn build_webview(
                 resource_dir.clone(),
                 resource_dir.clone(),
                 custom_head.clone(),
                 custom_head.clone(),
                 index_file.clone(),
                 index_file.clone(),
+                &root_name,
             )
             )
         })
         })
         .with_file_drop_handler(move |window, evet| {
         .with_file_drop_handler(move |window, evet| {

+ 19 - 7
packages/desktop/src/protocol.rs

@@ -1,22 +1,34 @@
+use dioxus_interpreter_js::INTERPRETER_JS;
 use std::path::{Path, PathBuf};
 use std::path::{Path, PathBuf};
 use wry::{
 use wry::{
     http::{status::StatusCode, Request, Response},
     http::{status::StatusCode, Request, Response},
     Result,
     Result,
 };
 };
 
 
-const MODULE_LOADER: &str = r#"
+fn module_loader(root_name: &str) -> String {
+    format!(
+        r#"
 <script>
 <script>
-    import("./index.js").then(function (module) {
-        module.main();
-    });
+    {INTERPRETER_JS}
+
+    let rootname = "{}";
+    let root = window.document.getElementById(rootname);
+    if (root != null) {{
+        window.interpreter = new Interpreter(root);
+        window.ipc.postMessage(serializeIpcMessage("initialize"));
+    }}
 </script>
 </script>
-"#;
+"#,
+        root_name
+    )
+}
 
 
 pub(super) fn desktop_handler(
 pub(super) fn desktop_handler(
     request: &Request<Vec<u8>>,
     request: &Request<Vec<u8>>,
     asset_root: Option<PathBuf>,
     asset_root: Option<PathBuf>,
     custom_head: Option<String>,
     custom_head: Option<String>,
     custom_index: Option<String>,
     custom_index: Option<String>,
+    root_name: &str,
 ) -> Result<Response<Vec<u8>>> {
 ) -> Result<Response<Vec<u8>>> {
     // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
     // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
     // For now, we only serve two pieces of content which get included as bytes into the final binary.
     // For now, we only serve two pieces of content which get included as bytes into the final binary.
@@ -30,7 +42,7 @@ pub(super) fn desktop_handler(
         // we'll look for the closing </body> tag and insert our little module loader there.
         // we'll look for the closing </body> tag and insert our little module loader there.
         if let Some(custom_index) = custom_index {
         if let Some(custom_index) = custom_index {
             let rendered = custom_index
             let rendered = custom_index
-                .replace("</body>", &format!("{}</body>", MODULE_LOADER))
+                .replace("</body>", &format!("{}</body>", module_loader(root_name)))
                 .into_bytes();
                 .into_bytes();
             Response::builder()
             Response::builder()
                 .header("Content-Type", "text/html")
                 .header("Content-Type", "text/html")
@@ -42,7 +54,7 @@ pub(super) fn desktop_handler(
             if let Some(custom_head) = custom_head {
             if let Some(custom_head) = custom_head {
                 template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
                 template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
             }
             }
-            template = template.replace("<!-- MODULE LOADER -->", MODULE_LOADER);
+            template = template.replace("<!-- MODULE LOADER -->", &module_loader(root_name));
 
 
             Response::builder()
             Response::builder()
                 .header("Content-Type", "text/html")
                 .header("Content-Type", "text/html")

+ 6 - 2
packages/html/Cargo.toml

@@ -19,6 +19,7 @@ euclid = "0.22.7"
 enumset = "1.0.11"
 enumset = "1.0.11"
 keyboard-types = "0.6.2"
 keyboard-types = "0.6.2"
 async-trait = "0.1.58"
 async-trait = "0.1.58"
+serde-value = "0.7.0"
 
 
 [dependencies.web-sys]
 [dependencies.web-sys]
 optional = true
 optional = true
@@ -39,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"]

+ 1 - 1
packages/html/src/events/animation.rs

@@ -3,7 +3,7 @@ use dioxus_core::Event;
 pub type AnimationEvent = Event<AnimationData>;
 pub type AnimationEvent = Event<AnimationData>;
 
 
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct AnimationData {
 pub struct AnimationData {
     pub animation_name: String,
     pub animation_name: String,
     pub pseudo_element: String,
     pub pseudo_element: String,

+ 1 - 1
packages/html/src/events/clipboard.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type ClipboardEvent = Event<ClipboardData>;
 pub type ClipboardEvent = Event<ClipboardData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ClipboardData {
 pub struct ClipboardData {
     // DOMDataTransfer clipboardData
     // DOMDataTransfer clipboardData
 }
 }

+ 1 - 1
packages/html/src/events/composition.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type CompositionEvent = Event<CompositionData>;
 pub type CompositionEvent = Event<CompositionData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct CompositionData {
 pub struct CompositionData {
     pub data: String,
     pub data: String,
 }
 }

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

@@ -1,5 +1,3 @@
-use std::any::Any;
-
 use dioxus_core::Event;
 use dioxus_core::Event;
 
 
 use crate::MouseData;
 use crate::MouseData;
@@ -10,12 +8,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, PartialEq)]
 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! {

+ 1 - 1
packages/html/src/events/focus.rs

@@ -3,7 +3,7 @@ use dioxus_core::Event;
 pub type FocusEvent = Event<FocusData>;
 pub type FocusEvent = Event<FocusData>;
 
 
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct FocusData {/* DOMEventInner:  Send + SyncTarget relatedTarget */}
 pub struct FocusData {/* DOMEventInner:  Send + SyncTarget relatedTarget */}
 
 
 impl_event! [
 impl_event! [

+ 6 - 0
packages/html/src/events/form.rs

@@ -16,6 +16,12 @@ pub struct FormData {
     pub files: Option<Arc<dyn FileEngine>>,
     pub files: Option<Arc<dyn FileEngine>>,
 }
 }
 
 
+impl PartialEq for FormData {
+    fn eq(&self, other: &Self) -> bool {
+        self.value == other.value && self.values == other.values
+    }
+}
+
 impl Debug for FormData {
 impl Debug for FormData {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("FormEvent")
         f.debug_struct("FormEvent")

+ 1 - 1
packages/html/src/events/image.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type ImageEvent = Event<ImageData>;
 pub type ImageEvent = Event<ImageData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ImageData {
 pub struct ImageData {
     pub load_error: bool,
     pub load_error: bool,
 }
 }

+ 1 - 1
packages/html/src/events/keyboard.rs

@@ -7,7 +7,7 @@ use std::str::FromStr;
 
 
 pub type KeyboardEvent = Event<KeyboardData>;
 pub type KeyboardEvent = Event<KeyboardData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone)]
+#[derive(Clone, PartialEq, Eq)]
 pub struct KeyboardData {
 pub struct KeyboardData {
     #[deprecated(
     #[deprecated(
         since = "0.3.0",
         since = "0.3.0",

+ 1 - 1
packages/html/src/events/media.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type MediaEvent = Event<MediaData>;
 pub type MediaEvent = Event<MediaData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct MediaData {}
 pub struct MediaData {}
 
 
 impl_event! [
 impl_event! [

+ 1 - 1
packages/html/src/events/mouse.rs

@@ -10,7 +10,7 @@ pub type MouseEvent = Event<MouseData>;
 
 
 /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone, Default)]
+#[derive(Clone, Default, PartialEq)]
 /// Data associated with a mouse event
 /// Data associated with a mouse event
 ///
 ///
 /// Do not use the deprecated fields; they may change or become private in the future.
 /// Do not use the deprecated fields; they may change or become private in the future.

+ 1 - 1
packages/html/src/events/pointer.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type PointerEvent = Event<PointerData>;
 pub type PointerEvent = Event<PointerData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct PointerData {
 pub struct PointerData {
     // Mouse only
     // Mouse only
     pub alt_key: bool,
     pub alt_key: bool,

+ 1 - 1
packages/html/src/events/scroll.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type ScrollEvent = Event<ScrollData>;
 pub type ScrollEvent = Event<ScrollData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ScrollData {}
 pub struct ScrollData {}
 
 
 impl_event! {
 impl_event! {

+ 1 - 1
packages/html/src/events/selection.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type SelectionEvent = Event<SelectionData>;
 pub type SelectionEvent = Event<SelectionData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct SelectionData {}
 pub struct SelectionData {}
 
 
 impl_event! [
 impl_event! [

+ 1 - 1
packages/html/src/events/toggle.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type ToggleEvent = Event<ToggleData>;
 pub type ToggleEvent = Event<ToggleData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ToggleData {}
 pub struct ToggleData {}
 
 
 impl_event! {
 impl_event! {

+ 1 - 1
packages/html/src/events/touch.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type TouchEvent = Event<TouchData>;
 pub type TouchEvent = Event<TouchData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct TouchData {
 pub struct TouchData {
     pub alt_key: bool,
     pub alt_key: bool,
     pub ctrl_key: bool,
     pub ctrl_key: bool,

+ 1 - 1
packages/html/src/events/transition.rs

@@ -2,7 +2,7 @@ use dioxus_core::Event;
 
 
 pub type TransitionEvent = Event<TransitionData>;
 pub type TransitionEvent = Event<TransitionData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct TransitionData {
 pub struct TransitionData {
     pub property_name: String,
     pub property_name: String,
     pub pseudo_element: String,
     pub pseudo_element: String,

+ 1 - 1
packages/html/src/events/wheel.rs

@@ -6,7 +6,7 @@ use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta};
 
 
 pub type WheelEvent = Event<WheelData>;
 pub type WheelEvent = Event<WheelData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Clone)]
+#[derive(Clone, PartialEq, Default)]
 pub struct WheelData {
 pub struct WheelData {
     #[deprecated(since = "0.3.0", note = "use delta() instead")]
     #[deprecated(since = "0.3.0", note = "use delta() instead")]
     pub delta_mode: u32,
     pub delta_mode: u32,

+ 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::*;

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

@@ -0,0 +1,217 @@
+use std::{any::Any, rc::Rc};
+
+use crate::events::*;
+use dioxus_core::ElementId;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Debug, Clone, PartialEq)]
+pub struct HtmlEvent {
+    pub element: ElementId,
+    pub name: String,
+    pub bubbles: bool,
+    pub data: EventData,
+}
+
+impl<'de> Deserialize<'de> for HtmlEvent {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        #[derive(Deserialize, Debug, Clone)]
+        struct Inner {
+            element: ElementId,
+            name: String,
+            bubbles: bool,
+            data: serde_value::Value,
+        }
+
+        let Inner {
+            element,
+            name,
+            bubbles,
+            data,
+        } = Inner::deserialize(deserializer)?;
+
+        Ok(HtmlEvent {
+            data: fun_name(&name, data).unwrap(),
+            element,
+            bubbles,
+            name,
+        })
+    }
+}
+
+fn fun_name(
+    name: &str,
+    data: serde_value::Value,
+) -> Result<EventData, serde_value::DeserializerError> {
+    use EventData::*;
+
+    // a little macro-esque thing to make the code below more readable
+    #[inline]
+    fn de<'de, F>(f: serde_value::Value) -> Result<F, serde_value::DeserializerError>
+    where
+        F: Deserialize<'de>,
+    {
+        F::deserialize(f)
+    }
+
+    let data = match name {
+        // Mouse
+        "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter"
+        | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => Mouse(de(data)?),
+
+        // Clipboard
+        "copy" | "cut" | "paste" => Clipboard(de(data)?),
+
+        // Composition
+        "compositionend" | "compositionstart" | "compositionupdate" => Composition(de(data)?),
+
+        // Keyboard
+        "keydown" | "keypress" | "keyup" => Keyboard(de(data)?),
+
+        // Focus
+        "blur" | "focus" | "focusin" | "focusout" => Focus(de(data)?),
+
+        // Form
+        "change" | "input" | "invalid" | "reset" | "submit" => Form(de(data)?),
+
+        // Drag
+        "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart"
+        | "drop" => Drag(de(data)?),
+
+        // Pointer
+        "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup"
+        | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture"
+        | "lostpointercapture" => Pointer(de(data)?),
+
+        // Selection
+        "selectstart" | "selectionchange" | "select" => Selection(de(data)?),
+
+        // Touch
+        "touchcancel" | "touchend" | "touchmove" | "touchstart" => Touch(de(data)?),
+
+        // Srcoll
+        "scroll" => Scroll(de(data)?),
+
+        // Wheel
+        "wheel" => Wheel(de(data)?),
+
+        // Media
+        "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" => Media(de(data)?),
+
+        // Animation
+        "animationstart" | "animationend" | "animationiteration" => Animation(de(data)?),
+
+        // Transition
+        "transitionend" => Transition(de(data)?),
+
+        // Toggle
+        "toggle" => Toggle(de(data)?),
+
+        // 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";
+        other => {
+            return Err(serde_value::DeserializerError::UnknownVariant(
+                other.to_string(),
+                &[],
+            ))
+        }
+    };
+
+    Ok(data)
+}
+
+impl HtmlEvent {
+    pub fn bubbles(&self) -> bool {
+        event_bubbles(&self.name)
+    }
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
+#[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();
+
+    assert_eq!(data, p);
+}

+ 10 - 14
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);
@@ -352,6 +344,9 @@ export class Interpreter {
         this.RemoveEventListener(edit.id, edit.name);
         this.RemoveEventListener(edit.id, edit.name);
         break;
         break;
       case "NewEventListener":
       case "NewEventListener":
+
+        let bubbles = event_bubbles(edit.name);
+
         // 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 implementation
         let handler = (event) => {
         let handler = (event) => {
@@ -435,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,
               })
               })
             );
             );
           }
           }
         };
         };
-        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":

+ 40 - 20
packages/liveview/Cargo.toml

@@ -10,43 +10,63 @@ 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]
-tokio = { version = "1", features = ["full"] }
-futures-util = { version = "0.3", default-features = false, features = [
+tokio = { version = "1.23.0", features = ["full"] }
+futures-util = { version = "0.3.25", default-features = false, features = [
     "sink",
     "sink",
 ] }
 ] }
-futures-channel = { version = "0.3.17", features = ["sink"] }
-pretty_env_logger = "0.4"
-tokio-stream = { version = "0.1.1", features = ["net"] }
+futures-channel = { version = "0.3.25", features = ["sink"] }
+pretty_env_logger = "0.4.0"
+tokio-stream = { version = "0.1.11", features = ["net"] }
 
 
-serde = { version = "1.0.136", features = ["derive"] }
-serde_json = "1.0.79"
-tokio-util = { version = "0.7.0", features = ["full"] }
+serde = { version = "1.0.151", features = ["derive"] }
+serde_json = "1.0.91"
+tokio-util = { version = "0.7.4", 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.3", optional = true }
 
 
 # axum
 # axum
-axum = { version = "0.5.1", optional = true, features = ["ws"] }
-tower = { version = "0.4.12", optional = true }
+axum = { version = "0.6.1", optional = true, features = ["ws"] }
+tower = { version = "0.4.13", optional = true }
 
 
 # salvo
 # salvo
-salvo = { version = "0.32.0", optional = true, features = ["ws"] }
+salvo = { version = "0.37.7", optional = true, features = ["ws"] }
+thiserror = "1.0.38"
+uuid = { version = "1.2.2", features = ["v4"] }
+anyhow = "1.0.68"
+
+# actix is ... complicated?
+# actix-files = { version = "0.6.2", optional = true }
+# actix-web = { version = "4.2.1", optional = true }
+# actix-ws = { version = "0.2.5", optional = true }
 
 
 [dev-dependencies]
 [dev-dependencies]
-tokio = { version = "1", features = ["full"] }
+tokio = { version = "1.23.0", features = ["full"] }
 dioxus = { path = "../dioxus" }
 dioxus = { path = "../dioxus" }
-warp = "0.3"
-axum = { version = "0.5.1", features = ["ws"] }
-salvo = { version = "0.32.0", features = ["affix", "ws"] }
-tower = "0.4.12"
+warp = "0.3.3"
+axum = { version = "0.6.1", features = ["ws"] }
+salvo = { version = "0.37.7", features = ["affix", "ws"] }
+tower = "0.4.13"
 
 
 [features]
 [features]
-default = []
+default = []
+# actix = ["actix-files", "actix-web", "actix-ws"]
+
+[[example]]
+name = "axum"
+required-features = ["axum"]
+
+[[example]]
+name = "salvo"
+required-features = ["salvo"]
+
+[[example]]
+name = "warp"
+required-features = ["warp"]

+ 1 - 47
packages/liveview/README.md

@@ -1,49 +1,3 @@
 # Dioxus LiveView
 # Dioxus LiveView
 
 
-Enabling server-rendered and hybrid applications with incredibly low latency (<1ms).
-
-```rust
-#[async_std::main]
-async fn main() -> tide::Result<()> {
-    let liveview_pool = dioxus::liveview::pool::default();
-    let mut app = tide::new();
-
-    // serve the liveview client
-    app.at("/").get(dioxus::liveview::liveview_frontend);
-
-    // and then connect the client to the backend
-    app.at("/app").get(|req| dioxus::liveview::launch(App, Props { req }))
-
-    app.listen("127.0.0.1:8080").await?;
-
-    Ok(())
-}
-```
-
-Dioxus LiveView runs your Dioxus apps on the server
-
-
-
-```rust
-use soyuz::prelude::*;
-
-#[tokio::main]
-async fn main() {
-    let mut app = soyuz::new();
-    app.at("/app").get(websocket(handler));
-    app.listen("127.0.0.1:8080").await.unwrap();
-}
-
-async fn order_shoes(mut req: WebsocketRequest) -> Response {
-    let stream = req.upgrade();
-    dioxus::liveview::launch(App, stream).await;
-}
-
-fn App(cx: Scope) -> Element {
-    let mut count = use_state(cx, || 0);
-    cx.render(rsx!(
-        button { onclick: move |_| count += 1, "Incr" }
-        button { onclick: move |_| count -= 1, "Decr" }
-    ))
-}
-```
+Server rendered apps with minimal latency

+ 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

+ 54 - 43
packages/liveview/examples/salvo.rs

@@ -1,55 +1,66 @@
-#[cfg(not(feature = "salvo"))]
-fn main() {}
+use dioxus::prelude::*;
+use dioxus_liveview::LiveViewPool;
+use salvo::affix;
+use salvo::prelude::*;
+use std::net::SocketAddr;
+use std::sync::Arc;
 
 
-#[cfg(feature = "salvo")]
-#[tokio::main]
-async fn main() {
-    use std::sync::Arc;
+fn app(cx: Scope) -> Element {
+    let mut num = use_state(cx, || 0);
 
 
-    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 {
-        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();
 
 
-    #[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(())
-    }
+    WebSocketUpgrade::new()
+        .upgrade(req, res, |ws| async move {
+            _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
+        })
+        .await
 }
 }

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

+ 19 - 104
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 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;
-    }
+use futures_util::{SinkExt, StreamExt};
+use salvo::ws::{Message, WebSocket};
+
+use crate::{LiveViewError, LiveViewSocket};
+
+/// 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)
 }
 }
 
 
-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();
-
-        let serialized = serde_json::to_string(&edits.edits).unwrap();
-        edits_tx.send(serialized).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)?;
 
 
-            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, 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 - 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>
+    "#
+    )
 }
 }

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

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

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

@@ -0,0 +1,149 @@
+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
+///
+/// Most websockets from most HTTP frameworks can be converted into a LiveViewSocket using the appropriate adapter.
+///
+/// You can also convert your own socket into a LiveViewSocket by implementing this trait. This trait is an auto trait,
+/// meaning that as long as your type implements Stream and Sink, you can use it as a LiveViewSocket.
+///
+/// For example, the axum implementation is a really small transform:
+///
+/// ```rust, ignore
+/// pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket {
+///     ws.map(transform_rx)
+///         .with(transform_tx)
+///         .sink_map_err(|_| LiveViewError::SendingFailed)
+/// }
+///
+/// fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
+///     message
+///         .map_err(|_| LiveViewError::SendingFailed)?
+///         .into_text()
+///         .map_err(|_| LiveViewError::SendingFailed)
+/// }
+///
+/// async fn transform_tx(message: String) -> Result<Message, axum::Error> {
+///     Ok(Message::Text(message))
+/// }
+/// ```
+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 - 1
packages/native-core/src/utils/cursor.rs

@@ -200,7 +200,7 @@ impl Cursor {
                                 }
                                 }
                                 change -= 1;
                                 change -= 1;
                             }
                             }
-                            c.move_col(change as i32, text);
+                            c.move_col(change, text);
                         },
                         },
                         data.modifiers().contains(Modifiers::SHIFT),
                         data.modifiers().contains(Modifiers::SHIFT),
                     );
                     );

+ 1 - 1
packages/router/src/components/redirect.rs

@@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
         router.replace_route(cx.props.to, None, None);
         router.replace_route(cx.props.to, None, None);
     }
     }
 
 
-    cx.render(rsx!(()))
+    None
 }
 }

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

@@ -52,6 +52,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
         cx.render(rsx!(&cx.props.children))
         cx.render(rsx!(&cx.props.children))
     } else {
     } else {
         log::debug!("Route should *not* render: {:?}", cx.scope_id());
         log::debug!("Route should *not* render: {:?}", cx.scope_id());
-        cx.render(rsx!(()))
+        None
     }
     }
 }
 }

+ 1 - 0
packages/rsx/README.md

@@ -0,0 +1 @@
+# The actual RSX language implemented using syn parsers.

+ 1 - 1
packages/rsx/src/component.rs

@@ -171,7 +171,7 @@ impl ToTokens for Component {
 
 
                     toks.append_all(quote! {
                     toks.append_all(quote! {
                         .children(
                         .children(
-                            Ok({ #renderer })
+                            Some({ #renderer })
                         )
                         )
                     });
                     });
                 }
                 }

+ 2 - 2
packages/rsx/src/lib.rs

@@ -34,7 +34,7 @@ use syn::{
 };
 };
 
 
 /// Fundametnally, every CallBody is a template
 /// Fundametnally, every CallBody is a template
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub struct CallBody {
 pub struct CallBody {
     pub roots: Vec<BodyNode>,
     pub roots: Vec<BodyNode>,
 
 
@@ -70,7 +70,7 @@ impl ToTokens for CallBody {
 
 
         if self.inline_cx {
         if self.inline_cx {
             out_tokens.append_all(quote! {
             out_tokens.append_all(quote! {
-                Ok({
+                Some({
                     let __cx = cx;
                     let __cx = cx;
                     #body
                     #body
                 })
                 })

+ 3 - 1
packages/rsx/src/node.rs

@@ -1,3 +1,5 @@
+// use crate::{raw_expr::RawExprNode, text::TextNode};
+
 use super::*;
 use super::*;
 
 
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use proc_macro2::{Span, TokenStream as TokenStream2};
@@ -29,7 +31,7 @@ pub enum BodyNode {
 
 
 impl BodyNode {
 impl BodyNode {
     pub fn is_litstr(&self) -> bool {
     pub fn is_litstr(&self) -> bool {
-        matches!(self, BodyNode::Text(_))
+        matches!(self, BodyNode::Text { .. })
     }
     }
 
 
     pub fn span(&self) -> Span {
     pub fn span(&self) -> Span {

+ 1 - 1
packages/ssr/src/lib.rs

@@ -23,7 +23,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
     fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
     fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
         let lazy = cx.props.caller.take().unwrap();
         let lazy = cx.props.caller.take().unwrap();
         let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
         let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
-        Ok(lazy.call(cx))
+        Some(lazy.call(cx))
     }
     }
 
 
     let props: RootProps = unsafe {
     let props: RootProps = unsafe {

+ 2 - 2
packages/ssr/src/renderer.rs

@@ -51,7 +51,7 @@ impl Renderer {
     ) -> std::fmt::Result {
     ) -> std::fmt::Result {
         // We should never ever run into async or errored nodes in SSR
         // We should never ever run into async or errored nodes in SSR
         // Error boundaries and suspense boundaries will convert these to sync
         // Error boundaries and suspense boundaries will convert these to sync
-        if let RenderReturn::Sync(Ok(node)) = dom.get_scope(scope).unwrap().root_node() {
+        if let RenderReturn::Sync(Some(node)) = dom.get_scope(scope).unwrap().root_node() {
             self.render_template(buf, dom, node)?
             self.render_template(buf, dom, node)?
         };
         };
 
 
@@ -89,7 +89,7 @@ impl Renderer {
                             let scope = dom.get_scope(id).unwrap();
                             let scope = dom.get_scope(id).unwrap();
                             let node = scope.root_node();
                             let node = scope.root_node();
                             match node {
                             match node {
-                                RenderReturn::Sync(Ok(node)) => {
+                                RenderReturn::Sync(Some(node)) => {
                                     self.render_template(buf, dom, node)?
                                     self.render_template(buf, dom, node)?
                                 }
                                 }
                                 _ => todo!(
                                 _ => todo!(

+ 1 - 1
packages/tui/examples/tui_colorpicker.rs

@@ -17,7 +17,7 @@ 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() {
+                if let RenderReturn::Sync(Some(node)) = cx.root_node() {
                     if let Some(id) = node.root_ids[0].get() {
                     if let Some(id) = node.root_ids[0].get() {
                         let node = tui_query.get(id);
                         let node = tui_query.get(id);
                         let Size{width, height} = node.size().unwrap();
                         let Size{width, height} = node.size().unwrap();

+ 1 - 1
packages/tui/src/widgets/mod.rs

@@ -10,7 +10,7 @@ use dioxus_core::{ElementId, RenderReturn, Scope};
 pub use input::*;
 pub use input::*;
 
 
 pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
 pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
-    if let RenderReturn::Sync(Ok(sync)) = cx.root_node() {
+    if let RenderReturn::Sync(Some(sync)) = cx.root_node() {
         sync.root_ids.get(0).and_then(|id| id.get())
         sync.root_ids.get(0).and_then(|id| id.get())
     } else {
     } else {
         None
         None

+ 4 - 4
packages/tui/src/widgets/number.rs

@@ -99,7 +99,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
         update(text.clone());
         update(text.clone());
     };
     };
 
 
-    render! {
+    cx.render(rsx! {
         div{
         div{
             width: "{width}",
             width: "{width}",
             height: "{height}",
             height: "{height}",
@@ -120,7 +120,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
                     let Point{ x, y } = node.pos().unwrap();
                     let Point{ x, y } = node.pos().unwrap();
 
 
                     let Pos { col, row } = cursor.read().start;
                     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});
+                    let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
                     if let Ok(pos) = crossterm::cursor::position() {
                     if let Ok(pos) = crossterm::cursor::position() {
                         if pos != (x, y){
                         if pos != (x, y){
                             execute!(stdout(), MoveTo(x, y)).unwrap();
                             execute!(stdout(), MoveTo(x, y)).unwrap();
@@ -172,7 +172,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
                 let Point{ x, y } = node.pos().unwrap();
                 let Point{ x, y } = node.pos().unwrap();
 
 
                 let Pos { col, row } = cursor.read().start;
                 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});
+                let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
                 if let Ok(pos) = crossterm::cursor::position() {
                 if let Ok(pos) = crossterm::cursor::position() {
                     if pos != (x, y){
                     if pos != (x, y){
                         execute!(stdout(), MoveTo(x, y)).unwrap();
                         execute!(stdout(), MoveTo(x, y)).unwrap();
@@ -205,5 +205,5 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
 
 
             "{text_after_second_cursor}"
             "{text_after_second_cursor}"
         }
         }
-    }
+    })
 }
 }

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