vizia_core/resource/
mod.rs

1//! Resource management for fonts, themes, images, and translations.
2
3mod image_id;
4
5pub use image_id::ImageId;
6use vizia_id::{GenerationalId, IdManager};
7
8use crate::context::ResourceContext;
9use crate::entity::Entity;
10use crate::prelude::IntoCssStr;
11// use crate::view::Canvas;
12use fluent_bundle::{FluentBundle, FluentResource};
13use hashbrown::{HashMap, HashSet};
14use unic_langid::LanguageIdentifier;
15
16pub(crate) enum ImageOrSvg {
17    Svg(skia_safe::svg::Dom),
18    Image(skia_safe::Image),
19}
20
21pub(crate) struct StoredImage {
22    pub image: ImageOrSvg,
23    pub retention_policy: ImageRetentionPolicy,
24    pub used: bool,
25    pub dirty: bool,
26    pub observers: HashSet<Entity>,
27}
28
29/// An image should be stored in the resource manager.
30#[derive(Copy, Clone, PartialEq)]
31pub enum ImageRetentionPolicy {
32    ///  The image should live for the entire duration of the application.
33    Forever,
34    /// The image should be dropped when not used for one frame.
35    DropWhenUnusedForOneFrame,
36    /// The image should be dropped when no views are using the image.
37    DropWhenNoObservers,
38}
39
40#[doc(hidden)]
41#[derive(Default)]
42pub struct ResourceManager {
43    pub themes: Vec<String>, // Themes are the string content stylesheets
44    pub styles: Vec<Box<dyn IntoCssStr>>,
45
46    pub(crate) image_id_manager: IdManager<ImageId>,
47    pub(crate) images: HashMap<ImageId, StoredImage>,
48    pub(crate) image_ids: HashMap<String, ImageId>,
49
50    pub translations: HashMap<LanguageIdentifier, FluentBundle<FluentResource>>,
51
52    pub language: LanguageIdentifier,
53
54    pub image_loader: Option<Box<dyn Fn(&mut ResourceContext, &str)>>,
55}
56
57impl ResourceManager {
58    pub fn new() -> Self {
59        // Get the system locale
60        let locale = sys_locale::get_locale().and_then(|l| l.parse().ok()).unwrap_or_default();
61
62        let default_image_loader: Option<Box<dyn Fn(&mut ResourceContext, &str)>> = None;
63
64        // Disable this for now because reqwest pulls in too many dependencies.
65        // let default_image_loader: Option<Box<dyn Fn(&mut ResourceContext, &str)>> =
66        //     Some(Box::new(|cx: &mut ResourceContext, path: &str| {
67        //         if path.starts_with("https://") {
68        //             let path = path.to_string();
69        //             cx.spawn(move |cx| {
70        //                 let data = reqwest::blocking::get(&path).unwrap().bytes().unwrap();
71        //                 cx.load_image(
72        //                     path,
73        //                     image::load_from_memory_with_format(
74        //                         &data,
75        //                         image::guess_format(&data).unwrap(),
76        //                     )
77        //                     .unwrap(),
78        //                     ImageRetentionPolicy::DropWhenUnusedForOneFrame,
79        //                 )
80        //                 .unwrap();
81        //             });
82        //         } else {
83        //             // TODO: Try to load path from file
84        //         }
85        //     }));
86
87        let mut image_id_manager = IdManager::new();
88
89        // Create root id for broken image
90        image_id_manager.create();
91
92        let mut images = HashMap::new();
93
94        images.insert(
95            ImageId::root(),
96            StoredImage {
97                image: ImageOrSvg::Image(
98                    skia_safe::Image::from_encoded(unsafe {
99                        skia_safe::Data::new_bytes(include_bytes!(
100                            "../../resources/images/broken_image.png"
101                        ))
102                    })
103                    .unwrap(),
104                ),
105
106                retention_policy: ImageRetentionPolicy::Forever,
107                used: true,
108                dirty: false,
109                observers: HashSet::new(),
110            },
111        );
112
113        ResourceManager {
114            themes: Vec::new(),
115
116            image_id_manager,
117            images,
118            image_ids: HashMap::new(),
119            styles: Vec::new(),
120
121            translations: HashMap::from([(
122                LanguageIdentifier::default(),
123                FluentBundle::new(vec![LanguageIdentifier::default()]),
124            )]),
125
126            language: locale,
127            image_loader: default_image_loader,
128        }
129    }
130
131    pub fn renegotiate_language(&mut self) {
132        let available = self
133            .translations
134            .keys()
135            .filter(|&x| x != &LanguageIdentifier::default())
136            .collect::<Vec<_>>();
137        let locale = sys_locale::get_locale()
138            .and_then(|l| l.parse().ok())
139            .unwrap_or_else(|| available.first().copied().cloned().unwrap_or_default());
140        let default = LanguageIdentifier::default();
141        let default_ref = &default; // ???
142        let langs = fluent_langneg::negotiate::negotiate_languages(
143            &[locale],
144            &available,
145            Some(&default_ref),
146            fluent_langneg::NegotiationStrategy::Filtering,
147        );
148        self.language = (**langs.first().unwrap()).clone();
149    }
150
151    pub fn add_translation(&mut self, lang: LanguageIdentifier, ftl: String) {
152        let res = fluent_bundle::FluentResource::try_new(ftl)
153            .expect("Failed to parse translation as FTL");
154        let bundle =
155            self.translations.entry(lang.clone()).or_insert_with(|| FluentBundle::new(vec![lang]));
156        bundle.add_resource(res).expect("Failed to add resource to bundle");
157        self.renegotiate_language();
158    }
159
160    pub fn current_translation(
161        &self,
162        locale: &LanguageIdentifier,
163    ) -> &FluentBundle<FluentResource> {
164        if let Some(bundle) = self.translations.get(locale) {
165            bundle
166        } else {
167            self.translations.get(&self.language).unwrap()
168        }
169    }
170
171    pub fn mark_images_unused(&mut self) {
172        for (_, img) in self.images.iter_mut() {
173            img.used = false;
174        }
175    }
176
177    pub fn evict_unused_images(&mut self) {
178        let rem = self
179            .images
180            .iter()
181            .filter_map(|(id, img)| match img.retention_policy {
182                ImageRetentionPolicy::DropWhenUnusedForOneFrame => (img.used).then_some(*id),
183
184                ImageRetentionPolicy::DropWhenNoObservers => {
185                    img.observers.is_empty().then_some(*id)
186                }
187
188                ImageRetentionPolicy::Forever => None,
189            })
190            .collect::<Vec<_>>();
191
192        for id in rem {
193            self.images.remove(&id);
194            self.image_ids.retain(|_, img| *img != id);
195            self.image_id_manager.destroy(id);
196        }
197    }
198}