Skip to main content

vizia_core/context/
resource.rs

1use hashbrown::{HashSet, hash_map::Entry};
2use unic_langid::LanguageIdentifier;
3
4use vizia_storage::Tree;
5
6use crate::{
7    entity::Entity,
8    resource::{
9        ImageId, ImageOrSvg, ImageRetentionPolicy, LoadingStatus, ResourceManager, ResourceRequest,
10        StoredImage,
11    },
12    style::Style,
13    text::TextContext,
14};
15
16use super::{Context, ContextProxy, EventProxy};
17
18#[cfg(feature = "tokio")]
19use std::sync::Arc;
20
21/// A context used when loading images.
22pub struct ResourceContext<'a> {
23    pub(crate) current: Entity,
24    pub(crate) event_proxy: &'a Option<Box<dyn EventProxy>>,
25    pub(crate) resource_manager: &'a mut ResourceManager,
26    pub(crate) style: &'a mut Style,
27    pub(crate) text_context: &'a mut TextContext,
28    pub(crate) tree: &'a Tree<Entity>,
29    #[cfg(feature = "tokio")]
30    pub(crate) task_runtime: Arc<tokio::runtime::Runtime>,
31    #[cfg(feature = "tokio")]
32    pub(crate) named_tasks: crate::context::NamedTaskMap,
33}
34
35impl<'a> ResourceContext<'a> {
36    /// Creates a new [ResourceContext].
37    pub(crate) fn new(cx: &'a mut Context) -> Self {
38        Self {
39            current: cx.current,
40            event_proxy: &cx.event_proxy,
41            resource_manager: &mut cx.resource_manager,
42            style: &mut cx.style,
43            text_context: &mut cx.text_context,
44            tree: &cx.tree,
45            #[cfg(feature = "tokio")]
46            task_runtime: cx.task_runtime.clone(),
47            #[cfg(feature = "tokio")]
48            named_tasks: cx.named_tasks.clone(),
49        }
50    }
51
52    /// Executes the given closure in a spawned thread.
53    pub fn spawn<F>(&self, target: F)
54    where
55        F: 'static + Send + FnOnce(&mut ContextProxy),
56    {
57        let mut cxp = ContextProxy {
58            current: self.current,
59            event_proxy: self.event_proxy.as_ref().map(|p| p.make_clone()),
60        };
61
62        std::thread::spawn(move || target(&mut cxp));
63    }
64
65    /// Submits a configured `TaskBuilder` for asynchronous execution (requires `tokio` feature).
66    #[cfg(feature = "tokio")]
67    pub fn spawn_task<T, E>(
68        &self,
69        task: crate::context::TaskBuilder<T, E>,
70    ) -> crate::context::TaskHandle
71    where
72        T: Send + 'static,
73        E: Send + 'static,
74    {
75        task.add_to_resource_context(self)
76    }
77
78    /// Dispatches a resource request through the configured loader chain.
79    ///
80    /// Returns `true` when a loader accepts the request.
81    pub fn request_resource(
82        &mut self,
83        request: ResourceRequest,
84        options: crate::resource::ResourceLoadOptions,
85    ) -> bool {
86        // Temporarily move loaders out so each `load` call can borrow `self` mutably.
87        let mut loaders = std::mem::take(&mut self.resource_manager.resource_loaders);
88
89        for loader in &loaders {
90            if loader.load(request.clone(), options, self) {
91                // Preserve any loaders registered while handling requests.
92                loaders.append(&mut self.resource_manager.resource_loaders);
93                self.resource_manager.resource_loaders = loaders;
94                return true;
95            }
96        }
97
98        // Preserve any loaders registered while handling requests.
99        loaders.append(&mut self.resource_manager.resource_loaders);
100        self.resource_manager.resource_loaders = loaders;
101
102        false
103    }
104
105    /// Loads the provided image into the resource manager.
106    pub fn load_image(
107        &mut self,
108        path: String,
109        image: skia_safe::Image,
110        policy: ImageRetentionPolicy,
111    ) {
112        let id = if let Some(image_id) = self.resource_manager.image_ids.get(&path) {
113            *image_id
114        } else {
115            let id = self.resource_manager.image_id_manager.create();
116            self.resource_manager.image_ids.insert(path.clone(), id);
117            id
118        };
119
120        // Register the same ImageId under every name in image_sources that resolves to the same
121        // source path as `path`. This ensures that multiple names registered via `add_image` with
122        // the same physical path all receive an image_ids entry when loading completes, even though
123        // only one ImageRequest was enqueued (the loader is called once, with the first name).
124        if let Some(source_path) = self.resource_manager.image_sources.get(&path).cloned() {
125            let aliases: Vec<String> = self
126                .resource_manager
127                .image_sources
128                .iter()
129                .filter(|(alias, alias_path)| {
130                    *alias_path == &source_path
131                        && !self.resource_manager.image_ids.contains_key(*alias)
132                })
133                .map(|(alias, _)| alias.clone())
134                .collect();
135            for alias in aliases {
136                self.resource_manager.image_ids.insert(alias, id);
137            }
138        }
139
140        match self.resource_manager.images.entry(id) {
141            Entry::Occupied(mut occ) => {
142                occ.get_mut().image = ImageOrSvg::Image(image);
143                occ.get_mut().dirty = true;
144                occ.get_mut().retention_policy = policy;
145            }
146            Entry::Vacant(vac) => {
147                vac.insert(StoredImage {
148                    image: ImageOrSvg::Image(image),
149                    retention_policy: policy,
150                    used: true,
151                    dirty: false,
152                    observers: HashSet::new(),
153                });
154            }
155        }
156        // Relayout only the entities that display this image (its observers) plus the current
157        // entity, rather than forcing a full tree relayout.
158        let observers: Vec<Entity> = self
159            .resource_manager
160            .images
161            .get(&id)
162            .map(|img| img.observers.iter().copied().collect())
163            .unwrap_or_default();
164        for observer in observers {
165            self.style.needs_relayout(observer);
166        }
167        self.style.needs_relayout(self.current);
168
169        // Resource loading can complete off the normal interaction path; request a redraw so
170        // newly available image content is painted immediately without waiting for external input.
171        if let Some(proxy) = self.event_proxy.as_ref() {
172            let mut cxp =
173                ContextProxy { current: self.current, event_proxy: Some(proxy.make_clone()) };
174            let _ = cxp.redraw();
175        }
176    }
177
178    /// Loads an SVG from bytes and associates it with the provided path.
179    pub fn load_svg(
180        &mut self,
181        path: String,
182        data: &[u8],
183        policy: ImageRetentionPolicy,
184    ) -> Option<ImageId> {
185        let id = if let Some(image_id) = self.resource_manager.image_ids.get(&path) {
186            *image_id
187        } else {
188            let id = self.resource_manager.image_id_manager.create();
189            self.resource_manager.image_ids.insert(path.clone(), id);
190            id
191        };
192
193        // Back-fill aliases: any other name registered via add_image that resolves to the same
194        // source path should share this ImageId (same logic as in load_image).
195        if let Some(source_path) = self.resource_manager.image_sources.get(&path).cloned() {
196            let aliases: Vec<String> = self
197                .resource_manager
198                .image_sources
199                .iter()
200                .filter(|(alias, alias_path)| {
201                    *alias_path == &source_path
202                        && !self.resource_manager.image_ids.contains_key(*alias)
203                })
204                .map(|(alias, _)| alias.clone())
205                .collect();
206            for alias in aliases {
207                self.resource_manager.image_ids.insert(alias, id);
208            }
209        }
210
211        if let Ok(svg) =
212            skia_safe::svg::Dom::from_bytes(data, self.text_context.default_font_manager.clone())
213        {
214            match self.resource_manager.images.entry(id) {
215                Entry::Occupied(mut occ) => {
216                    occ.get_mut().image = ImageOrSvg::Svg(svg);
217                    occ.get_mut().dirty = true;
218                    occ.get_mut().retention_policy = policy;
219                }
220                Entry::Vacant(vac) => {
221                    vac.insert(StoredImage {
222                        image: ImageOrSvg::Svg(svg),
223                        retention_policy: policy,
224                        used: true,
225                        dirty: false,
226                        observers: HashSet::new(),
227                    });
228                }
229            }
230            let observers: Vec<Entity> = self
231                .resource_manager
232                .images
233                .get(&id)
234                .map(|img| img.observers.iter().copied().collect())
235                .unwrap_or_default();
236            for observer in observers {
237                self.style.needs_relayout(observer);
238            }
239            self.style.needs_relayout(self.current);
240
241            if let Some(proxy) = self.event_proxy.as_ref() {
242                let mut cxp =
243                    ContextProxy { current: self.current, event_proxy: Some(proxy.make_clone()) };
244                let _ = cxp.redraw();
245            }
246
247            Some(id)
248        } else {
249            None
250        }
251    }
252
253    /// Loads a font from bytes, making it available to text layout.
254    pub fn load_font(&mut self, path: String, data: &[u8]) -> bool {
255        if let Some(typeface) = self.text_context.default_font_manager.new_from_data(data, None) {
256            self.text_context.asset_provider.register_typeface(typeface, None);
257
258            // Rebuild text for all entities so newly available fonts can be selected immediately.
259            let entities: Vec<Entity> = self.tree.into_iter().collect();
260            for entity in entities {
261                self.style.needs_text_update(entity);
262            }
263
264            self.resource_manager.set_resource_status(path, LoadingStatus::Loaded);
265
266            if let Some(proxy) = self.event_proxy.as_ref() {
267                let mut cxp =
268                    ContextProxy { current: self.current, event_proxy: Some(proxy.make_clone()) };
269                let _ = cxp.redraw();
270            }
271
272            true
273        } else {
274            self.resource_manager.set_resource_status(path, LoadingStatus::Error);
275            false
276        }
277    }
278
279    /// Loads a translation file and marks text as dirty so localized content can refresh.
280    pub fn load_translation(&mut self, lang: LanguageIdentifier, path: String, ftl: &str) -> bool {
281        if self.resource_manager.add_translation(lang, ftl.to_string()).is_ok() {
282            let entities: Vec<Entity> = self.tree.into_iter().collect();
283            for entity in entities {
284                self.style.needs_text_update(entity);
285            }
286
287            self.resource_manager.set_resource_status(path, LoadingStatus::Loaded);
288
289            if let Some(proxy) = self.event_proxy.as_ref() {
290                let mut cxp =
291                    ContextProxy { current: self.current, event_proxy: Some(proxy.make_clone()) };
292                let _ = cxp.redraw();
293            }
294
295            true
296        } else {
297            self.resource_manager.set_resource_status(path, LoadingStatus::Error);
298            false
299        }
300    }
301}