diff --git a/.vscode/settings.json b/.vscode/settings.json index e386d4b4c..1ebfd79ef 100644 --- a/.vscode/settings.json +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index 20c877a8a..e8b276778 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index c07bd33d4..0f68d3054 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/examples/assets/file_upload.css b/examples/assets/file_upload.css index 81aba2638..58d1bb3dc 100644 --- a/examples/assets/file_upload.css +++ b/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 { diff --git a/examples/file_upload.rs b/examples/file_upload.rs index 280144e44..26f01f9af 100644 --- a/examples/file_upload.rs +++ b/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); + let mut files_uploaded = use_signal(|| Vec::new() as Vec); + 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| 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}" } + } } } } diff --git a/examples/form.rs b/examples/form.rs index 7da92e65c..9f6215226 100644 --- a/examples/form.rs +++ b/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::>()); + }, + 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:#?}" } } } } diff --git a/examples/image_generator_openai.rs b/examples/image_generator_openai.rs index 8cebbb373..9c0492273 100644 --- a/examples/image_generator_openai.rs +++ b/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 { let client = reqwest::Client::new(); diff --git a/examples/router.rs b/examples/router.rs index 7b41e94e1..5752de3e6 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; fn main() { - launch_desktop(|| { + launch(|| { rsx! { style { {include_str!("./assets/router.css")} } Router:: {} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 2af2daf03..7b2405f40 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -131,7 +131,7 @@ fn TodoHeader(mut todos: Signal>) -> 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, } } diff --git a/examples/weather_app.rs b/examples/weather_app.rs index 9615c8d49..a5774a327 100644 --- a/examples/weather_app.rs +++ b/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) -> Element { let mut input = use_signal(|| "".to_string()); - let locations = use_resource(move || async move { - let current_location = input.read().clone(); - get_locations(¤t_location).await - }); + let locations = use_resource(move || async move { get_locations(&input()).await }); rsx! { div { @@ -150,27 +139,17 @@ fn SearchBox(mut country: Signal) -> 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..." } } } diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 8f4dc7245..cb8d414dd 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; fn main() { - launch_desktop(app); + launch(app); } fn app() -> Element { diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fdb68d734..5e05af658 100644 --- a/packages/desktop/Cargo.toml +++ b/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] diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index f36320e7e..c981144d5 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/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 = Signal::global(|| 0); - 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); -} - -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(); - }); - }) -} - -#[allow(deprecated)] fn app() -> Element { let desktop_context: DesktopContext = consume_context(); - let mut received_events = use_signal(|| 0); - // button - mock_event( + let received = RECEIVED_EVENTS(); + let expected = utils::EXPECTED_EVENTS(); + + use_effect(move || { + println!("expecting {} events", utils::EXPECTED_EVENTS()); + }); + + if expected != 0 && received == expected { + println!("all events recieved"); + desktop_context.close(); + } + + 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 test_mounted() -> Element { + use_hook(|| utils::EXPECTED_EVENTS.with_mut(|x| *x += 1)); + + 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); + } + } + } +} + +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( - "focus_in_div", - r#"new FocusEvent("focusin", {bubbles: true})"#, - ); - // focus_out_div - mock_event( - "focus_out_div", - r#"new FocusEvent("focusout",{bubbles: true})"#, - ); - - if received_events() == 13 { - println!("all events recieved"); - desktop_context.close(); - } - 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); - } - } - 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); - } + 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})"#, + ); + + 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})"#, + ); + 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 { + 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" } + } + } + } +} + +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::>(); + 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" } + } + } +} diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 882c70d21..df050ed10 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,28 +2,11 @@ use dioxus::prelude::*; use dioxus_core::Element; use dioxus_desktop::DesktopContext; +#[path = "./utils.rs"] +mod utils; + fn main() { - check_app_exits(check_html_renders); -} - -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); + utils::check_app_exits(check_html_renders); } fn use_inner_html(id: &'static str) -> Option { @@ -31,7 +14,7 @@ fn use_inner_html(id: &'static str) -> Option { 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('{}'); diff --git a/packages/desktop/headless_tests/utils.rs b/packages/desktop/headless_tests/utils.rs new file mode 100644 index 000000000..a497d3586 --- /dev/null +++ b/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 = 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(); + }); + }) +} diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 9adf6bc96..31c644c0a 100644 --- a/packages/desktop/src/app.rs +++ b/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>, pub(crate) shortcut_manager: ShortcutRegistry, - pub(crate) global_hotkey_channel: Receiver, pub(crate) proxy: EventLoopProxy, pub(crate) target: EventLoopWindowTarget, } @@ -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::(msg.params()) else { return; }; - struct DesktopFileUploadForm { - files: Arc, - } - - impl HasFileData for DesktopFileUploadForm { - fn files(&self) -> Option> { - 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")] diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index f84baf6ea..6178dbf93 100644 --- a/packages/desktop/src/config.rs +++ b/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, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) disable_context_menu: bool, @@ -35,8 +30,6 @@ pub struct Config { pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; - pub(crate) type WryProtocol = ( String, Box>) -> HttpResponse> + '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(mut self, name: String, handler: F) -> Self where diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 867516a64..a32436f0e 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/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, pub(crate) asset_handlers: AssetHandlerRegistry, + pub(crate) file_hover: NativeFileHover, #[cfg(target_os = "ios")] pub(crate) views: Rc>>, @@ -83,14 +88,16 @@ impl DesktopService { shared: Rc, 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 diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index ac5a3df0d..5e772320b 100644 --- a/packages/desktop/src/events.rs +++ b/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::() { + return _data.clone().into(); + } + + // If that failed then it's a file drag form event - .downcast::() + .downcast::() .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::() { + return _data.clone().into(); + } + + // If that failed then it's a file upload form event - .downcast::() + .downcast::() .cloned() .unwrap() .into() diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 98dbec437..b9e76b668 100644 --- a/packages/desktop/src/file_upload.rs +++ b/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, +} + +impl HasFileData for DesktopFileUploadForm { + fn files(&self) -> Option> { + 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>>, +} +impl NativeFileHover { + pub fn set(&self, event: FileDropEvent) { + self.event.borrow_mut().replace(event); + } + + pub fn current(&self) -> Option { + self.event.borrow_mut().clone() + } +} + +#[derive(Clone)] +pub(crate) struct DesktopFileDragEvent { + pub mouse: SerializedPointInteraction, + pub files: Arc, +} + +impl HasFileData for DesktopFileDragEvent { + fn files(&self) -> Option> { + 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 { + self.mouse.trigger_button() + } +} diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index c8810c63a..b4e86c64c 100644 --- a/packages/desktop/src/ipc.rs +++ b/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, diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index b5602d10e..ea37b2827 100644 --- a/packages/desktop/src/launch.rs +++ b/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(_) => {} }, }, diff --git a/packages/desktop/src/mobile_shortcut.rs b/packages/desktop/src/mobile_shortcut.rs index 03566bf36..5ff0683a1 100644 --- a/packages/desktop/src/mobile_shortcut.rs +++ b/packages/desktop/src/mobile_shortcut.rs @@ -80,10 +80,4 @@ pub struct GlobalHotKeyEvent { pub id: u32, } -impl GlobalHotKeyEvent { - pub fn receiver() -> crossbeam_channel::Receiver { - crossbeam_channel::unbounded().1 - } -} - pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index dbbd905ae..1e615aa6e 100644 --- a/packages/desktop/src/protocol.rs +++ b/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("").expect("Head element to exist"), &head); } @@ -237,20 +173,26 @@ fn serve_from_fs(path: PathBuf) -> Result>> { /// - 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#" "# diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 0e5ecbe98..d6ab3f07b 100644 --- a/packages/desktop/src/query.rs +++ b/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 { pub slab: Rc>>, } diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index b647d1337..34722faea 100644 --- a/packages/desktop/src/shortcut.rs +++ b/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() { diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 5c3e915e3..fd031afb3 100644 --- a/packages/desktop/src/waker.rs +++ b/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, id: WindowId) -> std::t fn wake_by_ref(arc_self: &Arc) { _ = arc_self .proxy - .send_event(UserWindowEvent(EventData::Poll, arc_self.id)); + .send_event(UserWindowEvent::Poll(arc_self.id)); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index ade023c2f..ae3d48260 100644 --- a/packages/desktop/src/webview.rs +++ b/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 = diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 9b58fc11c..0a761f719 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -59,6 +59,10 @@ impl PlatformEventData { Self { event } } + pub fn inner(&self) -> &Box { + &self.event + } + pub fn downcast(&self) -> Option<&T> { self.event.downcast_ref::() } diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index bbedf9a2b..4cd8119db 100644 --- a/packages/html/src/events/drag.rs +++ b/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, } diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 01d5793ec..bd8c45fec 100644 --- a/packages/html/src/events/form.rs +++ b/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; -/// 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) to Vec - serde(untagged) -)] -#[derive(Debug, Clone, PartialEq)] -pub enum FormValue { - Text(String), - VecText(Vec), -} - -impl From for Vec { - 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 - pub fn to_vec(self) -> Vec { - self.into() - } -} - /* DOMEvent: Send + SyncTarget relatedTarget */ pub struct FormData { inner: Box, @@ -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 { + /// Collect all the named form values from the containing form. + /// + /// Every input must be named! + pub fn values(&self) -> HashMap { self.inner.values() } @@ -120,6 +78,11 @@ impl FormData { pub fn downcast(&self) -> Option<&T> { self.inner.as_any().downcast_ref::() } + + /// 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 { + fn valid(&self) -> bool { + true + } + + fn values(&self) -> HashMap { 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, + + #[serde(default)] + values: HashMap, + + #[serde(default)] + valid: bool, + + #[serde(default)] files: Option, } @@ -174,13 +149,14 @@ impl SerializedFormData { /// Create a new serialized form data object pub fn new( value: String, - values: HashMap, + values: HashMap, files: Option, ) -> 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 { + fn values(&self) -> HashMap { self.values.clone() } + fn valid(&self) -> bool { + self.valid + } + fn as_any(&self) -> &dyn std::any::Any { self } diff --git a/packages/html/src/point_interaction.rs b/packages/html/src/point_interaction.rs index 9aa4e0f31..b0e34f780 100644 --- a/packages/html/src/point_interaction.rs +++ b/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. diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 17a8a5f2d..867313750 100644 --- a/packages/html/src/transit.rs +++ b/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 { @@ -136,7 +147,7 @@ fn fun_name( #[cfg(feature = "serialize")] impl HtmlEvent { pub fn bubbles(&self) -> bool { - event_bubbles(&self.name) + self.bubbles } } diff --git a/packages/interpreter/.gitignore b/packages/interpreter/.gitignore new file mode 100644 index 000000000..27babb65e --- /dev/null +++ b/packages/interpreter/.gitignore @@ -0,0 +1 @@ +gen2/ diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index ba813a538..7ec60ba38 100644 --- a/packages/interpreter/Cargo.toml +++ b/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"] diff --git a/packages/interpreter/NOTES.md b/packages/interpreter/NOTES.md new file mode 100644 index 000000000..db180b737 --- /dev/null +++ b/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 diff --git a/packages/interpreter/README.md b/packages/interpreter/README.md index 0c195d681..a53e4b470 100644 --- a/packages/interpreter/README.md +++ b/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). diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs new file mode 100644 index 000000000..f5d69db40 --- /dev/null +++ b/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 + ); + } +} diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js deleted file mode 100644 index 055353fa5..000000000 --- a/packages/interpreter/src/common.js +++ /dev/null @@ -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; -} diff --git a/packages/interpreter/src/common_exported.js b/packages/interpreter/src/common_exported.js deleted file mode 100644 index 0c64085f9..000000000 --- a/packages/interpreter/src/common_exported.js +++ /dev/null @@ -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; -} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js deleted file mode 100644 index 63ee6d630..000000000 --- a/packages/interpreter/src/interpreter.js +++ /dev/null @@ -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; -} diff --git a/packages/interpreter/src/js/README.md b/packages/interpreter/src/js/README.md new file mode 100644 index 000000000..851ef6751 --- /dev/null +++ b/packages/interpreter/src/js/README.md @@ -0,0 +1 @@ +this files are generated - do not edit them! diff --git a/packages/interpreter/src/js/common.js b/packages/interpreter/src/js/common.js new file mode 100644 index 000000000..9e4964594 --- /dev/null +++ b/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}; diff --git a/packages/interpreter/src/js/core.js b/packages/interpreter/src/js/core.js new file mode 100644 index 000000000..c15a50557 --- /dev/null +++ b/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;k0;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;i1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j1)this.nodes[ids[parseInt(split[1])]]=currentNode.nextSibling;currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter}; diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt new file mode 100644 index 000000000..9fe9e2627 --- /dev/null +++ b/packages/interpreter/src/js/hash.txt @@ -0,0 +1 @@ +12655652627 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js new file mode 100644 index 000000000..290823ee7 --- /dev/null +++ b/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;icontents={...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;i0;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;kresponse.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); + + /// Roll up all the values from the node into a JS object that we can deserialize + pub fn collectFormValues(node: JsValue) -> JsValue; } } diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs deleted file mode 100644 index bdba48439..000000000 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ /dev/null @@ -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, 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, 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, value: &str, ns: &str) { - "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - 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, tmpl_id: u16); - - pub fn hydrate(channel: &JSChannel, ids: Vec); - - 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) { - "{this.stack.push(document.createElement($element$))}" - } - fn create_element_ns(element: &'static str, ns: &'static str) { - "{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, 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, 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, value: &str, ns: &str) { - "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" - } - fn set_top_attribute(field: &str, value: &str, ns: &str) { - "{this.setAttributeInner(this.stack[this.stack.length-1], $field$, $value$, $ns$);}" - } - fn remove_attribute(id: u32, field: &str, ns: &str) { - 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$);}" - } - } -} diff --git a/packages/interpreter/src/ts/.gitignore b/packages/interpreter/src/ts/.gitignore new file mode 100644 index 000000000..68b79b0d3 --- /dev/null +++ b/packages/interpreter/src/ts/.gitignore @@ -0,0 +1,3 @@ +# please dont accidentally run tsc and commit your js in this dir. +*.js + diff --git a/packages/interpreter/src/ts/common.ts b/packages/interpreter/src/ts/common.ts new file mode 100644 index 000000000..643f36436 --- /dev/null +++ b/packages/interpreter/src/ts/common.ts @@ -0,0 +1,2 @@ +export { setAttributeInner } from "./set_attribute"; +export { retrieveFormValues } from "./form"; diff --git a/packages/interpreter/src/ts/core.ts b/packages/interpreter/src/ts/core.ts new file mode 100644 index 000000000..c0c91b640 --- /dev/null +++ b/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); + } +} + diff --git a/packages/interpreter/src/ts/form.ts b/packages/interpreter/src/ts/form.ts new file mode 100644 index 000000000..9f76e3c1b --- /dev/null +++ b/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; +} diff --git a/packages/interpreter/src/ts/native.ts b/packages/interpreter/src/ts/native.ts new file mode 100644 index 000000000..74f91a677 --- /dev/null +++ b/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(); +// }); +// } +// } +// } diff --git a/packages/interpreter/src/ts/serialize.ts b/packages/interpreter/src/ts/serialize.ts new file mode 100644 index 000000000..c9288ac3f --- /dev/null +++ b/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], + } + }, + }; +} diff --git a/packages/interpreter/src/ts/set_attribute.ts b/packages/interpreter/src/ts/set_attribute.ts new file mode 100644 index 000000000..378fd479b --- /dev/null +++ b/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; + } +} diff --git a/packages/interpreter/src/unified_bindings.rs b/packages/interpreter/src/unified_bindings.rs new file mode 100644 index 000000000..e64b412a5 --- /dev/null +++ b/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, tmpl_id: u16); + + #[wasm_bindgen(method)] + pub fn hydrate(this: &BaseInterpreter, ids: Vec); + + #[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, 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, 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, value: &str, ns: &str) { + "{let node = this.nodes[$id$]; this.setAttributeInner(node, $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + 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, value: &str, ns: &str) { + "{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) { + "{this.stack.push(document.createElement($element$))}" + } + + #[cfg(feature = "binary-protocol")] + fn create_element_ns(element: &'static str, ns: &'static str) { + "{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, 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);}" + } +} diff --git a/packages/interpreter/src/write_native_mutations.rs b/packages/interpreter/src/write_native_mutations.rs index 18a996240..1f4adbd13 100644 --- a/packages/interpreter/src/write_native_mutations.rs +++ b/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, + /// 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) { diff --git a/packages/interpreter/tests/e2e.rs b/packages/interpreter/tests/e2e.rs new file mode 100644 index 000000000..81f33b00b --- /dev/null +++ b/packages/interpreter/tests/e2e.rs @@ -0,0 +1 @@ +//! Ensure that the interpreter loads, has no errors, and writes mutations diff --git a/packages/interpreter/tests/serialize.rs b/packages/interpreter/tests/serialize.rs new file mode 100644 index 000000000..4698c285a --- /dev/null +++ b/packages/interpreter/tests/serialize.rs @@ -0,0 +1 @@ +//! Ensure that events are serialized and deserialized correctly. diff --git a/packages/interpreter/tsconfig.json b/packages/interpreter/tsconfig.json new file mode 100644 index 000000000..6a9dd215a --- /dev/null +++ b/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" + ] +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 6e7173e18..dac3ea6b6 100644 --- a/packages/liveview/Cargo.toml +++ b/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 } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 9ed7b85e8..045d180de 100644 --- a/packages/liveview/src/lib.rs +++ b/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 diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 59a32dd0b..9fb1f0d8e 100644 --- a/packages/liveview/src/main.js +++ b/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(); \ No newline at end of file +main(); diff --git a/packages/plasmo/src/hooks.rs b/packages/plasmo/src/hooks.rs index 81f6558b6..0c0ca1c21 100644 --- a/packages/plasmo/src/hooks.rs +++ b/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, + pub values: HashMap, pub(crate) files: Option, } @@ -77,7 +76,7 @@ impl HasFormData for FormData { self.value.clone() } - fn values(&self) -> HashMap { + fn values(&self) -> HashMap { self.values.clone() } diff --git a/packages/web/NOTES.md b/packages/web/NOTES.md new file mode 100644 index 000000000..e631eaa58 --- /dev/null +++ b/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 diff --git a/packages/web/ric_raf/README.md b/packages/web/ric_raf/README.md new file mode 100644 index 000000000..a1bc20e82 --- /dev/null +++ b/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. diff --git a/packages/web/src/ric_raf.rs b/packages/web/ric_raf/ric_raf.rs similarity index 100% rename from packages/web/src/ric_raf.rs rename to packages/web/ric_raf/ric_raf.rs diff --git a/packages/web/src/ricpolyfill.js b/packages/web/ric_raf/ricpolyfill.js similarity index 100% rename from packages/web/src/ricpolyfill.js rename to packages/web/ric_raf/ricpolyfill.js diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 00cfa1478..81da59e76 100644 --- a/packages/web/src/dom.rs +++ b/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, pub(crate) max_template_id: u16, - pub(crate) interpreter: Channel, + pub(crate) interpreter: Interpreter, + #[cfg(feature = "mounted")] pub(crate) event_channel: mpsc::UnboundedSender, + #[cfg(feature = "mounted")] pub(crate) queued_mounted_events: Vec, } @@ -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) -> Self { let (document, root) = match cfg.root { @@ -66,56 +66,61 @@ impl WebsysDom { } }; - let interpreter = Channel::default(); + let interpreter = Interpreter::default(); let handler: Closure = 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(); - // 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 { + 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(); } - - 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, diff --git a/packages/web/src/event.rs b/packages/web/src/event.rs index 8fa2b1aea..1765fd38a 100644 --- a/packages/web/src/event.rs +++ b/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 { + fn values(&self) -> HashMap { let mut values = HashMap::new(); - fn insert_value(map: &mut HashMap, 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, 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::() { // 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> { #[cfg(not(feature = "file_engine"))] let files = None; + #[cfg(feature = "file_engine")] let files = self .element diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e106a9dc3..e47634505 100644 --- a/packages/web/src/lib.rs +++ b/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), diff --git a/packages/web/src/mutations.rs b/packages/web/src/mutations.rs index c3c2a8147..dc7d91e75 100644 --- a/packages/web/src/mutations.rs +++ b/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::() { - 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::() { + 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) { diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 271b3daab..de137873a 100644 --- a/packages/web/src/rehydrate.rs +++ b/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, to_mount: &mut Vec, ) -> 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 }