blockworx_canvas2d/
images.rs1use 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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25enum Load {
26 Decoding,
27 Ready,
28 Failed,
29}
30
31struct Held {
34 element: HtmlImageElement,
35 url: String,
36 load: Rc<Cell<Load>>,
37 _handlers: [Closure<dyn FnMut()>; 2],
40}
41
42#[derive(Default)]
44pub struct Images(BTreeMap<AssetHash, Held>);
45
46impl Images {
47 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 #[must_use]
60 pub fn holds(&self, hash: AssetHash) -> bool {
61 self.0.contains_key(&hash)
62 }
63
64 #[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}