From 8d73ebfc89c958a0888fbbd25ae6cbbfc8706626 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:27:44 +0100 Subject: [PATCH 01/12] Create a minimal xilem_masonry --- Cargo.lock | 501 +++++++++++++++++++++++- Cargo.toml | 7 +- crates/masonry/Cargo.toml | 23 +- crates/masonry/examples/calc.rs | 6 +- crates/masonry/src/lib.rs | 2 +- crates/xilem_masonry/Cargo.toml | 19 + crates/xilem_masonry/examples/mason.rs | 32 ++ crates/xilem_masonry/src/id.rs | 22 ++ crates/xilem_masonry/src/lib.rs | 242 ++++++++++++ crates/xilem_masonry/src/view/button.rs | 52 +++ crates/xilem_masonry/src/view/mod.rs | 2 + 11 files changed, 885 insertions(+), 23 deletions(-) create mode 100644 crates/xilem_masonry/Cargo.toml create mode 100644 crates/xilem_masonry/examples/mason.rs create mode 100644 crates/xilem_masonry/src/id.rs create mode 100644 crates/xilem_masonry/src/lib.rs create mode 100644 crates/xilem_masonry/src/view/button.rs create mode 100644 crates/xilem_masonry/src/view/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b83376f14..c3ef9f7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.13" @@ -138,9 +147,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arrayref" @@ -169,6 +178,12 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -211,6 +226,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -274,6 +295,12 @@ dependencies = [ "syn 2.0.57", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.0" @@ -384,6 +411,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.0" @@ -440,6 +473,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -515,6 +560,18 @@ dependencies = [ "libc", ] +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics 0.23.1", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core_maths" version = "0.1.0" @@ -544,12 +601,46 @@ dependencies = [ "xilem_web", ] +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "cursor-icon" version = "1.1.0" @@ -613,6 +704,18 @@ dependencies = [ "wio", ] +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "equivalent" version = "1.0.1" @@ -638,6 +741,62 @@ dependencies = [ "num-traits", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -757,6 +916,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -896,6 +1065,16 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -1044,6 +1223,24 @@ dependencies = [ "syn 2.0.57", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1054,6 +1251,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + [[package]] name = "instant" version = "0.1.12" @@ -1103,6 +1312,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1146,6 +1364,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.153" @@ -1189,6 +1413,12 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1226,6 +1456,37 @@ dependencies = [ "libc", ] +[[package]] +name = "masonry" +version = "0.1.3" +dependencies = [ + "assert_matches", + "console_error_panic_hook", + "float-cmp", + "fnv", + "futures-intrusive", + "image", + "insta", + "instant", + "kurbo", + "once_cell", + "open", + "parley 0.0.1 (git+https://github.com/linebender/parley?rev=4f05e183be9b388c6748d3c531c9ac332672fb86)", + "pollster", + "pulldown-cmark", + "serde", + "serde_json", + "smallvec", + "swash", + "tempfile", + "tracing", + "tracing-subscriber 0.2.25", + "tracing-wasm", + "vello 0.1.0 (git+https://github.com/linebender/vello/?rev=b520a35addfa6bbb37d93491d2b8236528faf3b5)", + "wgpu", + "winit", +] + [[package]] name = "mathml_svg" version = "0.1.0" @@ -1282,6 +1543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1442,6 +1704,16 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "open" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea7a30d6b81a2423cc59c43554880feff7b57d12916f231a79f8d6d9470201" +dependencies = [ + "pathdiff", + "winapi", +] + [[package]] name = "orbclient" version = "0.3.47" @@ -1492,7 +1764,7 @@ dependencies = [ "bytemuck", "core-foundation", "core-foundation-sys", - "core-text", + "core-text 19.2.0", "dwrote", "fontconfig-cache-parser", "foreign-types 0.3.2", @@ -1510,12 +1782,44 @@ dependencies = [ "wio", ] +[[package]] +name = "parley" +version = "0.0.1" +source = "git+https://github.com/linebender/parley?rev=4f05e183be9b388c6748d3c531c9ac332672fb86#4f05e183be9b388c6748d3c531c9ac332672fb86" +dependencies = [ + "anyhow", + "bytemuck", + "core-foundation", + "core-foundation-sys", + "core-text 20.1.0", + "dwrote", + "fontconfig-cache-parser", + "hashbrown", + "icu_locid", + "icu_properties", + "memmap2 0.5.10", + "peniko", + "roxmltree", + "skrifa 0.19.0", + "smallvec", + "swash", + "thiserror", + "winapi", + "wio", +] + [[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "peniko" version = "0.1.0" @@ -1544,6 +1848,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.6.0" @@ -1559,6 +1876,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "presser" version = "0.3.1" @@ -1589,6 +1912,26 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -1625,6 +1968,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.15.6" @@ -1791,6 +2154,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" + [[package]] name = "skrifa" version = "0.15.5" @@ -1879,6 +2254,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -1985,6 +2369,18 @@ dependencies = [ "slotmap", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2024,6 +2420,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -2149,6 +2556,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -2169,7 +2589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" dependencies = [ "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.18", "wasm-bindgen", ] @@ -2179,6 +2599,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -2209,6 +2638,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vello" version = "0.1.0" @@ -2220,7 +2655,22 @@ dependencies = [ "peniko", "raw-window-handle 0.6.0", "skrifa 0.15.5", - "vello_encoding", + "vello_encoding 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wgpu", +] + +[[package]] +name = "vello" +version = "0.1.0" +source = "git+https://github.com/linebender/vello/?rev=b520a35addfa6bbb37d93491d2b8236528faf3b5#b520a35addfa6bbb37d93491d2b8236528faf3b5" +dependencies = [ + "bytemuck", + "futures-intrusive", + "log", + "peniko", + "raw-window-handle 0.6.0", + "skrifa 0.19.0", + "vello_encoding 0.1.0 (git+https://github.com/linebender/vello/?rev=b520a35addfa6bbb37d93491d2b8236528faf3b5)", "wgpu", ] @@ -2236,6 +2686,17 @@ dependencies = [ "skrifa 0.15.5", ] +[[package]] +name = "vello_encoding" +version = "0.1.0" +source = "git+https://github.com/linebender/vello/?rev=b520a35addfa6bbb37d93491d2b8236528faf3b5#b520a35addfa6bbb37d93491d2b8236528faf3b5" +dependencies = [ + "bytemuck", + "guillotiere", + "peniko", + "skrifa 0.19.0", +] + [[package]] name = "version_check" version = "0.9.4" @@ -2453,6 +2914,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" version = "0.19.3" @@ -2933,11 +3400,11 @@ dependencies = [ "fnv", "futures-task", "instant", - "parley", + "parley 0.0.1 (git+https://github.com/dfrg/parley?rev=4e6109f2ff5847a72dc77971b6fa0942b8474d88)", "taffy", "tokio", "tracing", - "vello", + "vello 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "wgpu", "winit", "xilem_core", @@ -2947,6 +3414,17 @@ dependencies = [ name = "xilem_core" version = "0.1.0" +[[package]] +name = "xilem_masonry" +version = "0.1.0" +dependencies = [ + "masonry", + "smallvec", + "tracing", + "vello 0.1.0 (git+https://github.com/linebender/vello/?rev=b520a35addfa6bbb37d93491d2b8236528faf3b5)", + "winit", +] + [[package]] name = "xilem_web" version = "0.1.0" @@ -3091,3 +3569,12 @@ dependencies = [ "quote", "syn 2.0.57", ] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 81ffe5f71..367bcc0dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "crates/xilem_web/web_examples/todomvc", "crates/xilem_web/web_examples/mathml_svg", "crates/xilem_web/web_examples/svgtoy", + "crates/masonry", + "crates/xilem_masonry", ] [workspace.package] @@ -17,7 +19,10 @@ license = "Apache-2.0" [workspace.dependencies] xilem_core = { version = "0.1.0", path = "crates/xilem_core" } +masonry = { version = "0.1.0", path = "crates/masonry" } kurbo = "0.11.0" +winit = { version = "0.29", features = ["rwh_05"] } +vello = { git = "https://github.com/linebender/vello/", rev = "b520a35addfa6bbb37d93491d2b8236528faf3b5" } [workspace.lints] clippy.semicolon_if_nothing_returned = "warn" @@ -29,7 +34,7 @@ description = "A next-generation cross-platform Rust UI framework." keywords = ["gui", "ui", "native", "gpu", "performance"] categories = ["gui", "graphics", "internationalization", "accessibility"] exclude = ["/.github/"] -publish = false # Until it's ready +publish = false # Until it's ready license.workspace = true edition.workspace = true homepage.workspace = true diff --git a/crates/masonry/Cargo.toml b/crates/masonry/Cargo.toml index 3ef48d557..d4e2675da 100644 --- a/crates/masonry/Cargo.toml +++ b/crates/masonry/Cargo.toml @@ -17,32 +17,35 @@ opt-level = 2 # NOTE: Make sure to keep wgpu version in sync with the version badge in README.md [dependencies] fnv = "1.0.7" -instant = {version = "0.1.6", features = ["wasm-bindgen"]} +instant = { version = "0.1.6", features = ["wasm-bindgen"] } smallvec = "1.6.1" tracing = "0.1.29" -tracing-subscriber = {version = "0.2.15", features = ["fmt", "ansi"], default-features = false} +tracing-subscriber = { version = "0.2.15", features = [ + "fmt", + "ansi", +], default-features = false } image = "0.24.0" once_cell = "1.9.0" -serde = {version = "1.0.133", features = ["derive"]} +serde = { version = "1.0.133", features = ["derive"] } serde_json = "1.0.74" -vello = { git = "https://github.com/linebender/vello/", rev = "b520a35addfa6bbb37d93491d2b8236528faf3b5" } +vello.workspace = true kurbo = "0.11.0" futures-intrusive = "0.5.0" pollster = "0.3.0" parley = { git = "https://github.com/linebender/parley", rev = "4f05e183be9b388c6748d3c531c9ac332672fb86" } wgpu = { version = "0.19.3" } swash = "0.1.15" -winit = "0.29.15" +winit.workspace = true [target.'cfg(target_arch="wasm32")'.dependencies] -console_error_panic_hook = {version = "0.1.6"} -tracing-wasm = {version = "0.2.0"} +console_error_panic_hook = { version = "0.1.6" } +tracing-wasm = { version = "0.2.0" } [dev-dependencies] -float-cmp = {version = "0.8.0", features = ["std"], default-features = false} -insta = {version = "1.8.0"} +float-cmp = { version = "0.8.0", features = ["std"], default-features = false } +insta = { version = "1.8.0" } assert_matches = "1.5.0" -pulldown-cmark = {version = "0.8", default-features = false} +pulldown-cmark = { version = "0.8", default-features = false } tempfile = "3.10.1" [target.'cfg(not(target_arch="wasm32"))'.dev-dependencies] diff --git a/crates/masonry/examples/calc.rs b/crates/masonry/examples/calc.rs index cf095dfc7..db7752510 100644 --- a/crates/masonry/examples/calc.rs +++ b/crates/masonry/examples/calc.rs @@ -11,12 +11,10 @@ use std::sync::Arc; use masonry::app_driver::{AppDriver, DriverCtx}; use masonry::event_loop_runner::EventLoopRunner; -use masonry::testing::TestHarness; use masonry::widget::{Align, CrossAxisAlignment, Flex, Label, SizedBox, WidgetRef}; use masonry::{ - assert_render_snapshot, Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, - WidgetPod, + Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, }; use smallvec::{smallvec, SmallVec}; use tracing::{trace, trace_span, Span}; diff --git a/crates/masonry/src/lib.rs b/crates/masonry/src/lib.rs index ab1f107a1..71adfbbf8 100644 --- a/crates/masonry/src/lib.rs +++ b/crates/masonry/src/lib.rs @@ -80,7 +80,7 @@ unsafe_code, clippy::trivially_copy_pass_by_ref )] -#![warn(missing_docs)] +// #![warn(missing_docs)] #![warn(unused_imports)] #![allow(clippy::new_ret_no_self)] #![allow(clippy::needless_doctest_main)] diff --git a/crates/xilem_masonry/Cargo.toml b/crates/xilem_masonry/Cargo.toml new file mode 100644 index 000000000..ca1cb052b --- /dev/null +++ b/crates/xilem_masonry/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "xilem_masonry" +version = "0.1.0" +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +masonry.workspace = true +winit.workspace = true +tracing = "0.1.40" +vello.workspace = true +smallvec = "1.13.2" + +[lints] +workspace = true diff --git a/crates/xilem_masonry/examples/mason.rs b/crates/xilem_masonry/examples/mason.rs new file mode 100644 index 000000000..17b723e43 --- /dev/null +++ b/crates/xilem_masonry/examples/mason.rs @@ -0,0 +1,32 @@ +// On Windows platform, don't show a console when opening the app. +#![windows_subsystem = "windows"] +use xilem_masonry::view::button; +use xilem_masonry::{MasonryView, Xilem}; + +fn app_logic(data: &mut AppData) -> impl MasonryView { + // here's some logic, deriving state for the view from our state + let count = data.count; + let label = if count == 1 { + "clicked 1 time".to_string() + } else { + format!("clicked {count} times") + }; + + // The actual UI Code starts here + + button(label, |data: &mut AppData| { + println!("clicked"); + data.count += 1; + }) +} + +struct AppData { + count: i32, +} + +fn main() { + let data = AppData { count: 0 }; + + let app = Xilem::new(data, app_logic); + app.run_windowed("First Example".into()).unwrap() +} diff --git a/crates/xilem_masonry/src/id.rs b/crates/xilem_masonry/src/id.rs new file mode 100644 index 000000000..e5b555158 --- /dev/null +++ b/crates/xilem_masonry/src/id.rs @@ -0,0 +1,22 @@ +use std::{fmt::Debug, num::NonZeroU64}; + +#[derive(Copy, Clone)] +pub struct Id { + routing_id: NonZeroU64, + debug: &'static str, +} + +impl Id { + pub fn for_type(raw: NonZeroU64) -> Self { + Self { + debug: std::any::type_name::(), + routing_id: raw, + } + } +} + +impl Debug for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@[{}]", self.routing_id, self.debug) + } +} diff --git a/crates/xilem_masonry/src/lib.rs b/crates/xilem_masonry/src/lib.rs new file mode 100644 index 000000000..c228822e9 --- /dev/null +++ b/crates/xilem_masonry/src/lib.rs @@ -0,0 +1,242 @@ +use std::{any::Any, collections::HashMap}; + +use masonry::{ + app_driver::AppDriver, + event_loop_runner::EventLoopRunner, + widget::{StoreInWidgetMut, WidgetMut, WidgetRef}, + BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, + Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, +}; +use smallvec::SmallVec; +use vello::Scene; +use winit::{ + dpi::LogicalSize, + error::EventLoopError, + event_loop::EventLoop, + window::{Window, WindowBuilder}, +}; + +mod id; +pub mod view; +pub use id::Id; + +pub struct Xilem +where + View: MasonryView, +{ + root_widget: RootWidget, + driver: MasonryDriver, +} + +pub struct MasonryDriver { + state: State, + logic: Logic, + current_view: View, + view_cx: ViewCx, +} + +// TODO: This is a hack to work around pod-racing +// TODO: `declare_widget` *forces* this to be pub +pub struct RootWidget { + pub(crate) pod: WidgetPod, +} + +masonry::declare_widget!(RootWidgetMut, RootWidget); + +impl Widget for RootWidget { + fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { + self.pod.on_pointer_event(ctx, event) + } + fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { + self.pod.on_text_event(ctx, event) + } + + fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) { + // Intentionally do nothing? + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { + self.pod.lifecycle(ctx, event) + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + let size = self.pod.layout(ctx, bc); + ctx.place_child(&mut self.pod, Point::ORIGIN); + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + self.pod.paint(ctx, scene) + } + + fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { + let mut vec = SmallVec::new(); + vec.push(self.pod.as_dyn()); + vec + } +} + +impl AppDriver for MasonryDriver +where + Logic: FnMut(&mut State) -> View, + View: MasonryView, +{ + fn on_action( + &mut self, + ctx: &mut masonry::app_driver::DriverCtx<'_>, + widget_id: masonry::WidgetId, + action: masonry::Action, + ) { + if let Some(id_path) = self.view_cx.widget_map.get(&widget_id) { + let message_result = + self.current_view + .message(id_path.as_slice(), Box::new(action), &mut self.state); + let build = match message_result { + MessageResult::Action(()) => { + // It's not entirely clear what to do here + true + } + MessageResult::RequestRebuild => true, + MessageResult::Nop => false, + }; + if build { + let next_view = (self.logic)(&mut self.state); + let mut root = ctx.get_root::>(); + let element = root.get_element(); + + let changed = next_view.rebuild(&mut self.view_cx, &self.current_view, element); + if !changed.changed { + tracing::debug!("TODO: Skip some work?") + } + self.current_view = next_view; + } + } else { + eprintln!("Got action {action:?} for unknown widget. Did you forget to use `with_action_widget`?"); + } + } +} + +impl RootWidgetMut<'_, E> { + pub fn get_element(&mut self) -> WidgetMut<'_, E> { + self.ctx.get_mut(&mut self.widget.pod) + } +} + +impl Xilem +where + Logic: FnMut(&mut State) -> View, + View: MasonryView, +{ + pub fn new(mut state: State, mut logic: Logic) -> Self { + let first_view = logic(&mut state); + let mut view_cx = ViewCx { + id_path: vec![], + widget_map: HashMap::new(), + }; + let root_widget = RootWidget { + pod: first_view.build(&mut view_cx), + }; + Xilem { + driver: MasonryDriver { + current_view: first_view, + logic, + state, + view_cx, + }, + root_widget, + } + } + + // TODO: Make windows a specific view + pub fn run_windowed(self, window_title: String) -> Result<(), EventLoopError> + where + State: 'static, + Logic: 'static, + View: 'static, + { + let event_loop = EventLoop::new().unwrap(); + let window_size = LogicalSize::new(600., 800.); + let window = WindowBuilder::new() + .with_title(window_title) + .with_resizable(true) + .with_min_inner_size(window_size) + .build(&event_loop) + .unwrap(); + self.run_windowed_in(window, event_loop) + } + + // TODO: Make windows into a custom view + pub fn run_windowed_in( + self, + window: Window, + event_loop: EventLoop<()>, + ) -> Result<(), EventLoopError> + where + State: 'static, + Logic: 'static, + View: 'static, + { + EventLoopRunner::new(self.root_widget, window, event_loop, self.driver).run() + } +} +pub trait MasonryView { + type Element: Widget + StoreInWidgetMut; + fn build(&self, cx: &mut ViewCx) -> WidgetPod; + fn message( + &self, + id_path: &[Id], + message: Box, + app_state: &mut State, + ) -> MessageResult; + fn rebuild( + &self, + _cx: &mut ViewCx, + prev: &Self, + // _id: &mut Id, + element: WidgetMut, + ) -> ChangeFlags; +} + +pub struct ChangeFlags { + changed: bool, +} + +impl ChangeFlags { + const CHANGED: Self = ChangeFlags { changed: true }; + const UNCHANGED: Self = ChangeFlags { changed: false }; +} + +pub struct ViewCx { + widget_map: HashMap>, + id_path: Vec, +} + +impl ViewCx { + pub fn with_action_widget( + &mut self, + f: impl FnOnce(&mut Self) -> WidgetPod, + ) -> WidgetPod { + let value = f(self); + let id = value.id(); + let path = self.id_path.clone(); + self.widget_map.insert(id, path); + value + } + + pub fn with_id(&mut self, id: Id, f: impl FnOnce(&mut Self) -> R) -> R { + self.id_path.push(id); + let res = f(self); + self.id_path.pop(); + res + } +} + +/// A result wrapper type for event handlers. +#[derive(Default)] +pub enum MessageResult { + Action(A), + RequestRebuild, + #[default] + Nop, + // Stale(Box), +} diff --git a/crates/xilem_masonry/src/view/button.rs b/crates/xilem_masonry/src/view/button.rs new file mode 100644 index 000000000..0ed7f0bdd --- /dev/null +++ b/crates/xilem_masonry/src/view/button.rs @@ -0,0 +1,52 @@ +use masonry::{widget::WidgetMut, ArcStr, WidgetPod}; + +use crate::{ChangeFlags, Id, MasonryView, ViewCx}; + +pub fn button(label: impl Into, callback: F) -> Button +where + F: Fn(&mut State) -> Action + Send + 'static, +{ + Button { + label: label.into(), + callback, + } +} + +pub struct Button { + label: ArcStr, + callback: F, +} + +impl MasonryView for Button +where + F: Fn(&mut State) -> Action + Send + 'static, +{ + type Element = masonry::widget::Button; + + fn build(&self, cx: &mut ViewCx) -> WidgetPod { + cx.with_action_widget(|_| WidgetPod::new(masonry::widget::Button::new(self.label.clone()))) + } + fn message( + &self, + _id_path: &[Id], + // TODO: Ensure is masonry button pressed action? + _message: Box, + app_state: &mut State, + ) -> crate::MessageResult { + crate::MessageResult::Action((self.callback)(app_state)) + } + fn rebuild( + &self, + _cx: &mut ViewCx, + prev: &Self, + // _id: &mut Id, + mut element: WidgetMut, + ) -> crate::ChangeFlags { + if prev.label != self.label { + element.set_text(self.label.clone()); + ChangeFlags::CHANGED + } else { + ChangeFlags::UNCHANGED + } + } +} diff --git a/crates/xilem_masonry/src/view/mod.rs b/crates/xilem_masonry/src/view/mod.rs new file mode 100644 index 000000000..ddf6d53ab --- /dev/null +++ b/crates/xilem_masonry/src/view/mod.rs @@ -0,0 +1,2 @@ +mod button; +pub use button::*; From ccf625f2b81a85398ed83e26f01f2022df81d544 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:17:16 +0100 Subject: [PATCH 02/12] Add a first draft of view sequences --- crates/xilem_masonry/src/id.rs | 10 +- crates/xilem_masonry/src/lib.rs | 27 ++- crates/xilem_masonry/src/sequence.rs | 215 ++++++++++++++++++++++++ crates/xilem_masonry/src/vec_splice.rs | 104 ++++++++++++ crates/xilem_masonry/src/view/button.rs | 4 +- 5 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 crates/xilem_masonry/src/sequence.rs create mode 100644 crates/xilem_masonry/src/vec_splice.rs diff --git a/crates/xilem_masonry/src/id.rs b/crates/xilem_masonry/src/id.rs index e5b555158..7ab1f4076 100644 --- a/crates/xilem_masonry/src/id.rs +++ b/crates/xilem_masonry/src/id.rs @@ -1,21 +1,25 @@ use std::{fmt::Debug, num::NonZeroU64}; #[derive(Copy, Clone)] -pub struct Id { +pub struct ViewId { routing_id: NonZeroU64, debug: &'static str, } -impl Id { +impl ViewId { pub fn for_type(raw: NonZeroU64) -> Self { Self { debug: std::any::type_name::(), routing_id: raw, } } + + pub fn routing_id(self) -> NonZeroU64 { + self.routing_id + } } -impl Debug for Id { +impl Debug for ViewId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}@[{}]", self.routing_id, self.debug) } diff --git a/crates/xilem_masonry/src/lib.rs b/crates/xilem_masonry/src/lib.rs index c228822e9..392bff0b9 100644 --- a/crates/xilem_masonry/src/lib.rs +++ b/crates/xilem_masonry/src/lib.rs @@ -17,8 +17,12 @@ use winit::{ }; mod id; +mod sequence; +mod vec_splice; pub mod view; -pub use id::Id; +pub use id::ViewId; +pub use sequence::{ElementSplice, ViewSequence}; +pub use vec_splice::VecSplice; pub struct Xilem where @@ -91,15 +95,19 @@ where let message_result = self.current_view .message(id_path.as_slice(), Box::new(action), &mut self.state); - let build = match message_result { + let rebuild = match message_result { MessageResult::Action(()) => { // It's not entirely clear what to do here true } MessageResult::RequestRebuild => true, MessageResult::Nop => false, + MessageResult::Stale(_) => { + tracing::info!("Discarding message"); + false + } }; - if build { + if rebuild { let next_view = (self.logic)(&mut self.state); let mut root = ctx.get_root::>(); let element = root.get_element(); @@ -179,12 +187,12 @@ where EventLoopRunner::new(self.root_widget, window, event_loop, self.driver).run() } } -pub trait MasonryView { +pub trait MasonryView: Send + 'static { type Element: Widget + StoreInWidgetMut; fn build(&self, cx: &mut ViewCx) -> WidgetPod; fn message( &self, - id_path: &[Id], + id_path: &[ViewId], message: Box, app_state: &mut State, ) -> MessageResult; @@ -197,6 +205,7 @@ pub trait MasonryView { ) -> ChangeFlags; } +#[must_use] pub struct ChangeFlags { changed: bool, } @@ -207,8 +216,8 @@ impl ChangeFlags { } pub struct ViewCx { - widget_map: HashMap>, - id_path: Vec, + widget_map: HashMap>, + id_path: Vec, } impl ViewCx { @@ -223,7 +232,7 @@ impl ViewCx { value } - pub fn with_id(&mut self, id: Id, f: impl FnOnce(&mut Self) -> R) -> R { + pub fn with_id(&mut self, id: ViewId, f: impl FnOnce(&mut Self) -> R) -> R { self.id_path.push(id); let res = f(self); self.id_path.pop(); @@ -238,5 +247,5 @@ pub enum MessageResult { RequestRebuild, #[default] Nop, - // Stale(Box), + Stale(Box), } diff --git a/crates/xilem_masonry/src/sequence.rs b/crates/xilem_masonry/src/sequence.rs new file mode 100644 index 000000000..44e545329 --- /dev/null +++ b/crates/xilem_masonry/src/sequence.rs @@ -0,0 +1,215 @@ +use std::num::NonZeroU64; + +use masonry::{Widget, WidgetCtx, WidgetPod}; + +use crate::{ChangeFlags, MasonryView, MessageResult, ViewCx, ViewId}; + +pub trait ElementSplice { + /// Insert a new element at the current index in the resulting collection (and increment the index by 1) + fn push(&mut self, element: WidgetPod>); + /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) + fn mutate<'a>(&'a mut self) -> &mut WidgetPod>; + /// Delete the next n existing elements (this doesn't change the index) + fn delete(&mut self, n: usize); + /// Current length of the elements collection + fn len(&self) -> usize; +} + +/// This trait represents a (possibly empty) sequence of views. +/// +/// It is up to the parent view how to lay out and display them. +pub trait ViewSequence: Send + 'static { + /// Build the associated widgets and initialize all states. + /// + /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, + /// this takes an element splice as well (when it could be just a `Vec` otherwise) + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice); + + /// Update the associated widget. + /// + /// Returns `true` when anything has changed. + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + widget_ctx: &mut WidgetCtx<'_>, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags; + + /// Propagate a message. + /// + /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice + /// of ids beginning at an element of this view_sequence. + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult; + + /// Returns the current amount of widgets built by this sequence. + fn count(&self) -> usize; +} + +struct WasAView; +struct WasASequence; + +impl> ViewSequence + for View +{ + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) { + let element = self.build(cx); + elements.push(element.boxed()); + } + + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + widget_ctx: &mut WidgetCtx<'_>, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + let element = elements.mutate(); + let mut mutable = widget_ctx.get_mut(element); + let downcast = mutable.downcast::(); + + if let Some(element) = downcast { + self.rebuild(cx, prev, element) + } else { + unreachable!("Tree structure tracking got wrong element type") + } + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + self.message(&id_path, message, app_state) + } + + fn count(&self) -> usize { + 1 + } +} + +impl> + ViewSequence for Option +{ + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) { + match self { + Some(this) => { + this.build(cx, elements); + } + None => (), + } + } + + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + widget_ctx: &mut WidgetCtx<'_>, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + match (self, prev) { + (Some(this), Some(prev)) => this.rebuild(cx, prev, widget_ctx, elements), + (None, Some(prev)) => { + let count = prev.count(); + elements.delete(count); + + ChangeFlags::CHANGED + } + (Some(this), None) => { + this.build(cx, elements); + ChangeFlags::CHANGED + } + (None, None) => ChangeFlags::UNCHANGED, + } + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + if let Some(this) = self { + this.message(&id_path, message, app_state) + } else { + MessageResult::Stale(message) + } + } + + fn count(&self) -> usize { + match self { + Some(this) => this.count(), + None => 0, + } + } +} + +// TODO: We use raw indexing for this value. What would make it invalid? +impl> ViewSequence + for Vec +{ + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) { + self.iter().enumerate().for_each(|(i, child)| { + let i: u64 = i.try_into().unwrap(); + let id = NonZeroU64::new(i + 1).unwrap(); + cx.with_id(ViewId::for_type::(id), |cx| child.build(cx, elements)); + }) + } + + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + widget_ctx: &mut WidgetCtx<'_>, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + let mut changed = ChangeFlags::UNCHANGED; + for (i, (child, child_prev)) in self.iter().zip(prev).enumerate() { + let i: u64 = i.try_into().unwrap(); + let id = NonZeroU64::new(i + 1).unwrap(); + cx.with_id(ViewId::for_type::(id), |cx| { + let el_changed = child.rebuild(cx, child_prev, widget_ctx, elements); + changed.changed |= el_changed.changed; + }); + } + let n = self.len(); + if n < prev.len() { + let n_delete = prev[n..].iter().map(ViewSequence::count).sum(); + elements.delete(n_delete); + changed.changed |= ChangeFlags::CHANGED.changed; + } else if n > prev.len() { + for ix in prev.len()..n { + let id_u64: u64 = ix.try_into().unwrap(); + let id = NonZeroU64::new(id_u64 + 1).unwrap(); + cx.with_id(ViewId::for_type::(id), |cx| { + self[ix].build(cx, elements); + }); + } + changed.changed |= ChangeFlags::CHANGED.changed; + } + changed + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut T, + ) -> MessageResult { + let (start, rest) = id_path + .split_first() + .expect("Id path has elements for vector"); + let index_plus_one: usize = start.routing_id().get().try_into().unwrap(); + self[index_plus_one - 1].message(rest, message, app_state) + } + + fn count(&self) -> usize { + self.iter().map(ViewSequence::count).sum() + } +} diff --git a/crates/xilem_masonry/src/vec_splice.rs b/crates/xilem_masonry/src/vec_splice.rs new file mode 100644 index 000000000..ac744fe6c --- /dev/null +++ b/crates/xilem_masonry/src/vec_splice.rs @@ -0,0 +1,104 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use masonry::WidgetPod; + +use crate::ElementSplice; + +pub struct VecSplice<'a, 'b, T> { + v: &'a mut Vec, + scratch: &'b mut Vec, + ix: usize, +} + +impl<'a, 'b, T> VecSplice<'a, 'b, T> { + pub fn new(v: &'a mut Vec, scratch: &'b mut Vec) -> Self { + let ix = 0; + VecSplice { v, scratch, ix } + } + + pub fn skip(&mut self, n: usize) { + if self.v.len() < self.ix + n { + let l = self.scratch.len(); + self.v.extend(self.scratch.splice(l - n.., [])); + self.v[self.ix..].reverse(); + } + self.ix += n; + } + + pub fn delete(&mut self, n: usize) { + if self.v.len() < self.ix + n { + self.scratch.truncate(self.scratch.len() - n); + } else { + if self.v.len() > self.ix + n { + let removed = self.v.splice(self.ix + n.., []).rev(); + self.scratch.extend(removed); + } + self.v.truncate(self.ix); + } + } + + pub fn push(&mut self, value: T) { + self.clear_tail(); + self.v.push(value); + self.ix += 1; + } + + pub fn mutate(&mut self) -> &mut T { + if self.v.len() == self.ix { + self.v.push(self.scratch.pop().unwrap()); + } + let ix = self.ix; + self.ix += 1; + &mut self.v[ix] + } + + pub fn last_mutated(&self) -> Option<&T> { + if self.ix == 0 { + None + } else { + self.v.get(self.ix - 1) + } + } + + pub fn last_mutated_mut(&mut self) -> Option<&mut T> { + if self.ix == 0 { + None + } else { + self.v.get_mut(self.ix - 1) + } + } + + pub fn len(&self) -> usize { + self.ix + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn clear_tail(&mut self) { + if self.v.len() > self.ix { + let removed = self.v.splice(self.ix.., []).rev(); + self.scratch.extend(removed); + } + } +} + +impl ElementSplice for VecSplice<'_, '_, masonry::WidgetPod>> { + fn push(&mut self, element: masonry::WidgetPod>) { + self.push(element) + } + + fn mutate<'a>(&'a mut self) -> &mut WidgetPod> { + self.mutate() + } + + fn delete(&mut self, n: usize) { + self.delete(n) + } + + fn len(&self) -> usize { + self.len() + } +} diff --git a/crates/xilem_masonry/src/view/button.rs b/crates/xilem_masonry/src/view/button.rs index 0ed7f0bdd..fbdc3fffb 100644 --- a/crates/xilem_masonry/src/view/button.rs +++ b/crates/xilem_masonry/src/view/button.rs @@ -1,6 +1,6 @@ use masonry::{widget::WidgetMut, ArcStr, WidgetPod}; -use crate::{ChangeFlags, Id, MasonryView, ViewCx}; +use crate::{ChangeFlags, MasonryView, ViewCx, ViewId}; pub fn button(label: impl Into, callback: F) -> Button where @@ -28,7 +28,7 @@ where } fn message( &self, - _id_path: &[Id], + _id_path: &[ViewId], // TODO: Ensure is masonry button pressed action? _message: Box, app_state: &mut State, From 5fd94746c0f16f3c7c872348cbdf2f67bb072529 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:23:00 +0100 Subject: [PATCH 03/12] Implement a flex view --- crates/masonry/src/widget/flex.rs | 28 +++--- crates/xilem_masonry/examples/mason.rs | 18 +++- crates/xilem_masonry/src/sequence.rs | 29 +++--- crates/xilem_masonry/src/vec_splice.rs | 13 ++- crates/xilem_masonry/src/view/flex.rs | 120 +++++++++++++++++++++++++ crates/xilem_masonry/src/view/mod.rs | 3 + 6 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 crates/xilem_masonry/src/view/flex.rs diff --git a/crates/masonry/src/widget/flex.rs b/crates/masonry/src/widget/flex.rs index ad009a100..b83f4c821 100644 --- a/crates/masonry/src/widget/flex.rs +++ b/crates/masonry/src/widget/flex.rs @@ -150,21 +150,20 @@ impl Flex { /// Builder-style variant of `add_child`. /// /// Convenient for assembling a group of widgets in a single expression. - pub fn with_child(mut self, child: impl Widget) -> Self { - let child = Child::Fixed { - widget: WidgetPod::new(Box::new(child)), - alignment: None, - }; - self.children.push(child); - self + pub fn with_child(self, child: impl Widget) -> Self { + self.with_child_pod(WidgetPod::new(Box::new(child))) } /// Builder-style variant of `add_child`, that takes the id that the child will have. /// /// Useful for unit tests. - pub fn with_child_id(mut self, child: impl Widget, id: WidgetId) -> Self { + pub fn with_child_id(self, child: impl Widget, id: WidgetId) -> Self { + self.with_child_pod(WidgetPod::new_with_id(Box::new(child), id)) + } + + pub fn with_child_pod(mut self, widget: WidgetPod>) -> Self { let child = Child::Fixed { - widget: WidgetPod::new_with_id(Box::new(child), id), + widget, alignment: None, }; self.children.push(child); @@ -234,6 +233,10 @@ impl Flex { self.children.push(new_child); self } + + pub fn len(&self) -> usize { + self.children.len() + } } // --- Mutate live Flex - WidgetMut --- @@ -367,8 +370,13 @@ impl<'a> FlexMut<'a> { /// /// [`with_child`]: Flex::with_child pub fn insert_child(&mut self, idx: usize, child: impl Widget) { + self.insert_child_pod(idx, WidgetPod::new(Box::new(child))) + } + + /// Add a non-flex child widget. + pub fn insert_child_pod(&mut self, idx: usize, widget: WidgetPod>) { let child = Child::Fixed { - widget: WidgetPod::new(Box::new(child)), + widget, alignment: None, }; self.widget.children.insert(idx, child); diff --git a/crates/xilem_masonry/examples/mason.rs b/crates/xilem_masonry/examples/mason.rs index 17b723e43..01542c6e3 100644 --- a/crates/xilem_masonry/examples/mason.rs +++ b/crates/xilem_masonry/examples/mason.rs @@ -1,6 +1,7 @@ // On Windows platform, don't show a console when opening the app. #![windows_subsystem = "windows"] -use xilem_masonry::view::button; +use masonry::ArcStr; +use xilem_masonry::view::{button, flex}; use xilem_masonry::{MasonryView, Xilem}; fn app_logic(data: &mut AppData) -> impl MasonryView { @@ -14,9 +15,20 @@ fn app_logic(data: &mut AppData) -> impl MasonryView { // The actual UI Code starts here - button(label, |data: &mut AppData| { + let mut sequence = vec![ + the_button(label, 1), + the_button("Decrement".to_string(), -1), + ]; + for x in 0..count { + sequence.push(the_button(format!("+{}", x), x)); + } + flex(sequence) +} + +fn the_button(label: impl Into, count: i32) -> impl MasonryView { + button(label, move |data: &mut AppData| { println!("clicked"); - data.count += 1; + data.count += count; }) } diff --git a/crates/xilem_masonry/src/sequence.rs b/crates/xilem_masonry/src/sequence.rs index 44e545329..d0056248a 100644 --- a/crates/xilem_masonry/src/sequence.rs +++ b/crates/xilem_masonry/src/sequence.rs @@ -1,6 +1,6 @@ use std::num::NonZeroU64; -use masonry::{Widget, WidgetCtx, WidgetPod}; +use masonry::{widget::WidgetMut, Widget, WidgetPod}; use crate::{ChangeFlags, MasonryView, MessageResult, ViewCx, ViewId}; @@ -8,10 +8,12 @@ pub trait ElementSplice { /// Insert a new element at the current index in the resulting collection (and increment the index by 1) fn push(&mut self, element: WidgetPod>); /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) - fn mutate<'a>(&'a mut self) -> &mut WidgetPod>; + // TODO: This should actually return `WidgetMut`, but that isn't supported in Masonry itself yet + fn mutate<'a>(&'a mut self) -> WidgetMut>; /// Delete the next n existing elements (this doesn't change the index) fn delete(&mut self, n: usize); /// Current length of the elements collection + // TODO: Is `len` needed? fn len(&self) -> usize; } @@ -32,7 +34,6 @@ pub trait ViewSequence: Send + 'static { &self, cx: &mut ViewCx, prev: &Self, - widget_ctx: &mut WidgetCtx<'_>, elements: &mut dyn ElementSplice, ) -> ChangeFlags; @@ -51,8 +52,14 @@ pub trait ViewSequence: Send + 'static { fn count(&self) -> usize; } -struct WasAView; -struct WasASequence; +/// Workaround for trait ambiguity +/// +/// These need to be public for type inference +#[doc(hidden)] +pub struct WasAView; +#[doc(hidden)] +/// See [`WasAView`] +pub struct WasASequence; impl> ViewSequence for View @@ -66,12 +73,10 @@ impl> ViewSequence, elements: &mut dyn ElementSplice, ) -> ChangeFlags { - let element = elements.mutate(); - let mut mutable = widget_ctx.get_mut(element); - let downcast = mutable.downcast::(); + let mut element = elements.mutate(); + let downcast = element.downcast::(); if let Some(element) = downcast { self.rebuild(cx, prev, element) @@ -110,11 +115,10 @@ impl> &self, cx: &mut ViewCx, prev: &Self, - widget_ctx: &mut WidgetCtx<'_>, elements: &mut dyn ElementSplice, ) -> ChangeFlags { match (self, prev) { - (Some(this), Some(prev)) => this.rebuild(cx, prev, widget_ctx, elements), + (Some(this), Some(prev)) => this.rebuild(cx, prev, elements), (None, Some(prev)) => { let count = prev.count(); elements.delete(count); @@ -166,7 +170,6 @@ impl> ViewSequence, elements: &mut dyn ElementSplice, ) -> ChangeFlags { let mut changed = ChangeFlags::UNCHANGED; @@ -174,7 +177,7 @@ impl> ViewSequence(id), |cx| { - let el_changed = child.rebuild(cx, child_prev, widget_ctx, elements); + let el_changed = child.rebuild(cx, child_prev, elements); changed.changed |= el_changed.changed; }); } diff --git a/crates/xilem_masonry/src/vec_splice.rs b/crates/xilem_masonry/src/vec_splice.rs index ac744fe6c..fef1dde23 100644 --- a/crates/xilem_masonry/src/vec_splice.rs +++ b/crates/xilem_masonry/src/vec_splice.rs @@ -1,7 +1,7 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 -use masonry::WidgetPod; +use masonry::widget::WidgetMut; use crate::ElementSplice; @@ -77,6 +77,13 @@ impl<'a, 'b, T> VecSplice<'a, 'b, T> { self.len() == 0 } + pub fn as_vec) -> R>(&mut self, f: F) -> R { + self.clear_tail(); + let ret = f(self.v); + self.ix = self.v.len(); + ret + } + fn clear_tail(&mut self) { if self.v.len() > self.ix { let removed = self.v.splice(self.ix.., []).rev(); @@ -90,8 +97,8 @@ impl ElementSplice for VecSplice<'_, '_, masonry::WidgetPod(&'a mut self) -> &mut WidgetPod> { - self.mutate() + fn mutate<'a>(&'a mut self) -> WidgetMut> { + unreachable!("VecSplice can only be used for `build`, not rebuild") } fn delete(&mut self, n: usize) { diff --git a/crates/xilem_masonry/src/view/flex.rs b/crates/xilem_masonry/src/view/flex.rs new file mode 100644 index 000000000..59231bb5e --- /dev/null +++ b/crates/xilem_masonry/src/view/flex.rs @@ -0,0 +1,120 @@ +use std::marker::PhantomData; + +use masonry::{ + widget::{self, WidgetMut}, + Widget, WidgetPod, +}; + +use crate::{ElementSplice, MasonryView, VecSplice, ViewSequence}; + +// TODO: Allow configuring flex properties. I think this actually needs its own view trait? +pub fn flex(sequence: VT) -> Flex { + Flex { + phantom: PhantomData, + sequence, + } +} + +pub struct Flex { + sequence: VT, + phantom: PhantomData Marker>, +} + +impl MasonryView for Flex +where + Seq: ViewSequence, +{ + type Element = widget::Flex; + + fn build(&self, cx: &mut crate::ViewCx) -> masonry::WidgetPod { + let mut elements = Vec::new(); + let mut scratch = Vec::new(); + let mut splice = VecSplice::new(&mut elements, &mut scratch); + self.sequence.build(cx, &mut splice); + let mut view = widget::Flex::column(); + debug_assert!( + scratch.is_empty(), + // TODO: Not at all confident about this, but linear_layout makes this assumption + "ViewSequence shouldn't leave splice in strange state" + ); + for item in elements.drain(..) { + view = view.with_child_pod(item); + } + WidgetPod::new(view) + } + + fn message( + &self, + id_path: &[crate::ViewId], + message: Box, + app_state: &mut State, + ) -> crate::MessageResult { + self.sequence.message(id_path, message, app_state) + } + + fn rebuild( + &self, + cx: &mut crate::ViewCx, + prev: &Self, + // _id: &mut Id, + element: widget::WidgetMut, + ) -> crate::ChangeFlags { + let mut splice = FlexSplice { ix: 0, element }; + self.sequence.rebuild(cx, &prev.sequence, &mut splice) + } +} + +struct FlexSplice<'w> { + ix: usize, + element: WidgetMut<'w, widget::Flex>, +} + +impl ElementSplice for FlexSplice<'_> { + fn push(&mut self, element: WidgetPod>) { + self.element.insert_child_pod(self.ix, element); + self.ix += 1; + } + + fn mutate<'a>(&'a mut self) -> WidgetMut<'a, Box> { + #[cfg(debug_assertions)] + let mut iterations = 0; + #[cfg(debug_assertions)] + let max = self.element.len(); + loop { + #[cfg(debug_assertions)] + { + if iterations > max { + panic!("Got into infinite loop in FlexSplice::mutate"); + } + iterations += 1; + } + let child = self.element.child_mut(self.ix); + if child.is_some() { + break; + } + self.ix += 1; + } + let child = self.element.child_mut(self.ix).unwrap(); + self.ix += 1; + child + } + + fn delete(&mut self, n: usize) { + let mut deleted_count = 0; + while deleted_count < n { + { + // TODO: use a drain/retain type method + let element = self.element.child_mut(self.ix); + if element.is_some() { + deleted_count += 1; + } + } + self.element.remove_child(self.ix) + } + } + + fn len(&self) -> usize { + // This is not correct because of the spacer items. Is `len` actually needed? + self.element.len() - self.ix + } +} diff --git a/crates/xilem_masonry/src/view/mod.rs b/crates/xilem_masonry/src/view/mod.rs index ddf6d53ab..42ffb8cc4 100644 --- a/crates/xilem_masonry/src/view/mod.rs +++ b/crates/xilem_masonry/src/view/mod.rs @@ -1,2 +1,5 @@ mod button; pub use button::*; + +mod flex; +pub use flex::*; From 7355a71b4b933b1df6900e0fb7231874fe81a4a8 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:01:37 +0100 Subject: [PATCH 04/12] Fix clippy Some of these suggestions are kind of bad but that's by-the-by --- crates/masonry/src/widget/flex.rs | 4 ++++ crates/xilem_masonry/src/lib.rs | 16 +++++++++++----- crates/xilem_masonry/src/sequence.rs | 11 +++++++---- crates/xilem_masonry/src/vec_splice.rs | 6 +++--- crates/xilem_masonry/src/view/flex.rs | 6 +++--- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/crates/masonry/src/widget/flex.rs b/crates/masonry/src/widget/flex.rs index b83f4c821..2f289f132 100644 --- a/crates/masonry/src/widget/flex.rs +++ b/crates/masonry/src/widget/flex.rs @@ -237,6 +237,10 @@ impl Flex { pub fn len(&self) -> usize { self.children.len() } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } // --- Mutate live Flex - WidgetMut --- diff --git a/crates/xilem_masonry/src/lib.rs b/crates/xilem_masonry/src/lib.rs index 392bff0b9..0ef328e5f 100644 --- a/crates/xilem_masonry/src/lib.rs +++ b/crates/xilem_masonry/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::comparison_chain)] use std::{any::Any, collections::HashMap}; use masonry::{ @@ -49,10 +50,10 @@ masonry::declare_widget!(RootWidgetMut, RootWidget); impl Widget for RootWidget { fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { - self.pod.on_pointer_event(ctx, event) + self.pod.on_pointer_event(ctx, event); } fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { - self.pod.on_text_event(ctx, event) + self.pod.on_text_event(ctx, event); } fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) { @@ -60,7 +61,7 @@ impl Widget for RootWidget { } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { - self.pod.lifecycle(ctx, event) + self.pod.lifecycle(ctx, event); } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { @@ -70,7 +71,7 @@ impl Widget for RootWidget { } fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { - self.pod.paint(ctx, scene) + self.pod.paint(ctx, scene); } fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { @@ -114,7 +115,8 @@ where let changed = next_view.rebuild(&mut self.view_cx, &self.current_view, element); if !changed.changed { - tracing::debug!("TODO: Skip some work?") + // Masonry manages all of this itself - ChangeFlags is probably not needed? + tracing::debug!("TODO: Skip some work?"); } self.current_view = next_view; } @@ -216,6 +218,10 @@ impl ChangeFlags { } pub struct ViewCx { + /// The map from a widgets id to its position in the View tree. + /// + /// This includes only the widgets which might send actions + /// This is currently never cleaned up widget_map: HashMap>, id_path: Vec, } diff --git a/crates/xilem_masonry/src/sequence.rs b/crates/xilem_masonry/src/sequence.rs index d0056248a..eefcee45a 100644 --- a/crates/xilem_masonry/src/sequence.rs +++ b/crates/xilem_masonry/src/sequence.rs @@ -4,12 +4,13 @@ use masonry::{widget::WidgetMut, Widget, WidgetPod}; use crate::{ChangeFlags, MasonryView, MessageResult, ViewCx, ViewId}; +#[allow(clippy::len_without_is_empty)] pub trait ElementSplice { /// Insert a new element at the current index in the resulting collection (and increment the index by 1) fn push(&mut self, element: WidgetPod>); /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) // TODO: This should actually return `WidgetMut`, but that isn't supported in Masonry itself yet - fn mutate<'a>(&'a mut self) -> WidgetMut>; + fn mutate(&mut self) -> WidgetMut>; /// Delete the next n existing elements (this doesn't change the index) fn delete(&mut self, n: usize); /// Current length of the elements collection @@ -91,7 +92,7 @@ impl> ViewSequence, app_state: &mut State, ) -> MessageResult { - self.message(&id_path, message, app_state) + self.message(id_path, message, app_state) } fn count(&self) -> usize { @@ -140,7 +141,7 @@ impl> app_state: &mut State, ) -> MessageResult { if let Some(this) = self { - this.message(&id_path, message, app_state) + this.message(id_path, message, app_state) } else { MessageResult::Stale(message) } @@ -163,7 +164,7 @@ impl> ViewSequence(id), |cx| child.build(cx, elements)); - }) + }); } fn rebuild( @@ -187,6 +188,8 @@ impl> ViewSequence prev.len() { + // This suggestion from clippy is kind of bad, because we use the absolute index in the id + #[allow(clippy::needless_range_loop)] for ix in prev.len()..n { let id_u64: u64 = ix.try_into().unwrap(); let id = NonZeroU64::new(id_u64 + 1).unwrap(); diff --git a/crates/xilem_masonry/src/vec_splice.rs b/crates/xilem_masonry/src/vec_splice.rs index fef1dde23..f7333c40e 100644 --- a/crates/xilem_masonry/src/vec_splice.rs +++ b/crates/xilem_masonry/src/vec_splice.rs @@ -94,15 +94,15 @@ impl<'a, 'b, T> VecSplice<'a, 'b, T> { impl ElementSplice for VecSplice<'_, '_, masonry::WidgetPod>> { fn push(&mut self, element: masonry::WidgetPod>) { - self.push(element) + self.push(element); } - fn mutate<'a>(&'a mut self) -> WidgetMut> { + fn mutate(&mut self) -> WidgetMut> { unreachable!("VecSplice can only be used for `build`, not rebuild") } fn delete(&mut self, n: usize) { - self.delete(n) + self.delete(n); } fn len(&self) -> usize { diff --git a/crates/xilem_masonry/src/view/flex.rs b/crates/xilem_masonry/src/view/flex.rs index 59231bb5e..a96cce337 100644 --- a/crates/xilem_masonry/src/view/flex.rs +++ b/crates/xilem_masonry/src/view/flex.rs @@ -38,7 +38,7 @@ where "ViewSequence shouldn't leave splice in strange state" ); for item in elements.drain(..) { - view = view.with_child_pod(item); + view = view.with_child_pod(item).with_default_spacer(); } WidgetPod::new(view) } @@ -75,7 +75,7 @@ impl ElementSplice for FlexSplice<'_> { self.ix += 1; } - fn mutate<'a>(&'a mut self) -> WidgetMut<'a, Box> { + fn mutate(&mut self) -> WidgetMut> { #[cfg(debug_assertions)] let mut iterations = 0; #[cfg(debug_assertions)] @@ -109,7 +109,7 @@ impl ElementSplice for FlexSplice<'_> { deleted_count += 1; } } - self.element.remove_child(self.ix) + self.element.remove_child(self.ix); } } From ff1b30a0e43c4ba57dc329f04081f6564f68eb93 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:13:21 +0100 Subject: [PATCH 05/12] Update CI to 1.77 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b9df9593..d0afb8cbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ env: # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still # come automatically. If the version specified here is no longer the latest stable version, # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. - RUST_STABLE_VER: "1.75" # In quotes because otherwise 1.70 would be interpreted as 1.7 + RUST_STABLE_VER: "1.77" # In quotes because otherwise 1.70 would be interpreted as 1.7 # Rationale # From 2c90b5dcce74f077325f61f05769391a065423b7 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:18:30 +0100 Subject: [PATCH 06/12] Skip masonry docs in CI --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0afb8cbf..fc24bd0c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,4 +105,5 @@ jobs: uses: Swatinem/rust-cache@v2 - name: cargo doc - run: cargo doc --workspace --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples + # We currently skip checking masonry's docs + run: cargo doc --workspace --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples --exclude masonry From a9f69a63a14f227233b2fcb576bc173fcd2c2166 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:22:49 +0100 Subject: [PATCH 07/12] Clippy, again --- crates/xilem_masonry/examples/mason.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/xilem_masonry/examples/mason.rs b/crates/xilem_masonry/examples/mason.rs index 01542c6e3..334e9cfa4 100644 --- a/crates/xilem_masonry/examples/mason.rs +++ b/crates/xilem_masonry/examples/mason.rs @@ -40,5 +40,5 @@ fn main() { let data = AppData { count: 0 }; let app = Xilem::new(data, app_logic); - app.run_windowed("First Example".into()).unwrap() + app.run_windowed("First Example".into()).unwrap(); } From cd00f02e4b1aca302fce8f40b65b860ae143497c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:29:26 +0100 Subject: [PATCH 08/12] Fix clippy --- crates/masonry/examples/hello.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/masonry/examples/hello.rs b/crates/masonry/examples/hello.rs index 939846bc2..648b1c1c7 100644 --- a/crates/masonry/examples/hello.rs +++ b/crates/masonry/examples/hello.rs @@ -27,7 +27,9 @@ impl AppDriver for Driver { Action::ButtonPressed => { println!("Hello"); } - _ => {} + action => { + eprintln!("Unexpected action {action:?}") + } } } } From 7e4f65b86ab4ce71958f62b3e89c98e0b458fa0a Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:42:57 +0100 Subject: [PATCH 09/12] Skip render snapshot tests --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc24bd0c9..4778fdee3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,11 @@ env: # come automatically. If the version specified here is no longer the latest stable version, # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. RUST_STABLE_VER: "1.77" # In quotes because otherwise 1.70 would be interpreted as 1.7 + # We do not run the masonry snapshot tests, because those require wgpu to be working + # (and specific font stacks at the moment) + # We will fix this + # See https://github.com/linebender/vello/pull/439 + SKIP_RENDER_SNAPSHOTS: 1 # Rationale # From 579c1171ee2243840d841e6428aff1ca7889f738 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:04:52 +0100 Subject: [PATCH 10/12] Skip testing which needs rendering on CI --- .github/workflows/ci.yml | 8 ++++---- crates/masonry/src/testing/harness.rs | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4778fdee3..1e4deae72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,11 @@ env: # come automatically. If the version specified here is no longer the latest stable version, # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. RUST_STABLE_VER: "1.77" # In quotes because otherwise 1.70 would be interpreted as 1.7 - # We do not run the masonry snapshot tests, because those require wgpu to be working - # (and specific font stacks at the moment) - # We will fix this - # See https://github.com/linebender/vello/pull/439 + # We do not run the masonry snapshot tests, because those require a specific font stack SKIP_RENDER_SNAPSHOTS: 1 + # We do not run the masonry render tests, because those require Vello rendering to be working + # See https://github.com/linebender/vello/pull/439 + SKIP_RENDER_TESTS: 1 # Rationale # diff --git a/crates/masonry/src/testing/harness.rs b/crates/masonry/src/testing/harness.rs index bfa90788f..e7305bd4d 100644 --- a/crates/masonry/src/testing/harness.rs +++ b/crates/masonry/src/testing/harness.rs @@ -7,7 +7,7 @@ use std::num::NonZeroUsize; use image::io::Reader as ImageReader; -use image::RgbaImage; +use image::{Rgba, RgbaImage}; use vello::util::RenderContext; use vello::{block_on_wgpu, RendererOptions}; use wgpu::{ @@ -230,6 +230,10 @@ impl TestHarness { // TODO - Should be async? /// Create a bitmap (an array of pixels), paint the window and return the bitmap as an 8-bits-per-channel RGB image. pub fn render(&mut self) -> RgbaImage { + let scene = self.render_root.redraw(); + if std::env::var("SKIP_RENDER_TESTS").is_ok_and(|it| !it.is_empty()) { + return RgbaImage::from_pixel(1, 1, Rgba([255, 255, 255, 255])); + } let mut context = RenderContext::new().expect("Got non-Send/Sync error from creating render context"); let device_id = @@ -250,8 +254,6 @@ impl TestHarness { ) .expect("Got non-Send/Sync error from creating renderer"); - let scene = self.render_root.redraw(); - // TODO - fix window_size let (width, height) = (self.window_size.width, self.window_size.height); let render_params = vello::RenderParams { From d3144862c642676b48744503d231ef3048855050 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:59:25 +0100 Subject: [PATCH 11/12] Add tuple sequences --- crates/xilem_masonry/examples/mason.rs | 26 ++--- crates/xilem_masonry/src/sequence.rs | 142 +++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 16 deletions(-) diff --git a/crates/xilem_masonry/examples/mason.rs b/crates/xilem_masonry/examples/mason.rs index 334e9cfa4..036b269fa 100644 --- a/crates/xilem_masonry/examples/mason.rs +++ b/crates/xilem_masonry/examples/mason.rs @@ -1,6 +1,6 @@ // On Windows platform, don't show a console when opening the app. #![windows_subsystem = "windows"] -use masonry::ArcStr; + use xilem_masonry::view::{button, flex}; use xilem_masonry::{MasonryView, Xilem}; @@ -15,21 +15,15 @@ fn app_logic(data: &mut AppData) -> impl MasonryView { // The actual UI Code starts here - let mut sequence = vec![ - the_button(label, 1), - the_button("Decrement".to_string(), -1), - ]; - for x in 0..count { - sequence.push(the_button(format!("+{}", x), x)); - } - flex(sequence) -} - -fn the_button(label: impl Into, count: i32) -> impl MasonryView { - button(label, move |data: &mut AppData| { - println!("clicked"); - data.count += count; - }) + let sequence = (0..count) + .map(|x| button(format!("+{x}"), move |data: &mut AppData| data.count += x)) + .collect::>(); + flex(( + button(label, |data: &mut AppData| data.count += 1), + button("Decrement", |data: &mut AppData| data.count -= 1), + button("Reset", |data: &mut AppData| data.count = 0), + sequence, + )) } struct AppData { diff --git a/crates/xilem_masonry/src/sequence.rs b/crates/xilem_masonry/src/sequence.rs index eefcee45a..eefcb5581 100644 --- a/crates/xilem_masonry/src/sequence.rs +++ b/crates/xilem_masonry/src/sequence.rs @@ -219,3 +219,145 @@ impl> ViewSequence ViewSequence for () { + fn build(&self, _: &mut ViewCx, _: &mut dyn ElementSplice) {} + + fn rebuild( + &self, + _cx: &mut ViewCx, + _prev: &Self, + _elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + ChangeFlags::UNCHANGED + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + _app_state: &mut T, + ) -> MessageResult { + tracing::warn!(?id_path, "Dispatched message to empty tuple"); + MessageResult::Stale(message) + } + + fn count(&self) -> usize { + 0 + } +} + +impl> ViewSequence + for (Seq0,) +{ + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) { + self.0.build(cx, elements) + } + + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + self.0.rebuild(cx, &prev.0, elements) + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + self.0.message(id_path, message, app_state) + } + + fn count(&self) -> usize { + self.0.count() + } +} + +const BASE_ID: NonZeroU64 = match NonZeroU64::new(1) { + Some(it) => it, + None => unreachable!(), +}; + +macro_rules! impl_view_tuple { + ( + $($marker: ident, $seq: ident, $idx: tt);+ + ) => { + impl< + State, + Action, + $( + $marker, + $seq: ViewSequence, + )+ + > ViewSequence for ($($seq,)+) + { + fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) { + $( + cx.with_id(ViewId::for_type::<$seq>(BASE_ID.saturating_add($idx)), |cx| { + self.$idx.build(cx, elements); + }); + )+ + } + + fn rebuild( + &self, + cx: &mut ViewCx, + prev: &Self, + elements: &mut dyn ElementSplice, + ) -> ChangeFlags { + let mut flags = ChangeFlags::UNCHANGED; + $( + cx.with_id(ViewId::for_type::<$seq>(BASE_ID.saturating_add($idx)), |cx| { + flags.changed |= self.$idx.rebuild(cx, &prev.$idx, elements).changed; + }); + )+ + flags + } + + fn message( + &self, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + let (start, rest) = id_path + .split_first() + .expect("Id path has elements for vector"); + let index_plus_one = start.routing_id().get(); + match index_plus_one - 1 { + $( + $idx => self.$idx.message(rest, message, app_state), + )+ + // TODO: Should not panic? Is this a dynamic viewsequence thing? + _ => unreachable!("Unexpected id path {start:?} in tuple"), + } + } + + fn count(&self) -> usize { + // Is there a way to do this which avoids the `+0`? + $(self.$idx.count()+)+ 0 + } + } + }; +} + +// We implement for tuples of length up to 15 +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14; M15, Seq15, 15); From 45d561bee75f1d7cd6858e1d89c9f08f26b7ebfa Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:23:47 +0100 Subject: [PATCH 12/12] Address review comments and clippy --- crates/xilem_masonry/src/sequence.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/xilem_masonry/src/sequence.rs b/crates/xilem_masonry/src/sequence.rs index eefcb5581..1b3e67cdb 100644 --- a/crates/xilem_masonry/src/sequence.rs +++ b/crates/xilem_masonry/src/sequence.rs @@ -251,7 +251,7 @@ impl> ViewSequence { impl< @@ -326,7 +328,7 @@ macro_rules! impl_view_tuple { ) -> MessageResult { let (start, rest) = id_path .split_first() - .expect("Id path has elements for vector"); + .expect("Id path has elements for tuple"); let index_plus_one = start.routing_id().get(); match index_plus_one - 1 { $( @@ -345,7 +347,7 @@ macro_rules! impl_view_tuple { }; } -// We implement for tuples of length up to 15 +// We implement for tuples of length up to 15. 0 and 1 are special cased to be more efficient impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1); impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2); impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3);