1use crate::{Context, Image, Pt};
7
8pub fn from_image(ctx: &mut Context, image: &image::DynamicImage) -> anyhow::Result<Image> {
13 let rgba = image.to_rgba8();
14 from_rgba_image(ctx, &rgba)
15}
16
17pub fn from_rgba_image(ctx: &mut Context, image: &image::RgbaImage) -> anyhow::Result<Image> {
22 let width_px = image.width();
23 let height_px = image.height();
24 let scale_factor = ctx.scale_factor().max(1.0);
25 let width = Pt::from_physical_px(width_px as f64, scale_factor);
26 let height = Pt::from_physical_px(height_px as f64, scale_factor);
27 Image::new_from_rgba8_with_pixels(ctx, width_px, height_px, width, height, image.as_raw())
28}
29
30use std::sync::mpsc::{self, Receiver};
31
32#[derive(Debug)]
34pub struct LoadingImage {
35 path: String,
36 rx: Receiver<Result<(u32, u32, Vec<u8>), String>>,
37 image: Option<Image>,
38 error: Option<String>,
39}
40
41impl LoadingImage {
42 pub fn path(&self) -> &str {
44 &self.path
45 }
46
47 pub fn error(&self) -> Option<&str> {
49 self.error.as_deref()
50 }
51
52 pub fn poll(&mut self, ctx: &mut Context) -> Option<Image> {
58 if let Some(img) = self.image {
59 return Some(img);
60 }
61
62 if self.error.is_some() {
63 return None;
64 }
65
66 match self.rx.try_recv() {
67 Ok(Ok((width_px, height_px, rgba))) => {
68 let scale_factor = ctx.scale_factor().max(1.0);
69 let width = Pt::from_physical_px(width_px as f64, scale_factor);
70 let height = Pt::from_physical_px(height_px as f64, scale_factor);
71 match Image::new_from_rgba8_with_pixels(
72 ctx, width_px, height_px, width, height, &rgba,
73 ) {
74 Ok(img) => {
75 self.image = Some(img);
76 Some(img)
77 }
78 Err(e) => {
79 self.error = Some(format!("Failed to register image on GPU: {:?}", e));
80 None
81 }
82 }
83 }
84 Ok(Err(e)) => {
85 self.error = Some(e);
86 None
87 }
88 Err(mpsc::TryRecvError::Empty) => None,
89 Err(mpsc::TryRecvError::Disconnected) => {
90 if self.image.is_none() {
91 self.error = Some("Loading thread disconnected unexpectedly".to_string());
92 }
93 None
94 }
95 }
96 }
97
98 pub fn get(&mut self, ctx: &mut Context) -> Option<Image> {
101 self.poll(ctx)
102 }
103
104 pub fn get_or(&mut self, ctx: &mut Context, fallback: Image) -> Image {
107 self.get(ctx).unwrap_or(fallback)
108 }
109}
110
111pub fn load_image_async(path: impl Into<String>) -> LoadingImage {
117 let path = path.into();
118 let (tx, rx) = mpsc::channel();
119 let path_clone = path.clone();
120
121 #[cfg(not(target_arch = "wasm32"))]
122 {
123 std::thread::spawn(move || {
124 let res = (|| -> Result<(u32, u32, Vec<u8>), anyhow::Error> {
125 let bytes = crate::assets::load_asset(&path_clone)?;
126 let img = image::load_from_memory(&bytes)?;
127 let rgba = img.to_rgba8();
128 Ok((rgba.width(), rgba.height(), rgba.into_raw()))
129 })();
130 let _ = tx.send(res.map_err(|e| e.to_string()));
131 });
132 }
133
134 #[cfg(target_arch = "wasm32")]
135 {
136 wasm_bindgen_futures::spawn_local(async move {
137 let res = (|| -> Result<(u32, u32, Vec<u8>), anyhow::Error> {
138 let bytes = crate::assets::load_asset(&path_clone)?;
139 let img = image::load_from_memory(&bytes)?;
140 let rgba = img.to_rgba8();
141 Ok((rgba.width(), rgba.height(), rgba.into_raw()))
142 })();
143 let _ = tx.send(res.map_err(|e| e.to_string()));
144 });
145 }
146
147 LoadingImage {
148 path,
149 rx,
150 image: None,
151 error: None,
152 }
153}
154
155use std::collections::HashMap;
156
157#[derive(Debug, Default)]
159pub struct AsyncImageLoader {
160 loading: HashMap<String, LoadingImage>,
161 loaded: HashMap<String, Image>,
162 errors: HashMap<String, String>,
163}
164
165impl AsyncImageLoader {
166 pub fn new() -> Self {
168 Self {
169 loading: HashMap::new(),
170 loaded: HashMap::new(),
171 errors: HashMap::new(),
172 }
173 }
174
175 pub fn load(&mut self, path: impl Into<String>) {
177 let path = path.into();
178 if !self.loaded.contains_key(&path) && !self.loading.contains_key(&path) {
179 self.errors.remove(&path);
180 let loader = load_image_async(&path);
181 self.loading.insert(path, loader);
182 }
183 }
184
185 pub fn progress(&mut self, ctx: &mut Context) -> (usize, usize) {
189 let mut finished = Vec::new();
190 for (path, loader) in self.loading.iter_mut() {
191 if let Some(img) = loader.get(ctx) {
192 finished.push((path.clone(), Ok(img)));
193 } else if let Some(err) = loader.error() {
194 finished.push((path.clone(), Err(err.to_string())));
195 }
196 }
197
198 for (path, result) in finished {
199 match result {
200 Ok(img) => {
201 self.loaded.insert(path.clone(), img);
202 }
203 Err(err) => {
204 self.errors.insert(path.clone(), err);
205 }
206 }
207 self.loading.remove(&path);
208 }
209
210 let done = self.loaded.len() + self.errors.len();
211 let total = done + self.loading.len();
212 (done, total)
213 }
214
215 pub fn progress_ratio(&mut self, ctx: &mut Context) -> f32 {
217 let (done, total) = self.progress(ctx);
218 if total == 0 {
219 1.0
220 } else {
221 done as f32 / total as f32
222 }
223 }
224
225 pub fn is_done(&mut self, ctx: &mut Context) -> bool {
227 let (done, total) = self.progress(ctx);
228 done == total && total > 0
229 }
230
231 pub fn is_ready(&mut self, ctx: &mut Context, path: &str) -> bool {
233 if self.loaded.contains_key(path) {
234 return true;
235 }
236
237 if let Some(mut loader) = self.loading.remove(path) {
238 if let Some(img) = loader.get(ctx) {
239 self.loaded.insert(path.to_string(), img);
240 return true;
241 }
242 self.loading.insert(path.to_string(), loader);
243 }
244
245 false
246 }
247
248 pub fn get(&mut self, ctx: &mut Context, path: &str) -> Option<Image> {
250 if let Some(&img) = self.loaded.get(path) {
251 return Some(img);
252 }
253
254 if let Some(mut loader) = self.loading.remove(path) {
255 if let Some(img) = loader.get(ctx) {
256 self.loaded.insert(path.to_string(), img);
257 return Some(img);
258 }
259 self.loading.insert(path.to_string(), loader);
260 }
261
262 None
263 }
264
265 pub fn get_or(&mut self, ctx: &mut Context, path: &str, fallback: Image) -> Image {
267 self.get(ctx, path).unwrap_or(fallback)
268 }
269
270 pub fn error(&self, path: &str) -> Option<&str> {
272 self.errors
273 .get(path)
274 .map(|s| s.as_str())
275 .or_else(|| self.loading.get(path).and_then(|loader| loader.error()))
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_async_loading() {
285 let mut ctx = Context::new();
286 let mut loading = load_image_async("assets/happy-tree.png");
288 assert_eq!(loading.path(), "assets/happy-tree.png");
289 assert!(loading.error().is_none());
290
291 let start = std::time::Instant::now();
293 let mut image = None;
294 while start.elapsed() < std::time::Duration::from_secs(5) {
295 if let Some(img) = loading.poll(&mut ctx) {
296 image = Some(img);
297 break;
298 }
299 if let Some(err) = loading.error() {
300 panic!("Failed to load asynchronously: {}", err);
301 }
302 std::thread::sleep(std::time::Duration::from_millis(10));
303 }
304
305 let img = image.expect("Failed to load image in 5 seconds");
306 assert!(img.width().as_f32() > 0.0);
308 assert!(img.height().as_f32() > 0.0);
309 }
310
311 #[test]
312 fn test_async_image_loader() {
313 let mut ctx = Context::new();
314 let fallback = Image::new(&mut ctx, Pt(1.0), Pt(1.0), &[255, 255, 255, 255]).unwrap();
315 let mut loader = AsyncImageLoader::new();
316
317 assert_eq!(loader.progress_ratio(&mut ctx), 1.0); assert!(!loader.is_done(&mut ctx)); loader.load("assets/happy-tree.png");
323
324 assert!(loader.progress_ratio(&mut ctx) < 1.0);
326 assert!(!loader.is_done(&mut ctx));
327
328 let start = std::time::Instant::now();
330 let mut ready = false;
331 while start.elapsed() < std::time::Duration::from_secs(5) {
332 if loader.is_done(&mut ctx) {
333 ready = true;
334 break;
335 }
336 if let Some(err) = loader.error("assets/happy-tree.png") {
337 panic!("Failed to load asynchronously via manager: {}", err);
338 }
339 std::thread::sleep(std::time::Duration::from_millis(10));
340 }
341
342 assert!(ready);
344 assert_eq!(loader.progress_ratio(&mut ctx), 1.0);
345 assert!(loader.is_ready(&mut ctx, "assets/happy-tree.png"));
346
347 let img = loader.get_or(&mut ctx, "assets/happy-tree.png", fallback);
348 assert_ne!(img, fallback);
349 assert!(img.width().as_f32() > 0.0);
350 }
351}