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 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 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}