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.
This commit is contained in:
Daniel McNab 2024-09-03 20:02:07 +01:00 committed by GitHub
parent 5944aa5d0b
commit 7be4dd2a2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1189 additions and 23 deletions

516
Cargo.lock generated
View File

@ -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"

View File

@ -157,8 +157,10 @@ cargo update -p package_name --precise 0.1.1
Licensed under the Apache License, Version 2.0
([LICENSE](LICENSE) or <http://www.apache.org/licenses/LICENSE-2.0>)
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

View File

@ -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
}

View File

@ -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"

View File

@ -64,9 +64,12 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or <http://www.apache.org/licenses/LICENSE-2.0>)
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

280
xilem/examples/http_cats.rs Normal file
View File

@ -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 <https://http.cat/> 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<Status>,
// The currently active (http status) code.
selected_code: Option<u32>,
}
#[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<HttpCats> {
let left_column = portal(flex((
prose("Status"),
self.statuses
.iter_mut()
.map(Status::list_view)
.collect::<Vec<_>>(),
)));
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<Image> {
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<HttpCats> {
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<HttpCats> {
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<Self> {
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<Self> {
let (code, message) = line.split_once(',')?;
Some(Self {
code: code.parse().ok()?,
message: message.trim(),
image: ImageState::NotRequested,
})
}
}
/// The status codes supported by <https://http.cat>, 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!()
}

View File

@ -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.

View File

@ -0,0 +1,9 @@
# Http Cats status data
The statuses from <https://http.cat>.
These were extracted from <https://github.com/httpcats/http.cat/blob/master/lib/statuses.js> 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).

View File

@ -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,Im 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
1 code message
2 100 Continue
3 101 Switching Protocols
4 102 Processing
5 103 Early Hints
6 200 OK
7 201 Created
8 202 Accepted
9 203 Non-Authoritative Information
10 204 No Content
11 206 Partial Content
12 205 Reset Content
13 207 Multi-Status
14 208 Already Reported
15 214 Transformation Applied
16 226 IM Used
17 300 Multiple Choices
18 301 Moved Permanently
19 302 Found
20 303 See Other
21 304 Not Modified
22 305 Use Proxy
23 307 Temporary Redirect
24 308 Permanent Redirect
25 400 Bad Request
26 401 Unauthorized
27 402 Payment Required
28 403 Forbidden
29 404 Not Found
30 405 Method Not Allowed
31 406 Not Acceptable
32 407 Proxy Authentication Required
33 408 Request Timeout
34 409 Conflict
35 410 Gone
36 411 Length Required
37 412 Precondition Failed
38 413 Payload Too Large
39 414 Request-URI Too Long
40 415 Unsupported Media Type
41 416 Request Range Not Satisfiable
42 417 Expectation Failed
43 418 I’m a teapot
44 420 Enhance Your Calm
45 421 Misdirected Request
46 422 Unprocessable Entity
47 423 Locked
48 424 Failed Dependency
49 425 Too Early
50 426 Upgrade Required
51 428 Precondition Required
52 429 Too Many Requests
53 431 Request Header Fields Too Large
54 444 No Response
55 450 Blocked by Windows Parental Controls
56 451 Unavailable For Legal Reasons
57 497 HTTP Request Sent to HTTPS Port
58 498 Token expired/invalid
59 499 Client Closed Request
60 500 Internal Server Error
61 501 Not Implemented
62 502 Bad Gateway
63 503 Service Unavailable
64 504 Gateway Timeout
65 506 Variant Also Negotiates
66 507 Insufficient Storage
67 508 Loop Detected
68 509 Bandwidth Limit Exceeded
69 510 Not Extended
70 511 Network Authentication Required
71 521 Web Server Is Down
72 522 Connection Timed Out
73 523 Origin Is Unreachable
74 525 SSL Handshake Failed
75 530 Site Frozen
76 599 Network Connect Timeout Error

84
xilem/src/view/image.rs Normal file
View File

@ -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<State, Action> View<State, Action, ViewCtx> for Image {
type Element = Pod<widget::Image>;
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<Action> {
tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug");
MessageResult::Stale(message)
}
}

View File

@ -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::*;

153
xilem/src/view/worker.rs Normal file
View File

@ -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<M, V, F, H, State, Action, Fut>(
value: V,
init_future: F,
on_response: H,
) -> Worker<F, H, M, V>
where
F: Fn(MessageProxy<M>, UnboundedReceiver<V>) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
H: Fn(&mut State, M) -> Action + 'static,
M: Message + 'static,
{
const {
assert!(
core::mem::size_of::<F>() == 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<M, V, F, H, State, Action, Fut>(
value: V,
init_future: F,
on_response: H,
) -> Worker<F, H, M, V>
where
F: Fn(MessageProxy<M>, UnboundedReceiver<V>) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
H: Fn(&mut State, M) -> Action + 'static,
M: Message + 'static,
{
Worker {
value,
init_future,
on_response,
message: PhantomData,
}
}
pub struct Worker<F, H, M, V> {
init_future: F,
value: V,
on_response: H,
message: PhantomData<fn() -> M>,
}
#[doc(hidden)] // Implementation detail, public because of trait visibility rules
pub struct WorkerState<V> {
handle: JoinHandle<()>,
sender: UnboundedSender<V>,
}
impl<F, H, M, V> ViewMarker for Worker<F, H, M, V> {}
impl<State, Action, V, F, H, M, Fut> View<State, Action, ViewCtx> for Worker<F, H, M, V>
where
F: Fn(MessageProxy<M>, UnboundedReceiver<V>) -> Fut + 'static,
V: Send + PartialEq + Clone + 'static,
Fut: Future<Output = ()> + Send + 'static,
H: Fn(&mut State, M) -> Action + 'static,
M: Message + 'static,
{
type Element = NoElement;
type ViewState = WorkerState<V>;
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<Action> {
debug_assert!(
id_path.is_empty(),
"id path should be empty in Task::message"
);
let message = message.downcast::<M>().unwrap();
xilem_core::MessageResult::Action((self.on_response)(app_state, *message))
}
}

View File

@ -55,6 +55,16 @@ pub struct MessageProxy<M: Message> {
message: PhantomData<fn(M)>,
}
impl<M: Message> Clone for MessageProxy<M> {
fn clone(&self) -> Self {
Self {
proxy: self.proxy.clone(),
path: self.path.clone(),
message: PhantomData,
}
}
}
impl<M: Message> MessageProxy<M> {
/// Create a new `MessageProxy`
pub fn new(proxy: Arc<dyn RawProxy<DynMessage>>, path: Arc<[ViewId]>) -> Self {