Skip to main content

blockworx_canvas2d/
images.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 — as a `Blob` URL the browser decodes for
6//! itself, SVG and PNG alike. Every replay after that is a lookup. A hash the
7//! table does not hold is one the front end never registered.
8
9use std::cell::Cell;
10use std::collections::BTreeMap;
11use std::rc::Rc;
12
13use blockworx_doc::{block_model::Asset, hash::AssetHash};
14use blockworx_geom::Rect;
15use wasm_bindgen::JsCast as _;
16use wasm_bindgen::closure::Closure;
17use web_sys::{CanvasRenderingContext2d, HtmlImageElement};
18
19use crate::Repaint;
20
21/// How far an image has got. The browser decodes off the frame, so a mark may
22/// be painted before its ink exists — and once decoding has failed it will
23/// not succeed on a later frame either.
24#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25enum Load {
26    Decoding,
27    Ready,
28    Failed,
29}
30
31/// One registered asset: the element the browser decodes into, the object URL
32/// backing it, and how far it has got.
33struct Held {
34    element: HtmlImageElement,
35    url: String,
36    load: Rc<Cell<Load>>,
37    /// The handlers that move `load` along, kept alive for as long as the
38    /// element they are bound to.
39    _handlers: [Closure<dyn FnMut()>; 2],
40}
41
42/// The artwork a display list may name, by the hash it names it under.
43#[derive(Default)]
44pub struct Images(BTreeMap<AssetHash, Held>);
45
46impl Images {
47    /// Hand `asset`'s bytes to the browser under `hash`. Idempotent: a hash
48    /// already held is left as it is, so re-sent bytes cost nothing.
49    pub fn register(&mut self, hash: AssetHash, asset: &Asset) {
50        if self.0.contains_key(&hash) {
51            return;
52        }
53        if let Some(held) = Held::decoding(asset) {
54            self.0.insert(hash, held);
55        }
56    }
57
58    /// Whether the bytes behind `hash` have been registered.
59    #[must_use]
60    pub fn holds(&self, hash: AssetHash) -> bool {
61        self.0.contains_key(&hash)
62    }
63
64    /// The artwork registered under `hash`, filling `rect`. A mark whose ink
65    /// is still decoding draws nothing and owes the caller a frame.
66    #[must_use]
67    pub fn draw(&self, ctx: &CanvasRenderingContext2d, rect: Rect, hash: AssetHash) -> Repaint {
68        let Some(held) = self.0.get(&hash) else {
69            return Repaint::Settled;
70        };
71        if rect.width() <= 0.0 || rect.height() <= 0.0 {
72            return Repaint::Settled;
73        }
74        match held.load.get() {
75            Load::Decoding => Repaint::Owed,
76            Load::Failed => Repaint::Settled,
77            Load::Ready => {
78                let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh(
79                    &held.element,
80                    rect.min.x.into(),
81                    rect.min.y.into(),
82                    rect.width().into(),
83                    rect.height().into(),
84                );
85                Repaint::Settled
86            }
87        }
88    }
89}
90
91impl Drop for Images {
92    fn drop(&mut self) {
93        for held in self.0.values() {
94            let _ = web_sys::Url::revoke_object_url(&held.url);
95        }
96    }
97}
98
99impl Held {
100    fn decoding(asset: &Asset) -> Option<Self> {
101        let url = object_url(asset).ok()?;
102        let element = HtmlImageElement::new().ok()?;
103        let load = Rc::new(Cell::new(Load::Decoding));
104        let handlers = [settle(&load, Load::Ready), settle(&load, Load::Failed)];
105        element.set_onload(Some(handlers[0].as_ref().unchecked_ref()));
106        element.set_onerror(Some(handlers[1].as_ref().unchecked_ref()));
107        element.set_src(&url);
108        Some(Self {
109            element,
110            url,
111            load,
112            _handlers: handlers,
113        })
114    }
115}
116
117fn settle(load: &Rc<Cell<Load>>, to: Load) -> Closure<dyn FnMut()> {
118    let load = Rc::clone(load);
119    Closure::new(move || load.set(to))
120}
121
122fn object_url(asset: &Asset) -> Result<String, wasm_bindgen::JsValue> {
123    let parts = js_sys::Array::new();
124    parts.push(&js_sys::Uint8Array::from(asset.bytes()));
125    let options = web_sys::BlobPropertyBag::new();
126    options.set_type(mime(asset));
127    let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &options)?;
128    web_sys::Url::create_object_url_with_blob(&blob)
129}
130
131fn mime(asset: &Asset) -> &'static str {
132    match asset {
133        Asset::Svg(_) => "image/svg+xml",
134        Asset::Png(_) => "image/png",
135    }
136}