Просмотр исходного кода

Merge pull request #1974 from DioxusLabs/jk/fix-form-inputs

Fix form inputs, form submits navigating, file drop, multiple root elements
Jonathan Kelley 1 год назад
Родитель
Сommit
6faa51a4a9
73 измененных файлов с 2684 добавлено и 2420 удалено
  1. 2 1
      .vscode/settings.json
  2. 157 202
      Cargo.lock
  3. 1 0
      Cargo.toml
  4. 3 6
      examples/assets/file_upload.css
  5. 60 40
      examples/file_upload.rs
  6. 99 12
      examples/form.rs
  7. 41 68
      examples/image_generator_openai.rs
  8. 1 1
      examples/router.rs
  9. 1 1
      examples/todomvc.rs
  10. 16 37
      examples/weather_app.rs
  11. 1 1
      examples/xss_safety.rs
  12. 1 3
      packages/desktop/Cargo.toml
  13. 409 246
      packages/desktop/headless_tests/events.rs
  14. 5 22
      packages/desktop/headless_tests/rendering.rs
  15. 56 0
      packages/desktop/headless_tests/utils.rs
  16. 61 47
      packages/desktop/src/app.rs
  17. 2 19
      packages/desktop/src/config.rs
  18. 13 11
      packages/desktop/src/desktop_context.rs
  19. 18 3
      packages/desktop/src/events.rs
  20. 114 1
      packages/desktop/src/file_upload.rs
  21. 8 10
      packages/desktop/src/ipc.rs
  22. 15 8
      packages/desktop/src/launch.rs
  23. 0 6
      packages/desktop/src/mobile_shortcut.rs
  24. 19 77
      packages/desktop/src/protocol.rs
  25. 6 3
      packages/desktop/src/query.rs
  26. 1 0
      packages/desktop/src/shortcut.rs
  27. 2 2
      packages/desktop/src/waker.rs
  28. 25 23
      packages/desktop/src/webview.rs
  29. 4 0
      packages/html/src/events.rs
  30. 3 1
      packages/html/src/events/drag.rs
  31. 33 51
      packages/html/src/events/form.rs
  32. 1 1
      packages/html/src/point_interaction.rs
  33. 14 3
      packages/html/src/transit.rs
  34. 1 0
      packages/interpreter/.gitignore
  35. 4 1
      packages/interpreter/Cargo.toml
  36. 7 0
      packages/interpreter/NOTES.md
  37. 12 0
      packages/interpreter/README.md
  38. 76 0
      packages/interpreter/build.rs
  39. 0 79
      packages/interpreter/src/common.js
  40. 0 79
      packages/interpreter/src/common_exported.js
  41. 0 751
      packages/interpreter/src/interpreter.js
  42. 1 0
      packages/interpreter/src/js/README.md
  43. 1 0
      packages/interpreter/src/js/common.js
  44. 1 0
      packages/interpreter/src/js/core.js
  45. 1 0
      packages/interpreter/src/js/hash.txt
  46. 1 0
      packages/interpreter/src/js/native.js
  47. 20 8
      packages/interpreter/src/lib.rs
  48. 0 413
      packages/interpreter/src/sledgehammer_bindings.rs
  49. 3 0
      packages/interpreter/src/ts/.gitignore
  50. 2 0
      packages/interpreter/src/ts/common.ts
  51. 173 0
      packages/interpreter/src/ts/core.ts
  52. 58 0
      packages/interpreter/src/ts/form.ts
  53. 382 0
      packages/interpreter/src/ts/native.ts
  54. 217 0
      packages/interpreter/src/ts/serialize.ts
  55. 106 0
      packages/interpreter/src/ts/set_attribute.ts
  56. 237 0
      packages/interpreter/src/unified_bindings.rs
  57. 10 8
      packages/interpreter/src/write_native_mutations.rs
  58. 1 0
      packages/interpreter/tests/e2e.rs
  59. 1 0
      packages/interpreter/tests/serialize.rs
  60. 17 0
      packages/interpreter/tsconfig.json
  61. 0 1
      packages/liveview/Cargo.toml
  62. 14 12
      packages/liveview/src/lib.rs
  63. 4 3
      packages/liveview/src/main.js
  64. 2 3
      packages/plasmo/src/hooks.rs
  65. 35 0
      packages/web/NOTES.md
  66. 3 0
      packages/web/ric_raf/README.md
  67. 0 0
      packages/web/ric_raf/ric_raf.rs
  68. 0 0
      packages/web/ric_raf/ricpolyfill.js
  69. 42 37
      packages/web/src/dom.rs
  70. 9 19
      packages/web/src/event.rs
  71. 13 55
      packages/web/src/lib.rs
  72. 32 36
      packages/web/src/mutations.rs
  73. 6 9
      packages/web/src/rehydrate.rs

+ 2 - 1
.vscode/settings.json

@@ -3,7 +3,8 @@
   "[toml]": {
     "editor.formatOnSave": false
   },
-  "rust-analyzer.check.workspace": true,
+  "rust-analyzer.check.workspace": false,
+  // "rust-analyzer.check.workspace": true,
   "rust-analyzer.check.features": "all",
   "rust-analyzer.cargo.features": "all",
   "rust-analyzer.check.allTargets": true

+ 157 - 202
Cargo.lock

@@ -71,9 +71,9 @@ dependencies = [
 
 [[package]]
 name = "ahash"
-version = "0.8.9"
+version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f"
+checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
 dependencies = [
  "cfg-if",
  "const-random",
@@ -83,15 +83,6 @@ dependencies = [
  "zerocopy",
 ]
 
-[[package]]
-name = "aho-corasick"
-version = "0.7.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "aho-corasick"
 version = "1.1.2"
@@ -154,9 +145,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.12"
+version = "0.6.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -256,7 +247,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -278,7 +269,7 @@ dependencies = [
  "argh_shared",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -463,7 +454,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -498,7 +489,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -737,7 +728,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -1012,7 +1003,7 @@ dependencies = [
  "proc-macro-crate 3.1.0",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
  "syn_derive",
 ]
 
@@ -1048,9 +1039,9 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.9.0"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
+checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
 dependencies = [
  "memchr",
  "regex-automata",
@@ -1083,9 +1074,9 @@ checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53"
 
 [[package]]
 name = "bumpalo"
-version = "3.15.2"
+version = "3.15.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3b1be7772ee4501dba05acbe66bb1e8760f6a6c474a36035631638e4415f130"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
 
 [[package]]
 name = "bytecheck"
@@ -1332,9 +1323,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.0.86"
+version = "1.0.88"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
+checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc"
 dependencies = [
  "libc",
 ]
@@ -1426,7 +1417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
 dependencies = [
  "ciborium-io",
- "half 2.3.1",
+ "half 2.4.0",
 ]
 
 [[package]]
@@ -1481,7 +1472,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -1858,9 +1849,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.11"
+version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
+checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -2003,7 +1994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
 dependencies = [
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2029,12 +2020,12 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
+checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c"
 dependencies = [
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2067,9 +2058,9 @@ dependencies = [
 
 [[package]]
 name = "darling"
-version = "0.20.6"
+version = "0.20.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955"
+checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
 dependencies = [
  "darling_core",
  "darling_macro",
@@ -2077,27 +2068,27 @@ dependencies = [
 
 [[package]]
 name = "darling_core"
-version = "0.20.6"
+version = "0.20.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855"
+checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
 dependencies = [
  "fnv",
  "ident_case",
  "proc-macro2",
  "quote",
  "strsim",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
 name = "darling_macro"
-version = "0.20.6"
+version = "0.20.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be"
+checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
 dependencies = [
  "darling_core",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2243,7 +2234,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2307,7 +2298,7 @@ dependencies = [
  "serde",
  "serde_json",
  "subprocess",
- "syn 2.0.50",
+ "syn 2.0.51",
  "tar",
  "tauri-bundler",
  "tempfile",
@@ -2377,7 +2368,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.50",
+ "syn 2.0.51",
  "trybuild",
 ]
 
@@ -2393,7 +2384,6 @@ version = "0.5.0-alpha.0"
 dependencies = [
  "async-trait",
  "core-foundation",
- "crossbeam-channel",
  "dioxus",
  "dioxus-cli-config",
  "dioxus-core",
@@ -2556,7 +2546,7 @@ dependencies = [
  "convert_case 0.6.0",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
  "trybuild",
 ]
 
@@ -2567,6 +2557,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-html",
  "js-sys",
+ "md5",
  "serde",
  "sledgehammer_bindgen",
  "sledgehammer_utils",
@@ -2601,7 +2592,6 @@ dependencies = [
  "futures-channel",
  "futures-util",
  "generational-box",
- "minify-js",
  "pretty_env_logger",
  "rustc-hash",
  "serde",
@@ -2653,7 +2643,7 @@ dependencies = [
  "quote",
  "rustc-hash",
  "smallvec",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2731,7 +2721,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "slab",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2744,7 +2734,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
- "syn 2.0.50",
+ "syn 2.0.51",
  "tracing",
 ]
 
@@ -2858,7 +2848,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "server_fn_macro",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -2981,7 +2971,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3002,7 +2992,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3023,7 +3013,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3163,7 +3153,7 @@ checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b"
 dependencies = [
  "execute-command-tokens",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3186,7 +3176,7 @@ checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
 dependencies = [
  "bit_field",
  "flume",
- "half 2.3.1",
+ "half 2.4.0",
  "lebe",
  "miniz_oxide",
  "rayon-core",
@@ -3331,7 +3321,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3514,7 +3504,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -3719,9 +3709,9 @@ dependencies = [
 
 [[package]]
 name = "gif"
-version = "0.12.0"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
+checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
 dependencies = [
  "color_quant",
  "weezl",
@@ -3797,7 +3787,7 @@ version = "0.28.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2eadca029ef716b4378f7afb19f7ee101fde9e58ba1f1445971315ac866db417"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "btoi",
  "gix-date",
  "itoa 1.0.10",
@@ -3811,7 +3801,7 @@ version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0341471d55d8676e98b88e121d7065dfa4c9c5acea4b6d6ecdd2846e85cce0c3"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "gix-config-value",
  "gix-features",
  "gix-glob",
@@ -3828,12 +3818,12 @@ dependencies = [
 
 [[package]]
 name = "gix-config-value"
-version = "0.14.4"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b8a1e7bfb37a46ed0b8468db37a6d8a0a61d56bdbe4603ae492cb322e5f3958"
+checksum = "74ab5d22bc21840f4be0ba2e78df947ba14d8ba6999ea798f86b5bdb999edd0c"
 dependencies = [
  "bitflags 2.4.2",
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "gix-path",
  "libc",
  "thiserror",
@@ -3841,11 +3831,11 @@ dependencies = [
 
 [[package]]
 name = "gix-date"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb7f3dfb72bebe3449b5e642be64e3c6ccbe9821c8b8f19f487cf5bfbbf4067e"
+checksum = "17077f0870ac12b55d2eed9cb3f56549e40def514c8a783a0a79177a8a76b7c5"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "itoa 1.0.10",
  "thiserror",
  "time",
@@ -3881,7 +3871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5db19298c5eeea2961e5b3bf190767a2d1f09b8802aeb5f258e42276350aff19"
 dependencies = [
  "bitflags 2.4.2",
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "gix-features",
  "gix-path",
 ]
@@ -3913,7 +3903,7 @@ version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "febf79c5825720c1c63fe974c7bbe695d0cb54aabad73f45671c60ce0e501e33"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "btoi",
  "gix-actor",
  "gix-date",
@@ -3928,11 +3918,11 @@ dependencies = [
 
 [[package]]
 name = "gix-path"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97e9ad649bf5e109562d6acba657ca428661ec08e77eaf3a755d8fa55485be9c"
+checksum = "69e0b521a5c345b7cd6a81e3e6f634407360a038c8b74ba14c621124304251b8"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "gix-trace",
  "home",
  "once_cell",
@@ -3962,9 +3952,9 @@ dependencies = [
 
 [[package]]
 name = "gix-sec"
-version = "0.10.4"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8d9bf462feaf05f2121cba7399dbc6c34d88a9cad58fc1e95027791d6a3c6d2"
+checksum = "022592a0334bdf77c18c06e12a7c0eaff28845c37e73c51a3e37d56dd495fb35"
 dependencies = [
  "bitflags 2.4.2",
  "gix-path",
@@ -3993,9 +3983,9 @@ checksum = "02b202d766a7fefc596e2cc6a89cda8ad8ad733aed82da635ac120691112a9b1"
 
 [[package]]
 name = "gix-utils"
-version = "0.1.9"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56e839f3d0798b296411263da6bee780a176ef8008a5dfc31287f7eda9266ab8"
+checksum = "60157a15b9f14b11af1c6817ad7a93b10b50b4e5136d98a127c46a37ff16eeb6"
 dependencies = [
  "fastrand 2.0.1",
  "unicode-normalization",
@@ -4007,7 +3997,7 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac7cc36f496bd5d96cdca0f9289bb684480725d40db60f48194aa7723b883854"
 dependencies = [
- "bstr 1.9.0",
+ "bstr 1.9.1",
  "thiserror",
 ]
 
@@ -4045,7 +4035,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -4066,9 +4056,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "global-hotkey"
-version = "0.4.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "927a00fd7c31d82029f99ce2481a8de1ae974758017d6a55ebbe7f22edcd1617"
+checksum = "34300b13d16b1593de1b6bd571a376704820a1e6f6fe57be2f106ded8d164030"
 dependencies = [
  "crossbeam-channel",
  "keyboard-types",
@@ -4084,8 +4074,8 @@ version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
 dependencies = [
- "aho-corasick 1.1.2",
- "bstr 1.9.0",
+ "aho-corasick",
+ "bstr 1.9.1",
  "log",
  "regex-automata",
  "regex-syntax",
@@ -4357,7 +4347,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -4400,15 +4390,15 @@ dependencies = [
 
 [[package]]
 name = "half"
-version = "1.8.2"
+version = "1.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
 
 [[package]]
 name = "half"
-version = "2.3.1"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
+checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
 dependencies = [
  "cfg-if",
  "crunchy",
@@ -4437,23 +4427,13 @@ dependencies = [
  "ahash 0.7.8",
 ]
 
-[[package]]
-name = "hashbrown"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
-dependencies = [
- "ahash 0.8.9",
- "bumpalo",
-]
-
 [[package]]
 name = "hashbrown"
 version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
 dependencies = [
- "ahash 0.8.9",
+ "ahash 0.8.10",
  "allocator-api2",
 ]
 
@@ -4543,9 +4523,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.3.6"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
+checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60"
 
 [[package]]
 name = "hex"
@@ -4723,7 +4703,7 @@ dependencies = [
  "httpdate",
  "itoa 1.0.10",
  "pin-project-lite",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "tokio",
  "tower-service",
  "tracing",
@@ -4810,7 +4790,7 @@ dependencies = [
  "http-body 1.0.0",
  "hyper 1.2.0",
  "pin-project-lite",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "tokio",
  "tower",
  "tower-service",
@@ -4890,9 +4870,9 @@ dependencies = [
 
 [[package]]
 name = "image"
-version = "0.24.8"
+version = "0.24.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
+checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
 dependencies = [
  "bytemuck",
  "byteorder",
@@ -5043,7 +5023,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -5084,7 +5064,7 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
 dependencies = [
- "hermit-abi 0.3.6",
+ "hermit-abi 0.3.8",
  "libc",
  "windows-sys 0.48.0",
 ]
@@ -5095,7 +5075,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
 dependencies = [
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "widestring",
  "windows-sys 0.48.0",
  "winreg",
@@ -5141,7 +5121,7 @@ version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
 dependencies = [
- "hermit-abi 0.3.6",
+ "hermit-abi 0.3.8",
  "libc",
  "windows-sys 0.52.0",
 ]
@@ -5509,11 +5489,11 @@ dependencies = [
 
 [[package]]
 name = "lightningcss"
-version = "1.0.0-alpha.53"
+version = "1.0.0-alpha.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae8ba2b1b450cefc6a7a6ee93868992f56f662869b90404de2a7319da18640f"
+checksum = "07d306844e5af1753490c420c0d6ae3d814b00725092d106332762827ca8f0fe"
 dependencies = [
- "ahash 0.8.9",
+ "ahash 0.8.10",
  "bitflags 2.4.2",
  "const-str",
  "cssparser 0.33.0",
@@ -5604,7 +5584,7 @@ checksum = "fc2fb41a9bb4257a3803154bdf7e2df7d45197d1941c9b1a90ad815231630721"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -5658,9 +5638,9 @@ dependencies = [
 
 [[package]]
 name = "lru"
-version = "0.12.2"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
 dependencies = [
  "hashbrown 0.14.3",
 ]
@@ -5779,7 +5759,7 @@ dependencies = [
  "manganis-common",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -5898,16 +5878,6 @@ dependencies = [
  "unicase",
 ]
 
-[[package]]
-name = "minify-js"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22d6c512a82abddbbc13b70609cb2beff01be2c7afff534d6e5e1c85e438fc8b"
-dependencies = [
- "lazy_static",
- "parse-js",
-]
-
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -6257,7 +6227,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -6308,7 +6278,7 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
 dependencies = [
- "hermit-abi 0.3.6",
+ "hermit-abi 0.3.8",
  "libc",
 ]
 
@@ -6416,9 +6386,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "open"
-version = "5.0.1"
+version = "5.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349"
+checksum = "eedff767bc49d336bff300224f73307ae36963c843e38dc9312a22171b012cbc"
 dependencies = [
  "is-wsl",
  "libc",
@@ -6448,7 +6418,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -6619,19 +6589,6 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
-[[package]]
-name = "parse-js"
-version = "0.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ec3b11d443640ec35165ee8f6f0559f1c6f41878d70330fe9187012b5935f02"
-dependencies = [
- "aho-corasick 0.7.20",
- "bumpalo",
- "hashbrown 0.13.2",
- "lazy_static",
- "memchr",
-]
-
 [[package]]
 name = "password-hash"
 version = "0.4.2"
@@ -6731,7 +6688,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -6859,7 +6816,7 @@ dependencies = [
  "phf_shared 0.11.2",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -6906,7 +6863,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7109,7 +7066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3"
 dependencies = [
  "proc-macro2",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7139,7 +7096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
 dependencies = [
  "proc-macro2",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7231,7 +7188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
 dependencies = [
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7518,9 +7475,9 @@ checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
 
 [[package]]
 name = "rayon"
-version = "1.8.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd"
 dependencies = [
  "either",
  "rayon-core",
@@ -7562,7 +7519,7 @@ version = "1.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
 dependencies = [
- "aho-corasick 1.1.2",
+ "aho-corasick",
  "memchr",
  "regex-automata",
  "regex-syntax",
@@ -7574,7 +7531,7 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
 dependencies = [
- "aho-corasick 1.1.2",
+ "aho-corasick",
  "memchr",
  "regex-syntax",
 ]
@@ -7706,7 +7663,7 @@ version = "1.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3625f343d89990133d013e39c46e350915178cf94f1bec9f49b0cbef98a3e3c"
 dependencies = [
- "ahash 0.8.9",
+ "ahash 0.8.10",
  "bitflags 2.4.2",
  "instant",
  "num-traits",
@@ -7724,7 +7681,7 @@ checksum = "853977598f084a492323fe2f7896b4100a86284ee8473612de60021ea341310f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7822,7 +7779,7 @@ dependencies = [
  "pretty_assertions",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -7971,9 +7928,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.3.0"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7"
+checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"
 
 [[package]]
 name = "rustls-webpki"
@@ -8173,7 +8130,7 @@ version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
 dependencies = [
- "half 1.8.2",
+ "half 1.8.3",
  "serde",
 ]
 
@@ -8185,7 +8142,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -8238,7 +8195,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -8289,7 +8246,7 @@ dependencies = [
  "darling",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -8337,7 +8294,7 @@ dependencies = [
  "convert_case 0.6.0",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
  "xxhash-rust",
 ]
 
@@ -8348,7 +8305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89732cbf095803f0a23dff6a9d2f469049d48affdaa80edee0d826c986330ced"
 dependencies = [
  "server_fn_macro",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -8527,8 +8484,7 @@ dependencies = [
 [[package]]
 name = "sledgehammer_bindgen"
 version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa1ca40134578bf7cf17973defcd4eb8d7d2adf7868b29892481722957bd543e"
+source = "git+https://github.com/ealmloff/sledgehammer_bindgen#91331b3f380883b8883cf5d5b81424c1846340cf"
 dependencies = [
  "sledgehammer_bindgen_macro",
  "wasm-bindgen",
@@ -8537,11 +8493,10 @@ dependencies = [
 [[package]]
 name = "sledgehammer_bindgen_macro"
 version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04559ded3de5c62f08457cadcb6c44649c4d90e72fdc0804c6c30ce1bc526304"
+source = "git+https://github.com/ealmloff/sledgehammer_bindgen#91331b3f380883b8883cf5d5b81424c1846340cf"
 dependencies = [
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -8593,12 +8548,12 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.5"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
 dependencies = [
  "libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -8691,7 +8646,7 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
 dependencies = [
- "ahash 0.8.9",
+ "ahash 0.8.10",
  "atoi",
  "bigdecimal",
  "bit-vec",
@@ -8983,7 +8938,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -9025,9 +8980,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.50"
+version = "2.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
+checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9043,7 +8998,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -9328,7 +9283,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -9439,7 +9394,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.5.5",
+ "socket2 0.5.6",
  "tokio-macros",
  "windows-sys 0.48.0",
 ]
@@ -9452,7 +9407,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -9614,7 +9569,7 @@ dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow 0.6.2",
+ "winnow 0.6.3",
 ]
 
 [[package]]
@@ -9635,9 +9590,9 @@ dependencies = [
 
 [[package]]
 name = "tower-http"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
 dependencies = [
  "async-compression",
  "base64",
@@ -9696,7 +9651,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -10154,7 +10109,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
  "wasm-bindgen-shared",
 ]
 
@@ -10220,7 +10175,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -10263,7 +10218,7 @@ checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -10455,7 +10410,7 @@ checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -10559,7 +10514,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -10570,7 +10525,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -10791,9 +10746,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.6.2"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178"
+checksum = "44e19b97e00a4d3db3cdb9b53c8c5f87151b5689b82cc86c2848cbdcccb2689b"
 dependencies = [
  "memchr",
 ]
@@ -10918,9 +10873,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zbus"
-version = "3.15.0"
+version = "3.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c45d06ae3b0f9ba1fb2671268b975557d8f5a84bb5ec6e43964f87e763d8bca8"
+checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6"
 dependencies = [
  "async-broadcast",
  "async-executor",
@@ -10959,9 +10914,9 @@ dependencies = [
 
 [[package]]
 name = "zbus_macros"
-version = "3.15.0"
+version = "3.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4a1ba45ed0ad344b85a2bb5a1fe9830aed23d67812ea39a586e7d0136439c7d"
+checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5"
 dependencies = [
  "proc-macro-crate 1.3.1",
  "proc-macro2",
@@ -10999,7 +10954,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.51",
 ]
 
 [[package]]
@@ -11086,9 +11041,9 @@ dependencies = [
 
 [[package]]
 name = "zvariant"
-version = "3.15.0"
+version = "3.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c"
+checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db"
 dependencies = [
  "byteorder",
  "enumflags2",
@@ -11100,9 +11055,9 @@ dependencies = [
 
 [[package]]
 name = "zvariant_derive"
-version = "3.15.0"
+version = "3.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd"
+checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9"
 dependencies = [
  "proc-macro-crate 1.3.1",
  "proc-macro2",

+ 1 - 0
Cargo.toml

@@ -155,6 +155,7 @@ form_urlencoded = "1.2.0"
 
 [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
 getrandom = { version = "0.2.12", features = ["js"] }
+tokio = { version = "1.16.1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 tokio = { version = "1.16.1", features = ["full"] }

+ 3 - 6
examples/assets/file_upload.css

@@ -1,14 +1,11 @@
 body {
     font-family: Arial, sans-serif;
-    margin: 0;
-    padding: 0;
+    margin: 10px;
+    padding: 10px;
     background-color: #f4f4f4;
-    display: flex;
     justify-content: center;
     align-items: center;
-    height: 100vh;
-    flex-direction: column;
-    gap: 20px;
+    min-height: 100vh;
 }
 
 #drop-zone {

+ 60 - 40
examples/file_upload.rs

@@ -3,73 +3,93 @@
 //! Dioxus intercepts these events and provides a Rusty interface to the file data. Since we want this interface to
 //! be crossplatform,
 
-use dioxus::html::HasFileData;
+use std::sync::Arc;
+
 use dioxus::prelude::*;
-use tokio::time::sleep;
+use dioxus::{html::HasFileData, prelude::dioxus_elements::FileEngine};
 
 fn main() {
     launch(app);
 }
 
+struct UploadedFile {
+    name: String,
+    contents: String,
+}
+
 fn app() -> Element {
     let mut enable_directory_upload = use_signal(|| false);
-    let mut files_uploaded = use_signal(|| Vec::new() as Vec<String>);
+    let mut files_uploaded = use_signal(|| Vec::new() as Vec<UploadedFile>);
+    let mut hovered = use_signal(|| false);
 
-    let upload_files = move |evt: FormEvent| async move {
-        for file_name in evt.files().unwrap().files() {
-            // no files on form inputs?
-            sleep(std::time::Duration::from_secs(1)).await;
-            files_uploaded.write().push(file_name);
+    let read_files = move |file_engine: Arc<dyn FileEngine>| async move {
+        let files = file_engine.files();
+        for file_name in &files {
+            if let Some(contents) = file_engine.read_file_to_string(file_name).await {
+                files_uploaded.write().push(UploadedFile {
+                    name: file_name.clone(),
+                    contents,
+                });
+            }
         }
     };
 
-    let handle_file_drop = move |evt: DragEvent| async move {
-        if let Some(file_engine) = &evt.files() {
-            let files = file_engine.files();
-            for file_name in &files {
-                if let Some(file) = file_engine.read_file_to_string(file_name).await {
-                    files_uploaded.write().push(file);
-                }
-            }
+    let upload_files = move |evt: FormEvent| async move {
+        if let Some(file_engine) = evt.files() {
+            read_files(file_engine).await;
         }
     };
 
     rsx! {
         style { {include_str!("./assets/file_upload.css")} }
 
-        input {
-            r#type: "checkbox",
-            id: "directory-upload",
-            checked: enable_directory_upload,
-            oninput: move |evt| enable_directory_upload.set(evt.checked()),
-        },
-        label {
-            r#for: "directory-upload",
-            "Enable directory upload"
-        }
+        h1 { "File Upload Example" }
+        p { "Drop a .txt, .rs, or .js file here to read it" }
+        button { onclick: move |_| files_uploaded.write().clear(), "Clear files" }
 
-        input {
-            r#type: "file",
-            accept: ".txt,.rs",
-            multiple: true,
-            directory: enable_directory_upload,
-            onchange: upload_files,
+        div {
+            label { r#for: "directory-upload", "Enable directory upload" }
+            input {
+                r#type: "checkbox",
+                id: "directory-upload",
+                checked: enable_directory_upload,
+                oninput: move |evt| enable_directory_upload.set(evt.checked()),
+            },
         }
 
         div {
-            // cheating with a little bit of JS...
-            "ondragover": "this.style.backgroundColor='#88FF88';",
-            "ondragleave": "this.style.backgroundColor='#FFFFFF';",
+            label { r#for: "textreader", "Upload text/rust files and read them" }
+            input {
+                r#type: "file",
+                accept: ".txt,.rs,.js",
+                multiple: true,
+                name: "textreader",
+                directory: enable_directory_upload,
+                onchange: upload_files,
+            }
+        }
 
+        div {
             id: "drop-zone",
-            prevent_default: "ondrop dragover dragenter",
-            ondrop: handle_file_drop,
-            ondragover: move |event| event.stop_propagation(),
+            prevent_default: "ondragover ondrop",
+            background_color: if hovered() { "lightblue" } else { "lightgray" },
+            ondragover: move |_| hovered.set(true),
+            ondragleave: move |_| hovered.set(false),
+            ondrop: move |evt| async move {
+                hovered.set(false);
+                if let Some(file_engine) = evt.files() {
+                    read_files(file_engine).await;
+                }
+            },
             "Drop files here"
         }
+
         ul {
-            for file in files_uploaded.read().iter() {
-                li { "{file}" }
+            for file in files_uploaded.read().iter().rev() {
+                li {
+                    span { "{file.name}" }
+                    pre  { "{file.contents}"  }
+                }
             }
         }
     }

+ 99 - 12
examples/form.rs

@@ -4,24 +4,111 @@
 //! in the "values" field.
 
 use dioxus::prelude::*;
+use std::collections::HashMap;
 
 fn main() {
-    launch_desktop(app);
+    launch(app);
 }
 
 fn app() -> Element {
+    let mut values = use_signal(HashMap::new);
+    let mut submitted_values = use_signal(HashMap::new);
+
     rsx! {
-        div {
-            h1 { "Form" }
-            form {
-                onsubmit: move |ev| println!("Submitted {:?}", ev.values()),
-                oninput: move |ev| println!("Input {:?}", ev.values()),
-                input { r#type: "text", name: "username" }
-                input { r#type: "text", name: "full-name" }
-                input { r#type: "password", name: "password" }
-                input { r#type: "radio", name: "color", value: "red" }
-                input { r#type: "radio", name: "color", value: "blue" }
-                button { r#type: "submit", value: "Submit", "Submit the form" }
+        div { style: "display: flex",
+            div { style: "width: 50%",
+                h1 { "Form" }
+
+                if !submitted_values.read().is_empty() {
+                    h2 { "Submitted! ✅" }
+                }
+
+                // The form element is used to create an HTML form for user input
+                // You can attach regular attributes to it
+                form {
+                    id: "cool-form",
+                    style: "display: flex; flex-direction: column;",
+
+                    // You can attach a handler to the entire form
+                    oninput: move |ev| {
+                        println!("Input event: {:#?}", ev);
+                        values.set(ev.values());
+                    },
+
+                    // On desktop/liveview, the form will not navigate the page - the expectation is that you handle
+                    // The form event.
+                    // Howver, if your form doesn't have a submit handler, it might navigate the page depending on the webview.
+                    // We suggest always attaching a submit handler to the form.
+                    onsubmit: move |ev| {
+                        println!("Submit event: {:#?}", ev);
+                        submitted_values.set(ev.values());
+                    },
+
+                    // Regular text inputs with handlers
+                    label { r#for: "username", "Username" }
+                    input {
+                        r#type: "text",
+                        name: "username",
+                        oninput: move |ev| {
+                            println!("setting username");
+                            values.set(ev.values());
+                        }
+                    }
+
+                    // And then the various inputs that might exist
+                    // Note for a value to be returned in .values(), it must be named!
+
+                    label { r#for: "full-name", "Full Name" }
+                    input { r#type: "text", name: "full-name" }
+
+                    label { r#for: "email", "Email" }
+                    input { r#type: "email", pattern: ".+@example\\.com", size: "30", required: "true", id: "email", name: "email" }
+
+                    label { r#for: "password", "Password" }
+                    input { r#type: "password", name: "password" }
+
+                    label { r#for: "color", "Color" }
+                    input { r#type: "radio", checked: true, name: "color", value: "red" }
+                    input { r#type: "radio", name: "color", value: "blue" }
+                    input { r#type: "radio", name: "color", value: "green" }
+
+                    // Select multiple comes in as a comma separated list of selected values
+                    // You should split them on the comma to get the values manually
+                    label { r#for: "country", "Country" }
+                    select {
+                        name: "country",
+                        multiple: true,
+                        oninput: move |ev| {
+                            println!("Input event: {:#?}", ev);
+                            println!("Values: {:#?}", ev.value().split(',').collect::<Vec<_>>());
+                        },
+                        option { value: "usa",  "USA" }
+                        option { value: "canada",  "Canada" }
+                        option { value: "mexico",  "Mexico" }
+                    }
+
+                    // Safari can be quirky with color inputs on mac.
+                    // We recommend always providing a text input for color as a fallback.
+                    label { r#for: "color", "Color" }
+                    input { r#type: "color", value: "#000002", name: "head", id: "head" }
+
+                    // Dates!
+                    input {
+                        min: "2018-01-01",
+                        value: "2018-07-22",
+                        r#type: "date",
+                        name: "trip-start",
+                        max: "2025-12-31",
+                        id: "start"
+                    }
+
+                    // Buttons will submit your form by default.
+                    button { r#type: "submit", value: "Submit", "Submit the form" }
+                }
+            }
+            div { style: "width: 50%",
+                h1 { "Oninput Values" }
+                pre { "{values:#?}" }
             }
         }
     }

+ 41 - 68
examples/image_generator_openai.rs

@@ -7,6 +7,7 @@ fn main() {
 }
 
 fn app() -> Element {
+    let mut loading = use_signal(|| "".to_string());
     let mut api = use_signal(|| "".to_string());
     let mut prompt = use_signal(|| "".to_string());
     let mut n_image = use_signal(|| 1.to_string());
@@ -14,7 +15,6 @@ fn app() -> Element {
         created: 0,
         data: Vec::new(),
     });
-    let mut loading = use_signal(|| "".to_string());
 
     let mut generate_images = use_resource(move || async move {
         let api_key = api.peek().clone();
@@ -26,79 +26,54 @@ fn app() -> Element {
         }
 
         loading.set("is-loading".to_string());
-        let images = request(api_key, prompt, number_of_images).await;
-        match images {
-            Ok(imgz) => {
-                image.set(imgz);
-            }
-            Err(e) => {
-                println!("Error: {:?}", e);
-            }
+
+        match request(api_key, prompt, number_of_images).await {
+            Ok(imgz) => image.set(imgz),
+            Err(e) => println!("Error: {:?}", e),
         }
+
         loading.set("".to_string());
     });
 
     rsx! {
-        head {
-            link {
-                rel: "stylesheet",
-                href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css",
-            }
-        }
+        head { link { rel: "stylesheet", href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" } }
         div { class: "container",
-        div { class: "columns",
-            div { class: "column",
-                input { class: "input is-primary mt-4",
-                value:"{api}",
-                    r#type: "text",
-                    placeholder: "API",
-                    oninput: move |evt| {
-                        api.set(evt.value().clone());
-                    },
-                }
-
-                input { class: "input is-primary mt-4",
-                    placeholder: "MAX 1000 Dgts",
-                    r#type: "text",
-                    value:"{prompt}",
-                    oninput: move |evt| {
-                        prompt.set(evt.value().clone());
-                    },
-                }
-
-                input { class: "input is-primary mt-4",
-                    r#type: "number",
-                    min:"1",
-                     max:"10",
-                    value:"{n_image}",
-                    oninput: move |evt| {
-                        n_image.set(evt.value().clone());
-                    },
+            div { class: "columns",
+                div { class: "column",
+                    input { class: "input is-primary mt-4",
+                        value: "{api}",
+                        r#type: "text",
+                        placeholder: "API",
+                        oninput: move |evt| api.set(evt.value()),
+                    }
+                    input { class: "input is-primary mt-4",
+                        placeholder: "MAX 1000 Dgts",
+                        r#type: "text",
+                        value:"{prompt}",
+                        oninput: move |evt| prompt.set(evt.value())
+                    }
+                    input { class: "input is-primary mt-4",
+                        r#type: "number",
+                        min:"1",
+                        max:"10",
+                        value:"{n_image}",
+                        oninput: move |evt| n_image.set(evt.value()),
+                    }
                 }
             }
-        }
-
-        button { class: "button is-primary {loading}",
-            onclick: move |_| {
-                generate_images.restart();
-            },
-            "Generate image"
-        }
-        br {
-        }
-    }
-    {image.read().data.iter().map(|image| {
-            rsx!(
+            button { class: "button is-primary {loading}",
+                onclick: move |_| generate_images.restart(),
+                "Generate image"
+            }
+            br {}
+            for image in image.read().data.as_slice() {
                 section { class: "is-flex",
-            div { class: "container is-fluid",
-                div { class: "container has-text-centered",
-                    div { class: "is-justify-content-center",
-                        div { class: "level",
-                            div { class: "level-item",
-                                figure { class: "image",
-                                    img {
-                                        alt: "",
-                                        src: "{image.url}",
+                    div { class: "container is-fluid",
+                        div { class: "container has-text-centered",
+                            div { class: "is-justify-content-center",
+                                div { class: "level",
+                                    div { class: "level-item",
+                                        figure { class: "image", img { alt: "", src: "{image.url}", } }
                                     }
                                 }
                             }
@@ -107,9 +82,7 @@ fn app() -> Element {
                 }
             }
         }
-            )
-        })
-    } }
+    }
 }
 async fn request(api: String, prompt: String, n_image: String) -> Result<ImageResponse, Error> {
     let client = reqwest::Client::new();

+ 1 - 1
examples/router.rs

@@ -9,7 +9,7 @@
 use dioxus::prelude::*;
 
 fn main() {
-    launch_desktop(|| {
+    launch(|| {
         rsx! {
             style { {include_str!("./assets/router.css")} }
             Router::<Route> {}

+ 1 - 1
examples/todomvc.rs

@@ -131,7 +131,7 @@ fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
                 placeholder: "What needs to be done?",
                 value: "{draft}",
                 autofocus: "true",
-                oninput: move |evt| draft.set(evt.value().clone()),
+                oninput: move |evt| draft.set(evt.value()),
                 onkeydown,
             }
         }

+ 16 - 37
examples/weather_app.rs

@@ -16,18 +16,14 @@ fn app() -> Element {
         id: 2950159,
     });
 
-    let current_weather =
-        use_resource(move || async move { get_weather(&country.read().clone()).await });
+    let current_weather = use_resource(move || async move { get_weather(&country()).await });
 
     rsx! {
-        link {
-            rel: "stylesheet",
-            href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css"
-        }
+        link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
         div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
             div { class: "flex items-center justify-center flex-row",
                 div { class: "flex items-start justify-center flex-row",
-                    SearchBox { country: country }
+                    SearchBox { country }
                     div { class: "flex flex-wrap w-full px-2",
                         div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full bg-white dark:bg-gray-600",
                             div { class: "px-6 py-6 relative",
@@ -36,14 +32,9 @@ fn app() -> Element {
                                         country: country.read().clone(),
                                         weather: weather.clone(),
                                     }
-                                    Forecast {
-                                        weather: weather.clone(),
-                                    }
-
+                                    Forecast { weather: weather.clone() }
                                 } else {
-                                    p {
-                                        "Loading.."
-                                    }
+                                    p { "Loading.." }
                                 }
                             }
                         }
@@ -93,6 +84,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
     let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap()
         + weather.daily.temperature_2m_max.get(2).unwrap())
         / 2.0;
+
     rsx! {
         div { class: "px-6 pt-4 relative",
             div { class: "w-full h-px bg-gray-100 mb-4" }
@@ -119,10 +111,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
 fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
     let mut input = use_signal(|| "".to_string());
 
-    let locations = use_resource(move || async move {
-        let current_location = input.read().clone();
-        get_locations(&current_location).await
-    });
+    let locations = use_resource(move || async move { get_locations(&input()).await });
 
     rsx! {
         div {
@@ -150,27 +139,17 @@ fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
                     }
                 }
                 ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto",
-                    {
-                        if let Some(Ok(locs)) = locations.read().as_ref() {
-                            rsx! {
-                                {
-                                    locs.iter().cloned().map(move |wl| {
-                                        rsx! {
-                                            li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
-                                                onclick: move |_| country.set(wl.clone()),
-                                                MapIcon {}
-                                                b {
-                                                    "{wl.name}"
-                                                }
-                                                " · {wl.country}"
-                                            }
-                                        }
-                                    })
-                                }
+                    if let Some(Ok(locs)) = locations.read().as_ref() {
+                        for wl in locs.iter().take(5).cloned() {
+                            li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
+                                onclick: move |_| country.set(wl.clone()),
+                                MapIcon {}
+                                b { "{wl.name}" }
+                                " · {wl.country}"
                             }
-                        } else {
-                            rsx! { "loading locations..." }
                         }
+                    } else {
+                        "loading locations..."
                     }
                 }
             }

+ 1 - 1
examples/xss_safety.rs

@@ -5,7 +5,7 @@
 use dioxus::prelude::*;
 
 fn main() {
-    launch_desktop(app);
+    launch(app);
 }
 
 fn app() -> Element {

+ 1 - 3
packages/desktop/Cargo.toml

@@ -46,16 +46,14 @@ dunce = "1.0.2"
 slab = { workspace = true }
 rustc-hash = { workspace = true }
 dioxus-hooks = { workspace = true }
-
 futures-util = { workspace = true }
 urlencoding = "2.1.2"
 async-trait = "0.1.68"
-crossbeam-channel = "0.5.8"
 tao = { version = "0.24.0", features = ["rwh_05"] }
 
 [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
+global-hotkey = "0.5.0"
 rfd = "0.12"
-global-hotkey = "0.4.1"
 muda = "0.11.3"
 
 [target.'cfg(target_os = "ios")'.dependencies]

+ 409 - 246
packages/desktop/headless_tests/events.rs

@@ -1,73 +1,104 @@
+use std::{collections::HashMap, ops::Deref};
+
 use dioxus::html::geometry::euclid::Vector3D;
 use dioxus::prelude::*;
 use dioxus_core::prelude::consume_context;
 use dioxus_desktop::DesktopContext;
 
+#[path = "./utils.rs"]
+mod utils;
+
 pub fn main() {
-    check_app_exits(app);
+    utils::check_app_exits(app);
 }
 
-pub(crate) fn check_app_exits(app: fn() -> Element) {
-    use dioxus_desktop::tao::window::WindowBuilder;
-    use dioxus_desktop::Config;
-    // This is a deadman's switch to ensure that the app exits
-    let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
-    let should_panic_clone = should_panic.clone();
-    std::thread::spawn(move || {
-        std::thread::sleep(std::time::Duration::from_secs(60));
-        if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
-            eprintln!("App did not exit in time");
-            std::process::exit(exitcode::SOFTWARE);
-        }
+static RECEIVED_EVENTS: GlobalSignal<usize> = Signal::global(|| 0);
+
+fn app() -> Element {
+    let desktop_context: DesktopContext = consume_context();
+
+    let received = RECEIVED_EVENTS();
+    let expected = utils::EXPECTED_EVENTS();
+
+    use_effect(move || {
+        println!("expecting {} events", utils::EXPECTED_EVENTS());
     });
 
-    LaunchBuilder::desktop()
-        .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true)))
-        .launch(app);
+    if expected != 0 && received == expected {
+        println!("all events recieved");
+        desktop_context.close();
+    }
 
-    // Stop deadman's switch
-    should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
+    rsx! {
+        div {
+            test_mounted {}
+            test_button {}
+            test_mouse_move_div {}
+            test_mouse_click_div {}
+            test_mouse_dblclick_div {}
+            test_mouse_down_div {}
+            test_mouse_up_div {}
+            test_mouse_scroll_div {}
+            test_key_down_div {}
+            test_key_up_div {}
+            test_key_press_div {}
+            test_focus_in_div {}
+            test_focus_out_div {}
+            test_form_input {}
+            test_form_submit {}
+            test_select_multiple_options {}
+        }
+    }
 }
 
-fn mock_event(id: &'static str, value: &'static str) {
-    use_hook(move || {
-        spawn(async move {
-            tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
-
-            let js = format!(
-                r#"
-                //console.log("ran");
-                // Dispatch a synthetic event
-                let event = {};
-                let element = document.getElementById('{}');
-                console.log(element, event);
-                element.dispatchEvent(event);
-                "#,
-                value, id
-            );
-
-            eval(&js).await.unwrap();
-        });
-    })
-}
+fn test_mounted() -> Element {
+    use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1));
 
-#[allow(deprecated)]
-fn app() -> Element {
-    let desktop_context: DesktopContext = consume_context();
-    let mut received_events = use_signal(|| 0);
+    rsx! {
+        div {
+            width: "100px",
+            height: "100px",
+            onmounted: move |evt| async move {
+                let rect = evt.get_client_rect().await.unwrap();
+                println!("rect: {:?}", rect);
+                assert_eq!(rect.width(), 100.0);
+                assert_eq!(rect.height(), 100.0);
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
 
-    // button
-    mock_event(
+fn test_button() -> Element {
+    utils::mock_event(
         "button",
         r#"new MouseEvent("click", {
-        view: window,
-        bubbles: true,
-        cancelable: true,
-        button: 0,
+            view: window,
+            bubbles: true,
+            cancelable: true,
+            button: 0,
         })"#,
     );
-    // mouse_move_div
-    mock_event(
+
+    rsx! {
+        button {
+            id: "button",
+            onclick: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(event.data.held_buttons().is_empty());
+                assert_eq!(
+                    event.data.trigger_button(),
+                    Some(dioxus_html::input_data::MouseButton::Primary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_move_div() -> Element {
+    utils::mock_event(
         "mouse_move_div",
         r#"new MouseEvent("mousemove", {
         view: window,
@@ -76,8 +107,27 @@ fn app() -> Element {
         buttons: 2,
         })"#,
     );
-    // mouse_click_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "mouse_move_div",
+            onmousemove: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(
+                    event
+                        .data
+                        .held_buttons()
+                        .contains(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_click_div() -> Element {
+    utils::mock_event(
         "mouse_click_div",
         r#"new MouseEvent("click", {
         view: window,
@@ -87,8 +137,31 @@ fn app() -> Element {
         button: 2,
         })"#,
     );
-    // mouse_dblclick_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "mouse_click_div",
+            onclick: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(
+                    event
+                        .data
+                        .held_buttons()
+                        .contains(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                assert_eq!(
+                    event.data.trigger_button(),
+                    Some(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_dblclick_div() -> Element {
+    utils::mock_event(
         "mouse_dblclick_div",
         r#"new MouseEvent("dblclick", {
         view: window,
@@ -98,8 +171,34 @@ fn app() -> Element {
         button: 2,
         })"#,
     );
-    // mouse_down_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "mouse_dblclick_div",
+            ondoubleclick: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(
+                    event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
+                );
+                assert!(
+                    event
+                        .data
+                        .held_buttons()
+                        .contains(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                assert_eq!(
+                    event.data.trigger_button(),
+                    Some(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_down_div() -> Element {
+    utils::mock_event(
         "mouse_down_div",
         r#"new MouseEvent("mousedown", {
         view: window,
@@ -109,8 +208,31 @@ fn app() -> Element {
         button: 2,
         })"#,
     );
-    // mouse_up_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "mouse_down_div",
+            onmousedown: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(
+                    event
+                        .data
+                        .held_buttons()
+                        .contains(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                assert_eq!(
+                    event.data.trigger_button(),
+                    Some(dioxus_html::input_data::MouseButton::Secondary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_up_div() -> Element {
+    utils::mock_event(
         "mouse_up_div",
         r#"new MouseEvent("mouseup", {
         view: window,
@@ -120,8 +242,26 @@ fn app() -> Element {
         button: 0,
         })"#,
     );
-    // wheel_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "mouse_up_div",
+            onmouseup: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert!(event.data.held_buttons().is_empty());
+                assert_eq!(
+                    event.data.trigger_button(),
+                    Some(dioxus_html::input_data::MouseButton::Primary),
+                );
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_mouse_scroll_div() -> Element {
+    utils::mock_event(
         "wheel_div",
         r#"new WheelEvent("wheel", {
         view: window,
@@ -132,8 +272,26 @@ fn app() -> Element {
         bubbles: true,
         })"#,
     );
-    // key_down_div
-    mock_event(
+
+    rsx! {
+        div {
+            id: "wheel_div",
+            width: "100px",
+            height: "100px",
+            background_color: "red",
+            onwheel: move |event| {
+                println!("{:?}", event.data);
+                let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
+                panic!("Expected delta to be in pixels") };
+                assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_key_down_div() -> Element {
+    utils::mock_event(
         "key_down_div",
         r#"new KeyboardEvent("keydown", {
         key: "a",
@@ -153,8 +311,24 @@ fn app() -> Element {
         bubbles: true,
         })"#,
     );
-    // key_up_div
-    mock_event(
+    rsx! {
+        input {
+            id: "key_down_div",
+            onkeydown: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert_eq!(event.data.key().to_string(), "a");
+                assert_eq!(event.data.code().to_string(), "KeyA");
+                assert_eq!(event.data.location(), Location::Standard);
+                assert!(event.data.is_auto_repeating());
+                assert!(event.data.is_composing());
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+fn test_key_up_div() -> Element {
+    utils::mock_event(
         "key_up_div",
         r#"new KeyboardEvent("keyup", {
         key: "a",
@@ -174,8 +348,25 @@ fn app() -> Element {
         bubbles: true,
         })"#,
     );
-    // key_press_div
-    mock_event(
+
+    rsx! {
+        input {
+            id: "key_up_div",
+            onkeyup: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert_eq!(event.data.key().to_string(), "a");
+                assert_eq!(event.data.code().to_string(), "KeyA");
+                assert_eq!(event.data.location(), Location::Standard);
+                assert!(!event.data.is_auto_repeating());
+                assert!(!event.data.is_composing());
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+fn test_key_press_div() -> Element {
+    utils::mock_event(
         "key_press_div",
         r#"new KeyboardEvent("keypress", {
         key: "a",
@@ -195,198 +386,170 @@ fn app() -> Element {
         bubbles: true,
         })"#,
     );
-    // focus_in_div
-    mock_event(
+    rsx! {
+        input {
+            id: "key_press_div",
+            onkeypress: move |event| {
+                println!("{:?}", event.data);
+                assert!(event.data.modifiers().is_empty());
+                assert_eq!(event.data.key().to_string(), "a");
+                assert_eq!(event.data.code().to_string(), "KeyA");
+                assert_eq!(event.data.location(), Location::Standard);
+                assert!(!event.data.is_auto_repeating());
+                assert!(!event.data.is_composing());
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_focus_in_div() -> Element {
+    utils::mock_event(
         "focus_in_div",
         r#"new FocusEvent("focusin", {bubbles: true})"#,
     );
-    // focus_out_div
-    mock_event(
+
+    rsx! {
+        input {
+            id: "focus_in_div",
+            onfocusin: move |event| {
+                println!("{:?}", event.data);
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
+    }
+}
+
+fn test_focus_out_div() -> Element {
+    utils::mock_event(
         "focus_out_div",
         r#"new FocusEvent("focusout",{bubbles: true})"#,
     );
-
-    if received_events() == 13 {
-        println!("all events recieved");
-        desktop_context.close();
+    rsx! {
+        input {
+            id: "focus_out_div",
+            onfocusout: move |event| {
+                println!("{:?}", event.data);
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            }
+        }
     }
+}
+
+fn test_form_input() -> Element {
+    let mut values = use_signal(HashMap::new);
+
+    utils::mock_event_with_extra(
+        "form-username",
+        r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
+        r#"element.value = "hello";"#,
+    );
+
+    let set_username = move |ev: FormEvent| {
+        values.set(ev.values());
+
+        // The value of the input should match
+        assert_eq!(ev.value(), "hello");
+
+        // And then the value the form gives us should also match
+        values.with_mut(|x| {
+            assert_eq!(x.get("username").unwrap().deref(), "hello");
+            assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
+            assert_eq!(x.get("password").unwrap().deref(), "ipsum");
+            assert_eq!(x.get("color").unwrap().deref(), "red");
+        });
+        RECEIVED_EVENTS.with_mut(|x| *x += 1);
+    };
 
     rsx! {
         div {
-            div {
-                width: "100px",
-                height: "100px",
-                onmounted: move |evt| async move {
-                    let rect = evt.get_client_rect().await.unwrap();
-                    println!("rect: {:?}", rect);
-                    assert_eq!(rect.width(), 100.0);
-                    assert_eq!(rect.height(), 100.0);
-                    received_events.with_mut(|x| *x += 1);
+            h1 { "Form" }
+            form {
+                id: "form",
+                oninput: move |ev| {
+                    values.set(ev.values());
+                },
+                onsubmit: move |ev| {
+                    println!("{:?}", ev);
+                },
+                input {
+                    r#type: "text",
+                    name: "username",
+                    id: "form-username",
+                    oninput: set_username,
                 }
+                input { r#type: "text", name: "full-name", value: "lorem" }
+                input { r#type: "password", name: "password", value: "ipsum" }
+                input { r#type: "radio", name: "color", value: "red", checked: true }
+                input { r#type: "radio", name: "color", value: "blue" }
+                button { r#type: "submit", value: "Submit", "Submit the form" }
             }
-            button {
-                id: "button",
-                onclick: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().is_empty());
-                    assert_eq!(
-                        event.data.trigger_button(),
-                        Some(dioxus_html::input_data::MouseButton::Primary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "mouse_move_div",
-                onmousemove: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(
-                        event
-                            .data
-                            .held_buttons()
-                            .contains(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "mouse_click_div",
-                onclick: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(
-                        event
-                            .data
-                            .held_buttons()
-                            .contains(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    assert_eq!(
-                        event.data.trigger_button(),
-                        Some(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "mouse_dblclick_div",
-                ondoubleclick: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(
-                        event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
-                    );
-                    assert!(
-                        event
-                            .data
-                            .held_buttons()
-                            .contains(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    assert_eq!(
-                        event.data.trigger_button(),
-                        Some(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "mouse_down_div",
-                onmousedown: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(
-                        event
-                            .data
-                            .held_buttons()
-                            .contains(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    assert_eq!(
-                        event.data.trigger_button(),
-                        Some(dioxus_html::input_data::MouseButton::Secondary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "mouse_up_div",
-                onmouseup: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert!(event.data.held_buttons().is_empty());
-                    assert_eq!(
-                        event.data.trigger_button(),
-                        Some(dioxus_html::input_data::MouseButton::Primary),
-                    );
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            div {
-                id: "wheel_div",
-                width: "100px",
-                height: "100px",
-                background_color: "red",
-                onwheel: move |event| {
-                    println!("{:?}", event.data);
-                    let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
-                    panic!("Expected delta to be in pixels") };
-                    assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            input {
-                id: "key_down_div",
-                onkeydown: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert_eq!(event.data.key().to_string(), "a");
-                    assert_eq!(event.data.code().to_string(), "KeyA");
-                    assert_eq!(event.data.location(), Location::Standard);
-                    assert!(event.data.is_auto_repeating());
-                    assert!(event.data.is_composing());
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            input {
-                id: "key_up_div",
-                onkeyup: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert_eq!(event.data.key().to_string(), "a");
-                    assert_eq!(event.data.code().to_string(), "KeyA");
-                    assert_eq!(event.data.location(), Location::Standard);
-                    assert!(!event.data.is_auto_repeating());
-                    assert!(!event.data.is_composing());
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            input {
-                id: "key_press_div",
-                onkeypress: move |event| {
-                    println!("{:?}", event.data);
-                    assert!(event.data.modifiers().is_empty());
-                    assert_eq!(event.data.key().to_string(), "a");
-                    assert_eq!(event.data.code().to_string(), "KeyA");
-                    assert_eq!(event.data.location(), Location::Standard);
-                    assert!(!event.data.is_auto_repeating());
-                    assert!(!event.data.is_composing());
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            input {
-                id: "focus_in_div",
-                onfocusin: move |event| {
-                    println!("{:?}", event.data);
-                    received_events.with_mut(|x| *x += 1);
-                }
-            }
-            input {
-                id: "focus_out_div",
-                onfocusout: move |event| {
-                    println!("{:?}", event.data);
-                    received_events.with_mut(|x| *x += 1);
-                }
+        }
+    }
+}
+
+fn test_form_submit() -> Element {
+    let mut values = use_signal(HashMap::new);
+
+    utils::mock_event_with_extra(
+        "form-submitter",
+        r#"new Event("submit", { bubbles: true, cancelable: true, composed: true })"#,
+        r#"element.submit();"#,
+    );
+
+    let set_values = move |ev: FormEvent| {
+        values.set(ev.values());
+        values.with_mut(|x| {
+            assert_eq!(x.get("username").unwrap().deref(), "goodbye");
+            assert_eq!(x.get("full-name").unwrap().deref(), "lorem");
+            assert_eq!(x.get("password").unwrap().deref(), "ipsum");
+            assert_eq!(x.get("color").unwrap().deref(), "red");
+        });
+        RECEIVED_EVENTS.with_mut(|x| *x += 1);
+    };
+
+    rsx! {
+        div {
+            h1 { "Form" }
+            form {
+                id: "form-submitter",
+                onsubmit: set_values,
+                input { r#type: "text", name: "username", id: "username", value: "goodbye" }
+                input { r#type: "text", name: "full-name", value: "lorem" }
+                input { r#type: "password", name: "password", value: "ipsum" }
+                input { r#type: "radio", name: "color", value: "red", checked: true }
+                input { r#type: "radio", name: "color", value: "blue" }
+                button { r#type: "submit", value: "Submit", "Submit the form" }
             }
         }
     }
 }
+
+fn test_select_multiple_options() -> Element {
+    utils::mock_event_with_extra(
+        "select-many",
+        r#"new Event("input", { bubbles: true, cancelable: true, composed: true })"#,
+        r#"
+            document.getElementById('usa').selected = true;
+            document.getElementById('canada').selected = true;
+            document.getElementById('mexico').selected = false;
+        "#,
+    );
+
+    rsx! {
+        select {
+            id: "select-many",
+            name: "country",
+            multiple: true,
+            oninput: move |ev| {
+                let values = ev.value();
+                let values = values.split(',').collect::<Vec<_>>();
+                assert_eq!(values, vec!["usa", "canada"]);
+                RECEIVED_EVENTS.with_mut(|x| *x += 1);
+            },
+            option { id: "usa", value: "usa",  "USA" }
+            option { id: "canada", value: "canada",  "Canada" }
+            option { id: "mexico", value: "mexico", selected: true,  "Mexico" }
+        }
+    }
+}

+ 5 - 22
packages/desktop/headless_tests/rendering.rs

@@ -2,28 +2,11 @@ use dioxus::prelude::*;
 use dioxus_core::Element;
 use dioxus_desktop::DesktopContext;
 
-fn main() {
-    check_app_exits(check_html_renders);
-}
+#[path = "./utils.rs"]
+mod utils;
 
-pub(crate) fn check_app_exits(app: fn() -> Element) {
-    use dioxus_desktop::Config;
-    use tao::window::WindowBuilder;
-    // This is a deadman's switch to ensure that the app exits
-    let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
-    let should_panic_clone = should_panic.clone();
-    std::thread::spawn(move || {
-        std::thread::sleep(std::time::Duration::from_secs(5));
-        if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
-            std::process::exit(exitcode::SOFTWARE);
-        }
-    });
-
-    LaunchBuilder::desktop()
-        .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true)))
-        .launch(app);
-
-    should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
+fn main() {
+    utils::check_app_exits(check_html_renders);
 }
 
 fn use_inner_html(id: &'static str) -> Option<String> {
@@ -31,7 +14,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
 
     use_effect(move || {
         spawn(async move {
-            tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
+            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
 
             let res = eval(&format!(
                 r#"let element = document.getElementById('{}');

+ 56 - 0
packages/desktop/headless_tests/utils.rs

@@ -0,0 +1,56 @@
+#![allow(unused)] // for whatever reason, the compiler is not recognizing the use of these functions
+
+use dioxus::prelude::*;
+use dioxus_core::Element;
+
+pub fn check_app_exits(app: fn() -> Element) {
+    use dioxus_desktop::tao::window::WindowBuilder;
+    use dioxus_desktop::Config;
+    // This is a deadman's switch to ensure that the app exits
+    let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
+    let should_panic_clone = should_panic.clone();
+    std::thread::spawn(move || {
+        std::thread::sleep(std::time::Duration::from_secs(60));
+        if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
+            eprintln!("App did not exit in time");
+            std::process::exit(exitcode::SOFTWARE);
+        }
+    });
+
+    LaunchBuilder::desktop()
+        .with_cfg(Config::new().with_window(WindowBuilder::new().with_visible(true)))
+        .launch(app);
+
+    // Stop deadman's switch
+    should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
+}
+
+pub static EXPECTED_EVENTS: GlobalSignal<usize> = Signal::global(|| 0);
+
+pub fn mock_event(id: &'static str, value: &'static str) {
+    mock_event_with_extra(id, value, "");
+}
+
+pub fn mock_event_with_extra(id: &'static str, value: &'static str, extra: &'static str) {
+    use_hook(move || {
+        EXPECTED_EVENTS.with_mut(|x| *x += 1);
+
+        spawn(async move {
+            // We need to wait for edits to be applied before we can send the event
+            // Sometimes (windows...) this takes a while
+            // we should really be running this check when mounted
+            tokio::time::sleep(std::time::Duration::from_millis(10000)).await;
+
+            let js = format!(
+                r#"
+                let event = {value};
+                let element = document.getElementById('{id}');
+                {extra}
+                element.dispatchEvent(event);
+                "#
+            );
+
+            eval(&js).await.unwrap();
+        });
+    })
+}

+ 61 - 47
packages/desktop/src/app.rs

@@ -2,20 +2,15 @@ use crate::{
     config::{Config, WindowCloseBehaviour},
     element::DesktopElement,
     event_handlers::WindowEventHandlers,
-    file_upload::FileDialogRequest,
-    ipc::IpcMessage,
-    ipc::{EventData, UserWindowEvent},
+    file_upload::{DesktopFileDragEvent, DesktopFileUploadForm, FileDialogRequest},
+    ipc::{IpcMessage, UserWindowEvent},
     query::QueryResult,
-    shortcut::{GlobalHotKeyEvent, ShortcutRegistry},
+    shortcut::ShortcutRegistry,
     webview::WebviewInstance,
 };
-use crossbeam_channel::Receiver;
 use dioxus_core::ElementId;
 use dioxus_core::VirtualDom;
-use dioxus_html::{
-    native_bind::NativeFileEngine, FileEngine, HasFileData, HasFormData, HtmlEvent,
-    PlatformEventData,
-};
+use dioxus_html::{native_bind::NativeFileEngine, HasFileData, HtmlEvent, PlatformEventData};
 use std::{
     cell::{Cell, RefCell},
     collections::HashMap,
@@ -54,7 +49,6 @@ pub(crate) struct SharedContext {
     pub(crate) event_handlers: WindowEventHandlers,
     pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
     pub(crate) shortcut_manager: ShortcutRegistry,
-    pub(crate) global_hotkey_channel: Receiver<GlobalHotKeyEvent>,
     pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
     pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
 }
@@ -74,7 +68,6 @@ impl App {
                 event_handlers: WindowEventHandlers::default(),
                 pending_webviews: Default::default(),
                 shortcut_manager: ShortcutRegistry::new(),
-                global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
                 proxy: event_loop.create_proxy(),
                 target: event_loop.clone(),
             }),
@@ -83,6 +76,10 @@ impl App {
         // Set the event converter
         dioxus_html::set_event_converter(Box::new(crate::events::SerializedHtmlEventConverter));
 
+        // Wire up the global hotkey handler
+        #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+        app.set_global_hotkey_handler();
+
         // Allow hotreloading to work - but only in debug mode
         #[cfg(all(feature = "hot-reload", debug_assertions))]
         app.connect_hotreload();
@@ -92,14 +89,14 @@ impl App {
 
     pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
         self.control_flow = ControlFlow::Wait;
-
         self.shared
             .event_handlers
             .apply_event(window_event, &self.shared.target);
+    }
 
-        if let Ok(event) = self.shared.global_hotkey_channel.try_recv() {
-            self.shared.shortcut_manager.call_handlers(event);
-        }
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    pub fn handle_global_hotkey(&self, event: global_hotkey::GlobalHotKeyEvent) {
+        self.shared.shortcut_manager.call_handlers(event);
     }
 
     #[cfg(all(feature = "hot-reload", debug_assertions))]
@@ -107,10 +104,7 @@ impl App {
         dioxus_hot_reload::connect({
             let proxy = self.shared.proxy.clone();
             move |template| {
-                let _ = proxy.send_event(UserWindowEvent(
-                    EventData::HotReloadEvent(template),
-                    unsafe { WindowId::dummy() },
-                ));
+                let _ = proxy.send_event(UserWindowEvent::HotReloadEvent(template));
             }
         });
     }
@@ -119,10 +113,7 @@ impl App {
         for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
             let id = handler.desktop_context.window.id();
             self.webviews.insert(id, handler);
-            _ = self
-                .shared
-                .proxy
-                .send_event(UserWindowEvent(EventData::Poll, id));
+            _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
         }
     }
 
@@ -172,11 +163,6 @@ impl App {
 
         let id = webview.desktop_context.window.id();
         self.webviews.insert(id, webview);
-
-        _ = self
-            .shared
-            .proxy
-            .send_event(UserWindowEvent(EventData::Poll, id));
     }
 
     pub fn handle_browser_open(&mut self, msg: IpcMessage) {
@@ -190,19 +176,29 @@ impl App {
         }
     }
 
+    /// The webview is finally loaded
+    ///
+    /// Let's rebuild it and then start polling it
     pub fn handle_initialize_msg(&mut self, id: WindowId) {
         let view = self.webviews.get_mut(&id).unwrap();
+
         view.dom
             .rebuild(&mut *view.desktop_context.mutation_state.borrow_mut());
+
         view.desktop_context.send_edits();
+
         view.desktop_context
             .window
             .set_visible(self.is_visible_before_start);
+
+        _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id));
     }
 
+    /// Todo: maybe we should poll the virtualdom asking if it has any final actions to apply before closing the webview
+    ///
+    /// Technically you can handle this with the use_window_event hook
     pub fn handle_close_msg(&mut self, id: WindowId) {
         self.webviews.remove(&id);
-
         if self.webviews.is_empty() {
             self.control_flow = ControlFlow::Exit
         }
@@ -235,6 +231,7 @@ impl App {
 
         let view = self.webviews.get_mut(&id).unwrap();
         let query = view.desktop_context.query.clone();
+        let recent_file = view.desktop_context.file_hover.clone();
 
         // check for a mounted event placeholder and replace it with a desktop specific element
         let as_any = match data {
@@ -242,6 +239,23 @@ impl App {
                 let element = DesktopElement::new(element, view.desktop_context.clone(), query);
                 Rc::new(PlatformEventData::new(Box::new(element)))
             }
+            dioxus_html::EventData::Drag(ref drag) => {
+                // we want to override this with a native file engine, provided by the most recent drag event
+                if drag.files().is_some() {
+                    let file_event = recent_file.current().unwrap();
+                    let paths = match file_event {
+                        wry::FileDropEvent::Hovered { paths, .. } => paths,
+                        wry::FileDropEvent::Dropped { paths, .. } => paths,
+                        _ => vec![],
+                    };
+                    Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
+                        mouse: drag.mouse.clone(),
+                        files: Arc::new(NativeFileEngine::new(paths)),
+                    })))
+                } else {
+                    data.into_any()
+                }
+            }
             _ => data.into_any(),
         };
 
@@ -270,30 +284,17 @@ impl App {
         let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
             return;
         };
-        struct DesktopFileUploadForm {
-            files: Arc<NativeFileEngine>,
-        }
-
-        impl HasFileData for DesktopFileUploadForm {
-            fn files(&self) -> Option<Arc<dyn FileEngine>> {
-                Some(self.files.clone())
-            }
-        }
-
-        impl HasFormData for DesktopFileUploadForm {
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-        }
 
         let id = ElementId(file_dialog.target);
         let event_name = &file_dialog.event;
         let event_bubbles = file_dialog.bubbles;
         let files = file_dialog.get_file_event();
 
-        let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
+        let as_any = Box::new(DesktopFileUploadForm {
             files: Arc::new(NativeFileEngine::new(files)),
-        })));
+        });
+
+        let data = Rc::new(PlatformEventData::new(as_any));
 
         let view = self.webviews.get_mut(&window).unwrap();
 
@@ -322,6 +323,20 @@ impl App {
 
         view.poll_vdom();
     }
+
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    fn set_global_hotkey_handler(&self) {
+        let receiver = self.shared.proxy.clone();
+
+        // The event loop becomes the hotkey receiver
+        // This means we don't need to poll the receiver on every tick - we just get the events as they come in
+        // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
+        // receiver will become inert.
+        global_hotkey::GlobalHotKeyEvent::set_event_handler(Some(move |t| {
+            // todo: should we unset the event handler when the app shuts down?
+            _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
+        }));
+    }
 }
 
 /// Different hide implementations per platform
@@ -331,7 +346,6 @@ pub fn hide_app_window(window: &wry::WebView) {
     {
         use tao::platform::windows::WindowExtWindows;
         window.set_visible(false);
-        // window.set_skip_taskbar(true);
     }
 
     #[cfg(target_os = "linux")]

+ 2 - 19
packages/desktop/src/config.rs

@@ -1,11 +1,7 @@
 use std::borrow::Cow;
 use std::path::PathBuf;
-
-use tao::window::{Icon, WindowBuilder, WindowId};
-use wry::{
-    http::{Request as HttpRequest, Response as HttpResponse},
-    FileDropEvent,
-};
+use tao::window::{Icon, WindowBuilder};
+use wry::http::{Request as HttpRequest, Response as HttpResponse};
 
 /// The behaviour of the application when the last window is closed.
 #[derive(Copy, Clone, Eq, PartialEq)]
@@ -21,7 +17,6 @@ pub enum WindowCloseBehaviour {
 /// The configuration for the desktop application.
 pub struct Config {
     pub(crate) window: WindowBuilder,
-    pub(crate) file_drop_handler: Option<DropHandler>,
     pub(crate) protocols: Vec<WryProtocol>,
     pub(crate) pre_rendered: Option<String>,
     pub(crate) disable_context_menu: bool,
@@ -35,8 +30,6 @@ pub struct Config {
     pub(crate) enable_default_menu_bar: bool,
 }
 
-type DropHandler = Box<dyn Fn(WindowId, FileDropEvent) -> bool>;
-
 pub(crate) type WryProtocol = (
     String,
     Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
@@ -56,7 +49,6 @@ impl Config {
         Self {
             window,
             protocols: Vec::new(),
-            file_drop_handler: None,
             pre_rendered: None,
             disable_context_menu: !cfg!(debug_assertions),
             resource_dir: None,
@@ -118,15 +110,6 @@ impl Config {
         self
     }
 
-    /// Set a file drop handler. If this is enabled, html drag events will be disabled.
-    pub fn with_file_drop_handler(
-        mut self,
-        handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static,
-    ) -> Self {
-        self.file_drop_handler = Some(Box::new(handler));
-        self
-    }
-
     /// Set a custom protocol
     pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
     where

+ 13 - 11
packages/desktop/src/desktop_context.rs

@@ -2,7 +2,8 @@ use crate::{
     app::SharedContext,
     assets::AssetHandlerRegistry,
     edits::EditQueue,
-    ipc::{EventData, UserWindowEvent},
+    file_upload::NativeFileHover,
+    ipc::UserWindowEvent,
     query::QueryEngine,
     shortcut::{HotKey, ShortcutHandle, ShortcutRegistryError},
     webview::WebviewInstance,
@@ -13,7 +14,10 @@ use dioxus_core::{
     VirtualDom,
 };
 use dioxus_interpreter_js::MutationState;
-use std::{cell::RefCell, rc::Rc, rc::Weak};
+use std::{
+    cell::RefCell,
+    rc::{Rc, Weak},
+};
 use tao::{
     event::Event,
     event_loop::EventLoopWindowTarget,
@@ -62,6 +66,7 @@ pub struct DesktopService {
     pub(crate) edit_queue: EditQueue,
     pub(crate) mutation_state: RefCell<MutationState>,
     pub(crate) asset_handlers: AssetHandlerRegistry,
+    pub(crate) file_hover: NativeFileHover,
 
     #[cfg(target_os = "ios")]
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -83,14 +88,16 @@ impl DesktopService {
         shared: Rc<SharedContext>,
         edit_queue: EditQueue,
         asset_handlers: AssetHandlerRegistry,
+        file_hover: NativeFileHover,
     ) -> Self {
         Self {
             window,
             webview,
             shared,
             edit_queue,
-            mutation_state: Default::default(),
             asset_handlers,
+            file_hover,
+            mutation_state: Default::default(),
             query: Default::default(),
             #[cfg(target_os = "ios")]
             views: Default::default(),
@@ -122,12 +129,7 @@ impl DesktopService {
 
         self.shared
             .proxy
-            .send_event(UserWindowEvent(EventData::NewWindow, cx.id()))
-            .unwrap();
-
-        self.shared
-            .proxy
-            .send_event(UserWindowEvent(EventData::Poll, cx.id()))
+            .send_event(UserWindowEvent::NewWindow)
             .unwrap();
 
         self.shared.pending_webviews.borrow_mut().push(window);
@@ -159,7 +161,7 @@ impl DesktopService {
         let _ = self
             .shared
             .proxy
-            .send_event(UserWindowEvent(EventData::CloseWindow, self.id()));
+            .send_event(UserWindowEvent::CloseWindow(self.id()));
     }
 
     /// Close a particular window, given its ID
@@ -167,7 +169,7 @@ impl DesktopService {
         let _ = self
             .shared
             .proxy
-            .send_event(UserWindowEvent(EventData::CloseWindow, id));
+            .send_event(UserWindowEvent::CloseWindow(id));
     }
 
     /// change window to fullscreen

+ 18 - 3
packages/desktop/src/events.rs

@@ -1,6 +1,9 @@
 //! Convert a serialized event to an event trigger
 
-use crate::element::DesktopElement;
+use crate::{
+    element::DesktopElement,
+    file_upload::{DesktopFileDragEvent, DesktopFileUploadForm},
+};
 use dioxus_html::*;
 
 pub(crate) struct SerializedHtmlEventConverter;
@@ -31,8 +34,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
     }
 
     fn convert_drag_data(&self, event: &PlatformEventData) -> DragData {
+        // Attempt a simple serialized data conversion
+        if let Some(_data) = event.downcast::<SerializedDragData>() {
+            return _data.clone().into();
+        }
+
+        // If that failed then it's a file drag form
         event
-            .downcast::<SerializedDragData>()
+            .downcast::<DesktopFileDragEvent>()
             .cloned()
             .unwrap()
             .into()
@@ -47,8 +56,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
     }
 
     fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
+        // Attempt a simple serialized form data conversion
+        if let Some(_data) = event.downcast::<SerializedFormData>() {
+            return _data.clone().into();
+        }
+
+        // If that failed then it's a file upload form
         event
-            .downcast::<SerializedFormData>()
+            .downcast::<DesktopFileUploadForm>()
             .cloned()
             .unwrap()
             .into()

+ 114 - 1
packages/desktop/src/file_upload.rs

@@ -1,7 +1,25 @@
 #![allow(unused)]
 
+use dioxus_html::{
+    geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint},
+    input_data::{MouseButton, MouseButtonSet},
+    native_bind::NativeFileEngine,
+    point_interaction::{
+        InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
+    },
+    prelude::{SerializedMouseData, SerializedPointInteraction},
+    FileEngine, HasDragData, HasFileData, HasFormData, HasMouseData,
+};
+
 use serde::Deserialize;
-use std::{path::PathBuf, str::FromStr};
+use std::{
+    cell::{Cell, RefCell},
+    path::PathBuf,
+    rc::Rc,
+    str::FromStr,
+    sync::Arc,
+};
+use wry::FileDropEvent;
 
 #[derive(Debug, Deserialize)]
 pub(crate) struct FileDialogRequest {
@@ -124,3 +142,98 @@ impl FromStr for Filters {
         }
     }
 }
+
+#[derive(Clone)]
+pub(crate) struct DesktopFileUploadForm {
+    pub files: Arc<NativeFileEngine>,
+}
+
+impl HasFileData for DesktopFileUploadForm {
+    fn files(&self) -> Option<Arc<dyn FileEngine>> {
+        Some(self.files.clone())
+    }
+}
+
+impl HasFormData for DesktopFileUploadForm {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct NativeFileHover {
+    event: Rc<RefCell<Option<FileDropEvent>>>,
+}
+impl NativeFileHover {
+    pub fn set(&self, event: FileDropEvent) {
+        self.event.borrow_mut().replace(event);
+    }
+
+    pub fn current(&self) -> Option<FileDropEvent> {
+        self.event.borrow_mut().clone()
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct DesktopFileDragEvent {
+    pub mouse: SerializedPointInteraction,
+    pub files: Arc<NativeFileEngine>,
+}
+
+impl HasFileData for DesktopFileDragEvent {
+    fn files(&self) -> Option<Arc<dyn FileEngine>> {
+        Some(self.files.clone())
+    }
+}
+
+impl HasDragData for DesktopFileDragEvent {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
+impl HasMouseData for DesktopFileDragEvent {
+    fn as_any(&self) -> &dyn std::any::Any {
+        self
+    }
+}
+
+impl InteractionLocation for DesktopFileDragEvent {
+    fn client_coordinates(&self) -> ClientPoint {
+        self.mouse.client_coordinates()
+    }
+
+    fn page_coordinates(&self) -> PagePoint {
+        self.mouse.page_coordinates()
+    }
+
+    fn screen_coordinates(&self) -> ScreenPoint {
+        self.mouse.screen_coordinates()
+    }
+}
+
+impl InteractionElementOffset for DesktopFileDragEvent {
+    fn element_coordinates(&self) -> ElementPoint {
+        self.mouse.element_coordinates()
+    }
+
+    fn coordinates(&self) -> Coordinates {
+        self.mouse.coordinates()
+    }
+}
+
+impl ModifiersInteraction for DesktopFileDragEvent {
+    fn modifiers(&self) -> dioxus_html::prelude::Modifiers {
+        self.mouse.modifiers()
+    }
+}
+
+impl PointerInteraction for DesktopFileDragEvent {
+    fn held_buttons(&self) -> MouseButtonSet {
+        self.mouse.held_buttons()
+    }
+
+    fn trigger_button(&self) -> Option<MouseButton> {
+        self.mouse.trigger_button()
+    }
+}

+ 8 - 10
packages/desktop/src/ipc.rs

@@ -1,18 +1,17 @@
 use serde::{Deserialize, Serialize};
 use tao::window::WindowId;
 
-/// A pair of data
 #[derive(Debug, Clone)]
-pub struct UserWindowEvent(pub EventData, pub WindowId);
+pub enum UserWindowEvent {
+    /// A global hotkey event
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    GlobalHotKeyEvent(global_hotkey::GlobalHotKeyEvent),
 
-/// The data that might eminate from any window/webview
-#[derive(Debug, Clone)]
-pub enum EventData {
     /// Poll the virtualdom
-    Poll,
+    Poll(WindowId),
 
     /// Handle an ipc message eminating from the window.postMessage of a given webview
-    Ipc(IpcMessage),
+    Ipc { id: WindowId, msg: IpcMessage },
 
     /// Handle a hotreload event, basically telling us to update our templates
     #[cfg(all(feature = "hot-reload", debug_assertions))]
@@ -22,7 +21,7 @@ pub enum EventData {
     NewWindow,
 
     /// Close a given window (could be any window!)
-    CloseWindow,
+    CloseWindow(WindowId),
 }
 
 /// A message struct that manages the communication between the webview and the eventloop code
@@ -48,8 +47,7 @@ pub enum IpcMethod<'a> {
 impl IpcMessage {
     pub(crate) fn method(&self) -> IpcMethod {
         match self.method.as_str() {
-            // todo: this is a misspelling, needs to be fixed
-            "file_diolog" => IpcMethod::FileDialog,
+            "file_dialog" => IpcMethod::FileDialog,
             "user_event" => IpcMethod::UserEvent,
             "query" => IpcMethod::Query,
             "browser_open" => IpcMethod::BrowserOpen,

+ 15 - 8
packages/desktop/src/launch.rs

@@ -1,7 +1,7 @@
 pub use crate::Config;
 use crate::{
     app::App,
-    ipc::{EventData, IpcMethod, UserWindowEvent},
+    ipc::{IpcMethod, UserWindowEvent},
 };
 use dioxus_core::*;
 use std::any::Any;
@@ -15,6 +15,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
     let (event_loop, mut app) = App::new(desktop_config, virtual_dom);
 
     event_loop.run(move |window_event, _, control_flow| {
+        // Set the control flow and check if any events need to be handled in the app itself
         app.tick(&window_event);
 
         match window_event {
@@ -26,18 +27,24 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
                 WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
                 _ => {}
             },
-            Event::UserEvent(UserWindowEvent(event, id)) => match event {
-                EventData::Poll => app.poll_vdom(id),
-                EventData::NewWindow => app.handle_new_window(),
-                EventData::CloseWindow => app.handle_close_msg(id),
+
+            Event::UserEvent(event) => match event {
+                UserWindowEvent::Poll(id) => app.poll_vdom(id),
+                UserWindowEvent::NewWindow => app.handle_new_window(),
+                UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id),
+
+                #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+                UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt),
+
                 #[cfg(all(feature = "hot-reload", debug_assertions))]
-                EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
-                EventData::Ipc(msg) => match msg.method() {
+                UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
+
+                UserWindowEvent::Ipc { id, msg } => match msg.method() {
+                    IpcMethod::Initialize => app.handle_initialize_msg(id),
                     IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
                     IpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
                     IpcMethod::Query => app.handle_query_msg(msg, id),
                     IpcMethod::BrowserOpen => app.handle_browser_open(msg),
-                    IpcMethod::Initialize => app.handle_initialize_msg(id),
                     IpcMethod::Other(_) => {}
                 },
             },

+ 0 - 6
packages/desktop/src/mobile_shortcut.rs

@@ -80,10 +80,4 @@ pub struct GlobalHotKeyEvent {
     pub id: u32,
 }
 
-impl GlobalHotKeyEvent {
-    pub fn receiver() -> crossbeam_channel::Receiver<GlobalHotKeyEvent> {
-        crossbeam_channel::unbounded().1
-    }
-}
-
 pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code;

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

@@ -1,82 +1,17 @@
 use crate::{assets::*, edits::EditQueue};
-use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
+use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
+use dioxus_interpreter_js::NATIVE_JS;
 use std::path::{Path, PathBuf};
 use wry::{
     http::{status::StatusCode, Request, Response},
     RequestAsyncResponder, Result,
 };
 
-fn handle_edits_code() -> String {
-    const EDITS_PATH: &str = {
-        #[cfg(any(target_os = "android", target_os = "windows"))]
-        {
-            "http://dioxus.index.html/edits"
-        }
-        #[cfg(not(any(target_os = "android", target_os = "windows")))]
-        {
-            "dioxus://index.html/edits"
-        }
-    };
+#[cfg(any(target_os = "android", target_os = "windows"))]
+const EDITS_PATH: &str = "http://dioxus.index.html/edits";
 
-    let prevent_file_upload = r#"// Prevent file inputs from opening the file dialog on click
-    let inputs = document.querySelectorAll("input");
-    for (let input of inputs) {
-      if (!input.getAttribute("data-dioxus-file-listener")) {
-        // prevent file inputs from opening the file dialog on click
-        const type = input.getAttribute("type");
-        if (type === "file") {
-          input.setAttribute("data-dioxus-file-listener", true);
-          input.addEventListener("click", (event) => {
-            let target = event.target;
-            let target_id = find_real_id(target);
-            if (target_id !== null) {
-              const send = (event_name) => {
-                const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
-                window.ipc.postMessage(message);
-              };
-              send("change&input");
-            }
-            event.preventDefault();
-          });
-        }
-      }
-    }"#;
-    let polling_request = format!(
-        r#"// Poll for requests
-    window.interpreter = new JSChannel();
-    window.interpreter.wait_for_request = (headless) => {{
-      fetch(new Request("{EDITS_PATH}"))
-          .then(response => {{
-              response.arrayBuffer()
-                  .then(bytes => {{
-                      // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
-                      if (headless) {{
-                        window.interpreter.run_from_bytes(bytes);
-                      }}
-                      else {{
-                        requestAnimationFrame(() => {{
-                            window.interpreter.run_from_bytes(bytes);
-                        }});
-                      }}
-                      window.interpreter.wait_for_request(headless);
-                  }});
-          }})
-    }}"#
-    );
-    let mut interpreter = SLEDGEHAMMER_JS
-        .replace("/*POST_HANDLE_EDITS*/", prevent_file_upload)
-        .replace("export", "")
-        + &polling_request;
-    while let Some(import_start) = interpreter.find("import") {
-        let import_end = interpreter[import_start..]
-            .find(|c| c == ';' || c == '\n')
-            .map(|i| i + import_start)
-            .unwrap_or_else(|| interpreter.len());
-        interpreter.replace_range(import_start..import_end, "");
-    }
-
-    format!("{interpreter}\nconst intercept_link_redirects = true;")
-}
+#[cfg(not(any(target_os = "android", target_os = "windows")))]
+const EDITS_PATH: &str = "dioxus://index.html/edits";
 
 static DEFAULT_INDEX: &str = include_str!("./index.html");
 
@@ -115,6 +50,7 @@ pub(super) fn index_request(
         }
         None => assets_head(),
     };
+
     if let Some(head) = head {
         index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
     }
@@ -237,20 +173,26 @@ fn serve_from_fs(path: PathBuf) -> Result<Response<Vec<u8>>> {
 /// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
 ///             interpreter can't connect until the window is ready.
 fn module_loader(root_id: &str, headless: bool) -> String {
-    let js = handle_edits_code();
     format!(
         r#"
 <script type="module">
-    {js}
-    // Wait for the page to load
+    // Bring the sledgehammer code
+    {SLEDGEHAMMER_JS}
+
+    // And then extend it with our native bindings
+    {NATIVE_JS}
+
+    // The nativeinterprerter extends the sledgehammer interpreter with a few extra methods that we use for IPC
+    window.interpreter = new NativeInterpreter("{EDITS_PATH}");
+
+    // Wait for the page to load before sending the initialize message
     window.onload = function() {{
-        let rootname = "{root_id}";
-        let root_element = window.document.getElementById(rootname);
+        let root_element = window.document.getElementById("{root_id}");
         if (root_element != null) {{
             window.interpreter.initialize(root_element);
             window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
         }}
-        window.interpreter.wait_for_request({headless});
+        window.interpreter.waitForRequest({headless});
     }}
 </script>
 "#

+ 6 - 3
packages/desktop/src/query.rs

@@ -1,13 +1,17 @@
-use std::{cell::RefCell, rc::Rc};
-
 use crate::DesktopContext;
 use futures_util::{FutureExt, StreamExt};
 use generational_box::Owner;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use slab::Slab;
+use std::{cell::RefCell, rc::Rc};
 use thiserror::Error;
 
+/*
+todo:
+- write this in the interpreter itself rather than in blobs of inline javascript...
+- it could also be simpler, probably?
+*/
 const DIOXUS_CODE: &str = r#"
 let dioxus = {
     recv: function () {
@@ -43,7 +47,6 @@ let dioxus = {
 }"#;
 
 /// Tracks what query ids are currently active
-
 pub(crate) struct SharedSlab<T = ()> {
     pub slab: Rc<RefCell<Slab<T>>>,
 }

+ 1 - 0
packages/desktop/src/shortcut.rs

@@ -64,6 +64,7 @@ impl ShortcutRegistry {
         }
     }
 
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
     pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) {
         if let Some(ShortcutInner { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) {
             for (_, callback) in callbacks.iter_mut() {

+ 2 - 2
packages/desktop/src/waker.rs

@@ -1,4 +1,4 @@
-use crate::ipc::{EventData, UserWindowEvent};
+use crate::ipc::UserWindowEvent;
 use futures_util::task::ArcWake;
 use std::sync::Arc;
 use tao::{event_loop::EventLoopProxy, window::WindowId};
@@ -24,7 +24,7 @@ pub fn tao_waker(proxy: EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::t
         fn wake_by_ref(arc_self: &Arc<Self>) {
             _ = arc_self
                 .proxy
-                .send_event(UserWindowEvent(EventData::Poll, arc_self.id));
+                .send_event(UserWindowEvent::Poll(arc_self.id));
         }
     }
 

+ 25 - 23
packages/desktop/src/webview.rs

@@ -1,12 +1,7 @@
 use crate::{
-    app::SharedContext,
-    assets::AssetHandlerRegistry,
-    edits::EditQueue,
-    eval::DesktopEvalProvider,
-    ipc::{EventData, UserWindowEvent},
-    protocol::{self},
-    waker::tao_waker,
-    Config, DesktopContext, DesktopService,
+    app::SharedContext, assets::AssetHandlerRegistry, edits::EditQueue, eval::DesktopEvalProvider,
+    file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
+    DesktopContext, DesktopService,
 };
 use dioxus_core::{ScopeId, VirtualDom};
 use dioxus_html::prelude::EvalProvider;
@@ -60,18 +55,19 @@ impl WebviewInstance {
 
         let mut web_context = WebContext::new(cfg.data_dir.clone());
         let edit_queue = EditQueue::default();
+        let file_hover = NativeFileHover::default();
         let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
         let headless = !cfg.window.window.visible;
 
         // Rust :(
         let window_id = window.id();
-        let file_handler = cfg.file_drop_handler.take();
         let custom_head = cfg.custom_head.clone();
         let index_file = cfg.custom_index.clone();
         let root_name = cfg.root_name.clone();
         let asset_handlers_ = asset_handlers.clone();
         let edit_queue_ = edit_queue.clone();
         let proxy_ = shared.proxy.clone();
+        let file_hover_ = file_hover.clone();
 
         let request_handler = move |request, responder: RequestAsyncResponder| {
             // Try to serve the index file first
@@ -97,11 +93,18 @@ impl WebviewInstance {
 
         let ipc_handler = move |payload: String| {
             // defer the event to the main thread
-            if let Ok(message) = serde_json::from_str(&payload) {
-                _ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
+            if let Ok(msg) = serde_json::from_str(&payload) {
+                _ = proxy_.send_event(UserWindowEvent::Ipc { id: window_id, msg });
             }
         };
 
+        let file_drop_handler = move |evt| {
+            // Update the most recent file drop event - when the event comes in from the webview we can use the
+            // most recent event to build a new event with the files in it.
+            file_hover_.set(evt);
+            false
+        };
+
         #[cfg(any(
             target_os = "windows",
             target_os = "macos",
@@ -128,12 +131,10 @@ impl WebviewInstance {
             .with_url("dioxus://index.html/")
             .unwrap()
             .with_ipc_handler(ipc_handler)
+            .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations
             .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
-            .with_web_context(&mut web_context);
-
-        if let Some(handler) = file_handler {
-            webview = webview.with_file_drop_handler(move |evt| handler(window_id, evt))
-        }
+            .with_web_context(&mut web_context)
+            .with_file_drop_handler(file_drop_handler);
 
         if let Some(color) = cfg.background_color {
             webview = webview.with_background_color(color);
@@ -145,15 +146,15 @@ impl WebviewInstance {
 
         const INITIALIZATION_SCRIPT: &str = r#"
         if (document.addEventListener) {
-        document.addEventListener('contextmenu', function(e) {
-            e.preventDefault();
-        }, false);
+            document.addEventListener('contextmenu', function(e) {
+                e.preventDefault();
+            }, false);
         } else {
-        document.attachEvent('oncontextmenu', function() {
-            window.event.returnValue = false;
-        });
+            document.attachEvent('oncontextmenu', function() {
+                window.event.returnValue = false;
+            });
         }
-    "#;
+        "#;
 
         if cfg.disable_context_menu {
             // in release mode, we don't want to show the dev tool or reload menus
@@ -178,6 +179,7 @@ impl WebviewInstance {
             shared.clone(),
             edit_queue,
             asset_handlers,
+            file_hover,
         ));
 
         let provider: Rc<dyn EvalProvider> =

+ 4 - 0
packages/html/src/events.rs

@@ -59,6 +59,10 @@ impl PlatformEventData {
         Self { event }
     }
 
+    pub fn inner(&self) -> &Box<dyn Any> {
+        &self.event
+    }
+
     pub fn downcast<T: 'static>(&self) -> Option<&T> {
         self.event.downcast_ref::<T>()
     }

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

@@ -109,7 +109,9 @@ impl PointerInteraction for DragData {
 /// A serialized version of DragData
 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
 pub struct SerializedDragData {
-    mouse: crate::point_interaction::SerializedPointInteraction,
+    pub mouse: crate::point_interaction::SerializedPointInteraction,
+
+    #[serde(default)]
     files: Option<crate::file_data::SerializedFileEngine>,
 }
 

+ 33 - 51
packages/html/src/events/form.rs

@@ -1,56 +1,11 @@
 use crate::file_data::FileEngine;
 use crate::file_data::HasFileData;
-use std::ops::Deref;
 use std::{collections::HashMap, fmt::Debug};
 
 use dioxus_core::Event;
 
 pub type FormEvent = Event<FormData>;
 
-/// A form value that may either be a list of values or a single value
-#[cfg_attr(
-    feature = "serialize",
-    derive(serde::Serialize, serde::Deserialize),
-    // this will serialize Text(String) -> String and VecText(Vec<String>) to Vec<String>
-    serde(untagged)
-)]
-#[derive(Debug, Clone, PartialEq)]
-pub enum FormValue {
-    Text(String),
-    VecText(Vec<String>),
-}
-
-impl From<FormValue> for Vec<String> {
-    fn from(value: FormValue) -> Self {
-        match value {
-            FormValue::Text(s) => vec![s],
-            FormValue::VecText(vec) => vec,
-        }
-    }
-}
-
-impl Deref for FormValue {
-    type Target = [String];
-
-    fn deref(&self) -> &Self::Target {
-        self.as_slice()
-    }
-}
-
-impl FormValue {
-    /// Convenient way to represent Value as slice
-    pub fn as_slice(&self) -> &[String] {
-        match self {
-            FormValue::Text(s) => std::slice::from_ref(s),
-            FormValue::VecText(vec) => vec.as_slice(),
-        }
-    }
-    /// Convert into Vec<String>
-    pub fn to_vec(self) -> Vec<String> {
-        self.into()
-    }
-}
-
 /* DOMEvent:  Send + SyncTarget relatedTarget */
 pub struct FormData {
     inner: Box<dyn HasFormData>,
@@ -73,6 +28,7 @@ impl Debug for FormData {
         f.debug_struct("FormEvent")
             .field("value", &self.value())
             .field("values", &self.values())
+            .field("valid", &self.valid())
             .finish()
     }
 }
@@ -106,8 +62,10 @@ impl FormData {
         self.value().parse().unwrap_or(false)
     }
 
-    /// Get the values of the form event
-    pub fn values(&self) -> HashMap<String, FormValue> {
+    /// Collect all the named form values from the containing form.
+    ///
+    /// Every input must be named!
+    pub fn values(&self) -> HashMap<String, String> {
         self.inner.values()
     }
 
@@ -120,6 +78,11 @@ impl FormData {
     pub fn downcast<T: 'static>(&self) -> Option<&T> {
         self.inner.as_any().downcast_ref::<T>()
     }
+
+    /// Did this form pass its own validation?
+    pub fn valid(&self) -> bool {
+        self.inner.value().is_empty()
+    }
 }
 
 /// An object that has all the data for a form event
@@ -128,7 +91,11 @@ pub trait HasFormData: HasFileData + std::any::Any {
         Default::default()
     }
 
-    fn values(&self) -> HashMap<String, FormValue> {
+    fn valid(&self) -> bool {
+        true
+    }
+
+    fn values(&self) -> HashMap<String, String> {
         Default::default()
     }
 
@@ -164,8 +131,16 @@ impl FormData {
 /// A serialized form data object
 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
 pub struct SerializedFormData {
+    #[serde(default)]
     value: String,
-    values: HashMap<String, FormValue>,
+
+    #[serde(default)]
+    values: HashMap<String, String>,
+
+    #[serde(default)]
+    valid: bool,
+
+    #[serde(default)]
     files: Option<crate::file_data::SerializedFileEngine>,
 }
 
@@ -174,13 +149,14 @@ impl SerializedFormData {
     /// Create a new serialized form data object
     pub fn new(
         value: String,
-        values: HashMap<String, FormValue>,
+        values: HashMap<String, String>,
         files: Option<crate::file_data::SerializedFileEngine>,
     ) -> Self {
         Self {
             value,
             values,
             files,
+            valid: true,
         }
     }
 
@@ -189,6 +165,7 @@ impl SerializedFormData {
         Self {
             value: data.value(),
             values: data.values(),
+            valid: data.valid(),
             files: match data.files() {
                 Some(files) => {
                     let mut resolved_files = HashMap::new();
@@ -211,6 +188,7 @@ impl SerializedFormData {
         Self {
             value: data.value(),
             values: data.values(),
+            valid: data.valid(),
             files: None,
         }
     }
@@ -222,10 +200,14 @@ impl HasFormData for SerializedFormData {
         self.value.clone()
     }
 
-    fn values(&self) -> HashMap<String, FormValue> {
+    fn values(&self) -> HashMap<String, String> {
         self.values.clone()
     }
 
+    fn valid(&self) -> bool {
+        self.valid
+    }
+
     fn as_any(&self) -> &dyn std::any::Any {
         self
     }

+ 1 - 1
packages/html/src/point_interaction.rs

@@ -50,7 +50,7 @@ pub trait ModifiersInteraction {
 
 #[cfg(feature = "serialize")]
 #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)]
-pub(crate) struct SerializedPointInteraction {
+pub struct SerializedPointInteraction {
     pub alt_key: bool,
 
     /// The button number that was pressed (if applicable) when the mouse event was fired.

+ 14 - 3
packages/html/src/transit.rs

@@ -34,8 +34,19 @@ impl<'de> Deserialize<'de> for HtmlEvent {
             data,
         } = Inner::deserialize(deserializer)?;
 
+        // in debug mode let's try and be helpful as to why the deserialization failed
+        #[cfg(debug_assertions)]
+        {
+            _ = deserialize_raw(&name, data.clone()).unwrap_or_else(|e| {
+                panic!(
+                    "Failed to deserialize event data for event {}:  {:#?}\n'{:#?}'",
+                    name, e, data,
+                )
+            });
+        }
+
         Ok(HtmlEvent {
-            data: fun_name(&name, data).unwrap(),
+            data: deserialize_raw(&name, data).unwrap(),
             element,
             bubbles,
             name,
@@ -44,7 +55,7 @@ impl<'de> Deserialize<'de> for HtmlEvent {
 }
 
 #[cfg(feature = "serialize")]
-fn fun_name(
+fn deserialize_raw(
     name: &str,
     data: serde_value::Value,
 ) -> Result<EventData, serde_value::DeserializerError> {
@@ -136,7 +147,7 @@ fn fun_name(
 #[cfg(feature = "serialize")]
 impl HtmlEvent {
     pub fn bubbles(&self) -> bool {
-        event_bubbles(&self.name)
+        self.bubbles
     }
 }
 

+ 1 - 0
packages/interpreter/.gitignore

@@ -0,0 +1 @@
+gen2/

+ 4 - 1
packages/interpreter/Cargo.toml

@@ -17,13 +17,16 @@ web-sys = { version = "0.3.56", optional = true, features = [
     "Element",
     "Node",
 ] }
-sledgehammer_bindgen = { version = "0.4.0", default-features = false, optional = true }
+sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true }
 sledgehammer_utils = { version = "0.2", optional = true }
 serde = { version = "1.0", features = ["derive"], optional = true }
 
 dioxus-core = { workspace = true, optional = true }
 dioxus-html = { workspace = true, optional = true }
 
+[build-dependencies]
+md5 = "0.7.0"
+
 [features]
 default = []
 serialize = ["serde"]

+ 7 - 0
packages/interpreter/NOTES.md

@@ -0,0 +1,7 @@
+# Notes on the web implementation
+
+Here's some useful resources if you ever need to splunk into the intricacies of how events are handled in HTML:
+
+
+- Not all event handlers are sync: https://w3c.github.io/uievents/#sync-async
+- Some attributes are truthy: https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364

+ 12 - 0
packages/interpreter/README.md

@@ -25,6 +25,18 @@
 
 This crate features bindings for the web and sledgehammer for increased performance.
 
+## Architecture
+
+We use TypeScript to write the bindings and a very simple build.rs along with bun to convert them to javascript, minify them, and glue them into the rest of the project.
+
+Not every snippet of JS will be used, so we split out the snippets from the core interpreter.
+
+In theory, we *could* use Rust in the browser to do everything these bindings are doing. In reality, we want to stick with JS to skip the need for a WASM build step when running the LiveView and WebView renderers. We also want to use JS to prevent diverging behavior of things like canceling events, uploading files, and collecting form inputs. These details are tough to ensure 1:1 compatibility when implementing them in two languages.
+
+If you want to contribute to the bindings, you'll need to have the typescript compiler installed on your machine as well as bun:
+
+https://bun.sh/docs/installation
+
 ## Contributing
 
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).

+ 76 - 0
packages/interpreter/build.rs

@@ -0,0 +1,76 @@
+use std::process::Command;
+
+fn main() {
+    // If any TS changes, re-run the build script
+    println!("cargo:rerun-if-changed=src/ts/*.ts");
+
+    // Compute the hash of the ts files
+    let hash = hash_ts_files();
+
+    // If the hash matches the one on disk, we're good and don't need to update bindings
+    let expected = include_str!("src/js/hash.txt").trim();
+    if expected == hash.to_string() {
+        return;
+    }
+
+    // Otherwise, generate the bindings and write the new hash to disk
+    // Generate the bindings for both native and web
+    gen_bindings("common", "common");
+    gen_bindings("native", "native");
+    gen_bindings("core", "core");
+
+    std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
+}
+
+/// Hashes the contents of a directory
+fn hash_ts_files() -> u128 {
+    let mut out = 0;
+
+    let files = [
+        include_str!("src/ts/common.ts"),
+        include_str!("src/ts/native.ts"),
+        include_str!("src/ts/core.ts"),
+    ];
+
+    // Let's make the dumbest hasher by summing the bytes of the files
+    // The location is multiplied by the byte value to make sure that the order of the bytes matters
+    let mut idx = 0;
+    for file in files {
+        // windows + git does a weird thing with line endings, so we need to normalize them
+        for line in file.lines() {
+            idx += 1;
+            for byte in line.bytes() {
+                idx += 1;
+                out += (byte as u128) * (idx as u128);
+            }
+        }
+    }
+    out
+}
+
+// okay...... so tsc might fail if the user doesn't have it installed
+// we don't really want to fail if that's the case
+// but if you started *editing* the .ts files, you're gonna have a bad time
+// so.....
+// we need to hash each of the .ts files and add that hash to the JS files
+// if the hashes don't match, we need to fail the build
+// that way we also don't need
+fn gen_bindings(input_name: &str, output_name: &str) {
+    // If the file is generated, and the hash is different, we need to generate it
+    let status = Command::new("bun")
+        .arg("build")
+        .arg(format!("src/ts/{input_name}.ts"))
+        .arg("--outfile")
+        .arg(format!("src/js/{output_name}.js"))
+        .arg("--minify-whitespace")
+        .arg("--minify-syntax")
+        .status()
+        .unwrap();
+
+    if !status.success() {
+        panic!(
+            "Failed to generate bindings for {}. Make sure you have tsc installed",
+            input_name
+        );
+    }
+}

+ 0 - 79
packages/interpreter/src/common.js

@@ -1,79 +0,0 @@
-this.setAttributeInner = function (node, field, value, ns) {
-  const name = field;
-  if (ns === "style") {
-    // ????? why do we need to do this
-    if (node.style === undefined) {
-      node.style = {};
-    }
-    node.style[name] = value;
-  } else if (!!ns) {
-    node.setAttributeNS(ns, name, value);
-  } else {
-    switch (name) {
-      case "value":
-        if (value !== node.value) {
-          node.value = value;
-        }
-        break;
-      case "initial_value":
-        node.defaultValue = value;
-        break;
-      case "checked":
-        node.checked = truthy(value);
-        break;
-      case "initial_checked":
-        node.defaultChecked = truthy(value);
-        break;
-      case "selected":
-        node.selected = truthy(value);
-        break;
-      case "initial_selected":
-        node.defaultSelected = truthy(value);
-        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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
-          node.removeAttribute(name);
-        } else {
-          node.setAttribute(name, value);
-        }
-    }
-  }
-}
-
-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,
-  webkitdirectory: true,
-};
-
-function truthy(val) {
-  return val === "true" || val === true;
-}

+ 0 - 79
packages/interpreter/src/common_exported.js

@@ -1,79 +0,0 @@
-export function setAttributeInner(node, field, value, ns) {
-  const name = field;
-  if (ns === "style") {
-    // ????? why do we need to do this
-    if (node.style === undefined) {
-      node.style = {};
-    }
-    node.style[name] = value;
-  } else if (!!ns) {
-    node.setAttributeNS(ns, name, value);
-  } else {
-    switch (name) {
-      case "value":
-        if (value !== node.value) {
-          node.value = value;
-        }
-        break;
-      case "initial_value":
-        node.defaultValue = value;
-        break;
-      case "checked":
-        node.checked = truthy(value);
-        break;
-      case "initial_checked":
-        node.defaultChecked = truthy(value);
-        break;
-      case "selected":
-        node.selected = truthy(value);
-        break;
-      case "initial_selected":
-        node.defaultSelected = truthy(value);
-        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 (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
-          node.removeAttribute(name);
-        } else {
-          node.setAttribute(name, value);
-        }
-    }
-  }
-}
-
-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,
-  webkitdirectory: true,
-};
-
-function truthy(val) {
-  return val === "true" || val === true;
-}

+ 0 - 751
packages/interpreter/src/interpreter.js

@@ -1,751 +0,0 @@
-// this handler is only provided on the desktop and liveview implementations since this
-// method is not used by the web implementation
-this.handler = async function (event, name, bubbles) {
-  let target = event.target;
-  if (target != null) {
-    let preventDefaultRequests = null;
-    // Some events can be triggered on text nodes, which don't have attributes
-    if (target instanceof Element) {
-      preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
-    }
-
-    if (event.type === "click") {
-      // todo call prevent default if it's the right type of event
-      if (intercept_link_redirects) {
-        let a_element = target.closest("a");
-        if (a_element != null) {
-          event.preventDefault();
-
-          let elementShouldPreventDefault =
-            preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
-          let aElementShouldPreventDefault = a_element.getAttribute(
-            `dioxus-prevent-default`
-          );
-          let linkShouldPreventDefault =
-            aElementShouldPreventDefault &&
-            aElementShouldPreventDefault.includes(`onclick`);
-
-          if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
-            const href = a_element.getAttribute("href");
-            if (href !== "" && href !== null && href !== undefined) {
-              window.ipc.postMessage(
-                this.serializeIpcMessage("browser_open", { href })
-              );
-            }
-          }
-        }
-      }
-
-      // also prevent buttons from submitting
-      if (target.tagName === "BUTTON" && event.type == "submit") {
-        event.preventDefault();
-      }
-    }
-
-    const realId = find_real_id(target);
-
-    if (
-      preventDefaultRequests &&
-      preventDefaultRequests.includes(`on${event.type}`)
-    ) {
-      event.preventDefault();
-    }
-
-    if (event.type === "submit") {
-      event.preventDefault();
-    }
-
-    let contents = await serialize_event(event);
-
-    // TODO: this should be liveview only
-    if (
-      target.tagName === "INPUT" &&
-      (event.type === "change" || event.type === "input")
-    ) {
-      const type = target.getAttribute("type");
-      if (type === "file") {
-        async function read_files() {
-          const files = target.files;
-          const file_contents = {};
-
-          for (let i = 0; i < files.length; i++) {
-            const file = files[i];
-
-            file_contents[file.name] = Array.from(
-              new Uint8Array(await file.arrayBuffer())
-            );
-          }
-          let file_engine = {
-            files: file_contents,
-          };
-          contents.files = file_engine;
-
-          if (realId === null) {
-            return;
-          }
-          const message = window.interpreter.serializeIpcMessage("user_event", {
-            name: name,
-            element: parseInt(realId),
-            data: contents,
-            bubbles,
-          });
-          window.ipc.postMessage(message);
-        }
-        read_files();
-        return;
-      }
-    }
-
-    if (
-      target.tagName === "FORM" &&
-      (event.type === "submit" || event.type === "input")
-    ) {
-      const formData = new FormData(target);
-
-      for (let name of formData.keys()) {
-        const fieldType = target.elements[name].type;
-
-        switch (fieldType) {
-          case "select-multiple":
-            contents.values[name] = formData.getAll(name);
-            break;
-
-          // add cases for fieldTypes that can hold multiple values here
-          default:
-            contents.values[name] = formData.get(name);
-            break;
-        }
-      }
-    }
-
-    if (
-      target.tagName === "SELECT" &&
-      event.type === "input"
-    ) {
-      const selectData = target.options;
-      contents.values["options"] = [];
-      for (let i = 0; i < selectData.length; i++) {
-        let option = selectData[i];
-        if (option.selected) {
-          contents.values["options"].push(option.value.toString());
-        }
-      }
-    }
-
-    if (realId === null) {
-      return;
-    }
-    window.ipc.postMessage(
-      this.serializeIpcMessage("user_event", {
-        name: name,
-        element: parseInt(realId),
-        data: contents,
-        bubbles,
-      })
-    );
-  }
-}
-
-function find_real_id(target) {
-  let realId = null;
-  if (target instanceof Element) {
-    realId = target.getAttribute(`data-dioxus-id`);
-  }
-  // 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;
-    if (target instanceof Element) {
-      realId = target.getAttribute(`data-dioxus-id`);
-    }
-  }
-  return realId;
-}
-
-class ListenerMap {
-  constructor(root) {
-    // bubbling events can listen at the root element
-    this.global = {};
-    // non bubbling events listen at the element the listener was created at
-    this.local = {};
-    this.root = null;
-  }
-
-  create(event_name, element, bubbles, handler) {
-    if (bubbles) {
-      if (this.global[event_name] === undefined) {
-        this.global[event_name] = {};
-        this.global[event_name].active = 1;
-        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] = {};
-      }
-      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, this.global[event_name].callback);
-    }
-  }
-
-  removeAllNonBubbling(element) {
-    const id = element.getAttribute("data-dioxus-id");
-    delete this.local[id];
-  }
-}
-this.LoadChild = function (array) {
-  // iterate through each number and get that child
-  let node = this.stack[this.stack.length - 1];
-
-  for (let i = 0; i < array.length; i++) {
-    this.end = array[i];
-    for (node = node.firstChild; this.end > 0; this.end--) {
-      node = node.nextSibling;
-    }
-  }
-  return node;
-}
-this.listeners = new ListenerMap();
-this.nodes = [];
-this.stack = [];
-this.templates = {};
-this.end = null;
-
-this.AppendChildren = function (id, many) {
-  let root = this.nodes[id];
-  let els = this.stack.splice(this.stack.length - many);
-  for (let k = 0; k < many; k++) {
-    root.appendChild(els[k]);
-  }
-}
-
-this.initialize = function (root) {
-  this.nodes = [root];
-  this.stack = [root];
-  this.listeners.root = root;
-}
-
-this.getClientRect = function (id) {
-  const node = this.nodes[id];
-  if (!node) {
-    return;
-  }
-  const rect = node.getBoundingClientRect();
-  return {
-    type: "GetClientRect",
-    origin: [rect.x, rect.y],
-    size: [rect.width, rect.height],
-  };
-}
-
-this.scrollTo = function (id, behavior) {
-  const node = this.nodes[id];
-  if (!node) {
-    return false;
-  }
-  node.scrollIntoView({
-    behavior: behavior,
-  });
-  return true;
-}
-
-/// Set the focus on the element
-this.setFocus = function (id, focus) {
-  const node = this.nodes[id];
-  if (!node) {
-    return false;
-  }
-  if (focus) {
-    node.focus();
-  } else {
-    node.blur();
-  }
-  return true;
-}
-
-function get_mouse_data(event) {
-  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,
-  };
-}
-
-async 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,
-        isComposing,
-        key,
-        altKey,
-        ctrlKey,
-        metaKey,
-        keyCode,
-        shiftKey,
-        location,
-        repeat,
-        which,
-        code,
-      } = event;
-      return {
-        char_code: charCode,
-        is_composing: isComposing,
-        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 "drag":
-    case "dragend":
-    case "dragenter":
-    case "dragexit":
-    case "dragleave":
-    case "dragover":
-    case "dragstart":
-    case "drop": {
-      let files = null;
-      if (event.dataTransfer && event.dataTransfer.files) {
-        files = await serializeFileList(event.dataTransfer.files);
-      }
-
-      return { mouse: get_mouse_data(event), files };
-    }
-    case "click":
-    case "contextmenu":
-    case "doubleclick":
-    case "dblclick":
-    case "mousedown":
-    case "mouseenter":
-    case "mouseleave":
-    case "mousemove":
-    case "mouseout":
-    case "mouseover":
-    case "mouseup": {
-      return get_mouse_data(event);
-    }
-    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 {};
-    }
-  }
-}
-this.serializeIpcMessage = function (method, params = {}) {
-  return JSON.stringify({ method, params });
-}
-
-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":
-    case "loadedmetadata":
-    case "loadstart":
-    case "load":
-      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;
-    case "mounted":
-      return false;
-  }
-
-  return true;
-}

+ 1 - 0
packages/interpreter/src/js/README.md

@@ -0,0 +1 @@
+this files are generated - do not edit them!

+ 1 - 0
packages/interpreter/src/js/common.js

@@ -0,0 +1 @@
+function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}export{setAttributeInner,retrieveFormValues};

+ 1 - 0
packages/interpreter/src/js/core.js

@@ -0,0 +1 @@
+function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate(ids){const hydrateNodes=document.querySelectorAll("[data-node-hydration]");for(let i=0;i<hydrateNodes.length;i++){const hydrateNode=hydrateNodes[i],split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}const treeWalker=document.createTreeWalker(document.body,NodeFilter.SHOW_COMMENT);let currentNode=treeWalker.nextNode();while(currentNode){const split=currentNode.textContent.split("node-id");if(split.length>1)this.nodes[ids[parseInt(split[1])]]=currentNode.nextSibling;currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter};

+ 1 - 0
packages/interpreter/src/js/hash.txt

@@ -0,0 +1 @@
+12655652627

+ 1 - 0
packages/interpreter/src/js/native.js

@@ -0,0 +1 @@
+function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key]+=","+value;else contents[key]=value}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>this.run_from_bytes(bytes));this.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};

+ 20 - 8
packages/interpreter/src/lib.rs

@@ -2,26 +2,38 @@
 #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
 #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
 
-pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
-pub static COMMON_JS: &str = include_str!("./common.js");
+/// The base class that the JS channel will extend
+pub static INTERPRETER_JS: &str = include_str!("./js/core.js");
 
-#[cfg(feature = "sledgehammer")]
-mod sledgehammer_bindings;
-
-#[cfg(feature = "sledgehammer")]
-pub use sledgehammer_bindings::*;
+/// The code explicitly for desktop/liveview that bridges the eval gap between the two
+pub static NATIVE_JS: &str = include_str!("./js/native.js");
 
 #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))]
 mod write_native_mutations;
+
 #[cfg(all(feature = "binary-protocol", feature = "sledgehammer"))]
 pub use write_native_mutations::*;
 
+#[cfg(feature = "sledgehammer")]
+pub mod unified_bindings;
+
+#[cfg(feature = "sledgehammer")]
+pub use unified_bindings::*;
+
 // Common bindings for minimal usage.
 #[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
 pub mod minimal_bindings {
     use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
-    #[wasm_bindgen(module = "/src/common_exported.js")]
+
+    /// Some useful snippets that we use to share common functionality between the different platforms we support.
+    ///
+    /// This maintains some sort of consistency between web, desktop, and liveview
+    #[wasm_bindgen(module = "/src/js/common.js")]
     extern "C" {
+        /// Set the attribute of the node
         pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
+
+        /// Roll up all the values from the node into a JS object that we can deserialize
+        pub fn collectFormValues(node: JsValue) -> JsValue;
     }
 }

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

@@ -1,413 +0,0 @@
-#[cfg(feature = "webonly")]
-use js_sys::Function;
-#[cfg(feature = "webonly")]
-use sledgehammer_bindgen::bindgen;
-#[cfg(feature = "webonly")]
-use web_sys::Node;
-
-#[cfg(feature = "webonly")]
-pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
-
-#[cfg(feature = "webonly")]
-#[bindgen(module)]
-mod js {
-    const JS_FILE: &str = "./src/common.js";
-    const JS: &str = r#"
-    class ListenerMap {
-        constructor(root) {
-            // bubbling events can listen at the root element
-            this.global = {};
-            // non bubbling events listen at the element the listener was created at
-            this.local = {};
-            this.root = root;
-            this.handler = null;
-        }
-
-        create(event_name, element, bubbles) {
-            if (bubbles) {
-                if (this.global[event_name] === undefined) {
-                    this.global[event_name] = {};
-                    this.global[event_name].active = 1;
-                    this.root.addEventListener(event_name, this.handler);
-                } else {
-                    this.global[event_name].active++;
-                }
-            }
-            else {
-                const id = element.getAttribute("data-dioxus-id");
-                if (!this.local[id]) {
-                    this.local[id] = {};
-                }
-                element.addEventListener(event_name, this.handler);
-            }
-        }
-
-        remove(element, event_name, bubbles) {
-            if (bubbles) {
-                this.global[event_name].active--;
-                if (this.global[event_name].active === 0) {
-                    this.root.removeEventListener(event_name, this.global[event_name].callback);
-                    delete this.global[event_name];
-                }
-            }
-            else {
-                const id = element.getAttribute("data-dioxus-id");
-                delete this.local[id][event_name];
-                if (this.local[id].length === 0) {
-                    delete this.local[id];
-                }
-                element.removeEventListener(event_name, this.handler);
-            }
-        }
-
-        removeAllNonBubbling(element) {
-            const id = element.getAttribute("data-dioxus-id");
-            delete this.local[id];
-        }
-    }
-    this.LoadChild = function(ptr, len) {
-        // iterate through each number and get that child
-        let node = this.stack[this.stack.length - 1];
-        let ptr_end = ptr + len;
-        for (; ptr < ptr_end; ptr++) {
-            let end = this.m.getUint8(ptr);
-            for (node = node.firstChild; end > 0; end--) {
-                node = node.nextSibling;
-            }
-        }
-        return node;
-    }
-    this.listeners = new ListenerMap();
-    this.nodes = [];
-    this.stack = [];
-    this.templates = {};
-    this.save_template = function(nodes, tmpl_id) {
-        this.templates[tmpl_id] = nodes;
-    }
-    this.hydrate = function (ids) {
-        const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
-        for (let i = 0; i < hydrateNodes.length; i++) {
-            const hydrateNode = hydrateNodes[i];
-            const hydration = hydrateNode.getAttribute('data-node-hydration');
-            const split = hydration.split(',');
-            const id = ids[parseInt(split[0])];
-            this.nodes[id] = hydrateNode;
-            if (split.length > 1) {
-                hydrateNode.listening = split.length - 1;
-                hydrateNode.setAttribute('data-dioxus-id', id);
-                for (let j = 1; j < split.length; j++) {
-                    const listener = split[j];
-                    const split2 = listener.split(':');
-                    const event_name = split2[0];
-                    const bubbles = split2[1] === '1';
-                    this.listeners.create(event_name, hydrateNode, bubbles);
-                }
-            }
-        }
-        const treeWalker = document.createTreeWalker(
-            document.body,
-            NodeFilter.SHOW_COMMENT,
-        );
-        let currentNode = treeWalker.nextNode();
-        while (currentNode) {
-            const id = currentNode.textContent;
-            const split = id.split('node-id');
-            if (split.length > 1) {
-                this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
-            }
-            currentNode = treeWalker.nextNode();
-        }
-    }
-    this.get_node = function(id) {
-        return this.nodes[id];
-    }
-    this.initialize = function(root, handler) {
-        this.listeners.handler = handler;
-        this.nodes = [root];
-        this.stack = [root];
-        this.listeners.root = root;
-    }
-    this.AppendChildren = function (id, many){
-        let root = this.nodes[id];
-        let els = this.stack.splice(this.stack.length-many);
-        for (let k = 0; k < many; k++) {
-            root.appendChild(els[k]);
-        }
-    }
-    "#;
-
-    fn mount_to_root() {
-        "{this.AppendChildren(this.listeners.root, this.stack.length-1);}"
-    }
-    fn push_root(root: u32) {
-        "{this.stack.push(this.nodes[$root$]);}"
-    }
-    fn append_children(id: u32, many: u16) {
-        "{this.AppendChildren($id$, $many$);}"
-    }
-    fn pop_root() {
-        "{this.stack.pop();}"
-    }
-    fn replace_with(id: u32, n: u16) {
-        "{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
-    }
-    fn insert_after(id: u32, n: u16) {
-        "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
-    }
-    fn insert_before(id: u32, n: u16) {
-        "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
-    }
-    fn remove(id: u32) {
-        "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}"
-    }
-    fn create_raw_text(text: &str) {
-        "{this.stack.push(document.createTextNode($text$));}"
-    }
-    fn create_text_node(text: &str, id: u32) {
-        "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}"
-    }
-    fn create_placeholder(id: u32) {
-        "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
-    }
-    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
-        r#"let node = this.nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); this.listeners.create($event_name$, node, $bubbles$);"#
-    }
-    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
-        "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}"
-    }
-    fn set_text(id: u32, text: &str) {
-        "{this.nodes[$id$].textContent = $text$;}"
-    }
-    fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
-        "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}"
-    }
-    fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
-        r#"{
-            let node = this.nodes[$id$];
-            if (!ns) {
-                switch (field) {
-                    case "value":
-                        node.value = "";
-                        break;
-                    case "checked":
-                        node.checked = false;
-                        break;
-                    case "selected":
-                        node.selected = false;
-                        break;
-                    case "dangerous_inner_html":
-                        node.innerHTML = "";
-                        break;
-                    default:
-                        node.removeAttribute(field);
-                        break;
-                }
-            } else if (ns == "style") {
-                node.style.removeProperty(name);
-            } else {
-                node.removeAttributeNS(ns, field);
-            }
-        }"#
-    }
-    fn assign_id(ptr: u32, len: u8, id: u32) {
-        "{this.nodes[$id$] = this.LoadChild($ptr$, $len$);}"
-    }
-    fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
-        r#"{
-            let node = this.LoadChild($ptr$, $len$);
-            if (node.nodeType == node.TEXT_NODE) {
-                node.textContent = value;
-            } else {
-                let text = document.createTextNode(value);
-                node.replaceWith(text);
-                node = text;
-            }
-            this.nodes[$id$] = node;
-        }"#
-    }
-    fn replace_placeholder(ptr: u32, len: u8, n: u16) {
-        "{let els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...els);}"
-    }
-    fn load_template(tmpl_id: u16, index: u16, id: u32) {
-        "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
-    }
-}
-
-#[cfg(feature = "webonly")]
-#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
-export function save_template(channel, nodes, tmpl_id) {
-    channel.save_template(nodes, tmpl_id);
-}
-export function hydrate(channel, ids) {
-    channel.hydrate(ids);
-}
-export function get_node(channel, id) {
-    return channel.get_node(id);
-}
-export function initialize(channel, root, handler) {
-    channel.initialize(root, handler);
-}
-"#)]
-extern "C" {
-    pub fn save_template(channel: &JSChannel, nodes: Vec<Node>, tmpl_id: u16);
-
-    pub fn hydrate(channel: &JSChannel, ids: Vec<u32>);
-
-    pub fn get_node(channel: &JSChannel, id: u32) -> Node;
-
-    pub fn initialize(channel: &JSChannel, root: Node, handler: &Function);
-}
-
-#[cfg(feature = "binary-protocol")]
-pub mod binary_protocol {
-    use sledgehammer_bindgen::bindgen;
-    pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
-
-    #[bindgen]
-    mod protocol_js {
-        const JS_FILE: &str = "./src/interpreter.js";
-        const JS_FILE: &str = "./src/common.js";
-
-        fn push_root(root: u32) {
-            "{this.stack.push(this.nodes[$root$]);}"
-        }
-        fn append_children(id: u32, many: u16) {
-            "{this.AppendChildren($id$, $many$);}"
-        }
-        fn append_children_to_top(many: u16) {
-            "{
-                let root = this.stack[this.stack.length-many-1];
-                let els = this.stack.splice(this.stack.length-many);
-                for (let k = 0; k < many; k++) {
-                    root.appendChild(els[k]);
-                }
-            }"
-        }
-        fn pop_root() {
-            "{this.stack.pop();}"
-        }
-        fn replace_with(id: u32, n: u16) {
-            "{let root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
-        }
-        fn insert_after(id: u32, n: u16) {
-            "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
-        }
-        fn insert_before(id: u32, n: u16) {
-            "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
-        }
-        fn remove(id: u32) {
-            "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.listeners.removeAllNonBubbling(node); } node.remove(); }}"
-        }
-        fn create_raw_text(text: &str) {
-            "{this.stack.push(document.createTextNode($text$));}"
-        }
-        fn create_text_node(text: &str, id: u32) {
-            "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}"
-        }
-        fn create_element(element: &'static str<u8, el>) {
-            "{this.stack.push(document.createElement($element$))}"
-        }
-        fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
-            "{this.stack.push(document.createElementNS($ns$, $element$))}"
-        }
-        fn create_placeholder(id: u32) {
-            "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
-        }
-        fn add_placeholder() {
-            "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}"
-        }
-        fn new_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
-            r#"
-            bubbles = bubbles == 1;
-            let node = this.nodes[id];
-            if(node.listening){
-                node.listening += 1;
-            } else {
-                node.listening = 1;
-            }
-            node.setAttribute('data-dioxus-id', `\${id}`);
-            const event_name = $event$;
-
-            // if this is a mounted listener, we send the event immediately
-            if (event_name === "mounted") {
-                window.ipc.postMessage(
-                    this.serializeIpcMessage("user_event", {
-                        name: event_name,
-                        element: id,
-                        data: null,
-                        bubbles,
-                    })
-                );
-            } else {
-                this.listeners.create(event_name, node, bubbles, (event) => {
-                    this.handler(event, event_name, bubbles);
-                });
-            }"#
-        }
-        fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
-            "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.listeners.remove(node, $event_name$, $bubbles$);}"
-        }
-        fn set_text(id: u32, text: &str) {
-            "{this.nodes[$id$].textContent = $text$;}"
-        }
-        fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
-            "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}"
-        }
-        fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
-            "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}"
-        }
-        fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
-            r#"{
-                let node = this.nodes[$id$];
-                if (!ns) {
-                    switch (field) {
-                        case "value":
-                            node.value = "";
-                            break;
-                        case "checked":
-                            node.checked = false;
-                            break;
-                        case "selected":
-                            node.selected = false;
-                            break;
-                        case "dangerous_inner_html":
-                            node.innerHTML = "";
-                            break;
-                        default:
-                            node.removeAttribute(field);
-                            break;
-                    }
-                } else if (ns == "style") {
-                    node.style.removeProperty(name);
-                } else {
-                    node.removeAttributeNS(ns, field);
-                }
-            }"#
-        }
-        fn assign_id(array: &[u8], id: u32) {
-            "{this.nodes[$id$] = this.LoadChild($array$);}"
-        }
-        fn hydrate_text(array: &[u8], value: &str, id: u32) {
-            r#"{
-                let node = this.LoadChild($array$);
-                if (node.nodeType == node.TEXT_NODE) {
-                    node.textContent = value;
-                } else {
-                    let text = document.createTextNode(value);
-                    node.replaceWith(text);
-                    node = text;
-                }
-                this.nodes[$id$] = node;
-            }"#
-        }
-        fn replace_placeholder(array: &[u8], n: u16) {
-            "{let els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...els);}"
-        }
-        fn load_template(tmpl_id: u16, index: u16, id: u32) {
-            "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
-        }
-        fn add_templates(tmpl_id: u16, len: u16) {
-            "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}"
-        }
-    }
-}

+ 3 - 0
packages/interpreter/src/ts/.gitignore

@@ -0,0 +1,3 @@
+# please dont accidentally run tsc and commit your js in this dir.
+*.js
+

+ 2 - 0
packages/interpreter/src/ts/common.ts

@@ -0,0 +1,2 @@
+export { setAttributeInner } from "./set_attribute";
+export { retrieveFormValues } from "./form";

+ 173 - 0
packages/interpreter/src/ts/core.ts

@@ -0,0 +1,173 @@
+// The root interpreter class that holds state about the mapping between DOM and VirtualDom
+// This always lives in the JS side of things, and is extended by the native and web interpreters
+
+import { setAttributeInner } from "./set_attribute";
+
+export type NodeId = number;
+
+export class BaseInterpreter {
+  // non bubbling events listen at the element the listener was created at
+  global: {
+    [key: string]: { active: number, callback: EventListener }
+  };
+  // bubbling events can listen at the root element
+  local: {
+    [key: string]: {
+      [key: string]: EventListener
+    }
+  };
+
+  root: HTMLElement;
+  handler: EventListener;
+  nodes: Node[];
+  stack: Node[];
+  templates: {
+    [key: number]: Node[]
+  };
+
+  // sledgehammer is generating this...
+  m: any;
+
+  constructor() { }
+
+  initialize(root: HTMLElement, handler: EventListener | null = null) {
+    this.global = {};
+    this.local = {};
+    this.root = root;
+
+    this.nodes = [root];
+    this.stack = [root];
+    this.templates = {};
+
+    if (handler) {
+      this.handler = handler;
+    }
+  }
+
+  createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
+    if (bubbles) {
+      if (this.global[event_name] === undefined) {
+        this.global[event_name] = { active: 1, callback: this.handler };
+        this.root.addEventListener(event_name, this.handler);
+      } else {
+        this.global[event_name].active++;
+      }
+    } else {
+      const id = element.getAttribute("data-dioxus-id");
+      if (!this.local[id]) {
+        this.local[id] = {};
+      }
+      element.addEventListener(event_name, this.handler);
+    }
+  }
+
+  removeListener(element: HTMLElement, event_name: string, bubbles: boolean) {
+    if (bubbles) {
+      this.removeBubblingListener(event_name);
+    } else {
+      this.removeNonBubblingListener(element, event_name);
+    }
+  }
+
+  removeBubblingListener(event_name: string) {
+    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];
+    }
+  }
+
+  removeNonBubblingListener(element: HTMLElement, event_name: string) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id][event_name];
+    if (Object.keys(this.local[id]).length === 0) {
+      delete this.local[id];
+    }
+    element.removeEventListener(event_name, this.handler);
+  }
+
+  removeAllNonBubblingListeners(element: HTMLElement) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+
+  getNode(id: NodeId): Node {
+    return this.nodes[id];
+  }
+
+  appendChildren(id: NodeId, many: number) {
+    const root = this.nodes[id];
+    const els = this.stack.splice(this.stack.length - many);
+    for (let k = 0; k < many; k++) {
+      root.appendChild(els[k]);
+    }
+  }
+
+  loadChild(ptr: number, len: number): Node {
+    // iterate through each number and get that child
+    let node = this.stack[this.stack.length - 1] as Node;
+    let ptr_end = ptr + len;
+
+    for (; ptr < ptr_end; ptr++) {
+      let end = this.m.getUint8(ptr);
+      for (node = node.firstChild; end > 0; end--) {
+        node = node.nextSibling;
+      }
+    }
+
+    return node;
+  }
+
+  saveTemplate(nodes: HTMLElement[], tmpl_id: number) {
+    this.templates[tmpl_id] = nodes;
+  }
+
+  hydrate(ids: { [key: number]: number }) {
+    const hydrateNodes = document.querySelectorAll('[data-node-hydration]');
+
+    for (let i = 0; i < hydrateNodes.length; i++) {
+      const hydrateNode = hydrateNodes[i] as HTMLElement;
+      const hydration = hydrateNode.getAttribute('data-node-hydration');
+      const split = hydration!.split(',');
+      const id = ids[parseInt(split[0])];
+
+      this.nodes[id] = hydrateNode;
+
+      if (split.length > 1) {
+        // @ts-ignore
+        hydrateNode.listening = split.length - 1;
+        hydrateNode.setAttribute('data-dioxus-id', id.toString());
+        for (let j = 1; j < split.length; j++) {
+          const listener = split[j];
+          const split2 = listener.split(':');
+          const event_name = split2[0];
+          const bubbles = split2[1] === '1';
+          this.createListener(event_name, hydrateNode, bubbles);
+        }
+      }
+    }
+
+    const treeWalker = document.createTreeWalker(
+      document.body,
+      NodeFilter.SHOW_COMMENT,
+    );
+
+    let currentNode = treeWalker.nextNode();
+
+    while (currentNode) {
+      const id = currentNode.textContent!;
+      const split = id.split('node-id');
+
+      if (split.length > 1) {
+        this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling;
+      }
+
+      currentNode = treeWalker.nextNode();
+    }
+  }
+
+  setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
+    setAttributeInner(node, field, value, ns);
+  }
+}
+

+ 58 - 0
packages/interpreter/src/ts/form.ts

@@ -0,0 +1,58 @@
+// Consistently deserialize forms and form elements for use across web/desktop/mobile
+
+type FormValues = {
+  valid?: boolean;
+  values: { [key: string]: FormDataEntryValue };
+}
+
+export function retriveValues(event: Event, target: HTMLElement): FormValues {
+  let contents: FormValues = {
+    values: {}
+  };
+
+  // If there's a form...
+  let form = target.closest("form");
+
+  // If the target is an input, and the event is input or change, we want to get the value without going through the form
+  if (form) {
+    if (
+      event.type === "input"
+      || event.type === "change"
+      || event.type === "submit"
+      || event.type === "reset"
+      || event.type === "click"
+    ) {
+      contents = retrieveFormValues(form);
+    }
+  }
+
+  return contents;
+}
+
+// todo: maybe encode spaces or something?
+// We encode select multiple as a comma separated list which breaks... when there's commas in the values
+export function retrieveFormValues(form: HTMLFormElement): FormValues {
+  const formData = new FormData(form);
+  const contents: { [key: string]: FormDataEntryValue } = {};
+  formData.forEach((value, key) => {
+    if (contents[key]) {
+      contents[key] += "," + value;
+    } else {
+      contents[key] = value;
+    }
+  });
+  return {
+    valid: form.checkValidity(),
+    values: contents
+  };
+}
+
+export function retriveSelectValue(target: HTMLSelectElement): string[] {
+  // there might be multiple...
+  let options = target.selectedOptions;
+  let values = [];
+  for (let i = 0; i < options.length; i++) {
+    values.push(options[i].value);
+  }
+  return values;
+}

+ 382 - 0
packages/interpreter/src/ts/native.ts

@@ -0,0 +1,382 @@
+// This file provides an extended variant of the interpreter used for desktop and liveview interaction
+//
+// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't
+// provide since it doesn't have access to the dom.
+
+import { BaseInterpreter, NodeId } from "./core";
+import { SerializedEvent, serializeEvent } from "./serialize";
+
+// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope
+// we want to extend it, and it technically extends base intepreter. To make typescript happy,
+// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it
+var JSChannel_: typeof BaseInterpreter;
+
+// @ts-ignore - this is coming from the host
+if (RawInterpreter !== undefined && RawInterpreter !== null) {
+  // @ts-ignore - this is coming from the host
+  JSChannel_ = RawInterpreter;
+};
+
+export class NativeInterpreter extends JSChannel_ {
+  intercept_link_redirects: boolean;
+  ipc: any;
+  editsPath: string;
+
+  // eventually we want to remove liveview and build it into the server-side-events of fullstack
+  // however, for now we need to support it since SSE in fullstack doesn't exist yet
+  liveview: boolean;
+
+  constructor(editsPath: string) {
+    super();
+    this.editsPath = editsPath;
+  }
+
+  initialize(root: HTMLElement): void {
+    this.intercept_link_redirects = true;
+    this.liveview = false;
+
+    // attach an event listener on the body that prevents file drops from navigating
+    // this is because the browser will try to navigate to the file if it's dropped on the window
+    window.addEventListener("dragover", function (e) {
+      // // check which element is our target
+      if (e.target instanceof Element && e.target.tagName != "INPUT") {
+        e.preventDefault();
+      }
+    }, false);
+
+    window.addEventListener("drop", function (e) {
+      let target = e.target;
+
+      if (!(target instanceof Element)) {
+        return;
+      }
+
+      // Dropping a file on the window will navigate to the file, which we don't want
+      e.preventDefault();
+    }, false);
+
+    // attach a listener to the route that listens for clicks and prevents the default file dialog
+    window.addEventListener("click", (event) => {
+      const target = event.target;
+      if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") {
+        // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
+        let target_id = getTargetId(target);
+        if (target_id !== null) {
+          const message = this.serializeIpcMessage("file_dialog", {
+            event: "change&input",
+            accept: target.getAttribute("accept"),
+            directory: target.getAttribute("webkitdirectory") === "true",
+            multiple: target.hasAttribute("multiple"),
+            target: target_id,
+            bubbles: event.bubbles,
+          });
+          this.ipc.postMessage(message);
+        }
+
+        // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate
+        event.preventDefault();
+      }
+    });
+
+
+    // @ts-ignore - wry gives us this
+    this.ipc = window.ipc;
+
+    // make sure we pass the handler to the base interpreter
+    const handler: EventListener = (event) => this.handleEvent(event, event.type, true);
+    super.initialize(root, handler);
+  }
+
+  serializeIpcMessage(method: string, params = {}) {
+    return JSON.stringify({ method, params });
+  }
+
+  scrollTo(id: NodeId, behavior: ScrollBehavior) {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      node.scrollIntoView({ behavior });
+    }
+  }
+
+  getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      const rect = node.getBoundingClientRect();
+      return {
+        type: "GetClientRect",
+        origin: [rect.x, rect.y],
+        size: [rect.width, rect.height],
+      };
+    }
+  }
+
+  setFocus(id: NodeId, focus: boolean) {
+    const node = this.nodes[id];
+
+    if (node instanceof HTMLElement) {
+      if (focus) {
+        node.focus();
+      } else {
+        node.blur();
+      }
+    }
+  }
+
+  // ignore the fact the base interpreter uses ptr + len but we use array...
+  // @ts-ignore
+  loadChild(array: number[]) {
+    // iterate through each number and get that child
+    let node = this.stack[this.stack.length - 1];
+
+    for (let i = 0; i < array.length; i++) {
+      let end = array[i];
+      for (node = node.firstChild; end > 0; end--) {
+        node = node.nextSibling;
+      }
+    }
+
+    return node;
+  }
+
+  appendChildren(id: NodeId, many: number) {
+    const root = this.nodes[id];
+    const els = this.stack.splice(this.stack.length - many);
+
+    for (let k = 0; k < many; k++) {
+      root.appendChild(els[k]);
+    }
+  }
+
+  handleEvent(event: Event, name: string, bubbles: boolean) {
+    const target = event.target!;
+    const realId = getTargetId(target)!;
+    const contents = serializeEvent(event, target);
+
+    // Handle the event on the virtualdom and then preventDefault if it also preventsDefault
+    // Some listeners
+    let body = {
+      name: name,
+      data: contents,
+      element: realId,
+      bubbles,
+    };
+
+    // Run any prevent defaults the user might've set
+    // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary
+    // now that we expose preventDefault to the virtualdom on desktop
+    // Liveview will still need to use this
+    this.preventDefaults(event, target);
+
+    // liveview does not have syncronous event handling, so we need to send the event to the host
+    if (this.liveview) {
+      // Okay, so the user might've requested some files to be read
+      if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) {
+        if (target.getAttribute("type") === "file") {
+          this.readFiles(target, contents, bubbles, realId, name);
+        }
+      }
+    } else {
+
+      const message = this.serializeIpcMessage("user_event", body);
+      this.ipc.postMessage(message);
+
+      // // Run the event handler on the virtualdom
+      // // capture/prevent default of the event if the virtualdom wants to
+      // const res = handleVirtualdomEventSync(JSON.stringify(body));
+
+      // if (res.preventDefault) {
+      //   event.preventDefault();
+      // }
+
+      // if (res.stopPropagation) {
+      //   event.stopPropagation();
+      // }
+    }
+  }
+
+
+
+  // This should:
+  // - prevent form submissions from navigating
+  // - prevent anchor tags from navigating
+  // - prevent buttons from submitting forms
+  // - let the virtualdom attempt to prevent the event
+  preventDefaults(event: Event, target: EventTarget) {
+    let preventDefaultRequests: string | null = null;
+
+    // Some events can be triggered on text nodes, which don't have attributes
+    if (target instanceof Element) {
+      preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
+    }
+
+    if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) {
+      event.preventDefault();
+    }
+
+    if (event.type === "submit") {
+      event.preventDefault();
+    }
+
+    // Attempt to intercept if the event is a click
+    if (target instanceof Element && event.type === "click") {
+      this.handleClickNavigate(event, target, preventDefaultRequests);
+    }
+  }
+
+  handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) {
+    // todo call prevent default if it's the right type of event
+    if (!this.intercept_link_redirects) {
+      return;
+    }
+
+    // prevent buttons in forms from submitting the form
+    if (target.tagName === "BUTTON" && event.type == "submit") {
+      event.preventDefault();
+    }
+
+    // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating
+    let a_element = target.closest("a");
+    if (a_element == null) {
+      return;
+    }
+
+    event.preventDefault();
+
+    let elementShouldPreventDefault =
+      preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
+
+    let aElementShouldPreventDefault = a_element.getAttribute(
+      `dioxus-prevent-default`
+    );
+
+    let linkShouldPreventDefault =
+      aElementShouldPreventDefault &&
+      aElementShouldPreventDefault.includes(`onclick`);
+
+    if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
+      const href = a_element.getAttribute("href");
+      if (href !== "" && href !== null && href !== undefined) {
+        this.ipc.postMessage(
+          this.serializeIpcMessage("browser_open", { href })
+        );
+      }
+    }
+  }
+
+  waitForRequest(headless: boolean) {
+    fetch(new Request(this.editsPath))
+      .then(response => response.arrayBuffer())
+      .then(bytes => {
+        // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
+        if (headless) {
+          // @ts-ignore
+          this.run_from_bytes(bytes);
+        } else {
+          // @ts-ignore
+          requestAnimationFrame(() => this.run_from_bytes(bytes));
+        }
+        this.waitForRequest(headless);
+      });
+  }
+
+
+  //  A liveview only function
+  // Desktop will intercept the event before it hits this
+  async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {
+    let files = target.files!;
+    let file_contents: { [name: string]: number[] } = {};
+
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+      file_contents[file.name] = Array.from(
+        new Uint8Array(await file.arrayBuffer())
+      );
+    }
+
+    contents.files = { files: file_contents };
+
+    const message = this.serializeIpcMessage("user_event", {
+      name: name,
+      element: realId,
+      data: contents,
+      bubbles,
+    });
+
+    this.ipc.postMessage(message);
+  }
+}
+
+type EventSyncResult = {
+  preventDefault: boolean;
+  stopPropagation: boolean;
+  stopImmediatePropagation: boolean;
+  filesRequested: boolean;
+};
+
+// This function sends the event to the virtualdom and then waits for the virtualdom to process it
+//
+// However, it's not really suitable for liveview, because it's synchronous and will block the main thread
+// We should definitely consider using a websocket if we want to block... or just not block on liveview
+// Liveview is a little bit of a tricky beast
+function handleVirtualdomEventSync(contents: string): EventSyncResult {
+  // Handle the event on the virtualdom and then process whatever its output was
+  const xhr = new XMLHttpRequest();
+
+  // Serialize the event and send it to the custom protocol in the Rust side of things
+  xhr.timeout = 1000;
+  xhr.open("GET", "/handle/event.please", false);
+  xhr.setRequestHeader("Content-Type", "application/json");
+  xhr.send(contents);
+
+  // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to
+  return JSON.parse(xhr.responseText);
+}
+
+function getTargetId(target: EventTarget): NodeId | null {
+  // Ensure that the target is a node, sometimes it's nota
+  if (!(target instanceof Node)) {
+    return null;
+  }
+
+  let ourTarget = target;
+  let realId = null;
+
+  while (realId == null) {
+    if (ourTarget === null) {
+      return null;
+    }
+
+    if (ourTarget instanceof Element) {
+      realId = ourTarget.getAttribute(`data-dioxus-id`);
+    }
+
+    ourTarget = ourTarget.parentNode;
+  }
+
+  return parseInt(realId);
+}
+
+
+// function applyFileUpload() {
+//   let inputs = document.querySelectorAll("input");
+//   for (let input of inputs) {
+//     if (!input.getAttribute("data-dioxus-file-listener")) {
+//       // prevent file inputs from opening the file dialog on click
+//       const type = input.getAttribute("type");
+//       if (type === "file") {
+//         input.setAttribute("data-dioxus-file-listener", true);
+//         input.addEventListener("click", (event) => {
+//           let target = event.target;
+//           let target_id = find_real_id(target);
+//           if (target_id !== null) {
+//             const send = (event_name) => {
+//               const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
+//               window.ipc.postMessage(message);
+//             };
+//             send("change&input");
+//           }
+//           event.preventDefault();
+//         });
+//       }
+//     }
+// }

+ 217 - 0
packages/interpreter/src/ts/serialize.ts

@@ -0,0 +1,217 @@
+// Handle serialization of the event data across the IPC boundarytype SerialziedEvent = {};
+
+import { retriveSelectValue, retriveValues } from "./form";
+
+export type AppTouchEvent = TouchEvent;
+
+export type SerializedEvent = {
+  values?: { [key: string]: FormDataEntryValue };
+  value?: string;
+  [key: string]: any;
+};
+
+export function serializeEvent(event: Event, target: EventTarget): SerializedEvent {
+  let contents = {};
+
+  // merge the object into the contents
+  let extend = (obj: any) => (contents = { ...contents, ...obj });
+
+  if (event instanceof WheelEvent) { extend(serializeWheelEvent(event)) };
+  if (event instanceof MouseEvent) { extend(serializeMouseEvent(event)) }
+  if (event instanceof KeyboardEvent) { extend(serializeKeyboardEvent(event)) }
+
+  if (event instanceof InputEvent) { extend(serializeInputEvent(event, target)) }
+  if (event instanceof PointerEvent) { extend(serializePointerEvent(event)) }
+  if (event instanceof AnimationEvent) { extend(serializeAnimationEvent(event)) }
+  if (event instanceof TransitionEvent) { extend({ property_name: event.propertyName, elapsed_time: event.elapsedTime, pseudo_element: event.pseudoElement, }) }
+  if (event instanceof CompositionEvent) { extend({ data: event.data, }) }
+  if (event instanceof DragEvent) { extend(serializeDragEvent(event)) }
+  if (event instanceof FocusEvent) { extend({}) }
+  if (event instanceof ClipboardEvent) { extend({}) }
+
+  // safari is quirky and doesn't have TouchEvent
+  if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) { extend(serializeTouchEvent(event)); }
+
+  if (event.type === "submit" || event.type === "reset" || event.type === "click" || event.type === "change" || event.type === "input") {
+    extend(serializeInputEvent(event as InputEvent, target));
+  }
+
+  // If there's any files, we need to serialize them
+  if (event instanceof DragEvent) {
+    // let files: { [key: string]: Uint8Array } = {};
+    // if (event.dataTransfer && event.dataTransfer.files) {
+    //   files["a"] = new Uint8Array(0);
+    //   // files = {
+    //   //   entries: Array.from(event.dataTransfer.files).map((file) => {
+    //   //     return {
+    //   //       name: file.name,
+    //   //       type: file.type,
+    //   //       size: file.size,
+    //   //       last_modified: file.lastModified,
+    //   //     };
+    //   //   }
+    //   // };
+    //   // files = await serializeFileList(event.dataTransfer.files);
+    // }
+    // extend({ files: files });
+  }
+
+  return contents;
+}
+
+function serializeInputEvent(event: InputEvent, target: EventTarget): SerializedEvent {
+  let contents: SerializedEvent = {};
+
+  // Attempt to retrieve the values from the form
+  if (target instanceof HTMLElement) {
+    let values = retriveValues(event, target);
+    contents.values = values.values;
+    contents.valid = values.valid;
+  }
+
+  if (event.target instanceof HTMLInputElement) {
+    let target = event.target;
+    let value = target.value ?? target.textContent ?? "";
+
+    if (target.type === "checkbox") {
+      value = target.checked ? "true" : "false";
+    } else if (target.type === "radio") {
+      value = target.value;
+    }
+
+    contents.value = value;
+  }
+
+  if (event.target instanceof HTMLTextAreaElement) {
+    contents.value = event.target.value;
+  }
+
+  if (event.target instanceof HTMLSelectElement) {
+    contents.value = retriveSelectValue(event.target).join(",");
+  }
+
+  // Ensure the serializer isn't quirky
+  if (contents.value === undefined) {
+    contents.value = "";
+  }
+
+
+  return contents;
+}
+
+
+
+function serializeWheelEvent(event: WheelEvent): SerializedEvent {
+  return {
+    delta_x: event.deltaX,
+    delta_y: event.deltaY,
+    delta_z: event.deltaZ,
+    delta_mode: event.deltaMode,
+  };
+}
+
+function serializeTouchEvent(event: TouchEvent): SerializedEvent {
+  return {
+    alt_key: event.altKey,
+    ctrl_key: event.ctrlKey,
+    meta_key: event.metaKey,
+    shift_key: event.shiftKey,
+    changed_touches: event.changedTouches,
+    target_touches: event.targetTouches,
+    touches: event.touches,
+  };
+}
+
+function serializePointerEvent(event: PointerEvent): SerializedEvent {
+  return {
+    alt_key: event.altKey,
+    button: event.button,
+    buttons: event.buttons,
+    client_x: event.clientX,
+    client_y: event.clientY,
+    ctrl_key: event.ctrlKey,
+    meta_key: event.metaKey,
+    page_x: event.pageX,
+    page_y: event.pageY,
+    screen_x: event.screenX,
+    screen_y: event.screenY,
+    shift_key: event.shiftKey,
+    pointer_id: event.pointerId,
+    width: event.width,
+    height: event.height,
+    pressure: event.pressure,
+    tangential_pressure: event.tangentialPressure,
+    tilt_x: event.tiltX,
+    tilt_y: event.tiltY,
+    twist: event.twist,
+    pointer_type: event.pointerType,
+    is_primary: event.isPrimary,
+  };
+}
+
+function serializeMouseEvent(event: MouseEvent): SerializedEvent {
+  return {
+    alt_key: event.altKey,
+    button: event.button,
+    buttons: event.buttons,
+    client_x: event.clientX,
+    client_y: event.clientY,
+    ctrl_key: event.ctrlKey,
+    meta_key: event.metaKey,
+    offset_x: event.offsetX,
+    offset_y: event.offsetY,
+    page_x: event.pageX,
+    page_y: event.pageY,
+    screen_x: event.screenX,
+    screen_y: event.screenY,
+    shift_key: event.shiftKey,
+  };
+}
+
+function serializeKeyboardEvent(event: KeyboardEvent): SerializedEvent {
+  return {
+    char_code: event.charCode,
+    is_composing: event.isComposing,
+    key: event.key,
+    alt_key: event.altKey,
+    ctrl_key: event.ctrlKey,
+    meta_key: event.metaKey,
+    key_code: event.keyCode,
+    shift_key: event.shiftKey,
+    location: event.location,
+    repeat: event.repeat,
+    which: event.which,
+    code: event.code,
+  };
+}
+
+function serializeAnimationEvent(event: AnimationEvent): SerializedEvent {
+  return {
+    animation_name: event.animationName,
+    elapsed_time: event.elapsedTime,
+    pseudo_element: event.pseudoElement,
+  };
+}
+
+function serializeDragEvent(event: DragEvent): SerializedEvent {
+  //     let files = [];
+  //     if (event.dataTransfer && event.dataTransfer.files) {
+  //       files = ["a", "b", "c"];
+  //       // files = await serializeFileList(event.dataTransfer.files);
+  //     }
+  //     return { mouse: get_mouse_data(event), files };
+  return {
+    mouse: {
+      alt_key: event.altKey,
+      ctrl_key: event.ctrlKey,
+      meta_key: event.metaKey,
+      shift_key: event.shiftKey,
+      ...serializeMouseEvent(event),
+    },
+    files: {
+      files: {
+        "a": [1, 2, 3],
+      }
+    },
+  };
+}

+ 106 - 0
packages/interpreter/src/ts/set_attribute.ts

@@ -0,0 +1,106 @@
+// A unified interface for setting attributes on a node
+
+// this function should try and stay fast, if possible
+export function setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) {
+  // we support a single namespace by default: style
+  if (ns === "style") {
+    node.style.setProperty(field, value);
+    return;
+  }
+
+  // If there's a namespace, use setAttributeNS (svg, mathml, etc.)
+  if (!!ns) {
+    node.setAttributeNS(ns, field, value);
+    return
+  }
+
+  // A few attributes are need to be set with either boolean values or require some sort of translation
+  switch (field) {
+    case "value":
+      // @ts-ignore
+      if (node.value !== value) {
+        // @ts-ignore
+        node.value = value;
+      }
+      break;
+
+    case "initial_value":
+      // @ts-ignore
+      node.defaultValue = value;
+      break;
+
+    case "checked":
+      // @ts-ignore
+      node.checked = truthy(value);
+      break;
+
+    case "initial_checked":
+      // @ts-ignore
+      node.defaultChecked = truthy(value);
+      break;
+
+    case "selected":
+      // @ts-ignore
+      node.selected = truthy(value);
+      break;
+
+    case "initial_selected":
+      // @ts-ignore
+      node.defaultSelected = truthy(value);
+      break;
+
+    case "dangerous_inner_html":
+      node.innerHTML = value;
+      break;
+
+    // The presence of a an attribute is enough to set it to true, provided the value is being set to a truthy value
+    // Again, kinda ugly and would prefer this logic to be baked into dioxus-html at compiile time
+    default:
+      // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+      if (!truthy(value) && isBoolAttr(field)) {
+        node.removeAttribute(field);
+      } else {
+        node.setAttribute(field, value);
+      }
+  }
+}
+
+
+function truthy(val: string | boolean) {
+  return val === "true" || val === true;
+}
+
+function isBoolAttr(field: string): boolean {
+  switch (field) {
+    case "allowfullscreen":
+    case "allowpaymentrequest":
+    case "async":
+    case "autofocus":
+    case "autoplay":
+    case "checked":
+    case "controls":
+    case "default":
+    case "defer":
+    case "disabled":
+    case "formnovalidate":
+    case "hidden":
+    case "ismap":
+    case "itemscope":
+    case "loop":
+    case "multiple":
+    case "muted":
+    case "nomodule":
+    case "novalidate":
+    case "open":
+    case "playsinline":
+    case "readonly":
+    case "required":
+    case "reversed":
+    case "selected":
+    case "truespeed":
+    case "webkitdirectory":
+      return true;
+    default:
+      return false;
+  }
+}

+ 237 - 0
packages/interpreter/src/unified_bindings.rs

@@ -0,0 +1,237 @@
+#[cfg(feature = "webonly")]
+use web_sys::Node;
+
+pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
+
+#[cfg(feature = "webonly")]
+#[wasm_bindgen::prelude::wasm_bindgen]
+extern "C" {
+    pub type BaseInterpreter;
+
+    #[wasm_bindgen(method)]
+    pub fn initialize(this: &BaseInterpreter, root: Node, handler: &js_sys::Function);
+
+    #[wasm_bindgen(method, js_name = "saveTemplate")]
+    pub fn save_template(this: &BaseInterpreter, nodes: Vec<Node>, tmpl_id: u16);
+
+    #[wasm_bindgen(method)]
+    pub fn hydrate(this: &BaseInterpreter, ids: Vec<u32>);
+
+    #[wasm_bindgen(method, js_name = "getNode")]
+    pub fn get_node(this: &BaseInterpreter, id: u32) -> Node;
+}
+
+// Note that this impl is for the sledgehammer interpreter to allow us dropping down to the base interpreter
+// During hydration and initialization we need to the base interpreter methods
+#[cfg(feature = "webonly")]
+impl Interpreter {
+    /// Convert the interpreter to its baseclass, giving
+    pub fn base(&self) -> &BaseInterpreter {
+        use wasm_bindgen::prelude::JsCast;
+        self.js_channel().unchecked_ref()
+    }
+}
+
+#[sledgehammer_bindgen::bindgen(module)]
+mod js {
+    // Extend the web base class
+    const BASE: &str = "./src/js/core.js";
+
+    /// The interpreter extends the core interpreter which contains the state for the interpreter along with some functions that all platforms use like `AppendChildren`.
+    #[extends(BaseInterpreter)]
+    pub struct Interpreter;
+
+    fn mount_to_root() {
+        "{this.appendChildren(this.root, this.stack.length-1);}"
+    }
+    fn push_root(root: u32) {
+        "{this.stack.push(this.nodes[$root$]);}"
+    }
+    fn append_children(id: u32, many: u16) {
+        "{this.appendChildren($id$, $many$);}"
+    }
+    fn pop_root() {
+        "{this.stack.pop();}"
+    }
+    fn replace_with(id: u32, n: u16) {
+        "{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.removeAllNonBubblingListeners(root); } root.replaceWith(...els);}"
+    }
+    fn insert_after(id: u32, n: u16) {
+        "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
+    }
+    fn insert_before(id: u32, n: u16) {
+        "{this.nodes[$id$].before(...this.stack.splice(this.stack.length-$n$));}"
+    }
+    fn remove(id: u32) {
+        "{let node = this.nodes[$id$]; if (node !== undefined) { if (node.listening) { this.removeAllNonBubblingListeners(node); } node.remove(); }}"
+    }
+    fn create_raw_text(text: &str) {
+        "{this.stack.push(document.createTextNode($text$));}"
+    }
+    fn create_text_node(text: &str, id: u32) {
+        "{let node = document.createTextNode($text$); this.nodes[$id$] = node; this.stack.push(node);}"
+    }
+    fn create_placeholder(id: u32) {
+        "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node); this.nodes[$id$] = node;}"
+    }
+    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        r#"
+            let node = this.nodes[id];
+            if(node.listening){node.listening += 1;}else{node.listening = 1;}
+            node.setAttribute('data-dioxus-id', `\${id}`);
+            this.createListener($event_name$, node, $bubbles$);
+        "#
+    }
+    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        "{let node = this.nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); this.removeListener(node, $event_name$, $bubbles$);}"
+    }
+    fn set_text(id: u32, text: &str) {
+        "{this.nodes[$id$].textContent = $text$;}"
+    }
+    fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}"
+    }
+    fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
+        r#"{
+            let node = this.nodes[$id$];
+            if (!ns) {
+                switch (field) {
+                    case "value":
+                        node.value = "";
+                        break;
+                    case "checked":
+                        node.checked = false;
+                        break;
+                    case "selected":
+                        node.selected = false;
+                        break;
+                    case "dangerous_inner_html":
+                        node.innerHTML = "";
+                        break;
+                    default:
+                        node.removeAttribute(field);
+                        break;
+                }
+            } else if (ns == "style") {
+                node.style.removeProperty(name);
+            } else {
+                node.removeAttributeNS(ns, field);
+            }
+        }"#
+    }
+    fn assign_id(ptr: u32, len: u8, id: u32) {
+        "{this.nodes[$id$] = this.loadChild($ptr$, $len$);}"
+    }
+    fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
+        r#"{
+            let node = this.loadChild($ptr$, $len$);
+            if (node.nodeType == node.TEXT_NODE) {
+                node.textContent = value;
+            } else {
+                let text = document.createTextNode(value);
+                node.replaceWith(text);
+                node = text;
+            }
+            this.nodes[$id$] = node;
+        }"#
+    }
+    fn replace_placeholder(ptr: u32, len: u8, n: u16) {
+        "{let els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($ptr$, $len$); node.replaceWith(...els);}"
+    }
+    fn load_template(tmpl_id: u16, index: u16, id: u32) {
+        "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn append_children_to_top(many: u16) {
+        "{
+        let root = this.stack[this.stack.length-many-1];
+        let els = this.stack.splice(this.stack.length-many);
+        for (let k = 0; k < many; k++) {
+            root.appendChild(els[k]);
+        }
+        }"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn add_placeholder() {
+        "{let node = document.createElement('pre'); node.hidden = true; this.stack.push(node);}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn create_element(element: &'static str<u8, el>) {
+        "{this.stack.push(document.createElement($element$))}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn create_element_ns(element: &'static str<u8, el>, ns: &'static str<u8, namespace>) {
+        "{this.stack.push(document.createElementNS($ns$, $element$))}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn add_templates(tmpl_id: u16, len: u16) {
+        "{this.templates[$tmpl_id$] = this.stack.splice(this.stack.length-$len$);}"
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn foreign_event_listener(event: &str<u8, evt>, id: u32, bubbles: u8) {
+        r#"
+    bubbles = bubbles == 1;
+    let this_node = this.nodes[id];
+    if(this_node.listening){
+        this_node.listening += 1;
+    } else {
+        this_node.listening = 1;
+    }
+    this_node.setAttribute('data-dioxus-id', `\${id}`);
+    const event_name = $event$;
+
+    // if this is a mounted listener, we send the event immediately
+    if (event_name === "mounted") {
+        window.ipc.postMessage(
+            this.serializeIpcMessage("user_event", {
+                name: event_name,
+                element: id,
+                data: null,
+                bubbles,
+            })
+        );
+    } else {
+        this.createListener(event_name, this_node, bubbles, (event) => {
+            this.handler(event, event_name, bubbles);
+        });
+    }"#
+    }
+
+    /// Assign the ID
+    #[cfg(feature = "binary-protocol")]
+    fn assign_id_ref(array: &[u8], id: u32) {
+        "{this.nodes[$id$] = this.loadChild($array$);}"
+    }
+
+    /// The coolest ID ever!
+    #[cfg(feature = "binary-protocol")]
+    fn hydrate_text_ref(array: &[u8], value: &str, id: u32) {
+        r#"{
+        let node = this.loadChild($array$);
+        if (node.nodeType == node.TEXT_NODE) {
+            node.textContent = value;
+        } else {
+            let text = document.createTextNode(value);
+            node.replaceWith(text);
+            node = text;
+        }
+        this.nodes[$id$] = node;
+    }"#
+    }
+
+    #[cfg(feature = "binary-protocol")]
+    fn replace_placeholder_ref(array: &[u8], n: u16) {
+        "{let els = this.stack.splice(this.stack.length - $n$); let node = this.loadChild($array$); node.replaceWith(...els);}"
+    }
+}

+ 10 - 8
packages/interpreter/src/write_native_mutations.rs

@@ -1,17 +1,17 @@
-use dioxus_html::event_bubbles;
-
+use crate::unified_bindings::Interpreter as Channel;
 use dioxus_core::{TemplateAttribute, TemplateNode, WriteMutations};
+use dioxus_html::event_bubbles;
 use sledgehammer_utils::rustc_hash::FxHashMap;
 
-use crate::binary_protocol::Channel;
-
 /// The state needed to apply mutations to a channel. This state should be kept across all mutations for the app
 #[derive(Default)]
 pub struct MutationState {
     /// The maximum number of templates that we have registered
     max_template_count: u16,
+
     /// The currently registered templates with the template ids
     templates: FxHashMap<String, u16>,
+
     /// The channel that we are applying mutations to
     channel: Channel,
 }
@@ -100,7 +100,7 @@ impl WriteMutations for MutationState {
     }
 
     fn assign_node_id(&mut self, path: &'static [u8], id: dioxus_core::ElementId) {
-        self.channel.assign_id(path, id.0 as u32);
+        self.channel.assign_id_ref(path, id.0 as u32);
     }
 
     fn create_placeholder(&mut self, id: dioxus_core::ElementId) {
@@ -112,7 +112,7 @@ impl WriteMutations for MutationState {
     }
 
     fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: dioxus_core::ElementId) {
-        self.channel.hydrate_text(path, value, id.0 as u32);
+        self.channel.hydrate_text_ref(path, value, id.0 as u32);
     }
 
     fn load_template(&mut self, name: &'static str, index: usize, id: dioxus_core::ElementId) {
@@ -127,7 +127,7 @@ impl WriteMutations for MutationState {
     }
 
     fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
-        self.channel.replace_placeholder(path, m as u16);
+        self.channel.replace_placeholder_ref(path, m as u16);
     }
 
     fn insert_nodes_after(&mut self, id: dioxus_core::ElementId, m: usize) {
@@ -181,8 +181,10 @@ impl WriteMutations for MutationState {
     }
 
     fn create_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {
+        // note that we use the foreign event listener here instead of the native one
+        // the native method assumes we have direct access to the dom, which we don't.
         self.channel
-            .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
+            .foreign_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
     }
 
     fn remove_event_listener(&mut self, name: &'static str, id: dioxus_core::ElementId) {

+ 1 - 0
packages/interpreter/tests/e2e.rs

@@ -0,0 +1 @@
+//! Ensure that the interpreter loads, has no errors, and writes mutations

+ 1 - 0
packages/interpreter/tests/serialize.rs

@@ -0,0 +1 @@
+//! Ensure that events are serialized and deserialized correctly.

+ 17 - 0
packages/interpreter/tsconfig.json

@@ -0,0 +1,17 @@
+{
+    "compilerOptions": {
+        "module": "CommonJS",
+        "lib": [
+            "ES2015",
+            "DOM",
+            "dom",
+            "dom.iterable"
+        ],
+        "noImplicitAny": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+    },
+    "exclude": [
+        "**/*.spec.ts"
+    ]
+}

+ 0 - 1
packages/liveview/Cargo.toml

@@ -24,7 +24,6 @@ serde = { version = "1.0.151", features = ["derive"] }
 serde_json = "1.0.91"
 dioxus-html = { workspace = true, features = ["serialize", "eval", "mounted"] }
 rustc-hash = { workspace = true }
-minify-js = "0.5.6"
 dioxus-core = { workspace = true, features = ["serialize"] }
 dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
 dioxus-hot-reload = { workspace = true, optional = true }

+ 14 - 12
packages/liveview/src/lib.rs

@@ -9,6 +9,7 @@ pub use adapters::*;
 mod element;
 pub mod pool;
 mod query;
+use dioxus_interpreter_js::NATIVE_JS;
 use futures_util::{SinkExt, StreamExt};
 pub use pool::*;
 mod config;
@@ -31,8 +32,7 @@ pub enum LiveViewError {
 }
 
 fn handle_edits_code() -> String {
-    use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
-    use minify_js::{minify, Session, TopLevelMode};
+    use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
 
     let serialize_file_uploads = r#"if (
         target.tagName === "INPUT" &&
@@ -71,9 +71,17 @@ fn handle_edits_code() -> String {
           return;
         }
       }"#;
-    let mut interpreter = SLEDGEHAMMER_JS
-        .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
-        .replace("export", "");
+    let mut interpreter = format!(
+        r#"
+    // Bring the sledgehammer code
+    {SLEDGEHAMMER_JS}
+
+    // And then extend it with our native bindings
+    {NATIVE_JS}
+    "#
+    )
+    .replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
+    .replace("export", "");
     while let Some(import_start) = interpreter.find("import") {
         let import_end = interpreter[import_start..]
             .find(|c| c == ';' || c == '\n')
@@ -81,15 +89,9 @@ fn handle_edits_code() -> String {
             .unwrap_or_else(|| interpreter.len());
         interpreter.replace_range(import_start..import_end, "");
     }
-
     let main_js = include_str!("./main.js");
-
     let js = format!("{interpreter}\n{main_js}");
-
-    let session = Session::new();
-    let mut out = Vec::new();
-    minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
-    String::from_utf8(out).unwrap()
+    js
 }
 
 /// This script that gets injected into your app connects this page to the websocket endpoint

+ 4 - 3
packages/liveview/src/main.js

@@ -9,8 +9,9 @@ function main() {
 
 class IPC {
   constructor(root) {
-    window.interpreter = new JSChannel();
+    window.interpreter = new NativeInterpreter();
     window.interpreter.initialize(root);
+    window.interpreter.ipc = this;
     const ws = new WebSocket(WS_ADDR);
     ws.binaryType = "arraybuffer";
 
@@ -42,7 +43,7 @@ class IPC {
 
         let decoder = new TextDecoder("utf-8");
 
-        // Using decode method to get string output 
+        // Using decode method to get string output
         let str = decoder.decode(messageData);
         // Ignore pongs
         if (str != "__pong__") {
@@ -64,4 +65,4 @@ class IPC {
   }
 }
 
-main();
+main();

+ 2 - 3
packages/plasmo/src/hooks.rs

@@ -18,7 +18,6 @@ use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
 use dioxus_html::input_data::{
     MouseButton as DioxusMouseButton, MouseButtonSet as DioxusMouseButtons,
 };
-use dioxus_html::FormValue;
 use dioxus_html::{event_bubbles, prelude::*};
 use std::any::Any;
 use std::collections::HashMap;
@@ -67,7 +66,7 @@ impl EventData {
 pub struct FormData {
     pub(crate) value: String,
 
-    pub values: HashMap<String, FormValue>,
+    pub values: HashMap<String, String>,
 
     pub(crate) files: Option<Files>,
 }
@@ -77,7 +76,7 @@ impl HasFormData for FormData {
         self.value.clone()
     }
 
-    fn values(&self) -> HashMap<String, FormValue> {
+    fn values(&self) -> HashMap<String, String> {
         self.values.clone()
     }
 

+ 35 - 0
packages/web/NOTES.md

@@ -0,0 +1,35 @@
+
+// ## RequestAnimationFrame and RequestIdleCallback
+// ------------------------------------------------
+// React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
+// running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
+// main thread.
+//
+// React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
+// the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase
+// is  finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
+//
+// Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
+// setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
+//
+//     From Google's guide on rAF and rIC:
+//     -----------------------------------
+//
+//     If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
+//     which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
+//      of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
+//      frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
+//      which is a potential performance bottleneck.
+//
+//     Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
+//     and as such we could easily go past the deadline the browser provided.
+//
+//     The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
+//     browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
+//     be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
+//     to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
+//
+//     Essentially:
+//     ------------
+//     - Do the VDOM work during the idlecallback
+//     - Do DOM work in the next requestAnimationFrame callback

+ 3 - 0
packages/web/ric_raf/README.md

@@ -0,0 +1,3 @@
+requestIdleCallback and requestAnimationFrame implemenation
+
+These currently actually slow down our DOM patching and thus are temporarily removed. Technically we can schedule around rIC and rAF but choose not to.

+ 0 - 0
packages/web/src/ric_raf.rs → packages/web/ric_raf/ric_raf.rs


+ 0 - 0
packages/web/src/ricpolyfill.js → packages/web/ric_raf/ricpolyfill.js


+ 42 - 37
packages/web/src/dom.rs

@@ -8,7 +8,7 @@
 
 use dioxus_core::ElementId;
 use dioxus_html::PlatformEventData;
-use dioxus_interpreter_js::Channel;
+use dioxus_interpreter_js::unified_bindings::Interpreter;
 use futures_channel::mpsc;
 use rustc_hash::FxHashMap;
 use wasm_bindgen::{closure::Closure, JsCast};
@@ -17,14 +17,16 @@ use web_sys::{Document, Element, Event};
 use crate::{load_document, virtual_event_from_websys_event, Config, WebEventConverter};
 
 pub struct WebsysDom {
-    pub(crate) document: Document,
     #[allow(dead_code)]
     pub(crate) root: Element,
+    pub(crate) document: Document,
     pub(crate) templates: FxHashMap<String, u16>,
     pub(crate) max_template_id: u16,
-    pub(crate) interpreter: Channel,
+    pub(crate) interpreter: Interpreter,
+
     #[cfg(feature = "mounted")]
     pub(crate) event_channel: mpsc::UnboundedSender<UiEvent>,
+
     #[cfg(feature = "mounted")]
     pub(crate) queued_mounted_events: Vec<ElementId>,
 }
@@ -36,8 +38,6 @@ pub struct UiEvent {
     pub data: PlatformEventData,
 }
 
-//fn get_document(elem: &web_sys::Element) ->
-
 impl WebsysDom {
     pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<UiEvent>) -> Self {
         let (document, root) = match cfg.root {
@@ -66,56 +66,61 @@ impl WebsysDom {
             }
         };
 
-        let interpreter = Channel::default();
+        let interpreter = Interpreter::default();
 
         let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
             let event_channel = event_channel.clone();
             move |event: &web_sys::Event| {
                 let name = event.type_();
                 let element = walk_event_for_id(event);
-                let bubbles = dioxus_html::event_bubbles(name.as_str());
-                if let Some((element, target)) = element {
-                    let prevent_event;
-                    if let Some(prevent_requests) = target
-                        .get_attribute("dioxus-prevent-default")
-                        .as_deref()
-                        .map(|f| f.split_whitespace())
-                    {
-                        prevent_event = prevent_requests
-                            .map(|f| f.trim_start_matches("on"))
-                            .any(|f| f == name);
-                    } else {
-                        prevent_event = false;
-                    }
+                let bubbles = event.bubbles();
+
+                let Some((element, target)) = element else {
+                    return;
+                };
+
+                let prevent_event;
+                if let Some(prevent_requests) = target
+                    .get_attribute("dioxus-prevent-default")
+                    .as_deref()
+                    .map(|f| f.split_whitespace())
+                {
+                    prevent_event = prevent_requests
+                        .map(|f| f.trim_start_matches("on"))
+                        .any(|f| f == name);
+                } else {
+                    prevent_event = false;
+                }
 
-                    // Prevent forms from submitting and redirecting
-                    if name == "submit" {
-                        // On forms the default behavior is not to submit, if prevent default is set then we submit the form
-                        if !prevent_event {
-                            event.prevent_default();
-                        }
-                    } else if prevent_event {
+                // Prevent forms from submitting and redirecting
+                if name == "submit" {
+                    // On forms the default behavior is not to submit, if prevent default is set then we submit the form
+                    if !prevent_event {
                         event.prevent_default();
                     }
-
-                    let data = virtual_event_from_websys_event(event.clone(), target);
-                    let _ = event_channel.unbounded_send(UiEvent {
-                        name,
-                        bubbles,
-                        element,
-                        data,
-                    });
+                } else if prevent_event {
+                    event.prevent_default();
                 }
+
+                let data = virtual_event_from_websys_event(event.clone(), target);
+                let _ = event_channel.unbounded_send(UiEvent {
+                    name,
+                    bubbles,
+                    element,
+                    data,
+                });
             }
         }));
 
-        dioxus_interpreter_js::initialize(
-            interpreter.js_channel(),
+        let _interpreter = interpreter.base();
+        _interpreter.initialize(
             root.clone().unchecked_into(),
             handler.as_ref().unchecked_ref(),
         );
+
         dioxus_html::set_event_converter(Box::new(WebEventConverter));
         handler.forget();
+
         Self {
             document,
             root,

+ 9 - 19
packages/web/src/event.rs

@@ -4,7 +4,6 @@ use dioxus_html::{
     point_interaction::{
         InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
     },
-    prelude::FormValue,
     DragData, FileEngine, FormData, HasDragData, HasFileData, HasFormData, HasImageData,
     HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
 };
@@ -385,24 +384,14 @@ impl HasFormData for WebFormData {
         .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
     }
 
-    fn values(&self) -> HashMap<String, FormValue> {
+    fn values(&self) -> HashMap<String, String> {
         let mut values = HashMap::new();
 
-        fn insert_value(map: &mut HashMap<String, FormValue>, key: String, new_value: String) {
-            match map.entry(key) {
-                std::collections::hash_map::Entry::Occupied(mut o) => {
-                    let first_value = match o.get_mut() {
-                        FormValue::Text(data) => std::mem::take(data),
-                        FormValue::VecText(vec) => {
-                            vec.push(new_value);
-                            return;
-                        }
-                    };
-                    let _ = o.insert(FormValue::VecText(vec![first_value, new_value]));
-                }
-                std::collections::hash_map::Entry::Vacant(v) => {
-                    let _ = v.insert(FormValue::Text(new_value));
-                }
+        fn insert_value(map: &mut HashMap<String, String>, key: String, new_value: String) {
+            if let Some(value) = map.get(&key) {
+                map.insert(key, format!("{},{}", value, new_value));
+            } else {
+                map.insert(key, new_value);
             }
         }
 
@@ -425,8 +414,8 @@ impl HasFormData for WebFormData {
             }
         } else if let Some(select) = self.element.dyn_ref::<web_sys::HtmlSelectElement>() {
             // try to fill in select element values
-            let options = get_select_data(select);
-            values.insert("options".to_string(), FormValue::VecText(options));
+            let options = get_select_data(select).join(",");
+            values.insert("options".to_string(), options);
         }
 
         values
@@ -441,6 +430,7 @@ impl HasFileData for WebFormData {
     fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
         #[cfg(not(feature = "file_engine"))]
         let files = None;
+
         #[cfg(feature = "file_engine")]
         let files = self
             .element

+ 13 - 55
packages/web/src/lib.rs

@@ -20,41 +20,6 @@
 //! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
 //! validation of websys-specific features and not the general use of Dioxus.
 
-// ## RequestAnimationFrame and RequestIdleCallback
-// ------------------------------------------------
-// React implements "jank free rendering" by deliberately not blocking the browser's main thread. For large diffs, long
-// running work, and integration with things like React-Three-Fiber, it's extremeley important to avoid blocking the
-// main thread.
-//
-// React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
-// the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase
-// is  finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
-//
-// Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
-// setting a timeout from the that completes when the idleperiod is over. Then, we call requestAnimationFrame
-//
-//     From Google's guide on rAF and rIC:
-//     -----------------------------------
-//
-//     If the callback is fired at the end of the frame, it will be scheduled to go after the current frame has been committed,
-//     which means that style changes will have been applied, and, importantly, layout calculated. If we make DOM changes inside
-//      of the idle callback, those layout calculations will be invalidated. If there are any kind of layout reads in the next
-//      frame, e.g. getBoundingClientRect, clientWidth, etc, the browser will have to perform a Forced Synchronous Layout,
-//      which is a potential performance bottleneck.
-//
-//     Another reason not trigger DOM changes in the idle callback is that the time impact of changing the DOM is unpredictable,
-//     and as such we could easily go past the deadline the browser provided.
-//
-//     The best practice is to only make DOM changes inside of a requestAnimationFrame callback, since it is scheduled by the
-//     browser with that type of work in mind. That means that our code will need to use a document fragment, which can then
-//     be appended in the next requestAnimationFrame callback. If you are using a VDOM library, you would use requestIdleCallback
-//     to make changes, but you would apply the DOM patches in the next requestAnimationFrame callback, not the idle callback.
-//
-//     Essentially:
-//     ------------
-//     - Do the VDOM work during the idlecallback
-//     - Do DOM work in the next requestAnimationFrame callback
-
 use std::rc::Rc;
 
 pub use crate::cfg::Config;
@@ -65,35 +30,33 @@ use futures_util::{pin_mut, select, FutureExt, StreamExt};
 
 mod cfg;
 mod dom;
-#[cfg(feature = "eval")]
-mod eval;
+
 mod event;
 pub mod launch;
 mod mutations;
 pub use event::*;
+
+#[cfg(feature = "eval")]
+mod eval;
+
 #[cfg(feature = "file_engine")]
 mod file_engine;
+
 #[cfg(all(feature = "hot_reload", debug_assertions))]
 mod hot_reload;
+
 #[cfg(feature = "hydrate")]
 mod rehydrate;
 
-// Currently disabled since it actually slows down immediate rendering
-// todo: only schedule non-immediate renders through ric/raf
-// mod ric_raf;
-// mod rehydrate;
-
 /// Runs the app as a future that can be scheduled around the main thread.
 ///
 /// Polls futures internal to the VirtualDOM, hence the async nature of this function.
 ///
 /// # Example
 ///
-/// ```ignore
-/// fn main() {
-///     let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("joe") });
-///     wasm_bindgen_futures::spawn_local(app_fut);
-/// }
+/// ```ignore, rust
+/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("foo") });
+/// wasm_bindgen_futures::spawn_local(app_fut);
 /// ```
 pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
     tracing::info!("Starting up");
@@ -101,12 +64,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
     let mut dom = virtual_dom;
 
     #[cfg(feature = "eval")]
-    {
-        // Eval
-        dom.in_runtime(|| {
-            eval::init_eval();
-        });
-    }
+    dom.in_runtime(eval::init_eval);
 
     #[cfg(feature = "panic_hook")]
     if web_config.default_panic_hook {
@@ -149,13 +107,12 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
     websys_dom.mount();
 
     loop {
-        tracing::trace!("waiting for work");
-
         // if virtual dom has nothing, wait for it to have something before requesting idle time
         // if there is work then this future resolves immediately.
         let (mut res, template) = {
             let work = dom.wait_for_work().fuse();
             pin_mut!(work);
+
             let mut rx_next = rx.select_next_some();
 
             #[cfg(all(feature = "hot_reload", debug_assertions))]
@@ -167,6 +124,7 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
                     evt = rx_next => (Some(evt), None),
                 }
             }
+
             #[cfg(not(all(feature = "hot_reload", debug_assertions)))]
             select! {
                 _ = work => (None, None),

+ 32 - 36
packages/web/src/mutations.rs

@@ -5,9 +5,7 @@ use dioxus_core::WriteMutations;
 use dioxus_core::{AttributeValue, ElementId};
 use dioxus_html::event_bubbles;
 use dioxus_html::PlatformEventData;
-use dioxus_interpreter_js::get_node;
 use dioxus_interpreter_js::minimal_bindings;
-use dioxus_interpreter_js::save_template;
 use wasm_bindgen::JsCast;
 use wasm_bindgen::JsValue;
 
@@ -58,19 +56,23 @@ impl WebsysDom {
 
     pub fn flush_edits(&mut self) {
         self.interpreter.flush();
-        #[cfg(feature = "mounted")]
+
         // Now that we've flushed the edits and the dom nodes exist, we can send the mounted events.
-        {
-            for id in self.queued_mounted_events.drain(..) {
-                let node = get_node(self.interpreter.js_channel(), id.0 as u32);
-                if let Some(element) = node.dyn_ref::<web_sys::Element>() {
-                    let _ = self.event_channel.unbounded_send(UiEvent {
-                        name: "mounted".to_string(),
-                        bubbles: false,
-                        element: id,
-                        data: PlatformEventData::new(Box::new(element.clone())),
-                    });
-                }
+        #[cfg(feature = "mounted")]
+        self.flush_queued_mounted_events();
+    }
+
+    #[cfg(feature = "mounted")]
+    fn flush_queued_mounted_events(&mut self) {
+        for id in self.queued_mounted_events.drain(..) {
+            let node = self.interpreter.base().get_node(id.0 as u32);
+            if let Some(element) = node.dyn_ref::<web_sys::Element>() {
+                let _ = self.event_channel.unbounded_send(UiEvent {
+                    name: "mounted".to_string(),
+                    bubbles: false,
+                    element: id,
+                    data: PlatformEventData::new(Box::new(element.clone())),
+                });
             }
         }
     }
@@ -84,14 +86,14 @@ impl WebsysDom {
 impl WriteMutations for WebsysDom {
     fn register_template(&mut self, template: Template) {
         let mut roots = vec![];
-
         for root in template.roots {
             roots.push(self.create_template_node(root))
         }
-
         self.templates
             .insert(template.name.to_owned(), self.max_template_id);
-        save_template(self.interpreter.js_channel(), roots, self.max_template_id);
+        self.interpreter
+            .base()
+            .save_template(roots, self.max_template_id);
         self.max_template_id += 1
     }
 
@@ -184,30 +186,24 @@ impl WriteMutations for WebsysDom {
     }
 
     fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
-        match name {
-            // mounted events are fired immediately after the element is mounted.
-            "mounted" => {
-                #[cfg(feature = "mounted")]
-                self.send_mount_event(id);
-            }
-            _ => {
-                self.interpreter
-                    .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
-            }
+        // mounted events are fired immediately after the element is mounted.
+        if name == "mounted" {
+            #[cfg(feature = "mounted")]
+            self.send_mount_event(id);
+            return;
         }
+
+        self.interpreter
+            .new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
     }
 
     fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
-        match name {
-            "mounted" => {}
-            _ => {
-                self.interpreter.remove_event_listener(
-                    name,
-                    id.0 as u32,
-                    event_bubbles(name) as u8,
-                );
-            }
+        if name == "mounted" {
+            return;
         }
+
+        self.interpreter
+            .remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
     }
 
     fn remove_node(&mut self, id: ElementId) {

+ 6 - 9
packages/web/src/rehydrate.rs

@@ -3,7 +3,6 @@ use dioxus_core::prelude::*;
 use dioxus_core::AttributeValue;
 use dioxus_core::WriteMutations;
 use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom};
-use dioxus_interpreter_js::save_template;
 
 #[derive(Debug)]
 pub enum RehydrationError {
@@ -23,7 +22,7 @@ impl WebsysDom {
         // Recursively rehydrate the dom from the VirtualDom
         self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?;
 
-        dioxus_interpreter_js::hydrate(self.interpreter.js_channel(), ids);
+        self.interpreter.base().hydrate(ids);
 
         #[cfg(feature = "mounted")]
         for id in to_mount {
@@ -40,8 +39,7 @@ impl WebsysDom {
         ids: &mut Vec<u32>,
         to_mount: &mut Vec<ElementId>,
     ) -> Result<(), RehydrationError> {
-        let vnode = scope.root_node();
-        self.rehydrate_vnode(dom, vnode, ids, to_mount)
+        self.rehydrate_vnode(dom, scope.root_node(), ids, to_mount)
     }
 
     fn rehydrate_vnode(
@@ -168,11 +166,10 @@ impl WriteMutations for OnlyWriteTemplates<'_> {
         self.0
             .templates
             .insert(template.name.to_owned(), self.0.max_template_id);
-        save_template(
-            self.0.interpreter.js_channel(),
-            roots,
-            self.0.max_template_id,
-        );
+        self.0
+            .interpreter
+            .base()
+            .save_template(roots, self.0.max_template_id);
         self.0.max_template_id += 1
     }