Skip to main content

blockworx_egui/
image.rs

1//! The backend's image table.
2//!
3//! A display list names artwork by its [`AssetHash`]; the bytes behind a hash
4//! arrive once, in the hand-off of the first frame that painted it, and the
5//! front end registers them here — minting a loader URI that carries the
6//! [`Asset`]'s own format so egui picks the right decoder. Every replay after
7//! that is a lookup. The table is fed from the hand-off and nothing else: a
8//! hash it does not hold is one the front end never registered.
9
10use std::collections::BTreeMap;
11use std::sync::Arc;
12
13use blockworx_doc::{block_model::Asset, hash::AssetHash};
14
15/// Loader URIs by the hash the display list names them under. Owned by the
16/// [`View`](crate::View) and read by each frame's replay.
17#[derive(Default)]
18pub struct ImageRegistry(BTreeMap<AssetHash, Arc<str>>);
19
20impl ImageRegistry {
21    /// Hand `asset`'s bytes to egui's loader under `hash`. Idempotent: a
22    /// hash already held is left as it is, so re-sent bytes cost nothing.
23    pub fn register(&mut self, ctx: &egui::Context, hash: AssetHash, asset: &Asset) {
24        if self.0.contains_key(&hash) {
25            return;
26        }
27        let ext = match asset {
28            Asset::Svg(_) => "svg",
29            Asset::Png(_) => "png",
30        };
31        let uri: Arc<str> = Arc::from(format!("bytes://image-{hash}.{ext}"));
32        ctx.include_bytes(uri.to_string(), asset.bytes().to_vec());
33        self.0.insert(hash, uri);
34    }
35
36    /// The loader URI registered under `hash`, if any.
37    pub fn uri(&self, hash: AssetHash) -> Option<&Arc<str>> {
38        self.0.get(&hash)
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn register_is_keyed_by_the_hash_and_carries_the_format() {
48        let ctx = egui::Context::default();
49        let mut reg = ImageRegistry::default();
50        let svg = Asset::Svg(r#"<svg viewBox="0 0 20 10"></svg>"#.as_bytes().into());
51        let png = Asset::Png(b"\x89PNG\r\n\x1a\n".as_slice().into());
52        assert!(
53            reg.uri(svg.hash()).is_none(),
54            "precondition: nothing registered yet"
55        );
56
57        reg.register(&ctx, svg.hash(), &svg);
58        let first = reg.uri(svg.hash()).expect("the SVG is registered").clone();
59        assert!(
60            first.ends_with(".svg"),
61            "the URI does not name the format: {first}"
62        );
63
64        reg.register(&ctx, svg.hash(), &svg);
65        assert_eq!(
66            reg.uri(svg.hash()),
67            Some(&first),
68            "a second registration of the same hash minted a new URI",
69        );
70
71        reg.register(&ctx, png.hash(), &png);
72        let png_uri = reg.uri(png.hash()).expect("the PNG is registered");
73        assert!(
74            png_uri.ends_with(".png"),
75            "the URI does not name the format: {png_uri}"
76        );
77        assert_ne!(png_uri, &first);
78    }
79}