From 1fd1ce78857586cdb41987b0d2a0360edea383e2 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 20 Mar 2023 17:19:22 -0700 Subject: [PATCH] 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 | 793 ++++++++++-------- 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, 4427 insertions(+), 387 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 62bf94d7..67291a43 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 6f0702a4..f70c4ce9 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" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" -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", @@ -2366,10 +2400,21 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.0.3" +name = "syn" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" dependencies = [ "cfg-expr", "heck", @@ -2379,16 +2424,23 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.4.0" +name = "target-lexicon" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" - [[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 b1f19c14..dfa2c9ff 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 843f6a42..657ff0e7 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 56f25e3f..19399ad8 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, + } -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) -> Message { + Message { + id_path, + body: Box::new(event), + } + } + } + }; + + ($($bounds:tt)*) => { + 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 + $($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 ce50d2e0..90dfbb65 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 8591430e..44b23161 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 00000000..849ddff3 --- /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 00000000..268fda5a --- /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 00000000..2fb1fbf6 --- /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 00000000..60de984e --- /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 00000000..1513c35a --- /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 00000000..81d9f887 --- /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 00000000..9afed2d4 --- /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 00000000..8f68d80c --- /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 00000000..98b63839 --- /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 00000000..aadd4bb3 --- /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 00000000..717fd5c7 --- /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 00000000..db3d94e4 --- /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 00000000..b38b1273 --- /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 00000000..db46cdb6 --- /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 00000000..548a7208 --- /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 00000000..98e3db89 --- /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 00000000..1c71757c --- /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 00000000..91659c49 --- /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 00000000..8774c9e4 --- /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 00000000..b835d7b5 --- /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 00000000..f6cff14c --- /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 00000000..c1a82a54 --- /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 00000000..50c2d80a --- /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 00000000..8b2d4e6f --- /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 00000000..849ddff3 --- /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 00000000..cb17a12b --- /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 00000000..2fb1fbf6 --- /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 00000000..9a850a6f --- /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 00000000..d3cf25ba --- /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 00000000..4e096ec3 --- /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 00000000..1eed1a40 --- /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 00000000..c59c05a1 --- /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 00000000..4cb25b2d --- /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 00000000..4921c902 --- /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 00000000..e7a733ab --- /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 00000000..0cebd2d0 --- /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 00000000..09b2e054 --- /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 00000000..db6ea116 --- /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 73d15a04..15af8cbc 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 a26f6735..6b3205c8 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 649cbd7c..d5e6e2ed 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.