raui_app/
asset_manager.rs

1use crate::Vertex;
2use fontdue::Font;
3use image::EncodableLayout;
4use raui_core::widget::{
5    unit::{WidgetUnit, image::ImageBoxMaterial, portal::PortalBoxSlot},
6    utils::{Rect, Vec2},
7};
8use raui_tesselate_renderer::TesselateResourceProvider;
9use serde::{Deserialize, Serialize};
10use spitfire_glow::{
11    graphics::{Graphics, Shader, Texture},
12    renderer::GlowTextureFormat,
13};
14use std::{collections::HashMap, path::PathBuf};
15
16#[derive(Serialize, Deserialize)]
17pub struct AssetAtlasRegion {
18    x: u32,
19    y: u32,
20    width: u32,
21    height: u32,
22}
23
24#[derive(Serialize, Deserialize)]
25struct AssetAtlas {
26    image: PathBuf,
27    regions: HashMap<String, AssetAtlasRegion>,
28}
29
30pub(crate) struct AssetTexture {
31    pub(crate) texture: Texture,
32    /// {name: uvs}
33    regions: HashMap<String, Rect>,
34    frames_left: usize,
35    forever_alive: bool,
36}
37
38pub(crate) struct AssetFont {
39    hash: usize,
40    frames_left: usize,
41    forever_alive: bool,
42}
43
44pub(crate) struct AssetShader {
45    pub(crate) shader: Shader,
46    frames_left: usize,
47    forever_alive: bool,
48}
49
50pub(crate) struct AssetsManager {
51    pub frames_alive: usize,
52    pub(crate) root_path: PathBuf,
53    pub(crate) textures: HashMap<String, AssetTexture>,
54    pub(crate) font_map: HashMap<String, AssetFont>,
55    pub(crate) fonts: Vec<Font>,
56    pub(crate) shaders: HashMap<String, AssetShader>,
57}
58
59impl Default for AssetsManager {
60    fn default() -> Self {
61        Self::new("./", 1024)
62    }
63}
64
65impl AssetsManager {
66    fn new(root_path: impl Into<PathBuf>, frames_alive: usize) -> Self {
67        Self {
68            frames_alive,
69            root_path: root_path.into(),
70            textures: Default::default(),
71            font_map: Default::default(),
72            fonts: Default::default(),
73            shaders: Default::default(),
74        }
75    }
76
77    pub(crate) fn maintain(&mut self) {
78        let to_remove = self
79            .textures
80            .iter_mut()
81            .filter(|(_, texture)| !texture.forever_alive)
82            .filter_map(|(id, texture)| {
83                if texture.frames_left > 0 {
84                    texture.frames_left -= 1;
85                    None
86                } else {
87                    Some(id.to_owned())
88                }
89            })
90            .collect::<Vec<_>>();
91        for id in to_remove {
92            self.textures.remove(&id);
93        }
94
95        let to_remove = self
96            .font_map
97            .iter_mut()
98            .filter(|(_, font)| !font.forever_alive)
99            .filter_map(|(id, font)| {
100                if font.frames_left > 0 {
101                    font.frames_left -= 1;
102                    None
103                } else {
104                    Some(id.to_owned())
105                }
106            })
107            .collect::<Vec<_>>();
108        for id in to_remove {
109            let hash = self.font_map.remove(&id).unwrap().hash;
110            if let Some(index) = self.fonts.iter().position(|font| font.file_hash() == hash) {
111                self.fonts.swap_remove(index);
112            }
113        }
114
115        let to_remove = self
116            .shaders
117            .iter_mut()
118            .filter(|(_, shader)| !shader.forever_alive)
119            .filter_map(|(id, shader)| {
120                if shader.frames_left > 0 {
121                    shader.frames_left -= 1;
122                    None
123                } else {
124                    Some(id.to_owned())
125                }
126            })
127            .collect::<Vec<_>>();
128        for id in to_remove {
129            self.shaders.remove(&id);
130        }
131    }
132
133    pub(crate) fn load(&mut self, node: &WidgetUnit, graphics: &Graphics<Vertex>) {
134        match node {
135            WidgetUnit::None => {}
136            WidgetUnit::AreaBox(node) => {
137                self.load(&node.slot, graphics);
138            }
139            WidgetUnit::PortalBox(node) => match &*node.slot {
140                PortalBoxSlot::Slot(node) => {
141                    self.load(node, graphics);
142                }
143                PortalBoxSlot::ContentItem(node) => {
144                    self.load(&node.slot, graphics);
145                }
146                PortalBoxSlot::FlexItem(node) => {
147                    self.load(&node.slot, graphics);
148                }
149                PortalBoxSlot::GridItem(node) => {
150                    self.load(&node.slot, graphics);
151                }
152            },
153            WidgetUnit::ContentBox(node) => {
154                for item in &node.items {
155                    self.load(&item.slot, graphics);
156                }
157            }
158            WidgetUnit::FlexBox(node) => {
159                for item in &node.items {
160                    self.load(&item.slot, graphics);
161                }
162            }
163            WidgetUnit::GridBox(node) => {
164                for item in &node.items {
165                    self.load(&item.slot, graphics);
166                }
167            }
168            WidgetUnit::SizeBox(node) => {
169                self.load(&node.slot, graphics);
170            }
171            WidgetUnit::ImageBox(node) => match &node.material {
172                ImageBoxMaterial::Image(image) => {
173                    let id = Self::parse_image_id(&image.id).0;
174                    self.try_load_image(id, graphics, false);
175                }
176                ImageBoxMaterial::Procedural(procedural) => {
177                    for id in &procedural.images {
178                        self.try_load_image(id, graphics, false);
179                    }
180                    if !procedural.id.is_empty() {
181                        self.try_load_shader(&procedural.id, graphics, false);
182                    }
183                }
184                _ => {}
185            },
186            WidgetUnit::TextBox(node) => {
187                self.try_load_font(&node.font.name, false);
188            }
189        }
190    }
191
192    pub(crate) fn parse_image_id(id: &str) -> (&str, Option<&str>) {
193        match id.find('@') {
194            Some(index) => (&id[..index], Some(&id[(index + b"@".len())..])),
195            None => (id, None),
196        }
197    }
198
199    pub(crate) fn add_texture(&mut self, id: impl ToString, texture: Texture) {
200        self.textures.insert(
201            id.to_string(),
202            AssetTexture {
203                texture,
204                regions: Default::default(),
205                frames_left: self.frames_alive,
206                forever_alive: true,
207            },
208        );
209    }
210
211    pub(crate) fn remove_texture(&mut self, id: impl ToString) {
212        self.textures.remove(&id.to_string());
213    }
214
215    // pub(crate) fn add_shader(&mut self, id: impl ToString, shader: Shader) {
216    //     self.shaders.insert(
217    //         id.to_string(),
218    //         AssetShader {
219    //             shader,
220    //             frames_left: self.frames_alive,
221    //             forever_alive: true,
222    //         },
223    //     );
224    // }
225
226    // pub(crate) fn remove_shader(&mut self, id: impl ToString) {
227    //     self.shaders.remove(&id.to_string());
228    // }
229
230    // pub(crate) fn add_font(&mut self, id: impl ToString, font: Font) {
231    //     self.font_map.insert(
232    //         id.to_string(),
233    //         AssetFont {
234    //             hash: font.file_hash(),
235    //             frames_left: self.frames_alive,
236    //             forever_alive: true,
237    //         },
238    //     );
239    //     self.fonts.push(font);
240    // }
241
242    // pub(crate) fn remove_font(&mut self, id: impl ToString) {
243    //     if let Some(font) = self.font_map.remove(&id.to_string()) {
244    //         if let Some(index) = self.fonts.iter().position(|f| f.file_hash() == font.hash) {
245    //             self.fonts.swap_remove(index);
246    //         }
247    //     }
248    // }
249
250    fn try_load_image(&mut self, id: &str, graphics: &Graphics<Vertex>, forever_alive: bool) {
251        if let Some(texture) = self.textures.get_mut(id) {
252            texture.frames_left = self.frames_alive;
253        } else {
254            let mut path = self.root_path.join(id);
255            match path.extension().and_then(|ext| ext.to_str()).unwrap_or("") {
256                "toml" => {
257                    let content = match std::fs::read_to_string(&path) {
258                        Ok(content) => content,
259                        _ => {
260                            eprintln!("Could not load image atlas file: {path:?}");
261                            return;
262                        }
263                    };
264                    let atlas = match toml::from_str::<AssetAtlas>(&content) {
265                        Ok(atlas) => atlas,
266                        _ => {
267                            eprintln!("Could not parse image atlas file: {path:?}");
268                            return;
269                        }
270                    };
271                    path.pop();
272                    let path = path.join(atlas.image);
273                    let image = match image::open(&path) {
274                        Ok(image) => image.to_rgba8(),
275                        _ => {
276                            eprintln!("Could not load image file: {path:?}");
277                            return;
278                        }
279                    };
280                    let texture = match graphics.texture(
281                        image.width(),
282                        image.height(),
283                        1,
284                        GlowTextureFormat::Rgba,
285                        Some(image.as_bytes()),
286                    ) {
287                        Ok(texture) => texture,
288                        _ => {
289                            eprintln!("Could not create texture for image file: {path:?}");
290                            return;
291                        }
292                    };
293                    let regions = atlas
294                        .regions
295                        .into_iter()
296                        .map(|(name, region)| {
297                            let left = region.x as f32 / image.width() as f32;
298                            let right = (region.x + region.width) as f32 / image.width() as f32;
299                            let top = region.y as f32 / image.height() as f32;
300                            let bottom = (region.y + region.height) as f32 / image.height() as f32;
301                            (
302                                name,
303                                Rect {
304                                    left,
305                                    right,
306                                    top,
307                                    bottom,
308                                },
309                            )
310                        })
311                        .collect();
312                    self.textures.insert(
313                        id.to_owned(),
314                        AssetTexture {
315                            texture,
316                            regions,
317                            frames_left: self.frames_alive,
318                            forever_alive,
319                        },
320                    );
321                }
322                _ => {
323                    let image = match image::open(&path) {
324                        Ok(image) => image.to_rgba8(),
325                        _ => {
326                            eprintln!("Could not load image file: {path:?}");
327                            return;
328                        }
329                    };
330                    let texture = match graphics.texture(
331                        image.width(),
332                        image.height(),
333                        1,
334                        GlowTextureFormat::Rgba,
335                        Some(image.as_bytes()),
336                    ) {
337                        Ok(texture) => texture,
338                        _ => {
339                            eprintln!("Could not create texture for image file: {path:?}");
340                            return;
341                        }
342                    };
343                    self.textures.insert(
344                        id.to_owned(),
345                        AssetTexture {
346                            texture,
347                            regions: Default::default(),
348                            frames_left: self.frames_alive,
349                            forever_alive,
350                        },
351                    );
352                }
353            }
354        }
355    }
356
357    fn try_load_font(&mut self, id: &str, forever_alive: bool) {
358        if let Some(font) = self.font_map.get_mut(id) {
359            font.frames_left = self.frames_alive;
360        } else {
361            let path = self.root_path.join(id);
362            let content = match std::fs::read(&path) {
363                Ok(content) => content,
364                _ => {
365                    eprintln!("Could not load font file: {path:?}");
366                    return;
367                }
368            };
369            let font = match Font::from_bytes(content, Default::default()) {
370                Ok(font) => font,
371                _ => return,
372            };
373            self.font_map.insert(
374                id.to_owned(),
375                AssetFont {
376                    hash: font.file_hash(),
377                    frames_left: self.frames_alive,
378                    forever_alive,
379                },
380            );
381            self.fonts.push(font);
382        }
383    }
384
385    fn try_load_shader(&mut self, id: &str, graphics: &Graphics<Vertex>, forever_alive: bool) {
386        if let Some(shader) = self.shaders.get_mut(id) {
387            shader.frames_left = self.frames_alive;
388        } else {
389            let shader = match id {
390                "@pass" => match graphics.shader(Shader::PASS_VERTEX_2D, Shader::PASS_FRAGMENT) {
391                    Ok(shader) => shader,
392                    _ => {
393                        eprintln!("Could not create shader for: {id:?}");
394                        return;
395                    }
396                },
397                "@colored" => {
398                    match graphics.shader(Shader::COLORED_VERTEX_2D, Shader::PASS_FRAGMENT) {
399                        Ok(shader) => shader,
400                        _ => {
401                            eprintln!("Could not create shader for: {id:?}");
402                            return;
403                        }
404                    }
405                }
406                "@textured" => {
407                    match graphics.shader(Shader::TEXTURED_VERTEX_2D, Shader::TEXTURED_FRAGMENT) {
408                        Ok(shader) => shader,
409                        _ => {
410                            eprintln!("Could not create shader for: {id:?}");
411                            return;
412                        }
413                    }
414                }
415                _ => {
416                    let path = self.root_path.join(format!("{id}.vs"));
417                    let vertex = match std::fs::read_to_string(&path) {
418                        Ok(content) => content,
419                        _ => {
420                            eprintln!("Could not load vertex shader file: {path:?}");
421                            return;
422                        }
423                    };
424                    let path = self.root_path.join(format!("{id}.fs"));
425                    let fragment = match std::fs::read_to_string(&path) {
426                        Ok(content) => content,
427                        _ => {
428                            eprintln!("Could not load fragment shader file: {path:?}");
429                            return;
430                        }
431                    };
432                    match graphics.shader(&vertex, &fragment) {
433                        Ok(shader) => shader,
434                        _ => {
435                            eprintln!("Could not create shader for: {id:?}");
436                            return;
437                        }
438                    }
439                }
440            };
441            self.shaders.insert(
442                id.to_owned(),
443                AssetShader {
444                    shader,
445                    frames_left: self.frames_alive,
446                    forever_alive,
447                },
448            );
449        }
450    }
451}
452
453impl TesselateResourceProvider for AssetsManager {
454    fn image_id_and_uv_and_size_by_atlas_id(&self, id: &str) -> Option<(String, Rect, Vec2)> {
455        let (id, region) = Self::parse_image_id(id);
456        let texture = self.textures.get(id)?;
457        let uvs = region
458            .and_then(|region| texture.regions.get(region))
459            .copied()
460            .unwrap_or(Rect {
461                left: 0.0,
462                right: 1.0,
463                top: 0.0,
464                bottom: 1.0,
465            });
466        let size = Vec2 {
467            x: texture.texture.width() as _,
468            y: texture.texture.height() as _,
469        };
470        Some((id.to_owned(), uvs, size))
471    }
472
473    fn fonts(&self) -> &[Font] {
474        &self.fonts
475    }
476
477    fn font_index_by_id(&self, id: &str) -> Option<usize> {
478        let hash = self.font_map.get(id)?.hash;
479        self.fonts.iter().position(|font| font.file_hash() == hash)
480    }
481}