From 1fd1ce78857586cdb41987b0d2a0360edea383e2 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 20 Mar 2023 17:19:22 -0700 Subject: [PATCH 01/12] Add wasm DOM implementation for xilem. I'm doing all the work in a single commit so it's easier to rebase. Below is a list of the individual changes I did - Squash prev commits - Remove top-level action handler (use Adapt instead). - Type events in the same way as elements (could also do attributes the same) - Allow users to avoid compiling the typed html/attributes/events - Use more specific types for mouse events from web_sys - change "click" from PointerEvent to MouseEvent (PointerEvent caused a panic on `dyn_into().unwrap()`) --- .gitignore | 3 + Cargo.lock | 781 ++++++++++-------- Cargo.toml | 6 + crates/xilem_core/src/lib.rs | 2 +- crates/xilem_core/src/message.rs | 62 +- crates/xilem_core/src/sequence.rs | 3 +- crates/xilem_core/src/view.rs | 101 ++- crates/xilem_html/.gitignore | 1 + crates/xilem_html/Cargo.toml | 92 +++ crates/xilem_html/README.md | 7 + crates/xilem_html/src/app.rs | 122 +++ crates/xilem_html/src/class.rs | 75 ++ crates/xilem_html/src/context.rs | 117 +++ crates/xilem_html/src/element/elements.rs | 226 +++++ crates/xilem_html/src/element/mod.rs | 256 ++++++ crates/xilem_html/src/event/events.rs | 219 +++++ crates/xilem_html/src/event/mod.rs | 236 ++++++ crates/xilem_html/src/lib.rs | 65 ++ crates/xilem_html/src/text.rs | 61 ++ crates/xilem_html/src/view.rs | 100 +++ crates/xilem_html/src/view_ext.rs | 63 ++ .../web_examples/counter/Cargo.toml | 13 + .../web_examples/counter/index.html | 19 + .../web_examples/counter/src/lib.rs | 80 ++ .../web_examples/counter_untyped/Cargo.toml | 13 + .../web_examples/counter_untyped/index.html | 21 + .../web_examples/counter_untyped/src/lib.rs | 52 ++ .../web_examples/todomvc/Cargo.toml | 16 + .../web_examples/todomvc/index.html | 537 ++++++++++++ .../web_examples/todomvc/src/lib.rs | 212 +++++ .../web_examples/todomvc/src/state.rs | 87 ++ crates/xilem_svg/.gitignore | 1 + crates/xilem_svg/Cargo.toml | 27 + crates/xilem_svg/README.md | 7 + crates/xilem_svg/index.html | 8 + crates/xilem_svg/src/app.rs | 119 +++ crates/xilem_svg/src/class.rs | 71 ++ crates/xilem_svg/src/clicked.rs | 86 ++ crates/xilem_svg/src/context.rs | 104 +++ crates/xilem_svg/src/group.rs | 91 ++ crates/xilem_svg/src/kurbo_shape.rs | 282 +++++++ crates/xilem_svg/src/lib.rs | 103 +++ crates/xilem_svg/src/pointer.rs | 143 ++++ crates/xilem_svg/src/view.rs | 77 ++ crates/xilem_svg/src/view_ext.rs | 25 + src/app.rs | 4 +- src/lib.rs | 4 +- src/widget/contexts.rs | 2 +- 48 files changed, 4421 insertions(+), 381 deletions(-) create mode 100644 crates/xilem_html/.gitignore create mode 100644 crates/xilem_html/Cargo.toml create mode 100644 crates/xilem_html/README.md create mode 100644 crates/xilem_html/src/app.rs create mode 100644 crates/xilem_html/src/class.rs create mode 100644 crates/xilem_html/src/context.rs create mode 100644 crates/xilem_html/src/element/elements.rs create mode 100644 crates/xilem_html/src/element/mod.rs create mode 100644 crates/xilem_html/src/event/events.rs create mode 100644 crates/xilem_html/src/event/mod.rs create mode 100644 crates/xilem_html/src/lib.rs create mode 100644 crates/xilem_html/src/text.rs create mode 100644 crates/xilem_html/src/view.rs create mode 100644 crates/xilem_html/src/view_ext.rs create mode 100644 crates/xilem_html/web_examples/counter/Cargo.toml create mode 100644 crates/xilem_html/web_examples/counter/index.html create mode 100644 crates/xilem_html/web_examples/counter/src/lib.rs create mode 100644 crates/xilem_html/web_examples/counter_untyped/Cargo.toml create mode 100644 crates/xilem_html/web_examples/counter_untyped/index.html create mode 100644 crates/xilem_html/web_examples/counter_untyped/src/lib.rs create mode 100644 crates/xilem_html/web_examples/todomvc/Cargo.toml create mode 100644 crates/xilem_html/web_examples/todomvc/index.html create mode 100644 crates/xilem_html/web_examples/todomvc/src/lib.rs create mode 100644 crates/xilem_html/web_examples/todomvc/src/state.rs create mode 100644 crates/xilem_svg/.gitignore create mode 100644 crates/xilem_svg/Cargo.toml create mode 100644 crates/xilem_svg/README.md create mode 100644 crates/xilem_svg/index.html create mode 100644 crates/xilem_svg/src/app.rs create mode 100644 crates/xilem_svg/src/class.rs create mode 100644 crates/xilem_svg/src/clicked.rs create mode 100644 crates/xilem_svg/src/context.rs create mode 100644 crates/xilem_svg/src/group.rs create mode 100644 crates/xilem_svg/src/kurbo_shape.rs create mode 100644 crates/xilem_svg/src/lib.rs create mode 100644 crates/xilem_svg/src/pointer.rs create mode 100644 crates/xilem_svg/src/view.rs create mode 100644 crates/xilem_svg/src/view_ext.rs diff --git a/.gitignore b/.gitignore index 62bf94d76..67291a435 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ target/ # We're a library, so ignore Cargo.lock Cargo.lock + +.vscode +.cspell diff --git a/Cargo.lock b/Cargo.lock index 6f0702a48..f70c4ce95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,16 +73,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -98,21 +98,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c" [[package]] name = "ash" -version = "0.37.2+1.3.238" +version = "0.37.3+1.3.251" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28bf19c1f0a470be5fbf7522a308a05df06610252c5bcf5143e1b23f629a9a03" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" dependencies = [ "libloading 0.7.4", ] @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", @@ -169,32 +169,31 @@ dependencies = [ [[package]] name = "async-io" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", + "cfg-if", "concurrent-queue", "futures-lite", - "libc", "log", "parking", "polling", + "rustix", "slab", "socket2", "waker-fn", - "windows-sys 0.42.0", ] [[package]] name = "async-lock" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", - "futures-lite", ] [[package]] @@ -205,24 +204,24 @@ checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "async-task" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.64" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -327,9 +326,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" +checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" [[package]] name = "bitmaps" @@ -367,9 +366,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" @@ -382,13 +381,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aca418a974d83d40a0c1f0c5cba6ff4bc28d8df099109ca459a2118d40b6322" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -454,11 +453,12 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.11.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" +checksum = "e70d3ad08698a0568b0562f22710fe6bfc1f4a61a367c77d0398c562eadd453a" dependencies = [ "smallvec", + "target-lexicon", ] [[package]] @@ -469,9 +469,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", @@ -480,9 +480,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", @@ -520,9 +520,9 @@ dependencies = [ [[package]] name = "cocoa-foundation" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" dependencies = [ "bitflags 1.3.2", "block", @@ -551,13 +551,34 @@ checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" [[package]] name = "concurrent-queue" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -570,9 +591,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "core-graphics" @@ -599,6 +620,24 @@ dependencies = [ "libc", ] +[[package]] +name = "counter" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", + "web-sys", + "xilem_html", +] + +[[package]] +name = "counter_untyped" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", + "web-sys", + "xilem_html", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -610,19 +649,13 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - [[package]] name = "d3d12" version = "0.6.0" @@ -652,7 +685,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -689,9 +722,9 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "enumflags2" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" dependencies = [ "enumflags2_derive", "serde", @@ -699,13 +732,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -736,13 +769,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -789,11 +822,11 @@ dependencies = [ [[package]] name = "field-offset" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset", + "memoffset 0.9.0", "rustc_version", ] @@ -833,9 +866,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -848,9 +881,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -858,15 +891,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -886,15 +919,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", @@ -907,32 +940,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1028,9 +1061,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -1039,9 +1072,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gio" @@ -1146,9 +1179,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.11" +version = "0.15.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" dependencies = [ "anyhow", "heck", @@ -1156,7 +1189,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1177,9 +1210,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "glow" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e007a07a24de5ecae94160f141029e9a347282cfe25d1d58d85d845cf3130f1" +checksum = "807edf58b70c0b5b2181dd39fe1839dbdb3ba02645630dc5f753e23da307f762" dependencies = [ "js-sys", "slotmap", @@ -1200,9 +1233,9 @@ dependencies = [ [[package]] name = "gpu-alloc" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc59e5f710e310e76e6707f86c561dd646f69a8876da9131703b2f717de818d" +checksum = "22beaafc29b38204457ea030f6fb7a84c9e4dd1b86e311ba0542533453d87f62" dependencies = [ "bitflags 1.3.2", "gpu-alloc-types", @@ -1293,16 +1326,16 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" dependencies = [ "anyhow", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1403,9 +1436,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -1425,31 +1458,32 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1476,9 +1510,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.9.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e119590a03caff1f7a582e8ee8c2164ddcc975791701188132fd1d1b518d3871" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" dependencies = [ "arrayvec", ] @@ -1497,9 +1531,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" @@ -1518,20 +1552,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1539,12 +1573,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "malloc_buf" @@ -1570,6 +1601,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.24.0" @@ -1610,21 +1650,20 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "naga" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00ce114f2867153c079d4489629dbd27aa4b5387a8ba5341bd3f6dfe870688f" +checksum = "80cd00bd6180a8790f1c020ed258a46b8d73dd5bd6af104a238c9d71f806938e" dependencies = [ "bit-set", "bitflags 1.3.2", @@ -1662,7 +1701,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -1674,7 +1713,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -1687,7 +1726,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", "pin-utils", ] @@ -1767,18 +1806,18 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.30.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "ordered-stream" @@ -1792,9 +1831,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "pango" @@ -1823,9 +1862,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -1839,15 +1878,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] [[package]] @@ -1861,9 +1900,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "peeking_take_while" @@ -1880,16 +1919,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "pest" -version = "2.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660" -dependencies = [ - "thiserror", - "ucd-trie", -] - [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1904,9 +1933,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "png" @@ -1922,16 +1951,18 @@ dependencies = [ [[package]] name = "polling" -version = "2.5.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", + "bitflags 1.3.2", "cfg-if", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "windows-sys 0.42.0", + "pin-project-lite", + "windows-sys", ] [[package]] @@ -1959,7 +1990,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -1976,24 +2007,24 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74605f360ce573babfe43964cbe520294dcb081afbf8c108fc6e23036b4da2df" +checksum = "332cd62e95873ea4f41f3dfd6bbbfc5b52aec892d7e8d534197c4720a0bbbab2" [[package]] name = "quote" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2057,7 +2088,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -2086,12 +2117,9 @@ checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" [[package]] name = "raw-window-handle" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" -dependencies = [ - "cty", -] +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "read-fonts" @@ -2110,22 +2138,31 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", - "redox_syscall", + "getrandom 0.2.10", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -2134,9 +2171,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "renderdoc-sys" @@ -2155,9 +2192,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -2167,25 +2204,25 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.36.8" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2196,51 +2233,48 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "serde_repr" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", +] + +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "serde", ] [[package]] @@ -2309,9 +2343,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2365,11 +2399,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "system-deps" -version = "6.0.3" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" dependencies = [ "cfg-expr", "heck", @@ -2378,17 +2423,24 @@ dependencies = [ "version-compare", ] +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + [[package]] name = "tempfile" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -2402,13 +2454,13 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f0c854faeb68a048f0f2dc410c5ddae3bf83854ef0e4977d58306a5edef50e" +checksum = "d9601d162c1d77e62c1ea0bc8116cd1caf143ce3af947536c3c9052a1677fe0c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2419,29 +2471,29 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "time" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "serde", "time-core", @@ -2449,20 +2501,31 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "todomvc" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "log", + "wasm-bindgen", + "web-sys", + "xilem_html", +] [[package]] name = "tokio" -version = "1.25.0" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -2470,42 +2533,50 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "toml" -version = "0.5.11" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -2524,20 +2595,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] @@ -2548,12 +2619,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "uds_windows" version = "1.0.2" @@ -2566,9 +2631,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-width" @@ -2641,9 +2706,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2651,24 +2716,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -2678,9 +2743,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2688,22 +2753,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wayland-client" @@ -2777,28 +2842,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "wgpu" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13edd72c7b08615b7179dd7e778ee3f0bdc870ef2de9019844ff2cceeee80b11" +checksum = "3059ea4ddec41ca14f356833e2af65e7e38c0a8f91273867ed526fb9bafcca95" dependencies = [ "arrayvec", "cfg-if", @@ -2820,13 +2876,13 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625bea30a0ba50d88025f95c80211d1a85c86901423647fb74f397f614abbd9a" +checksum = "8f478237b4bf0d5b70a39898a66fa67ca3a007d79f2520485b8b0c3dfc46f8c2" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.2.1", + "bitflags 2.3.2", "codespan-reporting", "log", "naga", @@ -2843,15 +2899,15 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41af2ea7d87bd41ad0a37146252d5f7c26490209f47f544b2ee3b3ff34c7732e" +checksum = "74851c2c8e5d97652e74c241d41b0656b31c924a45dcdecde83975717362cfa4" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.2.1", + "bitflags 2.3.2", "block", "core-graphics-types", "d3d12", @@ -2889,7 +2945,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd33a976130f03dcdcd39b3810c0c3fc05daf86f0aaf867db14bfb7c4a9a32b" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.2", "js-sys", "web-sys", ] @@ -2958,13 +3014,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0286ba339aa753e70765d521bb0242cc48e1194562bfa2a2ad7ac8a6de28f5d5" dependencies = [ "windows-implement", - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -2973,7 +3029,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" dependencies = [ - "windows-targets 0.42.1", + "windows-targets 0.42.2", ] [[package]] @@ -2984,31 +3040,7 @@ checksum = "9539b6bd3eadbd9de66c9666b22d802b833da7e996bc06896142e09854a61767" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.1", + "syn 1.0.109", ] [[package]] @@ -3022,17 +3054,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3052,9 +3084,9 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -3064,9 +3096,9 @@ checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -3076,9 +3108,9 @@ checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -3088,9 +3120,9 @@ checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -3100,9 +3132,9 @@ checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -3112,9 +3144,9 @@ checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -3124,9 +3156,9 @@ checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -3136,9 +3168,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.3.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] @@ -3212,11 +3244,34 @@ dependencies = [ name = "xilem_core" version = "0.1.0" +[[package]] +name = "xilem_html" +version = "0.1.0" +dependencies = [ + "bitflags 1.3.2", + "kurbo", + "log", + "wasm-bindgen", + "web-sys", + "xilem_core", +] + +[[package]] +name = "xilemsvg" +version = "0.1.0" +dependencies = [ + "bitflags 1.3.2", + "kurbo", + "wasm-bindgen", + "web-sys", + "xilem_core", +] + [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" [[package]] name = "xmlparser" @@ -3280,14 +3335,14 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 1.0.109", ] [[package]] name = "zbus_names" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" dependencies = [ "serde", "static_assertions", @@ -3302,9 +3357,9 @@ checksum = "c110ba09c9b3a43edd4803d570df0da2414fed6e822e22b976a4e3ef50860701" [[package]] name = "zvariant" -version = "3.11.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" +checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" dependencies = [ "byteorder", "enumflags2", @@ -3316,12 +3371,24 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.11.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" +checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index b1f19c14d..dfa2c9ffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,12 @@ [workspace] members = [ "crates/xilem_core", + "crates/xilem_svg", + "crates/xilem_html", + "crates/xilem_html/web_examples/counter", + "crates/xilem_html/web_examples/counter_untyped", + "crates/xilem_html/web_examples/todomvc", + ".", ] [package] diff --git a/crates/xilem_core/src/lib.rs b/crates/xilem_core/src/lib.rs index 843f6a424..657ff0e75 100644 --- a/crates/xilem_core/src/lib.rs +++ b/crates/xilem_core/src/lib.rs @@ -23,5 +23,5 @@ mod vec_splice; mod view; pub use id::{Id, IdPath}; -pub use message::{AsyncWake, Message, MessageResult}; +pub use message::{AsyncWake, MessageResult}; pub use vec_splice::VecSplice; diff --git a/crates/xilem_core/src/message.rs b/crates/xilem_core/src/message.rs index 56f25e3fb..19399ad8d 100644 --- a/crates/xilem_core/src/message.rs +++ b/crates/xilem_core/src/message.rs @@ -3,21 +3,60 @@ use std::any::Any; -use crate::id::IdPath; +#[macro_export] +macro_rules! message { + () => { + pub struct Message { + pub id_path: xilem_core::IdPath, + pub body: Box, + } + + impl Message { + pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any) -> Message { + Message { + id_path, + body: Box::new(event), + } + } + } + }; + + ($($bounds:tt)*) => { + pub struct Message { + pub id_path: xilem_core::IdPath, + pub body: Box, + } -pub struct Message { - pub id_path: IdPath, - pub body: Box, + impl Message { + pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any + $($bounds)*) -> Message { + Message { + id_path, + body: Box::new(event), + } + } + } + }; } /// A result wrapper type for event handlers. pub enum MessageResult { /// The event handler was invoked and returned an action. + /// + /// Use this return type if your widgets should respond to events by passing + /// a value up the tree, rather than changing their internal state. Action(A), /// The event handler received a change request that requests a rebuild. + /// + /// Note: A rebuild will always occur if there was a state change. This return + /// type can be used to indicate that a full rebuild is necessary even if the + /// state remained the same. It is expected that this type won't be used very + /// often. #[allow(unused)] RequestRebuild, /// The event handler discarded the event. + /// + /// This is the variant that you **almost always want** when you're not returning + /// an action. #[allow(unused)] Nop, /// The event was addressed to an id path no longer in the tree. @@ -27,6 +66,12 @@ pub enum MessageResult { Stale(Box), } +impl Default for MessageResult { + fn default() -> Self { + MessageResult::Nop + } +} + // TODO: does this belong in core? pub struct AsyncWake; @@ -47,12 +92,3 @@ impl MessageResult { } } } - -impl Message { - pub fn new(id_path: IdPath, event: impl Any + Send) -> Message { - Message { - id_path, - body: Box::new(event), - } - } -} diff --git a/crates/xilem_core/src/sequence.rs b/crates/xilem_core/src/sequence.rs index ce50d2e03..90dfbb65f 100644 --- a/crates/xilem_core/src/sequence.rs +++ b/crates/xilem_core/src/sequence.rs @@ -5,7 +5,7 @@ #[macro_export] macro_rules! impl_view_tuple { ( $viewseq:ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { - impl ),* > ViewSequence for ( $( $t, )* ) { + impl ),* > $viewseq for ( $( $t, )* ) { type State = ( $( $t::State, )*); fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State { @@ -292,6 +292,7 @@ macro_rules! generate_viewsequence_trait { #[doc = concat!("`", stringify!($viewmarker), "`.")] pub trait $viewmarker {} + $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, ;); $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, V0; 0); $crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, diff --git a/crates/xilem_core/src/view.rs b/crates/xilem_core/src/view.rs index 8591430e4..44b231611 100644 --- a/crates/xilem_core/src/view.rs +++ b/crates/xilem_core/src/view.rs @@ -1,25 +1,37 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 +/// Create the `View` trait for a particular xilem context (e.g. html, native, ...). +/// +/// Arguments are +/// +/// - `$viewtrait` - The name of the view trait we want to generate. +/// - `$bound` - A bound on all element types that will be used. +/// - `$cx` - The name of text context type that will be passed to the `build`/`rebuild` +/// methods, and be responsible for managing element creation & deletion. +/// - `$changeflags` - The type that reports down/up the tree. Can be used to avoid +/// doing work when we can prove nothing needs doing. +/// - `$ss` - (optional) parent traits to this trait (e.g. `:Send`). Also applied to +/// the state type requirements #[macro_export] macro_rules! generate_view_trait { - ($viewtrait:ident, $bound:ident, $cx:ty, $changeflags: ty; $($ss:tt)*) => { + ($viewtrait:ident, $bound:ident, $cx:ty, $changeflags:ty; $($ss:tt)*) => { /// A view object representing a node in the UI. /// /// This is a central trait for representing UI. An app will generate a tree of /// these objects (the view tree) as the primary interface for expressing UI. /// The view tree is transitory and is retained only long enough to dispatch - /// events and then serve as a reference for diffing for the next view tree. + /// messages and then serve as a reference for diffing for the next view tree. /// /// The framework will then run methods on these views to create the associated - /// state tree and element tree, as well as incremental updates and event + /// state tree and element tree, as well as incremental updates and message /// propagation. /// /// The #[doc = concat!("`", stringify!($viewtrait), "`")] // trait is parameterized by `T`, which is known as the "app state", - /// and also a type for actions which are passed up the tree in event - /// propagation. During event handling, mutable access to the app state is + /// and also a type for actions which are passed up the tree in message + /// propagation. During message handling, mutable access to the app state is /// given to view nodes, which in turn can expose it to callbacks. pub trait $viewtrait $( $ss )* { /// Associated state for the view. @@ -55,5 +67,84 @@ macro_rules! generate_view_trait { app_state: &mut T, ) -> $crate::MessageResult; } + + pub struct Adapt) -> $crate::MessageResult, V: View> { + f: F, + child: V, + phantom: std::marker::PhantomData (OutData, OutMsg, InData, InMsg)>, + } + + /// A "thunk" which dispatches an message to an adapt node's child. + /// + /// The closure passed to [`Adapt`][crate::Adapt] should call this thunk with the child's + /// app state. + pub struct AdaptThunk<'a, InData, InMsg, V: View> { + child: &'a V, + state: &'a mut V::State, + id_path: &'a [$crate::Id], + message: Box, + } + + impl) -> $crate::MessageResult, V: View> + Adapt + { + pub fn new(f: F, child: V) -> Self { + Adapt { + f, + child, + phantom: Default::default(), + } + } + } + + impl<'a, InData, InMsg, V: View> AdaptThunk<'a, InData, InMsg, V> { + pub fn call(self, app_state: &mut InData) -> $crate::MessageResult { + self.child + .message(self.id_path, self.state, self.message, app_state) + } + } + + impl) -> $crate::MessageResult + Send, V: View> + View for Adapt + { + type State = V::State; + + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> ($crate::Id, Self::State, Self::Element) { + self.child.build(cx) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut $crate::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> $changeflags { + self.child.rebuild(cx, &prev.child, id, state, element) + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut OutData, + ) -> $crate::MessageResult { + let thunk = AdaptThunk { + child: &self.child, + state, + id_path, + message, + }; + (self.f)(app_state, thunk) + } + } + + impl) -> $crate::MessageResult, V: View> + ViewMarker for Adapt {} + }; } diff --git a/crates/xilem_html/.gitignore b/crates/xilem_html/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/crates/xilem_html/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml new file mode 100644 index 000000000..268fda5ab --- /dev/null +++ b/crates/xilem_html/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "xilem_html" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" + +[features] +default = ["typed"] +typed = [ + 'web-sys/FocusEvent', + 'web-sys/HtmlAnchorElement', + 'web-sys/HtmlAreaElement', + 'web-sys/HtmlAudioElement', + 'web-sys/HtmlBrElement', + 'web-sys/HtmlButtonElement', + 'web-sys/HtmlCanvasElement', + 'web-sys/HtmlDataElement', + 'web-sys/HtmlDataListElement', + 'web-sys/HtmlDetailsElement', + 'web-sys/HtmlDialogElement', + 'web-sys/HtmlDivElement', + 'web-sys/HtmlDListElement', + 'web-sys/HtmlEmbedElement', + 'web-sys/HtmlFieldSetElement', + 'web-sys/HtmlFormElement', + 'web-sys/HtmlHeadingElement', + 'web-sys/HtmlHrElement', + 'web-sys/HtmlIFrameElement', + 'web-sys/HtmlImageElement', + 'web-sys/HtmlInputElement', + 'web-sys/HtmlLabelElement', + 'web-sys/HtmlLegendElement', + 'web-sys/HtmlLiElement', + 'web-sys/HtmlMapElement', + 'web-sys/HtmlMenuElement', + 'web-sys/HtmlMeterElement', + 'web-sys/HtmlModElement', + 'web-sys/HtmlObjectElement', + 'web-sys/HtmlOListElement', + 'web-sys/HtmlOptGroupElement', + 'web-sys/HtmlOptionElement', + 'web-sys/HtmlOutputElement', + 'web-sys/HtmlParagraphElement', + 'web-sys/HtmlPictureElement', + 'web-sys/HtmlPreElement', + 'web-sys/HtmlProgressElement', + 'web-sys/HtmlQuoteElement', + 'web-sys/HtmlScriptElement', + 'web-sys/HtmlSelectElement', + 'web-sys/HtmlSlotElement', + 'web-sys/HtmlSourceElement', + 'web-sys/HtmlSpanElement', + 'web-sys/HtmlTableElement', + 'web-sys/HtmlTableCellElement', + 'web-sys/HtmlTableColElement', + 'web-sys/HtmlTableCaptionElement', + 'web-sys/HtmlTableRowElement', + 'web-sys/HtmlTableSectionElement', + 'web-sys/HtmlTemplateElement', + 'web-sys/HtmlTextAreaElement', + 'web-sys/HtmlTimeElement', + 'web-sys/HtmlTrackElement', + 'web-sys/HtmlUListElement', + 'web-sys/HtmlVideoElement', + 'web-sys/InputEvent', + 'web-sys/KeyboardEvent', + 'web-sys/MouseEvent', + 'web-sys/PointerEvent', + 'web-sys/WheelEvent', +] + +[dependencies] +bitflags = "1.3.2" +wasm-bindgen = "0.2.87" +kurbo = "0.9.1" +xilem_core = { path = "../xilem_core" } +log = "0.4.19" + +[dependencies.web-sys] +version = "0.3.4" +features = [ + 'console', + 'Document', + 'Element', + 'Event', + 'HtmlElement', + 'Node', + 'NodeList', + 'SvgElement', + 'Text', + 'Window', +] diff --git a/crates/xilem_html/README.md b/crates/xilem_html/README.md new file mode 100644 index 000000000..2fb1fbf66 --- /dev/null +++ b/crates/xilem_html/README.md @@ -0,0 +1,7 @@ +# Xilemsvg prototype + +This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in. + +The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`). + +[Trunk]: https://trunkrs.dev/ diff --git a/crates/xilem_html/src/app.rs b/crates/xilem_html/src/app.rs new file mode 100644 index 000000000..60de984e0 --- /dev/null +++ b/crates/xilem_html/src/app.rs @@ -0,0 +1,122 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::{cell::RefCell, rc::Rc}; + +use crate::{ + context::Cx, + view::{DomNode, View}, + Message, +}; +use xilem_core::{Id, MessageResult}; + +pub struct App, F: FnMut(&mut T) -> V>(Rc>>); + +struct AppInner, F: FnMut(&mut T) -> V> { + data: T, + app_logic: F, + view: Option, + id: Option, + state: Option, + element: Option, + cx: Cx, +} + +pub(crate) trait AppRunner { + fn handle_message(&self, message: Message); + + fn clone_box(&self) -> Box; +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App { + fn clone(&self) -> Self { + App(self.0.clone()) + } +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> App { + pub fn new(data: T, app_logic: F) -> Self { + let inner = AppInner::new(data, app_logic); + let app = App(Rc::new(RefCell::new(inner))); + app.0.borrow_mut().cx.set_runner(app.clone()); + app + } + + pub fn run(self, root: &web_sys::HtmlElement) { + self.0.borrow_mut().ensure_app(root); + // Latter may not be necessary, we have an rc loop. + std::mem::forget(self) + } +} + +impl, F: FnMut(&mut T) -> V> AppInner { + pub fn new(data: T, app_logic: F) -> Self { + let cx = Cx::new(); + AppInner { + data, + app_logic, + view: None, + id: None, + state: None, + element: None, + cx, + } + } + + fn ensure_app(&mut self, root: &web_sys::HtmlElement) { + if self.view.is_none() { + let view = (self.app_logic)(&mut self.data); + let (id, state, element) = view.build(&mut self.cx); + self.view = Some(view); + self.id = Some(id); + self.state = Some(state); + + root.append_child(element.as_node_ref()).unwrap(); + self.element = Some(element); + } + } +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner for App { + // For now we handle the message synchronously, but it would also + // make sense to to batch them (for example with requestAnimFrame). + fn handle_message(&self, message: Message) { + let mut inner_guard = self.0.borrow_mut(); + let inner = &mut *inner_guard; + if let Some(view) = &mut inner.view { + let message_result = view.message( + &message.id_path[1..], + inner.state.as_mut().unwrap(), + message.body, + &mut inner.data, + ); + match message_result { + MessageResult::Nop | MessageResult::Action(_) => { + // Nothing to do. + } + MessageResult::RequestRebuild => { + // TODO force a rebuild? + } + MessageResult::Stale(_) => { + // TODO perhaps inform the user that a stale request bubbled to the top? + } + } + + let new_view = (inner.app_logic)(&mut inner.data); + let _changed = new_view.rebuild( + &mut inner.cx, + view, + inner.id.as_mut().unwrap(), + inner.state.as_mut().unwrap(), + inner.element.as_mut().unwrap(), + ); + // Not sure we have to do anything on changed, the rebuild + // traversal should cause the DOM to update. + *view = new_view; + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/crates/xilem_html/src/class.rs b/crates/xilem_html/src/class.rs new file mode 100644 index 000000000..1513c35ac --- /dev/null +++ b/crates/xilem_html/src/class.rs @@ -0,0 +1,75 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::{any::Any, borrow::Cow}; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomElement, View, ViewMarker}, +}; + +pub struct Class { + child: V, + // This could reasonably be static Cow also, but keep things simple + class: Cow<'static, str>, +} + +pub fn class(child: V, class: impl Into>) -> Class { + Class { + child, + class: class.into(), + } +} + +impl ViewMarker for Class {} + +// TODO: make generic over A (probably requires Phantom) +impl View for Class +where + V: View, + V::Element: DomElement, +{ + type State = V::State; + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + element + .as_element_ref() + .set_attribute("class", &self.class) + .unwrap(); + (id, child_state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut V::Element, + ) -> ChangeFlags { + let prev_id = *id; + let mut changed = self.child.rebuild(cx, &prev.child, id, state, element); + if self.class != prev.class || prev_id != *id { + element + .as_element_ref() + .set_attribute("class", &self.class) + .unwrap(); + changed.insert(ChangeFlags::OTHER_CHANGE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult<()> { + self.child.message(id_path, state, message, app_state) + } +} diff --git a/crates/xilem_html/src/context.rs b/crates/xilem_html/src/context.rs new file mode 100644 index 000000000..81d9f887d --- /dev/null +++ b/crates/xilem_html/src/context.rs @@ -0,0 +1,117 @@ +use std::any::Any; + +use bitflags::bitflags; +use wasm_bindgen::JsCast; +use web_sys::Document; + +use xilem_core::{Id, IdPath}; + +use crate::{app::AppRunner, Message, HTML_NS, SVG_NS}; + +// Note: xilem has derive Clone here. Not sure. +pub struct Cx { + id_path: IdPath, + document: Document, + app_ref: Option>, +} + +pub struct MessageThunk { + id_path: IdPath, + app_ref: Box, +} + +bitflags! { + #[derive(Default)] + pub struct ChangeFlags: u32 { + const STRUCTURE = 1; + const OTHER_CHANGE = 2; + } +} + +impl Cx { + pub fn new() -> Self { + Cx { + id_path: Vec::new(), + document: crate::document(), + app_ref: None, + } + } + + pub fn push(&mut self, id: Id) { + self.id_path.push(id); + } + + pub fn pop(&mut self) { + self.id_path.pop(); + } + + #[allow(unused)] + pub fn id_path(&self) -> &IdPath { + &self.id_path + } + + /// Run some logic with an id added to the id path. + /// + /// This is an ergonomic helper that ensures proper nesting of the id path. + pub fn with_id T>(&mut self, id: Id, f: F) -> T { + self.push(id); + let result = f(self); + self.pop(); + result + } + + /// Allocate a new id and run logic with the new id added to the id path. + /// + /// Also an ergonomic helper. + pub fn with_new_id T>(&mut self, f: F) -> (Id, T) { + let id = Id::next(); + self.push(id); + let result = f(self); + self.pop(); + (id, result) + } + + pub fn document(&self) -> &Document { + &self.document + } + + pub fn create_element(&self, ns: &str, name: &str) -> web_sys::Element { + self.document + .create_element_ns(Some(ns), name) + .expect("could not create element") + } + + pub fn create_html_element(&self, name: &str) -> web_sys::HtmlElement { + self.create_element(HTML_NS, name).unchecked_into() + } + + pub fn create_svg_element(&self, name: &str) -> web_sys::SvgElement { + self.create_element(SVG_NS, name).unchecked_into() + } + + pub fn message_thunk(&self) -> MessageThunk { + MessageThunk { + id_path: self.id_path.clone(), + app_ref: self.app_ref.as_ref().unwrap().clone_box(), + } + } + pub(crate) fn set_runner(&mut self, runner: impl AppRunner + 'static) { + self.app_ref = Some(Box::new(runner)); + } +} + +impl MessageThunk { + pub fn push_message(&self, message_body: impl Any + 'static) { + let message = Message { + id_path: self.id_path.clone(), + body: Box::new(message_body), + }; + self.app_ref.handle_message(message); + } +} + +impl ChangeFlags { + pub fn tree_structure() -> Self { + Self::STRUCTURE + } +} diff --git a/crates/xilem_html/src/element/elements.rs b/crates/xilem_html/src/element/elements.rs new file mode 100644 index 000000000..9afed2d44 --- /dev/null +++ b/crates/xilem_html/src/element/elements.rs @@ -0,0 +1,226 @@ +//! Macros to generate all the different html elements +//! +macro_rules! elements { + () => {}; + (($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => { + element!($ty_name, $builder_name, $name, $web_sys_ty); + elements!($($rest)*); + }; +} + +macro_rules! element { + ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { + pub struct $ty_name(crate::Element<$web_sys_ty, ViewSeq>); + + pub fn $builder_name(children: ViewSeq) -> $ty_name { + $ty_name(crate::element($name, children)) + } + + impl $ty_name { + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. + pub fn attr( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.0.set_attr(name, value); + self + } + + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. + pub fn set_attr( + &mut self, + name: impl Into>, + value: impl Into>, + ) -> &mut Self { + self.0.set_attr(name, value); + self + } + } + + impl crate::view::ViewMarker for $ty_name {} + + impl crate::view::View for $ty_name + where + ViewSeq: crate::view::ViewSequence, + { + type State = crate::ElementState; + type Element = $web_sys_ty; + + fn build( + &self, + cx: &mut crate::context::Cx, + ) -> (xilem_core::Id, Self::State, Self::Element) { + self.0.build(cx) + } + + fn rebuild( + &self, + cx: &mut crate::context::Cx, + prev: &Self, + id: &mut xilem_core::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> crate::ChangeFlags { + self.0.rebuild(cx, &prev.0, id, state, element) + } + + fn message( + &self, + id_path: &[xilem_core::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T_, + ) -> xilem_core::MessageResult { + self.0.message(id_path, state, message, app_state) + } + } + }; +} + +// void elements (those without children) are `area`, `base`, `br`, `col`, +// `embed`, `hr`, `img`, `input`, `link`, `meta`, `source`, `track`, `wbr` +elements!( + // the order is copied from + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element + // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends + + // content sectioning + (Address, address, "address", web_sys::HtmlElement), + (Article, article, "article", web_sys::HtmlElement), + (Aside, aside, "aside", web_sys::HtmlElement), + (Footer, footer, "footer", web_sys::HtmlElement), + (Header, header, "header", web_sys::HtmlElement), + (H1, h1, "h1", web_sys::HtmlHeadingElement), + (H2, h2, "h2", web_sys::HtmlHeadingElement), + (H3, h3, "h3", web_sys::HtmlHeadingElement), + (H4, h4, "h4", web_sys::HtmlHeadingElement), + (H5, h5, "h5", web_sys::HtmlHeadingElement), + (H6, h6, "h6", web_sys::HtmlHeadingElement), + (Hgroup, hgroup, "hgroup", web_sys::HtmlElement), + (Main, main, "main", web_sys::HtmlElement), + (Nav, nav, "nav", web_sys::HtmlElement), + (Section, section, "section", web_sys::HtmlElement), + // text content + ( + Blockquote, + blockquote, + "blockquote", + web_sys::HtmlQuoteElement + ), + (Dd, dd, "dd", web_sys::HtmlElement), + (Div, div, "div", web_sys::HtmlDivElement), + (Dl, dl, "dl", web_sys::HtmlDListElement), + (Dt, dt, "dt", web_sys::HtmlElement), + (Figcaption, figcaption, "figcaption", web_sys::HtmlElement), + (Figure, figure, "figure", web_sys::HtmlElement), + (Hr, hr, "hr", web_sys::HtmlHrElement), + (Li, li, "li", web_sys::HtmlLiElement), + (Menu, menu, "menu", web_sys::HtmlMenuElement), + (Ol, ol, "ol", web_sys::HtmlOListElement), + (P, p, "p", web_sys::HtmlParagraphElement), + (Pre, pre, "pre", web_sys::HtmlPreElement), + (Ul, ul, "ul", web_sys::HtmlUListElement), + // inline text + (A, a, "a", web_sys::HtmlAnchorElement), + (Abbr, abbr, "abbr", web_sys::HtmlElement), + (B, b, "b", web_sys::HtmlElement), + (Bdi, bdi, "bdi", web_sys::HtmlElement), + (Bdo, bdo, "bdo", web_sys::HtmlElement), + (Br, br, "br", web_sys::HtmlBrElement), + (Cite, cite, "cite", web_sys::HtmlElement), + (Code, code, "code", web_sys::HtmlElement), + (Data, data, "data", web_sys::HtmlDataElement), + (Dfn, dfn, "dfn", web_sys::HtmlElement), + (Em, em, "em", web_sys::HtmlElement), + (I, i, "i", web_sys::HtmlElement), + (Kbd, kbd, "kbd", web_sys::HtmlElement), + (Mark, mark, "mark", web_sys::HtmlElement), + (Q, q, "q", web_sys::HtmlQuoteElement), + (Rp, rp, "rp", web_sys::HtmlElement), + (Rt, rt, "rt", web_sys::HtmlElement), + (Ruby, ruby, "ruby", web_sys::HtmlElement), + (S, s, "s", web_sys::HtmlElement), + (Samp, samp, "samp", web_sys::HtmlElement), + (Small, small, "small", web_sys::HtmlElement), + (Span, span, "span", web_sys::HtmlSpanElement), + (Strong, strong, "strong", web_sys::HtmlElement), + (Sub, sub, "sub", web_sys::HtmlElement), + (Sup, sup, "sup", web_sys::HtmlElement), + (Time, time, "time", web_sys::HtmlTimeElement), + (U, u, "u", web_sys::HtmlElement), + (Var, var, "var", web_sys::HtmlElement), + (Wbr, wbr, "wbr", web_sys::HtmlElement), + // image and multimedia + (Area, area, "area", web_sys::HtmlAreaElement), + (Audio, audio, "audio", web_sys::HtmlAudioElement), + (Img, img, "img", web_sys::HtmlImageElement), + (Map, map, "map", web_sys::HtmlMapElement), + (Track, track, "track", web_sys::HtmlTrackElement), + (Video, video, "video", web_sys::HtmlVideoElement), + // embedded content + (Embed, embed, "embed", web_sys::HtmlEmbedElement), + (Iframe, iframe, "iframe", web_sys::HtmlIFrameElement), + (Object, object, "object", web_sys::HtmlObjectElement), + (Picture, picture, "picture", web_sys::HtmlPictureElement), + (Portal, portal, "portal", web_sys::HtmlElement), + (Source, source, "source", web_sys::HtmlSourceElement), + // SVG and MathML (TODO, svg and mathml elements) + (Svg, svg, "svg", web_sys::HtmlElement), + (Math, math, "math", web_sys::HtmlElement), + // scripting + (Canvas, canvas, "canvas", web_sys::HtmlCanvasElement), + (Noscript, noscript, "noscript", web_sys::HtmlElement), + (Script, script, "script", web_sys::HtmlScriptElement), + // demarcating edits + (Del, del, "del", web_sys::HtmlModElement), + (Ins, ins, "ins", web_sys::HtmlModElement), + // tables + ( + Caption, + caption, + "caption", + web_sys::HtmlTableCaptionElement + ), + (Col, col, "col", web_sys::HtmlTableColElement), + (Colgroup, colgroup, "colgroup", web_sys::HtmlTableColElement), + (Table, table, "table", web_sys::HtmlTableSectionElement), + (Tbody, tbody, "tbody", web_sys::HtmlTableSectionElement), + (Td, td, "td", web_sys::HtmlTableCellElement), + (Tfoot, tfoot, "tfoot", web_sys::HtmlTableSectionElement), + (Th, th, "th", web_sys::HtmlTableCellElement), + (Thead, thead, "thead", web_sys::HtmlTableSectionElement), + (Tr, tr, "tr", web_sys::HtmlTableRowElement), + // forms + (Button, button, "button", web_sys::HtmlButtonElement), + (Datalist, datalist, "datalist", web_sys::HtmlDataListElement), + (Fieldset, fieldset, "fieldset", web_sys::HtmlFieldSetElement), + (Form, form, "form", web_sys::HtmlFormElement), + (Input, input, "input", web_sys::HtmlInputElement), + (Label, label, "label", web_sys::HtmlLabelElement), + (Legend, legend, "legend", web_sys::HtmlLegendElement), + (Meter, meter, "meter", web_sys::HtmlMeterElement), + (Optgroup, optgroup, "optgroup", web_sys::HtmlOptGroupElement), + (Option, option, "option", web_sys::HtmlOptionElement), + (Output, output, "output", web_sys::HtmlOutputElement), + (Progress, progress, "progress", web_sys::HtmlProgressElement), + (Select, select, "select", web_sys::HtmlSelectElement), + (Textarea, textarea, "textarea", web_sys::HtmlTextAreaElement), + // interactive elements, + (Details, details, "details", web_sys::HtmlDetailsElement), + (Dialog, dialog, "dialog", web_sys::HtmlDialogElement), + (Summary, summary, "summary", web_sys::HtmlElement), + // web components, + (Slot, slot, "slot", web_sys::HtmlSlotElement), + (Template, template, "template", web_sys::HtmlTemplateElement), +); diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs new file mode 100644 index 000000000..8f68d80cd --- /dev/null +++ b/crates/xilem_html/src/element/mod.rs @@ -0,0 +1,256 @@ +//! The HTML element view and associated types/functions. +//! +//! If you are writing your own views, we recommend adding +//! `use xilem_html::elements as el` or similar to the top of your file. +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomElement, Pod, View, ViewMarker, ViewSequence}, +}; + +use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt, marker::PhantomData}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult, VecSplice}; + +#[cfg(feature = "typed")] +pub mod elements; + +/// A view representing a HTML element. +pub struct Element { + name: Cow<'static, str>, + attributes: BTreeMap, Cow<'static, str>>, + children: Children, + ty: PhantomData, +} + +impl Element { + pub fn debug_as_el(&self) -> impl fmt::Debug + '_ { + struct DebugFmt<'a, E, VS>(&'a Element); + impl<'a, E, VS> fmt::Debug for DebugFmt<'a, E, VS> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "<{}", self.0.name)?; + for (name, value) in &self.0.attributes { + write!(f, " {name}=\"{value}\"")?; + } + write!(f, ">") + } + } + DebugFmt(self) + } +} + +/// The state associated with a HTML element `View`. +/// +/// Stores handles to the child elements and any child state. +pub struct ElementState { + child_states: ViewSeqState, + child_elements: Vec, +} + +/// Create a new element +pub fn element( + name: impl Into>, + children: ViewSeq, +) -> Element { + Element { + name: name.into(), + attributes: BTreeMap::new(), + children, + ty: PhantomData, + } +} + +impl Element { + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. + pub fn attr( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { + self.attributes.insert(name.into(), value.into()); + self + } + + pub fn set_attr( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.attributes.insert(name.into(), value.into()); + } +} + +impl ViewMarker for Element {} + +impl View for Element +where + Children: ViewSequence, + // In addition, the `E` parameter is expected to be a child of `web_sys::Node` + El: JsCast + DomElement, +{ + type State = ElementState; + type Element = El; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, El) { + let el = cx.create_html_element(&self.name); + for (name, value) in &self.attributes { + el.set_attribute(name, value).unwrap(); + } + let mut child_elements = vec![]; + let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); + for child in &child_elements { + el.append_child(child.0.as_node_ref()).unwrap(); + } + let state = ElementState { + child_states, + child_elements, + }; + (id, state, el.dyn_into().unwrap_throw()) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut El, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update tag name + if prev.name != self.name { + // recreate element + let parent = element + .as_element_ref() + .parent_element() + .expect_throw("this element was mounted and so should have a parent"); + parent.remove_child(element.as_node_ref()).unwrap(); + let new_element = cx.create_html_element(&self.name); + // TODO could this be combined with child updates? + while element.as_element_ref().child_element_count() > 0 { + new_element + .append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw()) + .unwrap_throw(); + } + *element = new_element.dyn_into().unwrap_throw(); + changed |= ChangeFlags::STRUCTURE; + } + + let element = element.as_element_ref(); + + // update attributes + // TODO can I use VecSplice for this? + let mut prev_attrs = prev.attributes.iter().peekable(); + let mut self_attrs = self.attributes.iter().peekable(); + while let (Some((prev_name, prev_value)), Some((self_name, self_value))) = + (prev_attrs.peek(), self_attrs.peek()) + { + match prev_name.cmp(self_name) { + Ordering::Less => { + // attribute from prev is disappeared + remove_attribute(element, prev_name); + changed |= ChangeFlags::OTHER_CHANGE; + prev_attrs.next(); + } + Ordering::Greater => { + // new attribute has appeared + set_attribute(element, self_name, self_value); + changed |= ChangeFlags::OTHER_CHANGE; + self_attrs.next(); + } + Ordering::Equal => { + // attribute may has changed + if prev_value != self_value { + set_attribute(element, self_name, self_value); + changed |= ChangeFlags::OTHER_CHANGE; + } + prev_attrs.next(); + self_attrs.next(); + } + } + } + // Only max 1 of these loops will run + while let Some((name, _)) = prev_attrs.next() { + remove_attribute(element, name); + changed |= ChangeFlags::OTHER_CHANGE; + } + while let Some((name, value)) = self_attrs.next() { + set_attribute(element, name, value); + changed |= ChangeFlags::OTHER_CHANGE; + } + + // update children + // TODO avoid reallocation every render? + let mut scratch = vec![]; + let mut splice = VecSplice::new(&mut state.child_elements, &mut scratch); + changed |= cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, &mut state.child_states, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + element.remove_child(&child).unwrap(); + } + for child in &state.child_elements { + element.append_child(child.0.as_node_ref()).unwrap(); + } + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.children + .message(id_path, &mut state.child_states, message, app_state) + } +} + +#[cfg(feature = "typed")] +fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { + // we have to special-case `value` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "value" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_value(value) + } else if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(true) + } else { + element.set_attribute(name, value).unwrap_throw(); + } +} + +#[cfg(not(feature = "typed"))] +fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { + element.set_attribute(name, value).unwrap_throw(); +} + +#[cfg(feature = "typed")] +fn remove_attribute(element: &web_sys::Element, name: &str) { + // we have to special-case `value` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(false) + } else { + element.remove_attribute(name).unwrap_throw(); + } +} + +#[cfg(not(feature = "typed"))] +fn remove_attribute(element: &web_sys::Element, name: &str) { + element.remove_attribute(name).unwrap_throw(); +} diff --git a/crates/xilem_html/src/event/events.rs b/crates/xilem_html/src/event/events.rs new file mode 100644 index 000000000..98b638391 --- /dev/null +++ b/crates/xilem_html/src/event/events.rs @@ -0,0 +1,219 @@ +//! Macros to generate all the different html events +//! +macro_rules! events { + () => {}; + (($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => { + event!($ty_name, $builder_name, $name, $web_sys_ty); + events!($($rest)*); + }; +} + +macro_rules! event { + ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { + pub struct $ty_name(crate::OnEvent<$web_sys_ty, V, F>); + + pub fn $builder_name(child: V, callback: F) -> $ty_name { + $ty_name(crate::on_event($name, child, callback)) + } + + impl crate::view::ViewMarker for $ty_name {} + + impl crate::view::View for $ty_name + where + V: crate::view::View, + F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> $crate::MessageResult, + V::Element: 'static, + { + type State = crate::event::OnEventState; + type Element = V::Element; + + fn build( + &self, + cx: &mut crate::context::Cx, + ) -> (xilem_core::Id, Self::State, Self::Element) { + self.0.build(cx) + } + + fn rebuild( + &self, + cx: &mut crate::context::Cx, + prev: &Self, + id: &mut xilem_core::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> crate::ChangeFlags { + self.0.rebuild(cx, &prev.0, id, state, element) + } + + fn message( + &self, + id_path: &[xilem_core::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> xilem_core::MessageResult { + self.0.message(id_path, state, message, app_state) + } + } + }; +} + +// event list from +// https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions +// +// I didn't include the events on the window, since we aren't attaching +// any events to the window in xilem_html + +events!( + (OnAbort, on_abort, "abort", web_sys::Event), + (OnAuxClick, on_auxclick, "auxclick", web_sys::PointerEvent), + ( + OnBeforeInput, + on_beforeinput, + "beforeinput", + web_sys::InputEvent + ), + (OnBeforeMatch, on_beforematch, "beforematch", web_sys::Event), + ( + OnBeforeToggle, + on_beforetoggle, + "beforetoggle", + web_sys::Event + ), + (OnBlur, on_blur, "blur", web_sys::FocusEvent), + (OnCancel, on_cancel, "cancel", web_sys::Event), + (OnCanPlay, on_canplay, "canplay", web_sys::Event), + ( + OnCanPlayThrough, + on_canplaythrough, + "canplaythrough", + web_sys::Event + ), + (OnChange, on_change, "change", web_sys::Event), + (OnClick, on_click, "click", web_sys::MouseEvent), + (OnClose, on_close, "close", web_sys::Event), + (OnContextLost, on_contextlost, "contextlost", web_sys::Event), + ( + OnContextMenu, + on_contextmenu, + "contextmenu", + web_sys::PointerEvent + ), + ( + OnContextRestored, + on_contextrestored, + "contextrestored", + web_sys::Event + ), + (OnCopy, on_copy, "copy", web_sys::Event), + (OnCueChange, on_cuechange, "cuechange", web_sys::Event), + (OnCut, on_cut, "cut", web_sys::Event), + (OnDblClick, on_dblclick, "dblclick", web_sys::MouseEvent), + (OnDrag, on_drag, "drag", web_sys::Event), + (OnDragEnd, on_dragend, "dragend", web_sys::Event), + (OnDragEnter, on_dragenter, "dragenter", web_sys::Event), + (OnDragLeave, on_dragleave, "dragleave", web_sys::Event), + (OnDragOver, on_dragover, "dragover", web_sys::Event), + (OnDragStart, on_dragstart, "dragstart", web_sys::Event), + (OnDrop, on_drop, "drop", web_sys::Event), + ( + OnDurationChange, + on_durationchange, + "durationchange", + web_sys::Event + ), + (OnEmptied, on_emptied, "emptied", web_sys::Event), + (OnEnded, on_ended, "ended", web_sys::Event), + (OnError, on_error, "error", web_sys::Event), + (OnFocus, on_focus, "focus", web_sys::FocusEvent), + (OnFocusIn, on_focusin, "focusin", web_sys::FocusEvent), + (OnFocusOut, on_focusout, "focusout", web_sys::FocusEvent), + (OnFormData, on_formdata, "formdata", web_sys::Event), + (OnInput, on_input, "input", web_sys::InputEvent), + (OnInvalid, on_invalid, "invalid", web_sys::Event), + (OnKeyDown, on_keydown, "keydown", web_sys::KeyboardEvent), + (OnKeyUp, on_keyup, "keyup", web_sys::KeyboardEvent), + (OnLoad, on_load, "load", web_sys::Event), + (OnLoadedData, on_loadeddata, "loadeddata", web_sys::Event), + ( + OnLoadedMetadata, + on_loadedmetadata, + "loadedmetadata", + web_sys::Event + ), + (OnLoadStart, on_loadstart, "loadstart", web_sys::Event), + (OnMouseDown, on_mousedown, "mousedown", web_sys::MouseEvent), + ( + OnMouseEnter, + on_mouseenter, + "mouseenter", + web_sys::MouseEvent + ), + ( + OnMouseLeave, + on_mouseleave, + "mouseleave", + web_sys::MouseEvent + ), + (OnMouseMove, on_mousemove, "mousemove", web_sys::MouseEvent), + (OnMouseOut, on_mouseout, "mouseout", web_sys::MouseEvent), + (OnMouseOver, on_mouseover, "mouseover", web_sys::MouseEvent), + (OnMouseUp, on_mouseup, "mouseup", web_sys::MouseEvent), + (OnPaste, on_paste, "paste", web_sys::Event), + (OnPause, on_pause, "pause", web_sys::Event), + (OnPlay, on_play, "play", web_sys::Event), + (OnPlaying, on_playing, "playing", web_sys::Event), + (OnProgress, on_progress, "progress", web_sys::Event), + (OnRateChange, on_ratechange, "ratechange", web_sys::Event), + (OnReset, on_reset, "reset", web_sys::Event), + (OnResize, on_resize, "resize", web_sys::Event), + (OnScroll, on_scroll, "scroll", web_sys::Event), + (OnScrollEnd, on_scrollend, "scrollend", web_sys::Event), + ( + OnSecurityPolicyViolation, + on_securitypolicyviolation, + "securitypolicyviolation", + web_sys::Event + ), + (OnSeeked, on_seeked, "seeked", web_sys::Event), + (OnSeeking, on_seeking, "seeking", web_sys::Event), + (OnSelect, on_select, "select", web_sys::Event), + (OnSlotChange, on_slotchange, "slotchange", web_sys::Event), + (OnStalled, on_stalled, "stalled", web_sys::Event), + (OnSubmit, on_submit, "submit", web_sys::Event), + (OnSuspend, on_suspend, "suspend", web_sys::Event), + (OnTimeUpdate, on_timeupdate, "timeupdate", web_sys::Event), + (OnToggle, on_toggle, "toggle", web_sys::Event), + ( + OnVolumeChange, + on_volumechange, + "volumechange", + web_sys::Event + ), + (OnWaiting, on_waiting, "waiting", web_sys::Event), + ( + OnWebkitAnimationEnd, + on_webkitanimationend, + "webkitanimationend", + web_sys::Event + ), + ( + OnWebkitAnimationIteration, + on_webkitanimationiteration, + "webkitanimationiteration", + web_sys::Event + ), + ( + OnWebkitAnimationStart, + on_webkitanimationstart, + "webkitanimationstart", + web_sys::Event + ), + ( + OnWebkitTransitionEnd, + on_webkittransitionend, + "webkittransitionend", + web_sys::Event + ), + (OnWheel, on_wheel, "wheel", web_sys::WheelEvent), +); diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs new file mode 100644 index 000000000..aadd4bb3c --- /dev/null +++ b/crates/xilem_html/src/event/mod.rs @@ -0,0 +1,236 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "typed")] +pub mod events; + +use std::{any::Any, marker::PhantomData, ops::Deref}; + +use wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomNode, View, ViewMarker}, +}; + +pub struct OnEvent { + // TODO changing this after creation is unsupported for now, + // please create a new view instead. + event: &'static str, + child: V, + callback: F, + phantom_event_ty: PhantomData, +} + +impl OnEvent { + fn new(event: &'static str, child: V, callback: F) -> Self { + Self { + event, + child, + callback, + phantom_event_ty: PhantomData, + } + } +} + +impl ViewMarker for OnEvent {} + +impl View for OnEvent +where + F: Fn(&mut T, &Event) -> MessageResult, + V: View, + E: JsCast + 'static, + V::Element: 'static, +{ + type State = OnEventState; + + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { + let event = event.dyn_into::().unwrap_throw(); + let event: Event = Event::new(event); + thunk.push_message(EventMsg { event }); + }) as Box); + element + .as_node_ref() + .add_event_listener_with_callback(self.event, closure.as_ref().unchecked_ref()) + .unwrap_throw(); + // TODO add `remove_listener_with_callback` to clean up listener? + let state = OnEventState { + closure, + child_state, + }; + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + // TODO: if the child id changes (as can happen with AnyView), reinstall closure + self.child + .rebuild(cx, &prev.child, id, &mut state.child_state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + if let Some(msg) = message.downcast_ref::>>() { + (self.callback)(app_state, &msg.event) + } else { + self.child + .message(id_path, &mut state.child_state, message, app_state) + } + } +} + +// Attach an event listener to the child's element +pub fn on_event(name: &'static str, child: V, callback: F) -> OnEvent { + OnEvent::new(name, child, callback) +} + +pub struct OnEventState { + #[allow(unused)] + closure: Closure, + child_state: S, +} +struct EventMsg { + event: E, +} + +/* +// on input +pub fn on_input MessageResult, V: View>( + child: V, + callback: F, +) -> OnEvent { + OnEvent::new("input", child, callback) +} + +// on click +pub fn on_click MessageResult, V: View>( + child: V, + callback: F, +) -> OnEvent { + OnEvent::new("click", child, callback) +} + +// on click +pub fn on_dblclick MessageResult, V: View>( + child: V, + callback: F, +) -> OnEvent { + OnEvent::new("dblclick", child, callback) +} + +// on keydown +pub fn on_keydown< + T, + A, + F: Fn(&mut T, &web_sys::KeyboardEvent) -> MessageResult, + V: View, +>( + child: V, + callback: F, +) -> OnEvent { + OnEvent::new("keydown", child, callback) +} +*/ + +pub struct Event { + raw: Evt, + el: PhantomData, +} + +impl Event { + fn new(raw: Evt) -> Self { + Self { + raw, + el: PhantomData, + } + } +} + +impl Event +where + Evt: AsRef, + El: JsCast, +{ + pub fn target(&self) -> El { + let evt: &web_sys::Event = self.raw.as_ref(); + evt.target().unwrap_throw().dyn_into().unwrap_throw() + } +} + +impl Deref for Event { + type Target = Evt; + fn deref(&self) -> &Self::Target { + &self.raw + } +} + +/* +/// Types that can be created from a `web_sys::Event`. +/// +/// Implementations may make the assumption that the event +/// is a particular subtype (e.g. `InputEvent`) and panic +/// when this is not the case (although it's preferred to use +/// `throw_str` and friends). +pub trait FromEvent: 'static { + /// Convert the given event into `self`, or panic. + fn from_event(event: &web_sys::Event) -> Self; +} + +#[derive(Debug)] +pub struct InputEvent { + pub data: Option, + /// The value of `event.target.value`. + pub value: String, +} + +impl FromEvent for InputEvent { + fn from_event(event: &web_sys::Event) -> Self { + let event: &web_sys::InputEvent = event.dyn_ref().unwrap_throw(); + Self { + data: event.data(), + value: event + .target() + .unwrap_throw() + .dyn_into::() + .unwrap_throw() + .value(), + } + } +} + +pub struct Event {} + +impl FromEvent for Event { + fn from_event(_event: &web_sys::Event) -> Self { + Self {} + } +} + +pub struct KeyboardEvent { + pub key: String, +} + +impl FromEvent for KeyboardEvent { + fn from_event(event: &web_sys::Event) -> Self { + let event: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); + Self { key: event.key() } + } +} +*/ diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs new file mode 100644 index 000000000..717fd5c77 --- /dev/null +++ b/crates/xilem_html/src/lib.rs @@ -0,0 +1,65 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! A test program to exercise using xilem_core to generate SVG nodes that +//! render in a browser. +//! +//! Run using `trunk serve`. + +use wasm_bindgen::JsCast; + +mod app; +//mod button; +mod class; +mod context; +mod event; +//mod div; +mod element; +mod text; +mod view; +#[cfg(feature = "typed")] +mod view_ext; + +pub use xilem_core::MessageResult; + +pub use app::App; +pub use class::class; +pub use context::ChangeFlags; +#[cfg(feature = "typed")] +pub use element::elements; +pub use element::{element, Element, ElementState}; +#[cfg(feature = "typed")] +pub use event::events; +pub use event::{on_event, Event, OnEvent, OnEventState}; +pub use text::{text, Text}; +pub use view::{Adapt, AdaptThunk, Pod, View, ViewMarker, ViewSequence}; +#[cfg(feature = "typed")] +pub use view_ext::ViewExt; + +xilem_core::message!(); + +/// The HTML namespace: `http://www.w3.org/1999/xhtml` +pub const HTML_NS: &str = "http://www.w3.org/1999/xhtml"; +/// The SVG namespace: `http://www.w3.org/2000/svg` +pub const SVG_NS: &str = "http://www.w3.org/2000/svg"; +/// The MathML namespace: `http://www.w3.org/1998/Math/MathML` +pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML"; + +/// Helper to get the HTML document +pub fn document() -> web_sys::Document { + let window = web_sys::window().expect("no global `window` exists"); + window.document().expect("should have a document on window") +} + +/// Helper to get the HTML document body element +pub fn document_body() -> web_sys::HtmlElement { + document().body().expect("HTML document missing body") +} + +pub fn get_element_by_id(id: &str) -> web_sys::HtmlElement { + document() + .get_element_by_id(id) + .unwrap() + .dyn_into() + .unwrap() +} diff --git a/crates/xilem_html/src/text.rs b/crates/xilem_html/src/text.rs new file mode 100644 index 000000000..db3d94e40 --- /dev/null +++ b/crates/xilem_html/src/text.rs @@ -0,0 +1,61 @@ +use std::borrow::Cow; +use wasm_bindgen::JsCast; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{View, ViewMarker}, +}; + +pub struct Text { + text: Cow<'static, str>, +} + +/// Create a text node +pub fn text(text: impl Into>) -> Text { + Text { text: text.into() } +} + +impl ViewMarker for Text {} + +impl View for Text { + type State = (); + type Element = web_sys::Text; + + fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = new_text(&self.text); + let id = Id::next(); + (id, (), el.unchecked_into()) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::empty(); + if prev.text != self.text { + element.set_data(&self.text); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Nop + } +} + +fn new_text(text: &str) -> web_sys::Text { + web_sys::Text::new_with_data(text).unwrap() +} diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs new file mode 100644 index 000000000..b38b12735 --- /dev/null +++ b/crates/xilem_html/src/view.rs @@ -0,0 +1,100 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Integration with xilem_core. This instantiates the View and related +//! traits for DOM node generation. + +use std::{any::Any, ops::Deref}; + +use crate::{context::Cx, ChangeFlags}; + +// A possible refinement of xilem_core is to allow a single concrete type +// for a view element, rather than an associated type with a bound. +pub trait DomNode { + fn into_pod(self) -> Pod; + fn as_node_ref(&self) -> &web_sys::Node; +} + +impl + 'static> DomNode for N { + fn into_pod(self) -> Pod { + Pod(Box::new(self)) + } + + fn as_node_ref(&self) -> &web_sys::Node { + self.as_ref() + } +} + +pub trait DomElement: DomNode { + fn as_element_ref(&self) -> &web_sys::Element; +} + +impl> DomElement for N { + fn as_element_ref(&self) -> &web_sys::Element { + self.as_ref() + } +} + +pub trait AnyNode { + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn as_node_ref(&self) -> &web_sys::Node; +} + +impl + Any> AnyNode for N { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_node_ref(&self) -> &web_sys::Node { + self.as_ref() + } +} + +impl DomNode for Box { + fn into_pod(self) -> Pod { + Pod(self) + } + + fn as_node_ref(&self) -> &web_sys::Node { + self.deref().as_node_ref() + } +} + +struct Void; + +// Dummy implementation that should never be used. +impl DomNode for Void { + fn into_pod(self) -> Pod { + unreachable!() + } + + fn as_node_ref(&self) -> &web_sys::Node { + unreachable!() + } +} + +/// A container that holds a DOM element. +/// +/// This implementation may be overkill (it's possibly enough that everything is +/// just a `web_sys::Element`), but does allow element types that contain other +/// data, if needed. +pub struct Pod(pub Box); + +impl Pod { + fn new(node: impl DomNode) -> Self { + node.into_pod() + } + + fn downcast_mut<'a, T: 'static>(&'a mut self) -> Option<&'a mut T> { + self.0.as_any_mut().downcast_mut() + } + + fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags { + flags + } +} + +xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} +xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} +xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyNode} diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs new file mode 100644 index 000000000..db46cdb68 --- /dev/null +++ b/crates/xilem_html/src/view_ext.rs @@ -0,0 +1,63 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; + +use xilem_core::MessageResult; + +use crate::{class::Class, events as e, view::View, Event}; + +pub trait ViewExt: View + Sized { + fn on_click) -> MessageResult>( + self, + f: F, + ) -> e::OnClick; + fn on_dblclick) -> MessageResult>( + self, + f: F, + ) -> e::OnDblClick; + fn on_input) -> MessageResult>( + self, + f: F, + ) -> e::OnInput; + fn on_keydown< + F: Fn(&mut T, &Event) -> MessageResult, + >( + self, + f: F, + ) -> e::OnKeyDown; + fn class(self, class: impl Into>) -> Class { + crate::class::class(self, class) + } +} + +impl> ViewExt for V { + fn on_click) -> MessageResult>( + self, + f: F, + ) -> e::OnClick { + e::on_click(self, f) + } + fn on_dblclick< + F: Fn(&mut T, &Event) -> MessageResult, + >( + self, + f: F, + ) -> e::OnDblClick { + e::on_dblclick(self, f) + } + fn on_input) -> MessageResult>( + self, + f: F, + ) -> e::OnInput { + crate::events::on_input(self, f) + } + fn on_keydown< + F: Fn(&mut T, &Event) -> MessageResult, + >( + self, + f: F, + ) -> e::OnKeyDown { + crate::events::on_keydown(self, f) + } +} diff --git a/crates/xilem_html/web_examples/counter/Cargo.toml b/crates/xilem_html/web_examples/counter/Cargo.toml new file mode 100644 index 000000000..548a72083 --- /dev/null +++ b/crates/xilem_html/web_examples/counter/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "counter" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.87" +web-sys = "0.3.64" +xilem_html = { path = "../.." } diff --git a/crates/xilem_html/web_examples/counter/index.html b/crates/xilem_html/web_examples/counter/index.html new file mode 100644 index 000000000..98e3db89f --- /dev/null +++ b/crates/xilem_html/web_examples/counter/index.html @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/crates/xilem_html/web_examples/counter/src/lib.rs b/crates/xilem_html/web_examples/counter/src/lib.rs new file mode 100644 index 000000000..1c71757c4 --- /dev/null +++ b/crates/xilem_html/web_examples/counter/src/lib.rs @@ -0,0 +1,80 @@ +use wasm_bindgen::{prelude::*, JsValue}; +use xilem_html::{ + document_body, elements as el, events as evt, text, App, Event, MessageResult, Text, View, + ViewExt, +}; + +#[derive(Default)] +struct AppState { + clicks: i32, + class: &'static str, + text: String, +} + +impl AppState { + fn increment(&mut self) -> MessageResult<()> { + self.clicks += 1; + MessageResult::Nop + } + fn decrement(&mut self) -> MessageResult<()> { + self.clicks -= 1; + MessageResult::Nop + } + fn reset(&mut self) -> MessageResult<()> { + self.clicks = 0; + MessageResult::Nop + } + fn change_class(&mut self) -> MessageResult<()> { + if self.class == "gray" { + self.class = "green"; + } else { + self.class = "gray"; + } + MessageResult::Nop + } + + fn change_text(&mut self) -> MessageResult<()> { + if self.text == "test" { + self.text = "test2".into(); + } else { + self.text = "test".into(); + } + MessageResult::Nop + } +} + +/// You can create functions that generate views. +fn btn(label: &'static str, click_fn: F) -> evt::OnClick, F> +where + F: Fn( + &mut AppState, + &Event, + ) -> MessageResult<()>, +{ + el::button(text(label)).on_click(click_fn) +} + +fn app_logic(state: &mut AppState) -> impl View { + el::div(( + el::span(text(format!("clicked {} times", state.clicks))).attr("class", state.class), + el::br(()), + btn("+1 click", |state, _| AppState::increment(state)), + btn("-1 click", |state, _| AppState::decrement(state)), + btn("reset clicks", |state, _| AppState::reset(state)), + btn("a different class", |state, _| { + AppState::change_class(state) + }), + btn("change text", |state, _| AppState::change_text(state)), + el::br(()), + text(state.text.clone()), + )) +} + +// Called by our JS entry point to run the example +#[wasm_bindgen(start)] +pub fn run() -> Result<(), JsValue> { + let app = App::new(AppState::default(), app_logic); + app.run(&document_body()); + + Ok(()) +} diff --git a/crates/xilem_html/web_examples/counter_untyped/Cargo.toml b/crates/xilem_html/web_examples/counter_untyped/Cargo.toml new file mode 100644 index 000000000..91659c49b --- /dev/null +++ b/crates/xilem_html/web_examples/counter_untyped/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "counter_untyped" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.87" +web-sys = { version = "0.3.64", features = ["HtmlButtonElement"] } +xilem_html = { path = "../..", default-features = false } diff --git a/crates/xilem_html/web_examples/counter_untyped/index.html b/crates/xilem_html/web_examples/counter_untyped/index.html new file mode 100644 index 000000000..8774c9e45 --- /dev/null +++ b/crates/xilem_html/web_examples/counter_untyped/index.html @@ -0,0 +1,21 @@ + + + + + +

This is like the counter example, but does not use the typed + elements/events/attrs in xilem_html, instead using strings

+ \ No newline at end of file diff --git a/crates/xilem_html/web_examples/counter_untyped/src/lib.rs b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs new file mode 100644 index 000000000..b835d7b54 --- /dev/null +++ b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs @@ -0,0 +1,52 @@ +use wasm_bindgen::{prelude::*, JsValue}; +use xilem_html::{ + document_body, element as el, on_event, text, App, Event, MessageResult, View, ViewMarker, +}; + +#[derive(Default)] +struct AppState { + clicks: i32, +} + +impl AppState { + fn increment(&mut self) -> MessageResult<()> { + self.clicks += 1; + MessageResult::Nop + } + fn decrement(&mut self) -> MessageResult<()> { + self.clicks -= 1; + MessageResult::Nop + } + fn reset(&mut self) -> MessageResult<()> { + self.clicks = 0; + MessageResult::Nop + } +} + +fn btn(label: &'static str, click_fn: F) -> impl View + ViewMarker +where + F: Fn(&mut AppState, &Event) -> MessageResult<()>, +{ + on_event("click", el("button", text(label)), click_fn) +} + +fn app_logic(state: &mut AppState) -> impl View { + el::( + "div", + ( + el::("span", text(format!("clicked {} times", state.clicks))), + btn("+1 click", |state, _| AppState::increment(state)), + btn("-1 click", |state, _| AppState::decrement(state)), + btn("reset clicks", |state, _| AppState::reset(state)), + ), + ) +} + +// Called by our JS entry point to run the example +#[wasm_bindgen(start)] +pub fn run() -> Result<(), JsValue> { + let app = App::new(AppState::default(), app_logic); + app.run(&document_body()); + + Ok(()) +} diff --git a/crates/xilem_html/web_examples/todomvc/Cargo.toml b/crates/xilem_html/web_examples/todomvc/Cargo.toml new file mode 100644 index 000000000..f6cff14c1 --- /dev/null +++ b/crates/xilem_html/web_examples/todomvc/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "todomvc" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +console_error_panic_hook = "0.1.7" +console_log = { version = "1.0.0", features = ["color"] } +log = "0.4.19" +wasm-bindgen = "0.2.87" +web-sys = "0.3.64" +xilem_html = { path = "../.." } diff --git a/crates/xilem_html/web_examples/todomvc/index.html b/crates/xilem_html/web_examples/todomvc/index.html new file mode 100644 index 000000000..c1a82a542 --- /dev/null +++ b/crates/xilem_html/web_examples/todomvc/index.html @@ -0,0 +1,537 @@ + + + + + xilem_html • TodoMVC + + + +
+
+ + \ No newline at end of file diff --git a/crates/xilem_html/web_examples/todomvc/src/lib.rs b/crates/xilem_html/web_examples/todomvc/src/lib.rs new file mode 100644 index 000000000..50c2d80af --- /dev/null +++ b/crates/xilem_html/web_examples/todomvc/src/lib.rs @@ -0,0 +1,212 @@ +use std::panic; + +mod state; + +use state::{AppState, Filter, Todo}; + +use wasm_bindgen::{prelude::*, JsValue}; +use xilem_html::{ + elements as el, get_element_by_id, text, Adapt, App, MessageResult, View, ViewExt, ViewMarker, +}; + +// All of these actions arise from within a `Todo`, but we need access to the full state to reduce +// them. +enum TodoAction { + SetEditing(u64), + CancelEditing, + Destroy(u64), +} + +fn todo_item(todo: &mut Todo, editing: bool) -> impl View + ViewMarker { + let mut class = String::new(); + if todo.completed { + class.push_str(" completed"); + } + if editing { + class.push_str(" editing"); + } + let mut input = el::input(()) + .attr("class", "toggle") + .attr("type", "checkbox"); + if todo.completed { + input.set_attr("checked", "checked"); + }; + + el::li(( + el::div(( + input.on_click(|state: &mut Todo, _| { + state.completed = !state.completed; + MessageResult::RequestRebuild + }), + el::label(text(todo.title.clone())).on_dblclick(|state: &mut Todo, _| { + MessageResult::Action(TodoAction::SetEditing(state.id)) + }), + el::button(()) + .attr("class", "destroy") + .on_click(|state: &mut Todo, _| { + MessageResult::Action(TodoAction::Destroy(state.id)) + }), + )) + .attr("class", "view"), + el::input(()) + .attr("value", todo.title_editing.clone()) + .attr("class", "edit") + .on_keydown(|state: &mut Todo, evt| { + let key = evt.key(); + if key == "Enter" { + state.save_editing(); + MessageResult::Action(TodoAction::CancelEditing) + } else if key == "Escape" { + MessageResult::Action(TodoAction::CancelEditing) + } else { + MessageResult::Nop + } + }) + .on_input(|state: &mut Todo, evt| { + state.title_editing.clear(); + state.title_editing.push_str(&evt.target().value()); + evt.prevent_default(); + MessageResult::Nop + }), + )) + .attr("class", class) +} + +fn footer_view(state: &mut AppState) -> impl View + ViewMarker { + let item_str = if state.todos.len() == 1 { + "item" + } else { + "items" + }; + + let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 0).then(|| { + el::button(text("Clear completed")) + .attr("class", "clear-completed") + .on_click(|state: &mut AppState, _| { + state.todos.retain(|todo| !todo.completed); + MessageResult::RequestRebuild + }) + }); + + let filter_class = |filter| { + if state.filter == filter { + "selected" + } else { + "" + } + }; + + el::footer(( + el::span(( + el::strong(text(state.todos.len().to_string())), + text(format!(" {} left", item_str)), + )) + .attr("class", "todo-count"), + el::ul(( + el::li( + el::a(text("All")) + .attr("href", "#/") + .attr("class", filter_class(Filter::All)) + .on_click(|state: &mut AppState, _| { + state.filter = Filter::All; + MessageResult::RequestRebuild + }), + ), + text(" "), + el::li( + el::a(text("Active")) + .attr("href", "#/active") + .attr("class", filter_class(Filter::Active)) + .on_click(|state: &mut AppState, _| { + state.filter = Filter::Active; + MessageResult::RequestRebuild + }), + ), + text(" "), + el::li( + el::a(text("Completed")) + .attr("href", "#/completed") + .attr("class", filter_class(Filter::Completed)) + .on_click(|state: &mut AppState, _| { + state.filter = Filter::Completed; + MessageResult::RequestRebuild + }), + ), + )) + .attr("class", "filters"), + clear_button, + )) + .attr("class", "footer") +} + +fn main_view(state: &mut AppState) -> impl View + ViewMarker { + let editing_id = state.editing_id; + let todos: Vec<_> = state + .visible_todos() + .map(|(idx, todo)| { + Adapt::new( + move |data: &mut AppState, thunk| { + if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { + match action { + TodoAction::SetEditing(id) => data.start_editing(id), + TodoAction::CancelEditing => data.editing_id = None, + TodoAction::Destroy(id) => data.todos.retain(|todo| todo.id != id), + } + } + MessageResult::Nop + }, + todo_item(todo, editing_id == Some(todo.id)), + ) + }) + .collect(); + el::section(( + el::input(()) + .attr("id", "toggle-all") + .attr("class", "toggle-all") + .attr("type", "checkbox") + .attr("checked", "true"), + el::label(()).attr("for", "toggle-all"), + el::ul(todos).attr("class", "todo-list"), + )) + .attr("class", "main") +} + +fn app_logic(state: &mut AppState) -> impl View { + log::debug!("render: {state:?}"); + let main = (!state.todos.is_empty()).then(|| main_view(state)); + let footer = (!state.todos.is_empty()).then(|| footer_view(state)); + el::div(( + el::header(( + el::h1(text("TODOs")), + el::input(()) + .attr("class", "new-todo") + .attr("placeholder", "What needs to be done?") + .attr("value", state.new_todo.clone()) + .attr("autofocus", "true") + .on_keydown(|state: &mut AppState, evt| { + if evt.key() == "Enter" { + state.create_todo(); + } + MessageResult::RequestRebuild + }) + .on_input(|state: &mut AppState, evt| { + state.update_new_todo(&evt.target().value()); + evt.prevent_default(); + MessageResult::RequestRebuild + }), + )) + .attr("class", "header"), + main, + footer, + )) +} + +// Called by our JS entry point to run the example +#[wasm_bindgen(start)] +pub fn run() -> Result<(), JsValue> { + panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Debug).unwrap(); + App::new(AppState::default(), app_logic).run(&get_element_by_id("todoapp")); + + Ok(()) +} diff --git a/crates/xilem_html/web_examples/todomvc/src/state.rs b/crates/xilem_html/web_examples/todomvc/src/state.rs new file mode 100644 index 000000000..8b2d4e6f1 --- /dev/null +++ b/crates/xilem_html/web_examples/todomvc/src/state.rs @@ -0,0 +1,87 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +fn next_id() -> u64 { + static ID_GEN: AtomicU64 = AtomicU64::new(1); + ID_GEN.fetch_add(1, Ordering::Relaxed) +} + +#[derive(Default, Debug)] +pub struct AppState { + pub new_todo: String, + pub todos: Vec, + pub filter: Filter, + pub editing_id: Option, +} + +impl AppState { + pub fn create_todo(&mut self) { + if self.new_todo.is_empty() { + return; + } + let title = self.new_todo.trim().to_string(); + self.new_todo.clear(); + self.todos.push(Todo::new(title)); + } + + pub fn visible_todos(&mut self) -> impl Iterator { + self.todos + .iter_mut() + .enumerate() + .filter(|(_, todo)| match self.filter { + Filter::All => true, + Filter::Active => !todo.completed, + Filter::Completed => todo.completed, + }) + } + + pub fn update_new_todo(&mut self, new_text: &str) { + self.new_todo.clear(); + self.new_todo.push_str(new_text); + } + + pub fn start_editing(&mut self, id: u64) { + if let Some(ref mut todo) = self.todos.iter_mut().filter(|todo| todo.id == id).next() { + todo.title_editing.clear(); + todo.title_editing.push_str(&todo.title); + self.editing_id = Some(id) + } + } +} + +#[derive(Debug)] +pub struct Todo { + pub id: u64, + pub title: String, + pub title_editing: String, + pub completed: bool, +} + +impl Todo { + pub fn new(title: String) -> Self { + let title_editing = title.clone(); + Self { + id: next_id(), + title, + title_editing, + completed: false, + } + } + + pub fn save_editing(&mut self) { + self.title.clear(); + self.title.push_str(&self.title_editing); + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Self::All + } +} diff --git a/crates/xilem_svg/.gitignore b/crates/xilem_svg/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/crates/xilem_svg/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/crates/xilem_svg/Cargo.toml b/crates/xilem_svg/Cargo.toml new file mode 100644 index 000000000..cb17a12b8 --- /dev/null +++ b/crates/xilem_svg/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "xilemsvg" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +bitflags = "1.3.2" +wasm-bindgen = "0.2.84" +kurbo = "0.9.1" +xilem_core = { path = "../xilem_core" } + +[dependencies.web-sys] +version = "0.3.4" +features = [ + 'console', + 'Document', + 'Element', + 'HtmlElement', + 'Node', + 'PointerEvent', + 'SvgElement', + 'Window', +] diff --git a/crates/xilem_svg/README.md b/crates/xilem_svg/README.md new file mode 100644 index 000000000..2fb1fbf66 --- /dev/null +++ b/crates/xilem_svg/README.md @@ -0,0 +1,7 @@ +# Xilemsvg prototype + +This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in. + +The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`). + +[Trunk]: https://trunkrs.dev/ diff --git a/crates/xilem_svg/index.html b/crates/xilem_svg/index.html new file mode 100644 index 000000000..9a850a6fe --- /dev/null +++ b/crates/xilem_svg/index.html @@ -0,0 +1,8 @@ + diff --git a/crates/xilem_svg/src/app.rs b/crates/xilem_svg/src/app.rs new file mode 100644 index 000000000..d3cf25ba3 --- /dev/null +++ b/crates/xilem_svg/src/app.rs @@ -0,0 +1,119 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::{cell::RefCell, rc::Rc}; + +use crate::{ + context::Cx, + view::{DomElement, View}, + Message, +}; +use xilem_core::Id; + +pub struct App, F: FnMut(&mut T) -> V>(Rc>>); + +struct AppInner, F: FnMut(&mut T) -> V> { + data: T, + app_logic: F, + view: Option, + id: Option, + state: Option, + element: Option, + cx: Cx, +} + +pub(crate) trait AppRunner { + fn handle_message(&self, message: Message); + + fn clone_box(&self) -> Box; +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App { + fn clone(&self) -> Self { + App(self.0.clone()) + } +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> App { + pub fn new(data: T, app_logic: F) -> Self { + let inner = AppInner::new(data, app_logic); + let app = App(Rc::new(RefCell::new(inner))); + app.0.borrow_mut().cx.set_runner(app.clone()); + app + } + + pub fn run(self) { + self.0.borrow_mut().ensure_app(); + // Latter may not be necessary, we have an rc loop. + std::mem::forget(self) + } +} + +impl, F: FnMut(&mut T) -> V> AppInner { + pub fn new(data: T, app_logic: F) -> Self { + let cx = Cx::new(); + AppInner { + data, + app_logic, + view: None, + id: None, + state: None, + element: None, + cx, + } + } + + fn ensure_app(&mut self) { + if self.view.is_none() { + let view = (self.app_logic)(&mut self.data); + let (id, state, element) = view.build(&mut self.cx); + self.view = Some(view); + self.id = Some(id); + self.state = Some(state); + + let body = self.cx.document().body().unwrap(); + let svg = self + .cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") + .unwrap(); + svg.set_attribute("width", "800").unwrap(); + svg.set_attribute("height", "600").unwrap(); + body.append_child(&svg).unwrap(); + svg.append_child(element.as_element_ref()).unwrap(); + self.element = Some(element); + } + } +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner for App { + // For now we handle the message synchronously, but it would also + // make sense to to batch them (for example with requestAnimFrame). + fn handle_message(&self, message: Message) { + let mut inner_guard = self.0.borrow_mut(); + let inner = &mut *inner_guard; + if let Some(view) = &mut inner.view { + view.message( + &message.id_path[1..], + inner.state.as_mut().unwrap(), + message.body, + &mut inner.data, + ); + let new_view = (inner.app_logic)(&mut inner.data); + let _changed = new_view.rebuild( + &mut inner.cx, + view, + inner.id.as_mut().unwrap(), + inner.state.as_mut().unwrap(), + inner.element.as_mut().unwrap(), + ); + // Not sure we have to do anything on changed, the rebuild + // traversal should cause the DOM to update. + *view = new_view; + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/crates/xilem_svg/src/class.rs b/crates/xilem_svg/src/class.rs new file mode 100644 index 000000000..4e096ec35 --- /dev/null +++ b/crates/xilem_svg/src/class.rs @@ -0,0 +1,71 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomElement, View, ViewMarker}, +}; + +pub struct Class { + child: V, + // This could reasonably be static Cow also, but keep things simple + class: String, +} + +pub fn class(child: V, class: impl Into) -> Class { + Class { + child, + class: class.into(), + } +} + +impl ViewMarker for Class {} + +// TODO: make generic over A (probably requires Phantom) +impl> View for Class { + type State = V::State; + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + element + .as_element_ref() + .set_attribute("class", &self.class) + .unwrap(); + (id, child_state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut V::Element, + ) -> ChangeFlags { + let prev_id = *id; + let mut changed = self.child.rebuild(cx, &prev.child, id, state, element); + if self.class != prev.class || prev_id != *id { + element + .as_element_ref() + .set_attribute("class", &self.class) + .unwrap(); + changed.insert(ChangeFlags::OTHER_CHANGE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult<()> { + self.child.message(id_path, state, message, app_state) + } +} diff --git a/crates/xilem_svg/src/clicked.rs b/crates/xilem_svg/src/clicked.rs new file mode 100644 index 000000000..1eed1a408 --- /dev/null +++ b/crates/xilem_svg/src/clicked.rs @@ -0,0 +1,86 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; + +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::SvgElement; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomElement, View, ViewMarker}, +}; + +pub struct Clicked { + child: V, + callback: F, +} + +pub struct ClickedState { + // Closure is retained so it can be called by environment + #[allow(unused)] + closure: Closure, + child_state: S, +} + +struct ClickedMsg; + +pub fn clicked>(child: V, callback: F) -> Clicked { + Clicked { child, callback } +} + +impl ViewMarker for Clicked {} + +impl> View for Clicked { + type State = ClickedState; + + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let closure = + Closure::wrap(Box::new(move || thunk.push_message(ClickedMsg)) as Box); + element + .as_element_ref() + .dyn_ref::() + .expect("not an svg element") + .set_onclick(Some(closure.as_ref().unchecked_ref())); + let state = ClickedState { + closure, + child_state, + }; + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + // TODO: if the child id changes (as can happen with AnyView), reinstall closure + self.child + .rebuild(cx, &prev.child, id, &mut state.child_state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult<()> { + if message.downcast_ref::().is_some() { + (self.callback)(app_state); + MessageResult::Action(()) + } else { + self.child + .message(id_path, &mut state.child_state, message, app_state) + } + } +} diff --git a/crates/xilem_svg/src/context.rs b/crates/xilem_svg/src/context.rs new file mode 100644 index 000000000..c59c05a16 --- /dev/null +++ b/crates/xilem_svg/src/context.rs @@ -0,0 +1,104 @@ +use std::any::Any; + +use bitflags::bitflags; +use web_sys::Document; + +use xilem_core::{Id, IdPath}; + +use crate::{app::AppRunner, Message}; + +// Note: xilem has derive Clone here. Not sure. +pub struct Cx { + id_path: IdPath, + document: Document, + app_ref: Option>, +} + +pub struct MessageThunk { + id_path: IdPath, + app_ref: Box, +} + +bitflags! { + #[derive(Default)] + pub struct ChangeFlags: u32 { + const STRUCTURE = 1; + const OTHER_CHANGE = 2; + } +} + +impl ChangeFlags { + pub fn tree_structure() -> Self { + ChangeFlags::STRUCTURE + } +} + +impl Cx { + pub fn new() -> Self { + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + Cx { + id_path: Vec::new(), + document, + app_ref: None, + } + } + + pub fn push(&mut self, id: Id) { + self.id_path.push(id); + } + + pub fn pop(&mut self) { + self.id_path.pop(); + } + + #[allow(unused)] + pub fn id_path(&self) -> &IdPath { + &self.id_path + } + + /// Run some logic with an id added to the id path. + /// + /// This is an ergonomic helper that ensures proper nesting of the id path. + pub fn with_id T>(&mut self, id: Id, f: F) -> T { + self.push(id); + let result = f(self); + self.pop(); + result + } + + /// Allocate a new id and run logic with the new id added to the id path. + /// + /// Also an ergonomic helper. + pub fn with_new_id T>(&mut self, f: F) -> (Id, T) { + let id = Id::next(); + self.push(id); + let result = f(self); + self.pop(); + (id, result) + } + + pub fn document(&self) -> &Document { + &self.document + } + + pub fn message_thunk(&self) -> MessageThunk { + MessageThunk { + id_path: self.id_path.clone(), + app_ref: self.app_ref.as_ref().unwrap().clone_box(), + } + } + pub(crate) fn set_runner(&mut self, runner: impl AppRunner + 'static) { + self.app_ref = Some(Box::new(runner)); + } +} + +impl MessageThunk { + pub fn push_message(&self, message_body: impl Any + Send + 'static) { + let message = Message { + id_path: self.id_path.clone(), + body: Box::new(message_body), + }; + self.app_ref.handle_message(message); + } +} diff --git a/crates/xilem_svg/src/group.rs b/crates/xilem_svg/src/group.rs new file mode 100644 index 000000000..4cb25b2d6 --- /dev/null +++ b/crates/xilem_svg/src/group.rs @@ -0,0 +1,91 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Group + +use web_sys::Element; + +use xilem_core::{Id, MessageResult, VecSplice}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{Pod, View, ViewMarker, ViewSequence}, +}; + +pub struct Group { + children: VS, +} + +pub struct GroupState { + state: S, + elements: Vec, +} + +pub fn group(children: VS) -> Group { + Group { children } +} + +impl ViewMarker for Group {} + +impl View for Group +where + VS: ViewSequence, +{ + type State = GroupState; + type Element = web_sys::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) { + let el = cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "g") + .unwrap(); + let mut elements = vec![]; + let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut elements)); + for child in &elements { + el.append_child(child.0.as_element_ref()).unwrap(); + } + let group_state = GroupState { state, elements }; + (id, group_state, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Element, + ) -> ChangeFlags { + let mut scratch = vec![]; + let mut splice = VecSplice::new(&mut state.elements, &mut scratch); + let mut changed = cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, &mut state.state, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + _ = element.remove_child(&child); + } + for child in &state.elements { + _ = element.append_child(child.0.as_element_ref()); + } + // TODO: we may want to propagate that something changed + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.children + .message(id_path, &mut state.state, message, app_state) + } +} diff --git a/crates/xilem_svg/src/kurbo_shape.rs b/crates/xilem_svg/src/kurbo_shape.rs new file mode 100644 index 000000000..4921c902c --- /dev/null +++ b/crates/xilem_svg/src/kurbo_shape.rs @@ -0,0 +1,282 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of the View trait for various kurbo shapes. + +use kurbo::{BezPath, Circle, Line, Rect}; +use web_sys::Element; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + pointer::PointerMsg, + view::{View, ViewMarker}, +}; + +pub trait KurboShape: Sized { + fn class(self, class: impl Into) -> crate::class::Class { + crate::class::class(self, class) + } + + fn clicked(self, f: F) -> crate::clicked::Clicked + where + Self: View, + { + crate::clicked::clicked(self, f) + } + + fn pointer(self, f: F) -> crate::pointer::Pointer + where + Self: View, + { + crate::pointer::pointer(self, f) + } +} + +impl KurboShape for Line {} +impl KurboShape for Rect {} +impl KurboShape for Circle {} +impl KurboShape for BezPath {} + +impl ViewMarker for Line {} + +impl View for Line { + type State = (); + type Element = Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) { + let el = cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "line") + .unwrap(); + el.set_attribute("x1", &format!("{}", self.p0.x)).unwrap(); + el.set_attribute("y1", &format!("{}", self.p0.y)).unwrap(); + el.set_attribute("x2", &format!("{}", self.p1.x)).unwrap(); + el.set_attribute("y2", &format!("{}", self.p1.y)).unwrap(); + let id = Id::next(); + (id, (), el) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::default(); + if self.p0.x != prev.p0.x { + element + .set_attribute("x1", &format!("{}", self.p0.x)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.p0.y != prev.p0.y { + element + .set_attribute("y1", &format!("{}", self.p0.y)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.p1.x != prev.p1.x { + element + .set_attribute("x2", &format!("{}", self.p1.x)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.p1.y != prev.p1.y { + element + .set_attribute("y2", &format!("{}", self.p1.y)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult<()> { + MessageResult::Stale(message) + } +} + +impl ViewMarker for Rect {} + +impl View for Rect { + type State = (); + type Element = Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) { + let el = cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "rect") + .unwrap(); + el.set_attribute("x", &format!("{}", self.x0)).unwrap(); + el.set_attribute("y", &format!("{}", self.y0)).unwrap(); + let size = self.size(); + el.set_attribute("width", &format!("{}", size.width)) + .unwrap(); + el.set_attribute("height", &format!("{}", size.height)) + .unwrap(); + let id = Id::next(); + (id, (), el) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::default(); + if self.x0 != prev.x0 { + element.set_attribute("x", &format!("{}", self.x0)).unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.y0 != prev.y0 { + element.set_attribute("y", &format!("{}", self.y0)).unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + let size = self.size(); + let prev_size = prev.size(); + if size.width != prev_size.width { + element + .set_attribute("width", &format!("{}", size.width)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if size.height != prev_size.height { + element + .set_attribute("height", &format!("{}", size.height)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult<()> { + MessageResult::Stale(message) + } +} + +impl ViewMarker for Circle {} + +impl View for Circle { + type State = (); + type Element = Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) { + let el = cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "circle") + .unwrap(); + el.set_attribute("cx", &format!("{}", self.center.x)) + .unwrap(); + el.set_attribute("cy", &format!("{}", self.center.y)) + .unwrap(); + el.set_attribute("r", &format!("{}", self.radius)).unwrap(); + let id = Id::next(); + (id, (), el) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::default(); + if self.center.x != prev.center.x { + element + .set_attribute("cx", &format!("{}", self.center.x)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.center.y != prev.center.y { + element + .set_attribute("cy", &format!("{}", self.center.y)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + if self.radius != prev.radius { + element + .set_attribute("r", &format!("{}", self.radius)) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult<()> { + MessageResult::Stale(message) + } +} + +impl ViewMarker for BezPath {} + +impl View for BezPath { + type State = (); + type Element = Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) { + let el = cx + .document() + .create_element_ns(Some("http://www.w3.org/2000/svg"), "path") + .unwrap(); + el.set_attribute("d", &format!("{}", self.to_svg())) + .unwrap(); + let id = Id::next(); + (id, (), el) + } + + fn rebuild( + &self, + _d: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::default(); + if self != prev { + element + .set_attribute("d", &format!("{}", self.to_svg())) + .unwrap(); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult<()> { + MessageResult::Stale(message) + } +} + +// TODO: RoundedRect diff --git a/crates/xilem_svg/src/lib.rs b/crates/xilem_svg/src/lib.rs new file mode 100644 index 000000000..e7a733ab0 --- /dev/null +++ b/crates/xilem_svg/src/lib.rs @@ -0,0 +1,103 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! A test program to exercise using xilem_core to generate SVG nodes that +//! render in a browser. +//! +//! Run using `trunk serve`. + +mod app; +mod class; +mod clicked; +mod context; +mod group; +mod kurbo_shape; +mod pointer; +mod view; +mod view_ext; + +use app::App; +use group::group; +use kurbo::Rect; +use kurbo_shape::KurboShape; +use pointer::PointerMsg; +use view::View; +use wasm_bindgen::prelude::*; + +pub use context::ChangeFlags; + +xilem_core::message!(Send); + +#[derive(Default)] +struct AppState { + x: f64, + y: f64, + grab: GrabState, +} + +#[derive(Default)] +struct GrabState { + is_down: bool, + id: i32, + dx: f64, + dy: f64, +} + +impl GrabState { + fn handle(&mut self, x: &mut f64, y: &mut f64, p: &PointerMsg) { + match p { + PointerMsg::Down(e) => { + if e.button == 0 { + self.dx = *x - e.x; + self.dy = *y - e.y; + self.id = e.id; + self.is_down = true; + } + } + PointerMsg::Move(e) => { + if self.is_down && self.id == e.id { + *x = self.dx + e.x; + *y = self.dy + e.y; + } + } + PointerMsg::Up(e) => { + if self.id == e.id { + self.is_down = false; + } + } + } + } +} + +fn app_logic(state: &mut AppState) -> impl View { + let v = (0..10) + .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) + .collect::>(); + group(( + Rect::new(100.0, 100.0, 200.0, 200.0).clicked(|_| { + web_sys::console::log_1(&"app logic clicked".into()); + }), + Rect::new(210.0, 100.0, 310.0, 200.0), + Rect::new(320.0, 100.0, 420.0, 200.0).class("red"), + Rect::new(state.x, state.y, state.x + 100., state.y + 100.) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.x, &mut s.y, &msg)), + group(v), + Rect::new(210.0, 210.0, 310.0, 310.0).pointer(|_, e| { + web_sys::console::log_1(&format!("pointer event {e:?}").into()); + }), + kurbo::Line::new((310.0, 210.0), (410.0, 310.0)), + kurbo::Circle::new((460.0, 260.0), 45.0).clicked(|_| { + web_sys::console::log_1(&"circle clicked".into()); + }), + )) + //button(format!("Count {}", count), |count| *count += 1) +} + +// Called by our JS entry point to run the example +#[wasm_bindgen(start)] +pub fn run() -> Result<(), JsValue> { + let app = App::new(AppState::default(), app_logic); + app.run(); + + Ok(()) +} diff --git a/crates/xilem_svg/src/pointer.rs b/crates/xilem_svg/src/pointer.rs new file mode 100644 index 000000000..0cebd2d05 --- /dev/null +++ b/crates/xilem_svg/src/pointer.rs @@ -0,0 +1,143 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Interactivity with pointer events. + +use std::any::Any; + +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::PointerEvent; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomElement, View, ViewMarker}, +}; + +pub struct Pointer { + child: V, + callback: F, +} + +pub struct PointerState { + // Closures are retained so they can be called by environment + #[allow(unused)] + down_closure: Closure, + #[allow(unused)] + move_closure: Closure, + #[allow(unused)] + up_closure: Closure, + child_state: S, +} + +#[derive(Debug)] +pub enum PointerMsg { + Down(PointerDetails), + Move(PointerDetails), + Up(PointerDetails), +} + +#[derive(Debug)] +pub struct PointerDetails { + pub id: i32, + pub button: i16, + pub x: f64, + pub y: f64, +} + +impl PointerDetails { + fn from_pointer_event(e: &PointerEvent) -> Self { + PointerDetails { + id: e.pointer_id(), + button: e.button(), + x: e.client_x() as f64, + y: e.client_y() as f64, + } + } +} + +pub fn pointer>(child: V, callback: F) -> Pointer { + Pointer { child, callback } +} + +impl ViewMarker for Pointer {} + +impl> View for Pointer { + type State = PointerState; + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let el_clone = element.as_element_ref().clone(); + let down_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Down(PointerDetails::from_pointer_event(&e))); + el_clone.set_pointer_capture(e.pointer_id()).unwrap(); + e.prevent_default(); + e.stop_propagation(); + }); + element + .as_element_ref() + .add_event_listener_with_callback("pointerdown", down_closure.as_ref().unchecked_ref()) + .unwrap(); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let move_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Move(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + element + .as_element_ref() + .add_event_listener_with_callback("pointermove", move_closure.as_ref().unchecked_ref()) + .unwrap(); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let up_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Up(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + element + .as_element_ref() + .add_event_listener_with_callback("pointerup", up_closure.as_ref().unchecked_ref()) + .unwrap(); + let state = PointerState { + down_closure, + move_closure, + up_closure, + child_state, + }; + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + // TODO: if the child id changes (as can happen with AnyView), reinstall closure + self.child + .rebuild(cx, &prev.child, id, &mut state.child_state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult<()> { + match message.downcast() { + Ok(msg) => { + (self.callback)(app_state, *msg); + MessageResult::Action(()) + } + Err(message) => self + .child + .message(id_path, &mut state.child_state, message, app_state), + } + } +} diff --git a/crates/xilem_svg/src/view.rs b/crates/xilem_svg/src/view.rs new file mode 100644 index 000000000..09b2e054a --- /dev/null +++ b/crates/xilem_svg/src/view.rs @@ -0,0 +1,77 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Integration with xilem_core. This instantiates the View and related +//! traits for DOM node generation. + +use std::ops::Deref; + +use crate::{context::Cx, ChangeFlags}; + +// A possible refinement of xilem_core is to allow a single concrete type +// for a view element, rather than an associated type with a bound. +pub trait DomElement { + fn into_pod(self) -> Pod; + fn as_element_ref(&self) -> &web_sys::Element; +} + +pub trait AnyElement { + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + fn as_element_ref(&self) -> &web_sys::Element; +} + +impl AnyElement for web_sys::Element { + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn as_element_ref(&self) -> &web_sys::Element { + self + } +} + +impl DomElement for web_sys::Element { + fn into_pod(self) -> Pod { + Pod(Box::new(self)) + } + + fn as_element_ref(&self) -> &web_sys::Element { + self + } +} + +impl DomElement for Box { + fn into_pod(self) -> Pod { + Pod(self) + } + + fn as_element_ref(&self) -> &web_sys::Element { + self.deref().as_element_ref() + } +} + +/// A container that holds a DOM element. +/// +/// This implementation may be overkill (it's possibly enough that everything is +/// just a `web_sys::Element`), but does allow element types that contain other +/// data, if needed. +pub struct Pod(pub Box); + +impl Pod { + fn new(node: impl DomElement) -> Self { + node.into_pod() + } + + fn downcast_mut<'a, T: 'static>(&'a mut self) -> Option<&'a mut T> { + self.0.as_any_mut().downcast_mut() + } + + fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags { + flags + } +} + +xilem_core::generate_view_trait! {View, DomElement, Cx, ChangeFlags;} +xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomElement, Cx, ChangeFlags, Pod;} +xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyElement} diff --git a/crates/xilem_svg/src/view_ext.rs b/crates/xilem_svg/src/view_ext.rs new file mode 100644 index 000000000..db6ea116e --- /dev/null +++ b/crates/xilem_svg/src/view_ext.rs @@ -0,0 +1,25 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + class::Class, + clicked::Clicked, + pointer::{Pointer, PointerMsg}, + view::View, +}; + +pub trait ViewExt: View + Sized { + fn clicked(self, f: F) -> Clicked; + fn pointer(self, f: F) -> Pointer { + crate::pointer::pointer(self, f) + } + fn class(self, class: impl Into) -> Class { + crate::class::class(self, class) + } +} + +impl> ViewExt for V { + fn clicked(self, f: F) -> Clicked { + crate::clicked::clicked(self, f) + } +} diff --git a/src/app.rs b/src/app.rs index 73d15a045..15af8cbc0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,17 +23,17 @@ use parley::FontContext; use tokio::runtime::Runtime; use vello::kurbo::{Point, Rect}; use vello::SceneFragment; -use xilem_core::{AsyncWake, Message, MessageResult}; +use xilem_core::{AsyncWake, MessageResult}; use crate::widget::{ AccessCx, BoxConstraints, CxState, EventCx, LayoutCx, LifeCycle, LifeCycleCx, PaintCx, Pod, PodFlags, UpdateCx, ViewContext, WidgetState, }; -use crate::IdPath; use crate::{ view::{Cx, Id, View}, widget::Event, }; +use crate::{IdPath, Message}; /// App is the native backend implementation of Xilem. It contains the code interacting with glazier /// and vello. diff --git a/src/lib.rs b/src/lib.rs index a26f67359..6b3205c86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,9 @@ mod text; pub mod view; pub mod widget; -pub use xilem_core::{IdPath, Message, MessageResult}; +xilem_core::message!(Send); + +pub use xilem_core::{IdPath, MessageResult}; pub use app::App; pub use app_main::AppLauncher; diff --git a/src/widget/contexts.rs b/src/widget/contexts.rs index 649cbd7c8..d5e6e2ed1 100644 --- a/src/widget/contexts.rs +++ b/src/widget/contexts.rs @@ -24,9 +24,9 @@ use glazier::{ WindowHandle, }; use parley::FontContext; -use xilem_core::Message; use super::{PodFlags, WidgetState}; +use crate::Message; // These contexts loosely follow Druid. From da58556d15885bdb8e77b3afc6b993c1bb7bf270 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 14:35:24 +0100 Subject: [PATCH 02/12] Use `gloo` for registering event callback `gloo::events::EventListener` will automatically remove the callback when it is dropped. --- Cargo.lock | 20 +++++ crates/xilem_html/Cargo.toml | 1 + crates/xilem_html/src/event/mod.rs | 119 ++++------------------------- crates/xilem_html/src/lib.rs | 3 +- 4 files changed, 36 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f70c4ce95..352dd4732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,6 +1208,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-events", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.12.2" @@ -3249,6 +3268,7 @@ name = "xilem_html" version = "0.1.0" dependencies = [ "bitflags 1.3.2", + "gloo", "kurbo", "log", "wasm-bindgen", diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml index 268fda5ab..c7e879cb4 100644 --- a/crates/xilem_html/Cargo.toml +++ b/crates/xilem_html/Cargo.toml @@ -75,6 +75,7 @@ wasm-bindgen = "0.2.87" kurbo = "0.9.1" xilem_core = { path = "../xilem_core" } log = "0.4.19" +gloo = { version = "0.8.1", default-features = false, features = ["events"] } [dependencies.web-sys] version = "0.3.4" diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs index aadd4bb3c..ff8e25769 100644 --- a/crates/xilem_html/src/event/mod.rs +++ b/crates/xilem_html/src/event/mod.rs @@ -6,7 +6,8 @@ pub mod events; use std::{any::Any, marker::PhantomData, ops::Deref}; -use wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}; +use gloo::events::EventListener; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult}; use crate::{ @@ -50,18 +51,19 @@ where fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let (id, child_state, element) = self.child.build(cx); let thunk = cx.with_id(id, |cx| cx.message_thunk()); - let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { - let event = event.dyn_into::().unwrap_throw(); - let event: Event = Event::new(event); - thunk.push_message(EventMsg { event }); - }) as Box); - element - .as_node_ref() - .add_event_listener_with_callback(self.event, closure.as_ref().unchecked_ref()) - .unwrap_throw(); + let listener = EventListener::new( + element.as_node_ref(), + self.event, + move |event: &web_sys::Event| { + let event = (*event).clone(); + let event = event.dyn_into::().unwrap_throw(); + let event: Event = Event::new(event); + thunk.push_message(EventMsg { event }); + }, + ); // TODO add `remove_listener_with_callback` to clean up listener? let state = OnEventState { - closure, + listener, child_state, }; (id, state, element) @@ -103,52 +105,13 @@ pub fn on_event(name: &'static str, child: V, callback: F) -> OnEvent { #[allow(unused)] - closure: Closure, + listener: EventListener, child_state: S, } struct EventMsg { event: E, } -/* -// on input -pub fn on_input MessageResult, V: View>( - child: V, - callback: F, -) -> OnEvent { - OnEvent::new("input", child, callback) -} - -// on click -pub fn on_click MessageResult, V: View>( - child: V, - callback: F, -) -> OnEvent { - OnEvent::new("click", child, callback) -} - -// on click -pub fn on_dblclick MessageResult, V: View>( - child: V, - callback: F, -) -> OnEvent { - OnEvent::new("dblclick", child, callback) -} - -// on keydown -pub fn on_keydown< - T, - A, - F: Fn(&mut T, &web_sys::KeyboardEvent) -> MessageResult, - V: View, ->( - child: V, - callback: F, -) -> OnEvent { - OnEvent::new("keydown", child, callback) -} -*/ - pub struct Event { raw: Evt, el: PhantomData, @@ -180,57 +143,3 @@ impl Deref for Event { &self.raw } } - -/* -/// Types that can be created from a `web_sys::Event`. -/// -/// Implementations may make the assumption that the event -/// is a particular subtype (e.g. `InputEvent`) and panic -/// when this is not the case (although it's preferred to use -/// `throw_str` and friends). -pub trait FromEvent: 'static { - /// Convert the given event into `self`, or panic. - fn from_event(event: &web_sys::Event) -> Self; -} - -#[derive(Debug)] -pub struct InputEvent { - pub data: Option, - /// The value of `event.target.value`. - pub value: String, -} - -impl FromEvent for InputEvent { - fn from_event(event: &web_sys::Event) -> Self { - let event: &web_sys::InputEvent = event.dyn_ref().unwrap_throw(); - Self { - data: event.data(), - value: event - .target() - .unwrap_throw() - .dyn_into::() - .unwrap_throw() - .value(), - } - } -} - -pub struct Event {} - -impl FromEvent for Event { - fn from_event(_event: &web_sys::Event) -> Self { - Self {} - } -} - -pub struct KeyboardEvent { - pub key: String, -} - -impl FromEvent for KeyboardEvent { - fn from_event(event: &web_sys::Event) -> Self { - let event: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); - Self { key: event.key() } - } -} -*/ diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 717fd5c77..72dde60a6 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -1,8 +1,7 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 -//! A test program to exercise using xilem_core to generate SVG nodes that -//! render in a browser. +//! A highly experimental web framework using the xilem architecture. //! //! Run using `trunk serve`. From 89ab9531679a02786908b76df0c797f2537f9870 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 15:57:01 +0100 Subject: [PATCH 03/12] Impl `View` directly on text, and refactor action return types. --- Cargo.lock | 37 ++++++ crates/xilem_html/Cargo.toml | 65 +--------- crates/xilem_html/src/event/events.rs | 48 +++++-- crates/xilem_html/src/event/mod.rs | 46 ++++++- crates/xilem_html/src/lib.rs | 4 +- crates/xilem_html/src/text.rs | 61 --------- crates/xilem_html/src/view.rs | 122 +++++++++++++++++- crates/xilem_html/src/view_ext.rs | 54 +++++--- .../web_examples/counter/src/lib.rs | 48 +++---- .../web_examples/counter_untyped/src/lib.rs | 25 ++-- .../web_examples/todomvc/src/lib.rs | 102 +++++++-------- 11 files changed, 362 insertions(+), 250 deletions(-) delete mode 100644 crates/xilem_html/src/text.rs diff --git a/Cargo.lock b/Cargo.lock index 352dd4732..4771b05d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,6 +1215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" dependencies = [ "gloo-events", + "gloo-utils", ] [[package]] @@ -1227,6 +1228,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.12.2" @@ -1498,6 +1512,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "js-sys" version = "0.3.64" @@ -2244,6 +2264,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2276,6 +2302,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "serde_json" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.12" diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml index c7e879cb4..dab8ac124 100644 --- a/crates/xilem_html/Cargo.toml +++ b/crates/xilem_html/Cargo.toml @@ -6,68 +6,7 @@ edition = "2021" [features] default = ["typed"] -typed = [ - 'web-sys/FocusEvent', - 'web-sys/HtmlAnchorElement', - 'web-sys/HtmlAreaElement', - 'web-sys/HtmlAudioElement', - 'web-sys/HtmlBrElement', - 'web-sys/HtmlButtonElement', - 'web-sys/HtmlCanvasElement', - 'web-sys/HtmlDataElement', - 'web-sys/HtmlDataListElement', - 'web-sys/HtmlDetailsElement', - 'web-sys/HtmlDialogElement', - 'web-sys/HtmlDivElement', - 'web-sys/HtmlDListElement', - 'web-sys/HtmlEmbedElement', - 'web-sys/HtmlFieldSetElement', - 'web-sys/HtmlFormElement', - 'web-sys/HtmlHeadingElement', - 'web-sys/HtmlHrElement', - 'web-sys/HtmlIFrameElement', - 'web-sys/HtmlImageElement', - 'web-sys/HtmlInputElement', - 'web-sys/HtmlLabelElement', - 'web-sys/HtmlLegendElement', - 'web-sys/HtmlLiElement', - 'web-sys/HtmlMapElement', - 'web-sys/HtmlMenuElement', - 'web-sys/HtmlMeterElement', - 'web-sys/HtmlModElement', - 'web-sys/HtmlObjectElement', - 'web-sys/HtmlOListElement', - 'web-sys/HtmlOptGroupElement', - 'web-sys/HtmlOptionElement', - 'web-sys/HtmlOutputElement', - 'web-sys/HtmlParagraphElement', - 'web-sys/HtmlPictureElement', - 'web-sys/HtmlPreElement', - 'web-sys/HtmlProgressElement', - 'web-sys/HtmlQuoteElement', - 'web-sys/HtmlScriptElement', - 'web-sys/HtmlSelectElement', - 'web-sys/HtmlSlotElement', - 'web-sys/HtmlSourceElement', - 'web-sys/HtmlSpanElement', - 'web-sys/HtmlTableElement', - 'web-sys/HtmlTableCellElement', - 'web-sys/HtmlTableColElement', - 'web-sys/HtmlTableCaptionElement', - 'web-sys/HtmlTableRowElement', - 'web-sys/HtmlTableSectionElement', - 'web-sys/HtmlTemplateElement', - 'web-sys/HtmlTextAreaElement', - 'web-sys/HtmlTimeElement', - 'web-sys/HtmlTrackElement', - 'web-sys/HtmlUListElement', - 'web-sys/HtmlVideoElement', - 'web-sys/InputEvent', - 'web-sys/KeyboardEvent', - 'web-sys/MouseEvent', - 'web-sys/PointerEvent', - 'web-sys/WheelEvent', -] +typed = ['web-sys/FocusEvent', 'web-sys/HtmlAnchorElement', 'web-sys/HtmlAreaElement', 'web-sys/HtmlAudioElement', 'web-sys/HtmlBrElement', 'web-sys/HtmlButtonElement', 'web-sys/HtmlCanvasElement', 'web-sys/HtmlDataElement', 'web-sys/HtmlDataListElement', 'web-sys/HtmlDetailsElement', 'web-sys/HtmlDialogElement', 'web-sys/HtmlDivElement', 'web-sys/HtmlDListElement', 'web-sys/HtmlEmbedElement', 'web-sys/HtmlFieldSetElement', 'web-sys/HtmlFormElement', 'web-sys/HtmlHeadingElement', 'web-sys/HtmlHrElement', 'web-sys/HtmlIFrameElement', 'web-sys/HtmlImageElement', 'web-sys/HtmlInputElement', 'web-sys/HtmlLabelElement', 'web-sys/HtmlLegendElement', 'web-sys/HtmlLiElement', 'web-sys/HtmlMapElement', 'web-sys/HtmlMenuElement', 'web-sys/HtmlMeterElement', 'web-sys/HtmlModElement', 'web-sys/HtmlObjectElement', 'web-sys/HtmlOListElement', 'web-sys/HtmlOptGroupElement', 'web-sys/HtmlOptionElement', 'web-sys/HtmlOutputElement', 'web-sys/HtmlParagraphElement', 'web-sys/HtmlPictureElement', 'web-sys/HtmlPreElement', 'web-sys/HtmlProgressElement', 'web-sys/HtmlQuoteElement', 'web-sys/HtmlScriptElement', 'web-sys/HtmlSelectElement', 'web-sys/HtmlSlotElement', 'web-sys/HtmlSourceElement', 'web-sys/HtmlSpanElement', 'web-sys/HtmlTableElement', 'web-sys/HtmlTableCellElement', 'web-sys/HtmlTableColElement', 'web-sys/HtmlTableCaptionElement', 'web-sys/HtmlTableRowElement', 'web-sys/HtmlTableSectionElement', 'web-sys/HtmlTemplateElement', 'web-sys/HtmlTextAreaElement', 'web-sys/HtmlTimeElement', 'web-sys/HtmlTrackElement', 'web-sys/HtmlUListElement', 'web-sys/HtmlVideoElement', 'web-sys/InputEvent', 'web-sys/KeyboardEvent', 'web-sys/MouseEvent', 'web-sys/PointerEvent', 'web-sys/WheelEvent'] [dependencies] bitflags = "1.3.2" @@ -75,7 +14,7 @@ wasm-bindgen = "0.2.87" kurbo = "0.9.1" xilem_core = { path = "../xilem_core" } log = "0.4.19" -gloo = { version = "0.8.1", default-features = false, features = ["events"] } +gloo = { version = "0.8.1", default-features = false, features = ["events", "utils"] } [dependencies.web-sys] version = "0.3.4" diff --git a/crates/xilem_html/src/event/events.rs b/crates/xilem_html/src/event/events.rs index 98b638391..4beb695c9 100644 --- a/crates/xilem_html/src/event/events.rs +++ b/crates/xilem_html/src/event/events.rs @@ -10,19 +10,49 @@ macro_rules! events { macro_rules! event { ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { - pub struct $ty_name(crate::OnEvent<$web_sys_ty, V, F>); + pub struct $ty_name + where + V: crate::view::View, + F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, + V::Element: 'static, + OA: $crate::event::OptionalAction, + { + inner: crate::OnEvent<$web_sys_ty, V, F>, + data: std::marker::PhantomData, + action: std::marker::PhantomData, + optional_action: std::marker::PhantomData, + } - pub fn $builder_name(child: V, callback: F) -> $ty_name { - $ty_name(crate::on_event($name, child, callback)) + pub fn $builder_name(child: V, callback: F) -> $ty_name + where + V: crate::view::View, + F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, + V::Element: 'static, + OA: $crate::event::OptionalAction, + { + $ty_name { + inner: crate::on_event($name, child, callback), + data: std::marker::PhantomData, + action: std::marker::PhantomData, + optional_action: std::marker::PhantomData, + } } - impl crate::view::ViewMarker for $ty_name {} + impl crate::view::ViewMarker for $ty_name + where + V: crate::view::View, + F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, + V::Element: 'static, + OA: $crate::event::OptionalAction, + { + } - impl crate::view::View for $ty_name + impl crate::view::View for $ty_name where V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> $crate::MessageResult, + F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, V::Element: 'static, + OA: $crate::event::OptionalAction, { type State = crate::event::OnEventState; type Element = V::Element; @@ -31,7 +61,7 @@ macro_rules! event { &self, cx: &mut crate::context::Cx, ) -> (xilem_core::Id, Self::State, Self::Element) { - self.0.build(cx) + self.inner.build(cx) } fn rebuild( @@ -42,7 +72,7 @@ macro_rules! event { state: &mut Self::State, element: &mut Self::Element, ) -> crate::ChangeFlags { - self.0.rebuild(cx, &prev.0, id, state, element) + self.inner.rebuild(cx, &prev.inner, id, state, element) } fn message( @@ -52,7 +82,7 @@ macro_rules! event { message: Box, app_state: &mut T, ) -> xilem_core::MessageResult { - self.0.message(id_path, state, message, app_state) + self.inner.message(id_path, state, message, app_state) } } }; diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs index ff8e25769..7ac386d9e 100644 --- a/crates/xilem_html/src/event/mod.rs +++ b/crates/xilem_html/src/event/mod.rs @@ -37,12 +37,13 @@ impl OnEvent { impl ViewMarker for OnEvent {} -impl View for OnEvent +impl View for OnEvent where - F: Fn(&mut T, &Event) -> MessageResult, + F: Fn(&mut T, &Event) -> OA, V: View, E: JsCast + 'static, V::Element: 'static, + OA: OptionalAction, { type State = OnEventState; @@ -90,7 +91,10 @@ where app_state: &mut T, ) -> MessageResult { if let Some(msg) = message.downcast_ref::>>() { - (self.callback)(app_state, &msg.event) + match (self.callback)(app_state, &msg.event).action() { + Some(a) => MessageResult::Action(a), + None => MessageResult::Nop, + } } else { self.child .message(id_path, &mut state.child_state, message, app_state) @@ -143,3 +147,39 @@ impl Deref for Event { &self.raw } } + +/// Implement this trait for types you want to use as actions. +/// +/// The trait exists because otherwise we couldn't provide versions +/// of listeners that take `()`, `A` and `Option`. +pub trait Action {} + +/// Trait that allows callbacks to be polymorphic on return type +/// (`Action`, `Option` or `()`) +pub trait OptionalAction: sealed::Sealed { + fn action(self) -> Option; +} +mod sealed { + pub trait Sealed {} +} + +impl sealed::Sealed for () {} +impl OptionalAction for () { + fn action(self) -> Option { + None + } +} + +impl sealed::Sealed for A {} +impl OptionalAction for A { + fn action(self) -> Option { + Some(self) + } +} + +impl sealed::Sealed for Option {} +impl OptionalAction for Option { + fn action(self) -> Option { + self + } +} diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 72dde60a6..8dd60c51e 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -14,7 +14,6 @@ mod context; mod event; //mod div; mod element; -mod text; mod view; #[cfg(feature = "typed")] mod view_ext; @@ -29,8 +28,7 @@ pub use element::elements; pub use element::{element, Element, ElementState}; #[cfg(feature = "typed")] pub use event::events; -pub use event::{on_event, Event, OnEvent, OnEventState}; -pub use text::{text, Text}; +pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction}; pub use view::{Adapt, AdaptThunk, Pod, View, ViewMarker, ViewSequence}; #[cfg(feature = "typed")] pub use view_ext::ViewExt; diff --git a/crates/xilem_html/src/text.rs b/crates/xilem_html/src/text.rs deleted file mode 100644 index db3d94e40..000000000 --- a/crates/xilem_html/src/text.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::borrow::Cow; -use wasm_bindgen::JsCast; - -use xilem_core::{Id, MessageResult}; - -use crate::{ - context::{ChangeFlags, Cx}, - view::{View, ViewMarker}, -}; - -pub struct Text { - text: Cow<'static, str>, -} - -/// Create a text node -pub fn text(text: impl Into>) -> Text { - Text { text: text.into() } -} - -impl ViewMarker for Text {} - -impl View for Text { - type State = (); - type Element = web_sys::Text; - - fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let el = new_text(&self.text); - let id = Id::next(); - (id, (), el.unchecked_into()) - } - - fn rebuild( - &self, - _cx: &mut Cx, - prev: &Self, - _id: &mut Id, - _state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - let mut is_changed = ChangeFlags::empty(); - if prev.text != self.text { - element.set_data(&self.text); - is_changed |= ChangeFlags::OTHER_CHANGE; - } - is_changed - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - _message: Box, - _app_state: &mut T, - ) -> MessageResult { - MessageResult::Nop - } -} - -fn new_text(text: &str) -> web_sys::Text { - web_sys::Text::new_with_data(text).unwrap() -} diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index b38b12735..f8bec4ecb 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -4,7 +4,9 @@ //! Integration with xilem_core. This instantiates the View and related //! traits for DOM node generation. -use std::{any::Any, ops::Deref}; +use std::{any::Any, borrow::Cow, ops::Deref}; + +use xilem_core::{Id, MessageResult}; use crate::{context::Cx, ChangeFlags}; @@ -98,3 +100,121 @@ impl Pod { xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyNode} + +impl ViewMarker for &'static str {} +impl View for &'static str { + type State = (); + type Element = web_sys::Text; + + fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = new_text(self); + let id = Id::next(); + (id, (), el.into()) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::empty(); + if prev != self { + element.set_data(self); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Nop + } +} + +impl ViewMarker for String {} +impl View for String { + type State = (); + type Element = web_sys::Text; + + fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = new_text(self); + let id = Id::next(); + (id, (), el.into()) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::empty(); + if prev != self { + element.set_data(self); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Nop + } +} + +impl ViewMarker for Cow<'static, str> {} +impl View for Cow<'static, str> { + type State = (); + type Element = web_sys::Text; + + fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let el = new_text(self); + let id = Id::next(); + (id, (), el.into()) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut is_changed = ChangeFlags::empty(); + if prev != self { + element.set_data(self); + is_changed |= ChangeFlags::OTHER_CHANGE; + } + is_changed + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Nop + } +} + +fn new_text(text: &str) -> web_sys::Text { + web_sys::Text::new_with_data(text).unwrap() +} diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs index db46cdb68..7e07190e1 100644 --- a/crates/xilem_html/src/view_ext.rs +++ b/crates/xilem_html/src/view_ext.rs @@ -3,61 +3,77 @@ use std::borrow::Cow; -use xilem_core::MessageResult; - -use crate::{class::Class, events as e, view::View, Event}; +use crate::{class::Class, event::OptionalAction, events as e, view::View, Event}; pub trait ViewExt: View + Sized { - fn on_click) -> MessageResult>( + fn on_click< + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, + >( self, f: F, - ) -> e::OnClick; - fn on_dblclick) -> MessageResult>( + ) -> e::OnClick; + fn on_dblclick< + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, + >( self, f: F, - ) -> e::OnDblClick; - fn on_input) -> MessageResult>( + ) -> e::OnDblClick; + fn on_input< + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, + >( self, f: F, - ) -> e::OnInput; + ) -> e::OnInput; fn on_keydown< - F: Fn(&mut T, &Event) -> MessageResult, + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, >( self, f: F, - ) -> e::OnKeyDown; + ) -> e::OnKeyDown; fn class(self, class: impl Into>) -> Class { crate::class::class(self, class) } } impl> ViewExt for V { - fn on_click) -> MessageResult>( + fn on_click< + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, + >( self, f: F, - ) -> e::OnClick { + ) -> e::OnClick { e::on_click(self, f) } fn on_dblclick< - F: Fn(&mut T, &Event) -> MessageResult, + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, >( self, f: F, - ) -> e::OnDblClick { + ) -> e::OnDblClick { e::on_dblclick(self, f) } - fn on_input) -> MessageResult>( + fn on_input< + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, + >( self, f: F, - ) -> e::OnInput { + ) -> e::OnInput { crate::events::on_input(self, f) } fn on_keydown< - F: Fn(&mut T, &Event) -> MessageResult, + OA: OptionalAction, + F: Fn(&mut T, &Event) -> OA, >( self, f: F, - ) -> e::OnKeyDown { + ) -> e::OnKeyDown { crate::events::on_keydown(self, f) } } diff --git a/crates/xilem_html/web_examples/counter/src/lib.rs b/crates/xilem_html/web_examples/counter/src/lib.rs index 1c71757c4..8dcb7f26b 100644 --- a/crates/xilem_html/web_examples/counter/src/lib.rs +++ b/crates/xilem_html/web_examples/counter/src/lib.rs @@ -1,7 +1,8 @@ use wasm_bindgen::{prelude::*, JsValue}; use xilem_html::{ - document_body, elements as el, events as evt, text, App, Event, MessageResult, Text, View, - ViewExt, + document_body, elements as el, + events::{self as evt}, + App, Event, View, ViewExt, }; #[derive(Default)] @@ -12,61 +13,54 @@ struct AppState { } impl AppState { - fn increment(&mut self) -> MessageResult<()> { + fn increment(&mut self) { self.clicks += 1; - MessageResult::Nop } - fn decrement(&mut self) -> MessageResult<()> { + fn decrement(&mut self) { self.clicks -= 1; - MessageResult::Nop } - fn reset(&mut self) -> MessageResult<()> { + fn reset(&mut self) { self.clicks = 0; - MessageResult::Nop } - fn change_class(&mut self) -> MessageResult<()> { + fn change_class(&mut self) { if self.class == "gray" { self.class = "green"; } else { self.class = "gray"; } - MessageResult::Nop } - fn change_text(&mut self) -> MessageResult<()> { + fn change_text(&mut self) { if self.text == "test" { self.text = "test2".into(); } else { self.text = "test".into(); } - MessageResult::Nop } } /// You can create functions that generate views. -fn btn(label: &'static str, click_fn: F) -> evt::OnClick, F> +fn btn( + label: &'static str, + click_fn: F, +) -> evt::OnClick, F, ()> where - F: Fn( - &mut AppState, - &Event, - ) -> MessageResult<()>, + F: Fn(&mut AppState, &Event), { - el::button(text(label)).on_click(click_fn) + el::button(label).on_click(click_fn) } fn app_logic(state: &mut AppState) -> impl View { el::div(( - el::span(text(format!("clicked {} times", state.clicks))).attr("class", state.class), + el::span(format!("clicked {} times", state.clicks)).attr("class", state.class), el::br(()), - btn("+1 click", |state, _| AppState::increment(state)), - btn("-1 click", |state, _| AppState::decrement(state)), - btn("reset clicks", |state, _| AppState::reset(state)), - btn("a different class", |state, _| { - AppState::change_class(state) - }), - btn("change text", |state, _| AppState::change_text(state)), + btn("+1 click", |state, _| state.increment()), + btn("-1 click", |state, _| state.decrement()), + btn("reset clicks", |state, _| state.reset()), + btn("a different class", |state, _| state.change_class()), + btn("change text", |state, _| state.change_text()), el::br(()), - text(state.text.clone()), + state.text.clone(), )) } diff --git a/crates/xilem_html/web_examples/counter_untyped/src/lib.rs b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs index b835d7b54..49ad519ec 100644 --- a/crates/xilem_html/web_examples/counter_untyped/src/lib.rs +++ b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs @@ -1,7 +1,5 @@ use wasm_bindgen::{prelude::*, JsValue}; -use xilem_html::{ - document_body, element as el, on_event, text, App, Event, MessageResult, View, ViewMarker, -}; +use xilem_html::{document_body, element as el, on_event, App, Event, View, ViewMarker}; #[derive(Default)] struct AppState { @@ -9,32 +7,35 @@ struct AppState { } impl AppState { - fn increment(&mut self) -> MessageResult<()> { + fn increment(&mut self) { self.clicks += 1; - MessageResult::Nop } - fn decrement(&mut self) -> MessageResult<()> { + fn decrement(&mut self) { self.clicks -= 1; - MessageResult::Nop } - fn reset(&mut self) -> MessageResult<()> { + fn reset(&mut self) { self.clicks = 0; - MessageResult::Nop } } fn btn(label: &'static str, click_fn: F) -> impl View + ViewMarker where - F: Fn(&mut AppState, &Event) -> MessageResult<()>, + F: Fn(&mut AppState, &Event), { - on_event("click", el("button", text(label)), click_fn) + on_event( + "click", + el("button", label), + move |state: &mut AppState, evt: &Event<_, _>| { + click_fn(state, evt); + }, + ) } fn app_logic(state: &mut AppState) -> impl View { el::( "div", ( - el::("span", text(format!("clicked {} times", state.clicks))), + el::("span", format!("clicked {} times", state.clicks)), btn("+1 click", |state, _| AppState::increment(state)), btn("-1 click", |state, _| AppState::decrement(state)), btn("reset clicks", |state, _| AppState::reset(state)), diff --git a/crates/xilem_html/web_examples/todomvc/src/lib.rs b/crates/xilem_html/web_examples/todomvc/src/lib.rs index 50c2d80af..d8b5a3dff 100644 --- a/crates/xilem_html/web_examples/todomvc/src/lib.rs +++ b/crates/xilem_html/web_examples/todomvc/src/lib.rs @@ -6,7 +6,8 @@ use state::{AppState, Filter, Todo}; use wasm_bindgen::{prelude::*, JsValue}; use xilem_html::{ - elements as el, get_element_by_id, text, Adapt, App, MessageResult, View, ViewExt, ViewMarker, + elements as el, events::on_click, get_element_by_id, Action, Adapt, App, Event, MessageResult, + View, ViewExt, ViewMarker, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -17,6 +18,8 @@ enum TodoAction { Destroy(u64), } +impl Action for TodoAction {} + fn todo_item(todo: &mut Todo, editing: bool) -> impl View + ViewMarker { let mut class = String::new(); if todo.completed { @@ -36,16 +39,12 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi el::div(( input.on_click(|state: &mut Todo, _| { state.completed = !state.completed; - MessageResult::RequestRebuild - }), - el::label(text(todo.title.clone())).on_dblclick(|state: &mut Todo, _| { - MessageResult::Action(TodoAction::SetEditing(state.id)) }), + el::label(todo.title.clone()) + .on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)), el::button(()) .attr("class", "destroy") - .on_click(|state: &mut Todo, _| { - MessageResult::Action(TodoAction::Destroy(state.id)) - }), + .on_click(|state: &mut Todo, _| TodoAction::Destroy(state.id)), )) .attr("class", "view"), el::input(()) @@ -55,18 +54,17 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi let key = evt.key(); if key == "Enter" { state.save_editing(); - MessageResult::Action(TodoAction::CancelEditing) + Some(TodoAction::CancelEditing) } else if key == "Escape" { - MessageResult::Action(TodoAction::CancelEditing) + Some(TodoAction::CancelEditing) } else { - MessageResult::Nop + None } }) .on_input(|state: &mut Todo, evt| { state.title_editing.clear(); state.title_editing.push_str(&evt.target().value()); evt.prevent_default(); - MessageResult::Nop }), )) .attr("class", class) @@ -80,12 +78,12 @@ fn footer_view(state: &mut AppState) -> impl View + ViewMarker { }; let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 0).then(|| { - el::button(text("Clear completed")) - .attr("class", "clear-completed") - .on_click(|state: &mut AppState, _| { + on_click( + el::button("Clear completed").attr("class", "clear-completed"), + |state: &mut AppState, _| { state.todos.retain(|todo| !todo.completed); - MessageResult::RequestRebuild - }) + }, + ) }); let filter_class = |filter| { @@ -98,40 +96,37 @@ fn footer_view(state: &mut AppState) -> impl View + ViewMarker { el::footer(( el::span(( - el::strong(text(state.todos.len().to_string())), - text(format!(" {} left", item_str)), + el::strong(state.todos.len().to_string()), + format!(" {} left", item_str), )) .attr("class", "todo-count"), el::ul(( - el::li( - el::a(text("All")) + el::li(on_click( + el::a("All") .attr("href", "#/") - .attr("class", filter_class(Filter::All)) - .on_click(|state: &mut AppState, _| { - state.filter = Filter::All; - MessageResult::RequestRebuild - }), - ), - text(" "), - el::li( - el::a(text("Active")) + .attr("class", filter_class(Filter::All)), + |state: &mut AppState, _| { + state.filter = Filter::All; + }, + )), + " ", + el::li(on_click( + el::a("Active") .attr("href", "#/active") - .attr("class", filter_class(Filter::Active)) - .on_click(|state: &mut AppState, _| { - state.filter = Filter::Active; - MessageResult::RequestRebuild - }), - ), - text(" "), - el::li( - el::a(text("Completed")) + .attr("class", filter_class(Filter::Active)), + |state: &mut AppState, _| { + state.filter = Filter::Active; + }, + )), + " ", + el::li(on_click( + el::a("Completed") .attr("href", "#/completed") - .attr("class", filter_class(Filter::Completed)) - .on_click(|state: &mut AppState, _| { - state.filter = Filter::Completed; - MessageResult::RequestRebuild - }), - ), + .attr("class", filter_class(Filter::Completed)), + |state: &mut AppState, _| { + state.filter = Filter::Completed; + }, + )), )) .attr("class", "filters"), clear_button, @@ -177,23 +172,26 @@ fn app_logic(state: &mut AppState) -> impl View { let footer = (!state.todos.is_empty()).then(|| footer_view(state)); el::div(( el::header(( - el::h1(text("TODOs")), + el::h1("TODOs"), el::input(()) .attr("class", "new-todo") .attr("placeholder", "What needs to be done?") .attr("value", state.new_todo.clone()) .attr("autofocus", "true") - .on_keydown(|state: &mut AppState, evt| { + .on_keydown( + |state: &mut AppState, evt| { if evt.key() == "Enter" { state.create_todo(); } - MessageResult::RequestRebuild - }) - .on_input(|state: &mut AppState, evt| { + }, + ) + .on_input( + |state: &mut AppState, + evt: &Event| { state.update_new_todo(&evt.target().value()); evt.prevent_default(); - MessageResult::RequestRebuild - }), + }, + ), )) .attr("class", "header"), main, From 8eb4c810435de1fb70d3a6726d381f74086e442e Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 15:59:00 +0100 Subject: [PATCH 04/12] Don't shorten `element` to `el` --- crates/xilem_html/web_examples/counter_untyped/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/xilem_html/web_examples/counter_untyped/src/lib.rs b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs index 49ad519ec..310639d76 100644 --- a/crates/xilem_html/web_examples/counter_untyped/src/lib.rs +++ b/crates/xilem_html/web_examples/counter_untyped/src/lib.rs @@ -1,5 +1,5 @@ use wasm_bindgen::{prelude::*, JsValue}; -use xilem_html::{document_body, element as el, on_event, App, Event, View, ViewMarker}; +use xilem_html::{document_body, element, on_event, App, Event, View, ViewMarker}; #[derive(Default)] struct AppState { @@ -24,7 +24,7 @@ where { on_event( "click", - el("button", label), + element("button", label), move |state: &mut AppState, evt: &Event<_, _>| { click_fn(state, evt); }, @@ -32,10 +32,10 @@ where } fn app_logic(state: &mut AppState) -> impl View { - el::( + element::( "div", ( - el::("span", format!("clicked {} times", state.clicks)), + element::("span", format!("clicked {} times", state.clicks)), btn("+1 click", |state, _| AppState::increment(state)), btn("-1 click", |state, _| AppState::decrement(state)), btn("reset clicks", |state, _| AppState::reset(state)), From a7cc3a1e4e7776fdd3e3378131f85c7da4614a63 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 16:21:30 +0100 Subject: [PATCH 05/12] Remove unused `void` impl --- crates/xilem_html/src/view.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index f8bec4ecb..828e9adbc 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -63,19 +63,6 @@ impl DomNode for Box { } } -struct Void; - -// Dummy implementation that should never be used. -impl DomNode for Void { - fn into_pod(self) -> Pod { - unreachable!() - } - - fn as_node_ref(&self) -> &web_sys::Node { - unreachable!() - } -} - /// A container that holds a DOM element. /// /// This implementation may be overkill (it's possibly enough that everything is From cc9e81dbc9981c0bdded2785b10d1a1efd63bec6 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 17:05:05 +0100 Subject: [PATCH 06/12] Add documentation --- crates/xilem_core/src/sequence.rs | 7 ++- crates/xilem_core/src/view.rs | 53 +++++++++++++++++++++++ crates/xilem_html/src/app.rs | 6 +++ crates/xilem_html/src/element/elements.rs | 8 +++- crates/xilem_html/src/element/mod.rs | 14 +++++- crates/xilem_html/src/event/events.rs | 6 +++ crates/xilem_html/src/event/mod.rs | 11 ++++- crates/xilem_html/src/view.rs | 16 ++++++- crates/xilem_html/src/view_ext.rs | 7 +++ 9 files changed, 120 insertions(+), 8 deletions(-) diff --git a/crates/xilem_core/src/sequence.rs b/crates/xilem_core/src/sequence.rs index 90dfbb65f..60444b7c5 100644 --- a/crates/xilem_core/src/sequence.rs +++ b/crates/xilem_core/src/sequence.rs @@ -57,6 +57,9 @@ macro_rules! impl_view_tuple { #[macro_export] macro_rules! generate_viewsequence_trait { ($viewseq:ident, $view:ident, $viewmarker: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { + /// This trait represents a (possibly empty) sequence of views. + /// + /// It is up to the parent view how to lay out and display them. pub trait $viewseq $( $ss )* { /// Associated states for the views. type State $( $ss )*; @@ -283,8 +286,8 @@ macro_rules! generate_viewsequence_trait { /// This trait marks a type a #[doc = concat!(stringify!($view), ".")] /// - /// This trait is a workaround for Rust's orphan rules. It serves as a switch between default"] - /// and custom + /// This trait is a workaround for Rust's orphan rules. It serves as a switch between + /// default and custom #[doc = concat!("`", stringify!($viewseq), "`")] /// implementations. You can't implement #[doc = concat!("`", stringify!($viewseq), "`")] diff --git a/crates/xilem_core/src/view.rs b/crates/xilem_core/src/view.rs index 44b231611..7a249be5d 100644 --- a/crates/xilem_core/src/view.rs +++ b/crates/xilem_core/src/view.rs @@ -68,6 +68,59 @@ macro_rules! generate_view_trait { ) -> $crate::MessageResult; } + /// A view that wraps a child view and modifies the state that callbacks have access to. + /// + /// # Examples + /// + /// Suppose you have an outer type that looks like + /// + /// ``` + /// struct State { + /// todos: Vec + /// } + /// ``` + /// + /// and an inner type/view that looks like + /// + /// ```ignore + /// struct Todo { + /// label: String + /// } + /// + /// struct TodoView { + /// label: String + /// } + /// + /// enum TodoAction { + /// Delete + /// } + /// + /// impl View for TodoView { + /// // ... + /// } + /// ``` + /// + /// then your top-level action (`()`) and state type (`State`) don't match `TodoView`'s. + /// You can use the `Adapt` view to mediate between them: + /// + /// ```ignore + /// state + /// .todos + /// .enumerate() + /// .map(|(idx, todo)| { + /// Adapt::new( + /// move |data: &mut AppState, thunk| { + /// if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { + /// match action { + /// TodoAction::Delete => data.todos.remove(idx), + /// } + /// } + /// MessageResult::Nop + /// }, + /// TodoView { label: todo.label } + /// ) + /// }) + /// ``` pub struct Adapt) -> $crate::MessageResult, V: View> { f: F, child: V, diff --git a/crates/xilem_html/src/app.rs b/crates/xilem_html/src/app.rs index 60de984e0..56927615d 100644 --- a/crates/xilem_html/src/app.rs +++ b/crates/xilem_html/src/app.rs @@ -10,6 +10,7 @@ use crate::{ }; use xilem_core::{Id, MessageResult}; +/// The type responsible for running your app. pub struct App, F: FnMut(&mut T) -> V>(Rc>>); struct AppInner, F: FnMut(&mut T) -> V> { @@ -35,6 +36,7 @@ impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone fo } impl + 'static, F: FnMut(&mut T) -> V + 'static> App { + /// Create an instance of your app with the given logic and initial state. pub fn new(data: T, app_logic: F) -> Self { let inner = AppInner::new(data, app_logic); let app = App(Rc::new(RefCell::new(inner))); @@ -42,6 +44,10 @@ impl + 'static, F: FnMut(&mut T) -> V + 'static> App {}; @@ -10,8 +10,14 @@ macro_rules! elements { macro_rules! element { ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { + /// A view representing a + #[doc = concat!("`", $name, "`")] + /// element. pub struct $ty_name(crate::Element<$web_sys_ty, ViewSeq>); + /// Builder function for a + #[doc = concat!("`", $name, "`")] + /// view. pub fn $builder_name(children: ViewSeq) -> $ty_name { $ty_name(crate::element($name, children)) } diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index 8f68d80cd..7b30ccf10 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -15,6 +15,8 @@ use xilem_core::{Id, MessageResult, VecSplice}; pub mod elements; /// A view representing a HTML element. +/// +/// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). pub struct Element { name: Cow<'static, str>, attributes: BTreeMap, Cow<'static, str>>, @@ -46,7 +48,9 @@ pub struct ElementState { child_elements: Vec, } -/// Create a new element +/// Create a new element view +/// +/// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). pub fn element( name: impl Into>, children: ViewSeq, @@ -71,10 +75,16 @@ impl Element { name: impl Into>, value: impl Into>, ) -> Self { - self.attributes.insert(name.into(), value.into()); + self.set_attr(name, value); self } + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. pub fn set_attr( &mut self, name: impl Into>, diff --git a/crates/xilem_html/src/event/events.rs b/crates/xilem_html/src/event/events.rs index 4beb695c9..7f5e34037 100644 --- a/crates/xilem_html/src/event/events.rs +++ b/crates/xilem_html/src/event/events.rs @@ -10,6 +10,9 @@ macro_rules! events { macro_rules! event { ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { + /// A view that listens for the + #[doc = concat!("`", $name, "`")] + /// event. pub struct $ty_name where V: crate::view::View, @@ -23,6 +26,9 @@ macro_rules! event { optional_action: std::marker::PhantomData, } + /// Builder for the + #[doc = concat!("`", $name, "`")] + /// event listener. pub fn $builder_name(child: V, callback: F) -> $ty_name where V: crate::view::View, diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs index 7ac386d9e..01207e163 100644 --- a/crates/xilem_html/src/event/mod.rs +++ b/crates/xilem_html/src/event/mod.rs @@ -15,6 +15,10 @@ use crate::{ view::{DomNode, View, ViewMarker}, }; +/// Wraps a [`View`] `V` and attaches an event listener. +/// +/// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the +/// [`web_sys::HtmlElement`] subclass that matches `V::Element`. pub struct OnEvent { // TODO changing this after creation is unsupported for now, // please create a new view instead. @@ -107,6 +111,7 @@ pub fn on_event(name: &'static str, child: V, callback: F) -> OnEvent { #[allow(unused)] listener: EventListener, @@ -116,6 +121,7 @@ struct EventMsg { event: E, } +/// Wraps a `web_sys::Event` and provides auto downcasting for both the event and its target. pub struct Event { raw: Evt, el: PhantomData, @@ -135,6 +141,9 @@ where Evt: AsRef, El: JsCast, { + /// Get the event target element. + /// + /// Because this type knows its child view's element type, we can downcast to this type here. pub fn target(&self) -> El { let evt: &web_sys::Event = self.raw.as_ref(); evt.target().unwrap_throw().dyn_into().unwrap_throw() @@ -155,7 +164,7 @@ impl Deref for Event { pub trait Action {} /// Trait that allows callbacks to be polymorphic on return type -/// (`Action`, `Option` or `()`) +/// (`Action`, `Option` or `()`). An implementation detail. pub trait OptionalAction: sealed::Sealed { fn action(self) -> Option; } diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 828e9adbc..6a6614a10 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -10,13 +10,20 @@ use xilem_core::{Id, MessageResult}; use crate::{context::Cx, ChangeFlags}; +mod sealed { + pub trait Sealed {} +} + // A possible refinement of xilem_core is to allow a single concrete type // for a view element, rather than an associated type with a bound. -pub trait DomNode { +/// This trait is implemented for types that implement `AsRef`. +/// It is an implementation detail. +pub trait DomNode: sealed::Sealed { fn into_pod(self) -> Pod; fn as_node_ref(&self) -> &web_sys::Node; } +impl + 'static> sealed::Sealed for N {} impl + 'static> DomNode for N { fn into_pod(self) -> Pod { Pod(Box::new(self)) @@ -27,6 +34,8 @@ impl + 'static> DomNode for N { } } +/// This trait is implemented for types that implement `AsRef`. +/// It is an implementation detail. pub trait DomElement: DomNode { fn as_element_ref(&self) -> &web_sys::Element; } @@ -37,7 +46,9 @@ impl> DomElement for N { } } -pub trait AnyNode { +/// A trait for types that can be type-erased and impl `AsRef`. It is an +/// implementation detail. +pub trait AnyNode: sealed::Sealed { fn as_any_mut(&mut self) -> &mut dyn Any; fn as_node_ref(&self) -> &web_sys::Node; @@ -53,6 +64,7 @@ impl + Any> AnyNode for N { } } +impl sealed::Sealed for Box {} impl DomNode for Box { fn into_pod(self) -> Pod { Pod(self) diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs index 7e07190e1..7164a0626 100644 --- a/crates/xilem_html/src/view_ext.rs +++ b/crates/xilem_html/src/view_ext.rs @@ -5,7 +5,10 @@ use std::borrow::Cow; use crate::{class::Class, event::OptionalAction, events as e, view::View, Event}; +/// A trait that makes it possible to attach event listeners and more to views +/// in the continuation style. pub trait ViewExt: View + Sized { + /// Add an `onclick` event listener. fn on_click< OA: OptionalAction, F: Fn(&mut T, &Event) -> OA, @@ -13,6 +16,7 @@ pub trait ViewExt: View + Sized { self, f: F, ) -> e::OnClick; + /// Add an `ondblclick` event listener. fn on_dblclick< OA: OptionalAction, F: Fn(&mut T, &Event) -> OA, @@ -20,6 +24,7 @@ pub trait ViewExt: View + Sized { self, f: F, ) -> e::OnDblClick; + /// Add an `oninput` event listener. fn on_input< OA: OptionalAction, F: Fn(&mut T, &Event) -> OA, @@ -27,6 +32,7 @@ pub trait ViewExt: View + Sized { self, f: F, ) -> e::OnInput; + /// Add an `onkeydown` event listener. fn on_keydown< OA: OptionalAction, F: Fn(&mut T, &Event) -> OA, @@ -34,6 +40,7 @@ pub trait ViewExt: View + Sized { self, f: F, ) -> e::OnKeyDown; + /// Apply a CSS class to the child view. fn class(self, class: impl Into>) -> Class { crate::class::class(self, class) } From 5b29458aaafe419825e7e416f8d3340fe17acee7 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 17:08:17 +0100 Subject: [PATCH 07/12] Update readme --- crates/xilem_html/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/xilem_html/README.md b/crates/xilem_html/README.md index 2fb1fbf66..2eaa5eab1 100644 --- a/crates/xilem_html/README.md +++ b/crates/xilem_html/README.md @@ -1,6 +1,8 @@ -# Xilemsvg prototype +# `xilem_html` prototype -This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in. +This is an early prototype of a potential implementation of the Xilem architecture using HTML elements +as Xilem elements (unfortunately the two concepts have the same name). This uses xilem_core under the hood, +and offers a proof that it can be used outside of `xilem` proper. The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`). From 3501382f26845699423c315d0425c658010689aa Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 2 Jul 2023 17:12:54 +0100 Subject: [PATCH 08/12] Include attribution for todomvc assets --- crates/xilem_html/web_examples/todomvc/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 crates/xilem_html/web_examples/todomvc/README.md diff --git a/crates/xilem_html/web_examples/todomvc/README.md b/crates/xilem_html/web_examples/todomvc/README.md new file mode 100644 index 000000000..7303c0c2e --- /dev/null +++ b/crates/xilem_html/web_examples/todomvc/README.md @@ -0,0 +1,11 @@ +This crate uses parts of the TodoMVC website, used under the MIT license. This license is reproduced below. + +# License for TodoMVC assets + +Copyright (c) Addy Osmani, Sindre Sorhus, Pascal Hartig, Stephen Sawchuk. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 9c8dc149f1670c30154a2d17cc1f44e05f92a76c Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 5 Jul 2023 11:20:57 +0100 Subject: [PATCH 09/12] Add escape hatch for manipulating DOM during render cycle. Also simplify element macro --- crates/xilem_html/src/element/elements.rs | 241 +++++++++--------- crates/xilem_html/src/element/mod.rs | 73 ++++-- crates/xilem_html/src/event/mod.rs | 3 +- crates/xilem_html/src/lib.rs | 2 +- crates/xilem_html/src/view.rs | 113 ++++++++ .../web_examples/todomvc/src/lib.rs | 28 +- .../web_examples/todomvc/src/state.rs | 2 + 7 files changed, 297 insertions(+), 165 deletions(-) diff --git a/crates/xilem_html/src/element/elements.rs b/crates/xilem_html/src/element/elements.rs index 0c8dad54a..ebc385bed 100644 --- a/crates/xilem_html/src/element/elements.rs +++ b/crates/xilem_html/src/element/elements.rs @@ -2,24 +2,24 @@ //! macro_rules! elements { () => {}; - (($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => { - element!($ty_name, $builder_name, $name, $web_sys_ty); + (($ty_name:ident, $name:ident, $web_sys_ty:ty), $($rest:tt)*) => { + element!($ty_name, $name, $web_sys_ty); elements!($($rest)*); }; } macro_rules! element { - ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { + ($ty_name:ident, $name:ident, $web_sys_ty:ty) => { /// A view representing a - #[doc = concat!("`", $name, "`")] + #[doc = concat!("`", stringify!($name), "`")] /// element. pub struct $ty_name(crate::Element<$web_sys_ty, ViewSeq>); /// Builder function for a - #[doc = concat!("`", $name, "`")] + #[doc = concat!("`", stringify!($name), "`")] /// view. - pub fn $builder_name(children: ViewSeq) -> $ty_name { - $ty_name(crate::element($name, children)) + pub fn $name(children: ViewSeq) -> $ty_name { + $ty_name(crate::element(stringify!($name), children)) } impl $ty_name { @@ -52,6 +52,11 @@ macro_rules! element { self.0.set_attr(name, value); self } + + pub fn after_update(mut self, after_update: impl Fn(&$web_sys_ty) + 'static) -> Self { + self.0 = self.0.after_update(after_update); + self + } } impl crate::view::ViewMarker for $ty_name {} @@ -102,131 +107,121 @@ elements!( // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends // content sectioning - (Address, address, "address", web_sys::HtmlElement), - (Article, article, "article", web_sys::HtmlElement), - (Aside, aside, "aside", web_sys::HtmlElement), - (Footer, footer, "footer", web_sys::HtmlElement), - (Header, header, "header", web_sys::HtmlElement), - (H1, h1, "h1", web_sys::HtmlHeadingElement), - (H2, h2, "h2", web_sys::HtmlHeadingElement), - (H3, h3, "h3", web_sys::HtmlHeadingElement), - (H4, h4, "h4", web_sys::HtmlHeadingElement), - (H5, h5, "h5", web_sys::HtmlHeadingElement), - (H6, h6, "h6", web_sys::HtmlHeadingElement), - (Hgroup, hgroup, "hgroup", web_sys::HtmlElement), - (Main, main, "main", web_sys::HtmlElement), - (Nav, nav, "nav", web_sys::HtmlElement), - (Section, section, "section", web_sys::HtmlElement), + (Address, address, web_sys::HtmlElement), + (Article, article, web_sys::HtmlElement), + (Aside, aside, web_sys::HtmlElement), + (Footer, footer, web_sys::HtmlElement), + (Header, header, web_sys::HtmlElement), + (H1, h1, web_sys::HtmlHeadingElement), + (H2, h2, web_sys::HtmlHeadingElement), + (H3, h3, web_sys::HtmlHeadingElement), + (H4, h4, web_sys::HtmlHeadingElement), + (H5, h5, web_sys::HtmlHeadingElement), + (H6, h6, web_sys::HtmlHeadingElement), + (Hgroup, hgroup, web_sys::HtmlElement), + (Main, main, web_sys::HtmlElement), + (Nav, nav, web_sys::HtmlElement), + (Section, section, web_sys::HtmlElement), // text content - ( - Blockquote, - blockquote, - "blockquote", - web_sys::HtmlQuoteElement - ), - (Dd, dd, "dd", web_sys::HtmlElement), - (Div, div, "div", web_sys::HtmlDivElement), - (Dl, dl, "dl", web_sys::HtmlDListElement), - (Dt, dt, "dt", web_sys::HtmlElement), - (Figcaption, figcaption, "figcaption", web_sys::HtmlElement), - (Figure, figure, "figure", web_sys::HtmlElement), - (Hr, hr, "hr", web_sys::HtmlHrElement), - (Li, li, "li", web_sys::HtmlLiElement), - (Menu, menu, "menu", web_sys::HtmlMenuElement), - (Ol, ol, "ol", web_sys::HtmlOListElement), - (P, p, "p", web_sys::HtmlParagraphElement), - (Pre, pre, "pre", web_sys::HtmlPreElement), - (Ul, ul, "ul", web_sys::HtmlUListElement), + (Blockquote, blockquote, web_sys::HtmlQuoteElement), + (Dd, dd, web_sys::HtmlElement), + (Div, div, web_sys::HtmlDivElement), + (Dl, dl, web_sys::HtmlDListElement), + (Dt, dt, web_sys::HtmlElement), + (Figcaption, figcaption, web_sys::HtmlElement), + (Figure, figure, web_sys::HtmlElement), + (Hr, hr, web_sys::HtmlHrElement), + (Li, li, web_sys::HtmlLiElement), + (Menu, menu, web_sys::HtmlMenuElement), + (Ol, ol, web_sys::HtmlOListElement), + (P, p, web_sys::HtmlParagraphElement), + (Pre, pre, web_sys::HtmlPreElement), + (Ul, ul, web_sys::HtmlUListElement), // inline text - (A, a, "a", web_sys::HtmlAnchorElement), - (Abbr, abbr, "abbr", web_sys::HtmlElement), - (B, b, "b", web_sys::HtmlElement), - (Bdi, bdi, "bdi", web_sys::HtmlElement), - (Bdo, bdo, "bdo", web_sys::HtmlElement), - (Br, br, "br", web_sys::HtmlBrElement), - (Cite, cite, "cite", web_sys::HtmlElement), - (Code, code, "code", web_sys::HtmlElement), - (Data, data, "data", web_sys::HtmlDataElement), - (Dfn, dfn, "dfn", web_sys::HtmlElement), - (Em, em, "em", web_sys::HtmlElement), - (I, i, "i", web_sys::HtmlElement), - (Kbd, kbd, "kbd", web_sys::HtmlElement), - (Mark, mark, "mark", web_sys::HtmlElement), - (Q, q, "q", web_sys::HtmlQuoteElement), - (Rp, rp, "rp", web_sys::HtmlElement), - (Rt, rt, "rt", web_sys::HtmlElement), - (Ruby, ruby, "ruby", web_sys::HtmlElement), - (S, s, "s", web_sys::HtmlElement), - (Samp, samp, "samp", web_sys::HtmlElement), - (Small, small, "small", web_sys::HtmlElement), - (Span, span, "span", web_sys::HtmlSpanElement), - (Strong, strong, "strong", web_sys::HtmlElement), - (Sub, sub, "sub", web_sys::HtmlElement), - (Sup, sup, "sup", web_sys::HtmlElement), - (Time, time, "time", web_sys::HtmlTimeElement), - (U, u, "u", web_sys::HtmlElement), - (Var, var, "var", web_sys::HtmlElement), - (Wbr, wbr, "wbr", web_sys::HtmlElement), + (A, a, web_sys::HtmlAnchorElement), + (Abbr, abbr, web_sys::HtmlElement), + (B, b, web_sys::HtmlElement), + (Bdi, bdi, web_sys::HtmlElement), + (Bdo, bdo, web_sys::HtmlElement), + (Br, br, web_sys::HtmlBrElement), + (Cite, cite, web_sys::HtmlElement), + (Code, code, web_sys::HtmlElement), + (Data, data, web_sys::HtmlDataElement), + (Dfn, dfn, web_sys::HtmlElement), + (Em, em, web_sys::HtmlElement), + (I, i, web_sys::HtmlElement), + (Kbd, kbd, web_sys::HtmlElement), + (Mark, mark, web_sys::HtmlElement), + (Q, q, web_sys::HtmlQuoteElement), + (Rp, rp, web_sys::HtmlElement), + (Rt, rt, web_sys::HtmlElement), + (Ruby, ruby, web_sys::HtmlElement), + (S, s, web_sys::HtmlElement), + (Samp, samp, web_sys::HtmlElement), + (Small, small, web_sys::HtmlElement), + (Span, span, web_sys::HtmlSpanElement), + (Strong, strong, web_sys::HtmlElement), + (Sub, sub, web_sys::HtmlElement), + (Sup, sup, web_sys::HtmlElement), + (Time, time, web_sys::HtmlTimeElement), + (U, u, web_sys::HtmlElement), + (Var, var, web_sys::HtmlElement), + (Wbr, wbr, web_sys::HtmlElement), // image and multimedia - (Area, area, "area", web_sys::HtmlAreaElement), - (Audio, audio, "audio", web_sys::HtmlAudioElement), - (Img, img, "img", web_sys::HtmlImageElement), - (Map, map, "map", web_sys::HtmlMapElement), - (Track, track, "track", web_sys::HtmlTrackElement), - (Video, video, "video", web_sys::HtmlVideoElement), + (Area, area, web_sys::HtmlAreaElement), + (Audio, audio, web_sys::HtmlAudioElement), + (Img, img, web_sys::HtmlImageElement), + (Map, map, web_sys::HtmlMapElement), + (Track, track, web_sys::HtmlTrackElement), + (Video, video, web_sys::HtmlVideoElement), // embedded content - (Embed, embed, "embed", web_sys::HtmlEmbedElement), - (Iframe, iframe, "iframe", web_sys::HtmlIFrameElement), - (Object, object, "object", web_sys::HtmlObjectElement), - (Picture, picture, "picture", web_sys::HtmlPictureElement), - (Portal, portal, "portal", web_sys::HtmlElement), - (Source, source, "source", web_sys::HtmlSourceElement), + (Embed, embed, web_sys::HtmlEmbedElement), + (Iframe, iframe, web_sys::HtmlIFrameElement), + (Object, object, web_sys::HtmlObjectElement), + (Picture, picture, web_sys::HtmlPictureElement), + (Portal, portal, web_sys::HtmlElement), + (Source, source, web_sys::HtmlSourceElement), // SVG and MathML (TODO, svg and mathml elements) - (Svg, svg, "svg", web_sys::HtmlElement), - (Math, math, "math", web_sys::HtmlElement), + (Svg, svg, web_sys::HtmlElement), + (Math, math, web_sys::HtmlElement), // scripting - (Canvas, canvas, "canvas", web_sys::HtmlCanvasElement), - (Noscript, noscript, "noscript", web_sys::HtmlElement), - (Script, script, "script", web_sys::HtmlScriptElement), + (Canvas, canvas, web_sys::HtmlCanvasElement), + (Noscript, noscript, web_sys::HtmlElement), + (Script, script, web_sys::HtmlScriptElement), // demarcating edits - (Del, del, "del", web_sys::HtmlModElement), - (Ins, ins, "ins", web_sys::HtmlModElement), + (Del, del, web_sys::HtmlModElement), + (Ins, ins, web_sys::HtmlModElement), // tables - ( - Caption, - caption, - "caption", - web_sys::HtmlTableCaptionElement - ), - (Col, col, "col", web_sys::HtmlTableColElement), - (Colgroup, colgroup, "colgroup", web_sys::HtmlTableColElement), - (Table, table, "table", web_sys::HtmlTableSectionElement), - (Tbody, tbody, "tbody", web_sys::HtmlTableSectionElement), - (Td, td, "td", web_sys::HtmlTableCellElement), - (Tfoot, tfoot, "tfoot", web_sys::HtmlTableSectionElement), - (Th, th, "th", web_sys::HtmlTableCellElement), - (Thead, thead, "thead", web_sys::HtmlTableSectionElement), - (Tr, tr, "tr", web_sys::HtmlTableRowElement), + (Caption, caption, web_sys::HtmlTableCaptionElement), + (Col, col, web_sys::HtmlTableColElement), + (Colgroup, colgroup, web_sys::HtmlTableColElement), + (Table, table, web_sys::HtmlTableSectionElement), + (Tbody, tbody, web_sys::HtmlTableSectionElement), + (Td, td, web_sys::HtmlTableCellElement), + (Tfoot, tfoot, web_sys::HtmlTableSectionElement), + (Th, th, web_sys::HtmlTableCellElement), + (Thead, thead, web_sys::HtmlTableSectionElement), + (Tr, tr, web_sys::HtmlTableRowElement), // forms - (Button, button, "button", web_sys::HtmlButtonElement), - (Datalist, datalist, "datalist", web_sys::HtmlDataListElement), - (Fieldset, fieldset, "fieldset", web_sys::HtmlFieldSetElement), - (Form, form, "form", web_sys::HtmlFormElement), - (Input, input, "input", web_sys::HtmlInputElement), - (Label, label, "label", web_sys::HtmlLabelElement), - (Legend, legend, "legend", web_sys::HtmlLegendElement), - (Meter, meter, "meter", web_sys::HtmlMeterElement), - (Optgroup, optgroup, "optgroup", web_sys::HtmlOptGroupElement), - (Option, option, "option", web_sys::HtmlOptionElement), - (Output, output, "output", web_sys::HtmlOutputElement), - (Progress, progress, "progress", web_sys::HtmlProgressElement), - (Select, select, "select", web_sys::HtmlSelectElement), - (Textarea, textarea, "textarea", web_sys::HtmlTextAreaElement), + (Button, button, web_sys::HtmlButtonElement), + (Datalist, datalist, web_sys::HtmlDataListElement), + (Fieldset, fieldset, web_sys::HtmlFieldSetElement), + (Form, form, web_sys::HtmlFormElement), + (Input, input, web_sys::HtmlInputElement), + (Label, label, web_sys::HtmlLabelElement), + (Legend, legend, web_sys::HtmlLegendElement), + (Meter, meter, web_sys::HtmlMeterElement), + (Optgroup, optgroup, web_sys::HtmlOptGroupElement), + (Option, option, web_sys::HtmlOptionElement), + (Output, output, web_sys::HtmlOutputElement), + (Progress, progress, web_sys::HtmlProgressElement), + (Select, select, web_sys::HtmlSelectElement), + (Textarea, textarea, web_sys::HtmlTextAreaElement), // interactive elements, - (Details, details, "details", web_sys::HtmlDetailsElement), - (Dialog, dialog, "dialog", web_sys::HtmlDialogElement), - (Summary, summary, "summary", web_sys::HtmlElement), + (Details, details, web_sys::HtmlDetailsElement), + (Dialog, dialog, web_sys::HtmlDialogElement), + (Summary, summary, web_sys::HtmlElement), // web components, - (Slot, slot, "slot", web_sys::HtmlSlotElement), - (Template, template, "template", web_sys::HtmlTemplateElement), + (Slot, slot, web_sys::HtmlSlotElement), + (Template, template, web_sys::HtmlTemplateElement), ); diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index 7b30ccf10..dff2c0046 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -7,13 +7,17 @@ use crate::{ view::{DomElement, Pod, View, ViewMarker, ViewSequence}, }; -use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt, marker::PhantomData}; +use std::{borrow::Cow, cell::RefCell, cmp::Ordering, collections::BTreeMap, fmt}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult, VecSplice}; #[cfg(feature = "typed")] pub mod elements; +thread_local! { + static SCRATCH: RefCell> = RefCell::new(Vec::new()); +} + /// A view representing a HTML element. /// /// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). @@ -21,13 +25,13 @@ pub struct Element { name: Cow<'static, str>, attributes: BTreeMap, Cow<'static, str>>, children: Children, - ty: PhantomData, + after_update: Option>, } -impl Element { +impl Element { pub fn debug_as_el(&self) -> impl fmt::Debug + '_ { - struct DebugFmt<'a, E, VS>(&'a Element); - impl<'a, E, VS> fmt::Debug for DebugFmt<'a, E, VS> { + struct DebugFmt<'a, El, VS>(&'a Element); + impl<'a, El, VS> fmt::Debug for DebugFmt<'a, El, VS> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "<{}", self.0.name)?; for (name, value) in &self.0.attributes { @@ -51,19 +55,19 @@ pub struct ElementState { /// Create a new element view /// /// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). -pub fn element( +pub fn element( name: impl Into>, children: ViewSeq, -) -> Element { +) -> Element { Element { name: name.into(), attributes: BTreeMap::new(), children, - ty: PhantomData, + after_update: None, } } -impl Element { +impl Element { /// Set an attribute on this element. /// /// # Panics @@ -92,6 +96,20 @@ impl Element { ) { self.attributes.insert(name.into(), value.into()); } + + /// Set a function to run after the new view tree has been created. + /// + /// This offers functionality similar to `ref` in React. + /// + /// # Rules for correct use + /// + /// It is important that the structure of the DOM tree is *not* modified using this function. + /// If the DOM tree is modified, then future reconciliation will have undefined and possibly + /// suprising results. + pub fn after_update(mut self, after_update: impl Fn(&El) + 'static) -> Self { + self.after_update = Some(Box::new(after_update)); + self + } } impl ViewMarker for Element {} @@ -99,27 +117,30 @@ impl ViewMarker for Element {} impl View for Element where Children: ViewSequence, - // In addition, the `E` parameter is expected to be a child of `web_sys::Node` El: JsCast + DomElement, { type State = ElementState; type Element = El; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, El) { + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let el = cx.create_html_element(&self.name); for (name, value) in &self.attributes { - el.set_attribute(name, value).unwrap(); + el.set_attribute(name, value).unwrap_throw(); } let mut child_elements = vec![]; let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); for child in &child_elements { - el.append_child(child.0.as_node_ref()).unwrap(); + el.append_child(child.0.as_node_ref()).unwrap_throw(); + } + let el = el.dyn_into().unwrap_throw(); + if let Some(after_update) = &self.after_update { + (after_update)(&el); } let state = ElementState { child_states, child_elements, }; - (id, state, el.dyn_into().unwrap_throw()) + (id, state, el) } fn rebuild( @@ -128,7 +149,7 @@ where prev: &Self, id: &mut Id, state: &mut Self::State, - element: &mut El, + element: &mut Self::Element, ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); // update tag name @@ -138,7 +159,7 @@ where .as_element_ref() .parent_element() .expect_throw("this element was mounted and so should have a parent"); - parent.remove_child(element.as_node_ref()).unwrap(); + parent.remove_child(element.as_node_ref()).unwrap_throw(); let new_element = cx.create_html_element(&self.name); // TODO could this be combined with child updates? while element.as_element_ref().child_element_count() > 0 { @@ -195,24 +216,30 @@ where // update children // TODO avoid reallocation every render? - let mut scratch = vec![]; - let mut splice = VecSplice::new(&mut state.child_elements, &mut scratch); - changed |= cx.with_id(*id, |cx| { - self.children - .rebuild(cx, &prev.children, &mut state.child_states, &mut splice) + SCRATCH.with(|scratch| { + let mut scratch = scratch.borrow_mut(); + let mut splice = VecSplice::new(&mut state.child_elements, &mut *scratch); + changed |= cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, &mut state.child_states, &mut splice) + }); }); if changed.contains(ChangeFlags::STRUCTURE) { // This is crude and will result in more DOM traffic than needed. // The right thing to do is diff the new state of the children id // vector against the old, and derive DOM mutations from that. while let Some(child) = element.first_child() { - element.remove_child(&child).unwrap(); + element.remove_child(&child).unwrap_throw(); } for child in &state.child_elements { - element.append_child(child.0.as_node_ref()).unwrap(); + element.append_child(child.0.as_node_ref()).unwrap_throw(); } changed.remove(ChangeFlags::STRUCTURE); } + if let Some(after_update) = &self.after_update { + (after_update)(element.dyn_ref().unwrap_throw()); + changed |= ChangeFlags::OTHER_CHANGE; + } changed } diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs index 01207e163..616a83570 100644 --- a/crates/xilem_html/src/event/mod.rs +++ b/crates/xilem_html/src/event/mod.rs @@ -60,8 +60,7 @@ where element.as_node_ref(), self.event, move |event: &web_sys::Event| { - let event = (*event).clone(); - let event = event.dyn_into::().unwrap_throw(); + let event = (*event).clone().dyn_into::().unwrap_throw(); let event: Event = Event::new(event); thunk.push_message(EventMsg { event }); }, diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 8dd60c51e..646cc8448 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -29,7 +29,7 @@ pub use element::{element, Element, ElementState}; #[cfg(feature = "typed")] pub use event::events; pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction}; -pub use view::{Adapt, AdaptThunk, Pod, View, ViewMarker, ViewSequence}; +pub use view::{Adapt, AdaptThunk, Either, Pod, View, ViewMarker, ViewSequence}; #[cfg(feature = "typed")] pub use view_ext::ViewExt; diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 6a6614a10..65ffcdfd9 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -6,6 +6,7 @@ use std::{any::Any, borrow::Cow, ops::Deref}; +use wasm_bindgen::throw_str; use xilem_core::{Id, MessageResult}; use crate::{context::Cx, ChangeFlags}; @@ -100,6 +101,118 @@ xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyNode} +/// This view container can switch between two views. +/// +/// It is a statically-typed alternative to the type-erased `AnyView`. +pub enum Either { + Left(T1), + Right(T2), +} + +impl AsRef for Either +where + E1: AsRef, + E2: AsRef, +{ + fn as_ref(&self) -> &web_sys::Node { + match self { + Either::Left(view) => view.as_ref(), + Either::Right(view) => view.as_ref(), + } + } +} + +impl View for Either +where + V1: View, + V2: View, + V1::Element: AsRef + 'static, + V2::Element: AsRef + 'static, +{ + type State = Either; + type Element = Either; + + fn build(&self, cx: &mut Cx) -> (xilem_core::Id, Self::State, Self::Element) { + match self { + Either::Left(view) => { + let (id, state, el) = view.build(cx); + (id, Either::Left(state), Either::Left(el)) + } + Either::Right(view) => { + let (id, state, el) = view.build(cx); + (id, Either::Right(state), Either::Right(el)) + } + } + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut xilem_core::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut change_flags = ChangeFlags::empty(); + match (prev, self) { + (Either::Left(_), Either::Right(view)) => { + let (new_id, new_state, new_element) = view.build(cx); + *id = new_id; + *state = Either::Right(new_state); + *element = Either::Right(new_element); + change_flags |= ChangeFlags::STRUCTURE; + } + (Either::Right(_), Either::Left(view)) => { + let (new_id, new_state, new_element) = view.build(cx); + *id = new_id; + *state = Either::Left(new_state); + *element = Either::Left(new_element); + change_flags |= ChangeFlags::STRUCTURE; + } + (Either::Left(prev_view), Either::Left(view)) => { + let (Either::Left(state), Either::Left(element)) = (state, element) else { + throw_str("invalid state/view in Either (unreachable)"); + }; + // Cannot do mutable casting, so take ownership of state. + change_flags |= view.rebuild(cx, prev_view, id, state, element); + } + (Either::Right(prev_view), Either::Right(view)) => { + let (Either::Right(state), Either::Right(element)) = (state, element) else { + throw_str("invalid state/view in Either (unreachable)"); + }; + // Cannot do mutable casting, so take ownership of state. + change_flags |= view.rebuild(cx, prev_view, id, state, element); + } + } + change_flags + } + + fn message( + &self, + id_path: &[xilem_core::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> xilem_core::MessageResult { + match self { + Either::Left(view) => { + let Either::Left(state) = state else { + throw_str("invalid state/view in Either (unreachable)"); + }; + view.message(id_path, state, message, app_state) + } + Either::Right(view) => { + let Either::Right(state) = state else { + throw_str("invalid state/view in Either (unreachable)"); + }; + view.message(id_path, state, message, app_state) + } + } + } +} + +// strings -> text nodes + impl ViewMarker for &'static str {} impl View for &'static str { type State = (); diff --git a/crates/xilem_html/web_examples/todomvc/src/lib.rs b/crates/xilem_html/web_examples/todomvc/src/lib.rs index d8b5a3dff..49e262ae6 100644 --- a/crates/xilem_html/web_examples/todomvc/src/lib.rs +++ b/crates/xilem_html/web_examples/todomvc/src/lib.rs @@ -6,8 +6,8 @@ use state::{AppState, Filter, Todo}; use wasm_bindgen::{prelude::*, JsValue}; use xilem_html::{ - elements as el, events::on_click, get_element_by_id, Action, Adapt, App, Event, MessageResult, - View, ViewExt, ViewMarker, + elements as el, events::on_click, get_element_by_id, Action, Adapt, App, MessageResult, View, + ViewExt, ViewMarker, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -170,28 +170,24 @@ fn app_logic(state: &mut AppState) -> impl View { log::debug!("render: {state:?}"); let main = (!state.todos.is_empty()).then(|| main_view(state)); let footer = (!state.todos.is_empty()).then(|| footer_view(state)); + let input = el::input(()) + .attr("class", "new-todo") + .attr("placeholder", "What needs to be done?") + .attr("value", state.new_todo.clone()) + .attr("autofocus", "true"); el::div(( el::header(( el::h1("TODOs"), - el::input(()) - .attr("class", "new-todo") - .attr("placeholder", "What needs to be done?") - .attr("value", state.new_todo.clone()) - .attr("autofocus", "true") - .on_keydown( - |state: &mut AppState, evt| { + input + .on_keydown(|state: &mut AppState, evt| { if evt.key() == "Enter" { state.create_todo(); } - }, - ) - .on_input( - |state: &mut AppState, - evt: &Event| { + }) + .on_input(|state: &mut AppState, evt| { state.update_new_todo(&evt.target().value()); evt.prevent_default(); - }, - ), + }), )) .attr("class", "header"), main, diff --git a/crates/xilem_html/web_examples/todomvc/src/state.rs b/crates/xilem_html/web_examples/todomvc/src/state.rs index 8b2d4e6f1..2b5bdaf5d 100644 --- a/crates/xilem_html/web_examples/todomvc/src/state.rs +++ b/crates/xilem_html/web_examples/todomvc/src/state.rs @@ -11,6 +11,7 @@ pub struct AppState { pub todos: Vec, pub filter: Filter, pub editing_id: Option, + pub focus_new_todo: bool, } impl AppState { @@ -21,6 +22,7 @@ impl AppState { let title = self.new_todo.trim().to_string(); self.new_todo.clear(); self.todos.push(Todo::new(title)); + self.focus_new_todo = true; } pub fn visible_todos(&mut self) -> impl Iterator { From 357472ddb2629a446ae33b29bde26b4898fdea5f Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 5 Jul 2023 11:46:58 +0100 Subject: [PATCH 10/12] Implement review suggestions --- crates/xilem_core/src/view.rs | 134 +-------------- crates/xilem_core/src/view/adapt.rs | 162 ++++++++++++++++++ crates/xilem_html/src/element/mod.rs | 5 +- crates/xilem_html/src/view.rs | 3 +- .../web_examples/todomvc/src/state.rs | 9 +- 5 files changed, 170 insertions(+), 143 deletions(-) create mode 100644 crates/xilem_core/src/view/adapt.rs diff --git a/crates/xilem_core/src/view.rs b/crates/xilem_core/src/view.rs index 7a249be5d..53115747f 100644 --- a/crates/xilem_core/src/view.rs +++ b/crates/xilem_core/src/view.rs @@ -1,6 +1,8 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 +mod adapt; + /// Create the `View` trait for a particular xilem context (e.g. html, native, ...). /// /// Arguments are @@ -67,137 +69,5 @@ macro_rules! generate_view_trait { app_state: &mut T, ) -> $crate::MessageResult; } - - /// A view that wraps a child view and modifies the state that callbacks have access to. - /// - /// # Examples - /// - /// Suppose you have an outer type that looks like - /// - /// ``` - /// struct State { - /// todos: Vec - /// } - /// ``` - /// - /// and an inner type/view that looks like - /// - /// ```ignore - /// struct Todo { - /// label: String - /// } - /// - /// struct TodoView { - /// label: String - /// } - /// - /// enum TodoAction { - /// Delete - /// } - /// - /// impl View for TodoView { - /// // ... - /// } - /// ``` - /// - /// then your top-level action (`()`) and state type (`State`) don't match `TodoView`'s. - /// You can use the `Adapt` view to mediate between them: - /// - /// ```ignore - /// state - /// .todos - /// .enumerate() - /// .map(|(idx, todo)| { - /// Adapt::new( - /// move |data: &mut AppState, thunk| { - /// if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { - /// match action { - /// TodoAction::Delete => data.todos.remove(idx), - /// } - /// } - /// MessageResult::Nop - /// }, - /// TodoView { label: todo.label } - /// ) - /// }) - /// ``` - pub struct Adapt) -> $crate::MessageResult, V: View> { - f: F, - child: V, - phantom: std::marker::PhantomData (OutData, OutMsg, InData, InMsg)>, - } - - /// A "thunk" which dispatches an message to an adapt node's child. - /// - /// The closure passed to [`Adapt`][crate::Adapt] should call this thunk with the child's - /// app state. - pub struct AdaptThunk<'a, InData, InMsg, V: View> { - child: &'a V, - state: &'a mut V::State, - id_path: &'a [$crate::Id], - message: Box, - } - - impl) -> $crate::MessageResult, V: View> - Adapt - { - pub fn new(f: F, child: V) -> Self { - Adapt { - f, - child, - phantom: Default::default(), - } - } - } - - impl<'a, InData, InMsg, V: View> AdaptThunk<'a, InData, InMsg, V> { - pub fn call(self, app_state: &mut InData) -> $crate::MessageResult { - self.child - .message(self.id_path, self.state, self.message, app_state) - } - } - - impl) -> $crate::MessageResult + Send, V: View> - View for Adapt - { - type State = V::State; - - type Element = V::Element; - - fn build(&self, cx: &mut Cx) -> ($crate::Id, Self::State, Self::Element) { - self.child.build(cx) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - self.child.rebuild(cx, &prev.child, id, state, element) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut OutData, - ) -> $crate::MessageResult { - let thunk = AdaptThunk { - child: &self.child, - state, - id_path, - message, - }; - (self.f)(app_state, thunk) - } - } - - impl) -> $crate::MessageResult, V: View> - ViewMarker for Adapt {} - }; } diff --git a/crates/xilem_core/src/view/adapt.rs b/crates/xilem_core/src/view/adapt.rs new file mode 100644 index 000000000..27ae79cc4 --- /dev/null +++ b/crates/xilem_core/src/view/adapt.rs @@ -0,0 +1,162 @@ +#[macro_export] +macro_rules! impl_adapt_view { + ($viewtrait:ident, $cx:ty, $changeflags:ty) => { + /// A view that wraps a child view and modifies the state that callbacks have access to. + /// + /// # Examples + /// + /// Suppose you have an outer type that looks like + /// + /// ```ignore + /// struct State { + /// todos: Vec + /// } + /// ``` + /// + /// and an inner type/view that looks like + /// + /// ```ignore + /// struct Todo { + /// label: String + /// } + /// + /// struct TodoView { + /// label: String + /// } + /// + /// enum TodoAction { + /// Delete + /// } + /// + /// impl View for TodoView { + /// // ... + /// } + /// ``` + /// + /// then your top-level action (`()`) and state type (`State`) don't match `TodoView`'s. + /// You can use the `Adapt` view to mediate between them: + /// + /// ```ignore + /// state + /// .todos + /// .enumerate() + /// .map(|(idx, todo)| { + /// Adapt::new( + /// move |data: &mut AppState, thunk| { + /// if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { + /// match action { + /// TodoAction::Delete => data.todos.remove(idx), + /// } + /// } + /// MessageResult::Nop + /// }, + /// TodoView { label: todo.label } + /// ) + /// }) + /// ``` + pub struct Adapt< + ParentT, + ParentA, + ChildT, + ChildA, + F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult, + V: $viewtrait, + > { + f: F, + child: V, + phantom: std::marker::PhantomData (ParentT, ParentA, ChildT, ChildA)>, + } + + /// A "thunk" which dispatches an message to an adapt node's child. + /// + /// The closure passed to [`Adapt`][crate::Adapt] should call this thunk with the child's + /// app state. + pub struct AdaptThunk<'a, ChildT, ChildA, V: $viewtrait> { + child: &'a V, + state: &'a mut V::State, + id_path: &'a [$crate::Id], + message: Box, + } + + impl< + ParentT, + ParentA, + ChildT, + ChildA, + F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult, + V: $viewtrait, + > Adapt + { + pub fn new(f: F, child: V) -> Self { + Adapt { + f, + child, + phantom: Default::default(), + } + } + } + + impl<'a, ChildT, ChildA, V: $viewtrait> AdaptThunk<'a, ChildT, ChildA, V> { + pub fn call(self, app_state: &mut ChildT) -> $crate::MessageResult { + self.child + .message(self.id_path, self.state, self.message, app_state) + } + } + + impl< + ParentT, + ParentA, + ChildT, + ChildA, + F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult + Send, + V: $viewtrait, + > $viewtrait for Adapt + { + type State = V::State; + + type Element = V::Element; + + fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { + self.child.build(cx) + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + id: &mut $crate::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> $changeflags { + self.child.rebuild(cx, &prev.child, id, state, element) + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut ParentT, + ) -> $crate::MessageResult { + let thunk = AdaptThunk { + child: &self.child, + state, + id_path, + message, + }; + (self.f)(app_state, thunk) + } + } + + impl< + ParentT, + ParentA, + ChildT, + ChildA, + F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult, + V: $viewtrait, + > ViewMarker for Adapt + { + } + }; +} diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index dff2c0046..c71e55896 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -20,7 +20,7 @@ thread_local! { /// A view representing a HTML element. /// -/// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). +/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). pub struct Element { name: Cow<'static, str>, attributes: BTreeMap, Cow<'static, str>>, @@ -54,7 +54,7 @@ pub struct ElementState { /// Create a new element view /// -/// If the element has no chilcdren, use the unit type (e.g. `let view = element("div", ())`). +/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). pub fn element( name: impl Into>, children: ViewSeq, @@ -215,7 +215,6 @@ where } // update children - // TODO avoid reallocation every render? SCRATCH.with(|scratch| { let mut scratch = scratch.borrow_mut(); let mut splice = VecSplice::new(&mut state.child_elements, &mut *scratch); diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 65ffcdfd9..ccbf2f308 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -88,7 +88,7 @@ impl Pod { node.into_pod() } - fn downcast_mut<'a, T: 'static>(&'a mut self) -> Option<&'a mut T> { + fn downcast_mut(&mut self) -> Option<&mut T> { self.0.as_any_mut().downcast_mut() } @@ -100,6 +100,7 @@ impl Pod { xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyNode} +xilem_core::impl_adapt_view! {View, Cx, ChangeFlags} /// This view container can switch between two views. /// diff --git a/crates/xilem_html/web_examples/todomvc/src/state.rs b/crates/xilem_html/web_examples/todomvc/src/state.rs index 2b5bdaf5d..920253496 100644 --- a/crates/xilem_html/web_examples/todomvc/src/state.rs +++ b/crates/xilem_html/web_examples/todomvc/src/state.rs @@ -75,15 +75,10 @@ impl Todo { } } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, Default, PartialEq, Copy, Clone)] pub enum Filter { + #[default] All, Active, Completed, } - -impl Default for Filter { - fn default() -> Self { - Self::All - } -} From 61347778a2a033c7975842ed0a69407a3d62b4d6 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 5 Jul 2023 11:52:11 +0100 Subject: [PATCH 11/12] Implement more suggestions. --- crates/xilem_html/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 646cc8448..ca82a84fb 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -8,12 +8,10 @@ use wasm_bindgen::JsCast; mod app; -//mod button; mod class; mod context; -mod event; -//mod div; mod element; +mod event; mod view; #[cfg(feature = "typed")] mod view_ext; From 65641bc72e232ed45ad7e1dc1a0433adc9150f3f Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 5 Jul 2023 11:59:14 +0100 Subject: [PATCH 12/12] Implement clippy suggestions --- crates/xilem_core/src/message.rs | 8 ++------ crates/xilem_core/src/vec_splice.rs | 6 +++++- crates/xilem_html/src/context.rs | 6 ++++++ crates/xilem_html/src/element/mod.rs | 5 +++-- crates/xilem_html/src/view.rs | 6 +++--- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/xilem_core/src/message.rs b/crates/xilem_core/src/message.rs index 19399ad8d..11a9ad68a 100644 --- a/crates/xilem_core/src/message.rs +++ b/crates/xilem_core/src/message.rs @@ -39,6 +39,7 @@ macro_rules! message { } /// A result wrapper type for event handlers. +#[derive(Default)] pub enum MessageResult { /// The event handler was invoked and returned an action. /// @@ -58,6 +59,7 @@ pub enum MessageResult { /// This is the variant that you **almost always want** when you're not returning /// an action. #[allow(unused)] + #[default] Nop, /// The event was addressed to an id path no longer in the tree. /// @@ -66,12 +68,6 @@ pub enum MessageResult { Stale(Box), } -impl Default for MessageResult { - fn default() -> Self { - MessageResult::Nop - } -} - // TODO: does this belong in core? pub struct AsyncWake; diff --git a/crates/xilem_core/src/vec_splice.rs b/crates/xilem_core/src/vec_splice.rs index 4e2fb0af1..aa0af8b75 100644 --- a/crates/xilem_core/src/vec_splice.rs +++ b/crates/xilem_core/src/vec_splice.rs @@ -54,9 +54,13 @@ impl<'a, 'b, T> VecSplice<'a, 'b, T> { self.ix } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn as_vec) -> R>(&mut self, f: F) -> R { self.clear_tail(); - let ret = f(&mut self.v); + let ret = f(self.v); self.ix = self.v.len(); ret } diff --git a/crates/xilem_html/src/context.rs b/crates/xilem_html/src/context.rs index 81d9f887d..39e234fe3 100644 --- a/crates/xilem_html/src/context.rs +++ b/crates/xilem_html/src/context.rs @@ -100,6 +100,12 @@ impl Cx { } } +impl Default for Cx { + fn default() -> Self { + Self::new() + } +} + impl MessageThunk { pub fn push_message(&self, message_body: impl Any + 'static) { let message = Message { diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs index c71e55896..a40c2a515 100644 --- a/crates/xilem_html/src/element/mod.rs +++ b/crates/xilem_html/src/element/mod.rs @@ -25,6 +25,7 @@ pub struct Element { name: Cow<'static, str>, attributes: BTreeMap, Cow<'static, str>>, children: Children, + #[allow(clippy::type_complexity)] after_update: Option>, } @@ -205,11 +206,11 @@ where } } // Only max 1 of these loops will run - while let Some((name, _)) = prev_attrs.next() { + for (name, _) in prev_attrs { remove_attribute(element, name); changed |= ChangeFlags::OTHER_CHANGE; } - while let Some((name, value)) = self_attrs.next() { + for (name, value) in self_attrs { set_attribute(element, name, value); changed |= ChangeFlags::OTHER_CHANGE; } diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index ccbf2f308..c451bca00 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -222,7 +222,7 @@ impl View for &'static str { fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { let el = new_text(self); let id = Id::next(); - (id, (), el.into()) + (id, (), el) } fn rebuild( @@ -260,7 +260,7 @@ impl View for String { fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { let el = new_text(self); let id = Id::next(); - (id, (), el.into()) + (id, (), el) } fn rebuild( @@ -298,7 +298,7 @@ impl View for Cow<'static, str> { fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { let el = new_text(self); let id = Id::next(); - (id, (), el.into()) + (id, (), el) } fn rebuild(