From 7be4dd2a2f8bf820fc19a3af2a00600d05e4d4c7 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:02:07 +0100 Subject: [PATCH] Xilem example for http cats API, requiring `worker`s and `image` component (#571) This example is inspired by: https://troz.net/post/2024/swiftui-mac-2024/ Current status: - Lists status code - Can select status code - Downloads image from status code - Shows image from status code This adds two new features to Xilem: - The worker view, which is the obvious extension to `task` for multiple operations - The `image` view, which just uses Masonry `Image`. It also fixes a the Masonry Image view's layout to use the already extant but unused method. --- Cargo.lock | 516 +++++++++++++++++- README.md | 6 +- masonry/src/widget/image.rs | 24 +- xilem/Cargo.toml | 32 +- xilem/README.md | 9 +- xilem/examples/http_cats.rs | 280 ++++++++++ xilem/resources/data/http_cats_status/LICENSE | 7 + .../resources/data/http_cats_status/README.md | 9 + .../data/http_cats_status/status.csv | 76 +++ xilem/src/view/image.rs | 84 +++ xilem/src/view/mod.rs | 6 + xilem/src/view/worker.rs | 153 ++++++ xilem_core/src/deferred.rs | 10 + 13 files changed, 1189 insertions(+), 23 deletions(-) create mode 100644 xilem/examples/http_cats.rs create mode 100644 xilem/resources/data/http_cats_status/LICENSE create mode 100644 xilem/resources/data/http_cats_status/README.md create mode 100644 xilem/resources/data/http_cats_status/status.csv create mode 100644 xilem/src/view/image.rs create mode 100644 xilem/src/view/worker.rs diff --git a/Cargo.lock b/Cargo.lock index 73335c408..5a751aeed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arrayref" version = "0.3.8" @@ -291,7 +297,7 @@ dependencies = [ "polling 2.8.0", "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] @@ -472,6 +478,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.5.3" @@ -1184,6 +1196,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.30" @@ -1545,6 +1566,92 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1639,6 +1746,16 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.25.2" @@ -1649,6 +1766,8 @@ dependencies = [ "byteorder-lite", "num-traits", "png", + "zune-core", + "zune-jpeg", ] [[package]] @@ -1702,6 +1821,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.11" @@ -1974,6 +2099,12 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1984,6 +2115,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "naga" version = "0.20.0" @@ -1998,7 +2141,7 @@ dependencies = [ "indexmap", "log", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror", @@ -2630,6 +2773,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls", + "socket2 0.5.7", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -2780,6 +2971,63 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roxmltree" version = "0.19.0" @@ -2798,6 +3046,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.37.27" @@ -2825,6 +3079,47 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.18" @@ -2908,6 +3203,18 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3037,6 +3344,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spawn_tasks" version = "0.0.0" @@ -3049,6 +3366,12 @@ dependencies = [ "xilem_web", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -3076,6 +3399,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "svg_fmt" version = "0.4.3" @@ -3125,6 +3454,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -3256,6 +3594,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "todomvc" version = "0.1.0" @@ -3277,7 +3630,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", + "bytes", + "libc", + "mio", "pin-project-lite", + "socket2 0.5.7", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", ] [[package]] @@ -3308,6 +3677,33 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.40" @@ -3395,6 +3791,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.24.0" @@ -3418,12 +3820,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -3442,6 +3859,23 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3514,6 +3948,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3715,6 +4158,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "wgpu" version = "0.20.1" @@ -3760,7 +4212,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "web-sys", @@ -3804,7 +4256,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror", "wasm-bindgen", @@ -3898,7 +4350,7 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -3924,6 +4376,17 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3933,6 +4396,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -4282,8 +4764,11 @@ name = "xilem" version = "0.1.0" dependencies = [ "accesskit", + "anyhow", + "image", "masonry", "profiling", + "reqwest", "smallvec", "time", "tokio", @@ -4482,6 +4967,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" @@ -4504,6 +4995,21 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "3.15.2" diff --git a/README.md b/README.md index b061c5c80..e68d46461 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,10 @@ cargo update -p package_name --precise 0.1.1 Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or ) -The font file (`RobotoFlex-Subset.ttf`) in `xilem/resources/fonts/roboto_flex/` is licensed solely as documented in that folder, -(and is not licensed under the Apache License, Version 2.0). +Some files used for examples are under different licenses: + +- The font file (`RobotoFlex-Subset.ttf`) in `xilem/resources/fonts/roboto_flex/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0). +- The data file (`status.csv`) in `xilem/resources/data/http_cats_status/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0). ## Contribution diff --git a/masonry/src/widget/image.rs b/masonry/src/widget/image.rs index f3e4823ac..019984d6e 100644 --- a/masonry/src/widget/image.rs +++ b/masonry/src/widget/image.rs @@ -22,6 +22,10 @@ use crate::{ /// A widget that renders a bitmap Image. /// /// The underlying image uses `Arc` for buffer data, making it cheap to clone. +/// +/// This currently uses bilinear interpolation, which falls down when the image is +/// larger than its layout size (e.g. it is in a [sized box](super::SizedBox) smaller +/// than the image size). pub struct Image { image_data: ImageBuf, fill: FillStrat, @@ -32,7 +36,6 @@ impl Image { /// Create an image drawing widget from an image buffer. /// /// By default, the Image will scale to fit its box constraints ([`FillStrat::Fill`]). - #[inline] pub fn new(image_data: ImageBuf) -> Self { Image { @@ -82,17 +85,16 @@ impl Widget for Image { // If either the width or height is constrained calculate a value so that the image fits // in the size exactly. If it is unconstrained by both width and height take the size of // the image. - let max = bc.max(); let image_size = Size::new(self.image_data.width as f64, self.image_data.height as f64); - let size = if bc.is_width_bounded() && !bc.is_height_bounded() { - let ratio = max.width / image_size.width; - Size::new(max.width, ratio * image_size.height) - } else if bc.is_height_bounded() && !bc.is_width_bounded() { - let ratio = max.height / image_size.height; - Size::new(ratio * image_size.width, max.height) - } else { - bc.constrain(image_size) - }; + if image_size.is_empty() { + let size = bc.min(); + trace!("Computed size: {}", size); + return size; + } + // This size logic has NOT been carefully considered, in particular with regards to self.fill. + // TODO: Carefully consider it + let size = + bc.constrain_aspect_ratio(image_size.height / image_size.width, image_size.width); trace!("Computed size: {}", size); size } diff --git a/xilem/Cargo.toml b/xilem/Cargo.toml index 3d28144f5..deca00c65 100644 --- a/xilem/Cargo.toml +++ b/xilem/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true repository.workspace = true homepage.workspace = true rust-version.workspace = true -exclude = ["/resources/fonts/roboto_flex/"] +exclude = ["/resources/fonts/roboto_flex/", "/resources/data/http_cats_status/"] [package.metadata.docs.rs] all-features = true @@ -36,6 +36,16 @@ path = "examples/calc.rs" # cdylib is required for cargo-apk crate-type = ["cdylib"] +[[example]] +name = "http_cats" + +[[example]] +name = "http_cats_android" +path = "examples/http_cats.rs" +# cdylib is required for cargo-apk +crate-type = ["cdylib"] + + [[example]] name = "stopwatch" @@ -65,14 +75,28 @@ tracing.workspace = true vello.workspace = true smallvec.workspace = true accesskit.workspace = true -tokio = { version = "1.39.1", features = ["rt", "rt-multi-thread", "time"] } +tokio = { version = "1.39.1", features = [ + "rt", + "rt-multi-thread", + "time", + "sync", +] } [dev-dependencies] # Used for `variable_clock` time = { workspace = true, features = ["local-offset"] } +# Used for http_cats +reqwest = { version = "0.12.7", default-features = false, features = [ + # We use rustls as Android doesn't ship with openssl + # and this is likely to be easiest to get working. + "rustls-tls", +] } +image = { workspace = true, features = ["jpeg"] } + # Make wgpu use tracing for its spans. profiling = { version = "1.0.15", features = ["profile-with-tracing"] } +anyhow = "1.0.86" [target.'cfg(target_os = "android")'.dev-dependencies] winit = { features = ["android-native-activity"], workspace = true } @@ -81,3 +105,7 @@ winit = { features = ["android-native-activity"], workspace = true } # Do not use when releasing a production app. [package.metadata.android.application] debuggable = true + +[[package.metadata.android.uses_permission]] +# Needed for http_cats +name = "android.permission.INTERNET" diff --git a/xilem/README.md b/xilem/README.md index b7e5cb9ed..41b48cb57 100644 --- a/xilem/README.md +++ b/xilem/README.md @@ -64,9 +64,12 @@ Unless you explicitly state otherwise, any contribution intentionally submitted Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or ) -The font file (`RobotoFlex-Subset.ttf`) in `resources/fonts/roboto_flex/` is licensed solely as documented in that folder, -(and is not licensed under the Apache License, Version 2.0). -Note that this file is *not* distributed with the. +Some files used for examples are under different licenses: + +* The font file (`RobotoFlex-Subset.ttf`) in `resources/fonts/roboto_flex/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0). +* The data file (`status.csv`) in `resources/data/http_cats_status/` is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0). + +Note that these files are *not* distributed with the released crate. [Masonry]: https://crates.io/crates/masonry [Druid]: https://crates.io/crates/druid diff --git a/xilem/examples/http_cats.rs b/xilem/examples/http_cats.rs new file mode 100644 index 000000000..121c6658b --- /dev/null +++ b/xilem/examples/http_cats.rs @@ -0,0 +1,280 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! An example demonstrating the use of Async web requests in Xilem to access the API. +//! This also demonstrates image loading. + +use std::sync::Arc; + +use vello::peniko::{Blob, Image}; +use winit::{dpi::LogicalSize, error::EventLoopError, window::Window}; +use xilem::{ + view::{ + button, flex, image, portal, prose, sized_box, spinner, worker, Axis, CrossAxisAlignment, + FlexExt, FlexSpacer, + }, + Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem, +}; +use xilem_core::{fork, one_of::OneOf3}; + +/// The main state of the application. +struct HttpCats { + statuses: Vec, + // The currently active (http status) code. + selected_code: Option, +} + +#[derive(Debug)] +struct Status { + code: u32, + message: &'static str, + image: ImageState, +} + +#[derive(Debug)] +/// The state of the download of an image from a URL +enum ImageState { + NotRequested, + Pending, + // Error, + Available(Image), +} + +impl HttpCats { + fn view(&mut self) -> impl WidgetView { + let left_column = portal(flex(( + prose("Status"), + self.statuses + .iter_mut() + .map(Status::list_view) + .collect::>(), + ))); + let (info_area, worker_value) = if let Some(selected_code) = self.selected_code { + if let Some(selected_status) = + self.statuses.iter_mut().find(|it| it.code == selected_code) + { + // If we haven't requested the image yet, make sure we do so. + let value = match selected_status.image { + ImageState::NotRequested => { + // TODO: Should a view_function be editing `self`? + // This feels too imperative. + selected_status.image = ImageState::Pending; + Some(selected_code) + } + // If the image is pending, that means that worker already knows about it. + // We don't set the requested code to `selected_code` here because we could have been on + // a different view in-between, so we don't want to request the same image twice. + ImageState::Pending => None, + ImageState::Available(_) => None, + }; + (OneOf3::A(selected_status.details_view()), value) + } else { + ( + OneOf3::B( + prose(format!( + "Status code {selected_code} selected, but this was not found." + )) + .alignment(TextAlignment::Middle) + .brush(Color::YELLOW), + ), + None, + ) + } + } else { + ( + OneOf3::C( + prose("No selection yet made. Select an item from the sidebar to continue.") + .alignment(TextAlignment::Middle), + ), + None, + ) + }; + + // TODO: Should `web_image` be a built-in component? + + fork( + flex(( + // Add padding to the top for Android. Still a horrible hack + FlexSpacer::Fixed(40.), + flex(( + left_column.flex(1.), + portal(sized_box(info_area).expand_width()).flex(1.), + )) + .direction(Axis::Horizontal) + .cross_axis_alignment(CrossAxisAlignment::Fill) + .must_fill_major_axis(true) + .flex(1.), + )) + .must_fill_major_axis(true) + .cross_axis_alignment(CrossAxisAlignment::Fill), + worker( + worker_value, + |proxy, mut rx| async move { + while let Some(request) = rx.recv().await { + if let Some(code) = request { + let proxy = proxy.clone(); + tokio::task::spawn(async move { + let url = format!("https://http.cat/{code}"); + let result = image_from_url(&url).await; + match result { + // We choose not to handle the case where the event loop has ended + Ok(image) => drop(proxy.message((code, image))), + // TODO: Report in the frontend + Err(err) => { + tracing::warn!( + "Loading image for HTTP status code {code} from {url} failed: {err:?}" + ); + } + } + }); + } + } + }, + |state: &mut HttpCats, (code, image): (u32, Image)| { + if let Some(status) = state.statuses.iter_mut().find(|it| it.code == code) { + status.image = ImageState::Available(image); + } else { + // TODO: Error handling? + } + }, + ), + ) + } +} + +/// Load a [`vello::peniko::Image`] from the given url. +async fn image_from_url(url: &str) -> anyhow::Result { + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + let image = image::load_from_memory(&bytes)?.into_rgba8(); + let width = image.width(); + let height = image.height(); + let data = image.into_vec(); + Ok(Image::new( + Blob::new(Arc::new(data)), + vello::peniko::Format::Rgba8, + width, + height, + )) +} + +impl Status { + fn list_view(&mut self) -> impl WidgetView { + let code = self.code; + flex(( + // TODO: Reduce allocations here? + prose(self.code.to_string()), + prose(self.message), + FlexSpacer::Flex(1.), + // TODO: Spinner if image pending? + // TODO: Tick if image loaded? + button("Select", move |state: &mut HttpCats| { + state.selected_code = Some(code); + }), + FlexSpacer::Fixed(masonry::theme::SCROLLBAR_WIDTH), + )) + .direction(Axis::Horizontal) + } + + fn details_view(&mut self) -> impl WidgetView { + let image = match &self.image { + ImageState::NotRequested => OneOf3::A( + prose("Failed to start fetching image. This is a bug!") + .alignment(TextAlignment::Middle), + ), + ImageState::Pending => OneOf3::B(sized_box(spinner()).width(80.).height(80.)), + // TODO: Alt text? + ImageState::Available(image_data) => OneOf3::C(image(image_data)), + }; + flex(( + prose(format!("HTTP Status Code: {}", self.code)).alignment(TextAlignment::Middle), + prose(self.message) + .text_size(20.) + .alignment(TextAlignment::Middle), + FlexSpacer::Fixed(10.), + image, + // TODO: Overlay on top of the image? + // HACK: Trailing spaces workaround scrollbar covering content + prose("Copyright ©️ https://http.cat ").alignment(TextAlignment::End), + )) + .main_axis_alignment(xilem::view::MainAxisAlignment::Start) + .cross_axis_alignment(CrossAxisAlignment::Fill) + } +} + +fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> { + let data = HttpCats { + statuses: Status::parse_file(), + selected_code: None, + }; + + let app = Xilem::new(data, HttpCats::view); + let min_window_size = LogicalSize::new(200., 200.); + + let window_attributes = Window::default_attributes() + .with_title("HTTP cats") + .with_resizable(true) + .with_min_inner_size(min_window_size); + + app.run_windowed_in(event_loop, window_attributes) +} + +impl Status { + /// Parse the supported HTTP cats. + fn parse_file() -> Vec { + let mut lines = STATUS_CODES_CSV.lines(); + let first_line = lines.next(); + assert_eq!(first_line, Some("code,message")); + lines.flat_map(Status::parse_single).collect() + } + + fn parse_single(line: &'static str) -> Option { + let (code, message) = line.split_once(',')?; + Some(Self { + code: code.parse().ok()?, + message: message.trim(), + image: ImageState::NotRequested, + }) + } +} + +/// The status codes supported by , used under the MIT license. +/// Full details can be found in `xilem/resources/data/http_cats_status/README.md` from +/// the workspace root. +const STATUS_CODES_CSV: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/resources/data/http_cats_status/status.csv", +)); + +#[cfg(not(target_os = "android"))] +#[allow(dead_code)] +// This is treated as dead code by the Android version of the example, but is actually live +// This hackery is required because Cargo doesn't care to support this use case, of one +// example which works across Android and desktop +fn main() -> Result<(), EventLoopError> { + run(EventLoop::with_user_event()) +} + +// Boilerplate code for android: Identical across all applications + +#[cfg(target_os = "android")] +// Safety: We are following `android_activity`'s docs here +// We believe that there are no other declarations using this name in the compiled objects here +#[allow(unsafe_code)] +#[no_mangle] +fn android_main(app: winit::platform::android::activity::AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + let mut event_loop = EventLoop::with_user_event(); + event_loop.with_android_app(app); + + run(event_loop).expect("Can create app"); +} + +// TODO: This is a hack because of how we handle our examples in Cargo.toml +// Ideally, we change Cargo to be more sensible here? +#[cfg(target_os = "android")] +#[allow(dead_code)] +fn main() { + unreachable!() +} diff --git a/xilem/resources/data/http_cats_status/LICENSE b/xilem/resources/data/http_cats_status/LICENSE new file mode 100644 index 000000000..edd070c1b --- /dev/null +++ b/xilem/resources/data/http_cats_status/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2015 Rogério Vicente + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/xilem/resources/data/http_cats_status/README.md b/xilem/resources/data/http_cats_status/README.md new file mode 100644 index 000000000..2beb43396 --- /dev/null +++ b/xilem/resources/data/http_cats_status/README.md @@ -0,0 +1,9 @@ +# Http Cats status data + +The statuses from . +These were extracted from on 2024-09-03, +specifically as at commit [`a92bb0415ef92179be6b4d34312956c7ad608d85`](https://github.com/httpcats/http.cat/blob/a92bb0415ef92179be6b4d34312956c7ad608d85/lib/statuses.js ). + +## License + +These are licensed solely under the MIT license, as found in [LICENSE](./LICENSE). diff --git a/xilem/resources/data/http_cats_status/status.csv b/xilem/resources/data/http_cats_status/status.csv new file mode 100644 index 000000000..3258d36ce --- /dev/null +++ b/xilem/resources/data/http_cats_status/status.csv @@ -0,0 +1,76 @@ +code,message +100,Continue +101,Switching Protocols +102,Processing +103,Early Hints +200,OK +201,Created +202,Accepted +203,Non-Authoritative Information +204,No Content +206,Partial Content +205,Reset Content +207,Multi-Status +208,Already Reported +214,Transformation Applied +226,IM Used +300,Multiple Choices +301,Moved Permanently +302,Found +303,See Other +304,Not Modified +305,Use Proxy +307,Temporary Redirect +308,Permanent Redirect +400,Bad Request +401,Unauthorized +402,Payment Required +403,Forbidden +404,Not Found +405,Method Not Allowed +406,Not Acceptable +407,Proxy Authentication Required +408,Request Timeout +409,Conflict +410,Gone +411,Length Required +412,Precondition Failed +413,Payload Too Large +414,Request-URI Too Long +415,Unsupported Media Type +416,Request Range Not Satisfiable +417,Expectation Failed +418,I’m a teapot +420,Enhance Your Calm +421,Misdirected Request +422,Unprocessable Entity +423,Locked +424,Failed Dependency +425,Too Early +426,Upgrade Required +428,Precondition Required +429,Too Many Requests +431,Request Header Fields Too Large +444,No Response +450,Blocked by Windows Parental Controls +451,Unavailable For Legal Reasons +497,HTTP Request Sent to HTTPS Port +498,Token expired/invalid +499,Client Closed Request +500,Internal Server Error +501,Not Implemented +502,Bad Gateway +503,Service Unavailable +504,Gateway Timeout +506,Variant Also Negotiates +507,Insufficient Storage +508,Loop Detected +509,Bandwidth Limit Exceeded +510,Not Extended +511,Network Authentication Required +521,Web Server Is Down +522,Connection Timed Out +523,Origin Is Unreachable +525,SSL Handshake Failed +530,Site Frozen +599,Network Connect Timeout Error diff --git a/xilem/src/view/image.rs b/xilem/src/view/image.rs new file mode 100644 index 000000000..f114fb70d --- /dev/null +++ b/xilem/src/view/image.rs @@ -0,0 +1,84 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The bitmap image widget. + +use masonry::widget::{self, FillStrat}; +use xilem_core::{Mut, ViewMarker}; + +use crate::{MessageResult, Pod, View, ViewCtx, ViewId}; + +/// Displays the bitmap `image`. +/// +/// By default, the Image will scale to fit its box constraints ([`FillStrat::Fill`]). +/// To configure this, call [`fill`](Image::fill) on the returned value. +/// +/// Corresponds to the [`Image`](widget::Image) widget. +/// +/// It is not currently supported to use a GPU-resident [texture](vello::wgpu::Texture) in this widget. +/// See [#gpu>vello adding wgpu texture buffers to scene](https://xi.zulipchat.com/#narrow/stream/197075-gpu/topic/vello.20adding.20wgpu.20texture.20buffers.20to.20scene) +/// for discussion. +pub fn image(image: &vello::peniko::Image) -> Image { + Image { + // Image only contains a `Blob` and Copy fields, and so is cheap to clone. + // We take by reference as we expect all users of this API will need to clone, and it's + // easier than documenting that cloning is cheap. + image: image.clone(), + fill: FillStrat::default(), + } +} + +/// The [`View`] created by [`image`]. +/// +/// See `image`'s docs for more details. +pub struct Image { + image: vello::peniko::Image, + fill: FillStrat, +} + +impl Image { + /// Specify the fill strategy. + pub fn fill(mut self, fill: FillStrat) -> Self { + self.fill = fill; + self + } +} + +impl ViewMarker for Image {} +impl View for Image { + type Element = Pod; + type ViewState = (); + + fn build(&self, _: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + (Pod::new(widget::Image::new(self.image.clone())), ()) + } + + fn rebuild<'el>( + &self, + prev: &Self, + (): &mut Self::ViewState, + _: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + if prev.fill != self.fill { + element.set_fill_mode(self.fill); + } + if prev.image != self.image { + element.set_image_data(self.image.clone()); + } + element + } + + fn teardown(&self, (): &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {} + + fn message( + &self, + (): &mut Self::ViewState, + _: &[ViewId], + message: xilem_core::DynMessage, + _: &mut State, + ) -> MessageResult { + tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug"); + MessageResult::Stale(message) + } +} diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index 5b14f8d5a..5b8485464 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -4,6 +4,9 @@ mod task; pub use task::*; +mod worker; +pub use worker::*; + mod button; pub use button::*; @@ -19,6 +22,9 @@ pub use sized_box::*; mod spinner; pub use spinner::*; +mod image; +pub use image::*; + mod label; pub use label::*; diff --git a/xilem/src/view/worker.rs b/xilem/src/view/worker.rs new file mode 100644 index 000000000..6a922f9e7 --- /dev/null +++ b/xilem/src/view/worker.rs @@ -0,0 +1,153 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{future::Future, marker::PhantomData, sync::Arc}; + +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; +use xilem_core::{ + DynMessage, Message, MessageProxy, NoElement, View, ViewId, ViewMarker, ViewPathTracker, +}; + +use crate::ViewCtx; + +/// Launch a task which will run until the view is no longer in the tree. +/// `init_future` is given a [`MessageProxy`], which it will store in the future it returns. +/// This `MessageProxy` can be used to send a message to `on_event`, which can then update +/// the app's state. +/// +/// For exampe, this can be used with the time functions in [`crate::tokio::time`]. +/// +/// Note that this task will not be updated if the view is rebuilt, so `init_future` +/// cannot capture. +// TODO: More thorough documentation. +/// See [`run_once`](crate::core::run_once) for details. +pub fn worker( + value: V, + init_future: F, + on_response: H, +) -> Worker +where + F: Fn(MessageProxy, UnboundedReceiver) -> Fut, + Fut: Future + Send + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + const { + assert!( + core::mem::size_of::() == 0, + "`worker` will not be ran again when its captured variables are updated.\n\ + To ignore this warning, use `worker_raw`. + To provide an updating value to this task, use the " + ); + }; + Worker { + value, + init_future, + on_response, + message: PhantomData, + } +} + +/// Launch a worker which will run until the view is no longer in the tree. +/// +/// This is [`worker`] without the capturing rules. +/// See `worker` for full documentation. +pub fn worker_raw( + value: V, + init_future: F, + on_response: H, +) -> Worker +where + F: Fn(MessageProxy, UnboundedReceiver) -> Fut, + Fut: Future + Send + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + Worker { + value, + init_future, + on_response, + message: PhantomData, + } +} + +pub struct Worker { + init_future: F, + value: V, + on_response: H, + message: PhantomData M>, +} + +#[doc(hidden)] // Implementation detail, public because of trait visibility rules +pub struct WorkerState { + handle: JoinHandle<()>, + sender: UnboundedSender, +} + +impl ViewMarker for Worker {} + +impl View for Worker +where + F: Fn(MessageProxy, UnboundedReceiver) -> Fut + 'static, + V: Send + PartialEq + Clone + 'static, + Fut: Future + Send + 'static, + H: Fn(&mut State, M) -> Action + 'static, + M: Message + 'static, +{ + type Element = NoElement; + + type ViewState = WorkerState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let path: Arc<[ViewId]> = ctx.view_path().into(); + + let proxy = ctx.proxy.clone(); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + // No opportunity for the channel to be closed. + tx.send(self.value.clone()).unwrap(); + let handle = ctx + .runtime() + .spawn((self.init_future)(MessageProxy::new(proxy, path), rx)); + (NoElement, WorkerState { handle, sender: tx }) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + _: &mut ViewCtx, + (): xilem_core::Mut<'el, Self::Element>, + ) -> xilem_core::Mut<'el, Self::Element> { + if self.value != prev.value { + // TODO: Error handling + drop(view_state.sender.send(self.value.clone())); + } + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + _: &mut ViewCtx, + _: xilem_core::Mut<'_, Self::Element>, + ) { + view_state.handle.abort(); + } + + fn message( + &self, + _: &mut Self::ViewState, + id_path: &[xilem_core::ViewId], + message: DynMessage, + app_state: &mut State, + ) -> xilem_core::MessageResult { + debug_assert!( + id_path.is_empty(), + "id path should be empty in Task::message" + ); + let message = message.downcast::().unwrap(); + xilem_core::MessageResult::Action((self.on_response)(app_state, *message)) + } +} diff --git a/xilem_core/src/deferred.rs b/xilem_core/src/deferred.rs index dc1af783a..79d880cc4 100644 --- a/xilem_core/src/deferred.rs +++ b/xilem_core/src/deferred.rs @@ -55,6 +55,16 @@ pub struct MessageProxy { message: PhantomData, } +impl Clone for MessageProxy { + fn clone(&self) -> Self { + Self { + proxy: self.proxy.clone(), + path: self.path.clone(), + message: PhantomData, + } + } +} + impl MessageProxy { /// Create a new `MessageProxy` pub fn new(proxy: Arc>, path: Arc<[ViewId]>) -> Self {